diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 9c32dffa..409a71c5 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -122,5 +122,10 @@
// updateContent runs inside the container after repo content is updated
// with new commits. We use it to build new content so users' builds are
// mostly incremental.
- "updateContentCommand": ".devcontainer/update_content.sh"
+ "updateContentCommand": ".devcontainer/update_content.sh",
+ // postStartCommand runs inside the container every time it starts, so on
+ // every `devpod up`. We use it to (re-)fetch the shared workstation
+ // secrets from Secret Manager on each start; it is a no-op off GCP
+ // (e.g. Codespaces).
+ "postStartCommand": ".devcontainer/devpod/fetch-workstation-secrets.sh || true"
}
diff --git a/.devcontainer/devpod/fetch-workstation-secrets.sh b/.devcontainer/devpod/fetch-workstation-secrets.sh
new file mode 100755
index 00000000..dc297c68
--- /dev/null
+++ b/.devcontainer/devpod/fetch-workstation-secrets.sh
@@ -0,0 +1,76 @@
+#!/bin/bash
+#
+# Fetches the shared workstation secrets from Google Secret Manager and
+# writes them as `export` lines into a profile snippet that every shell
+# sources, so they appear as environment variables in every terminal on
+# the workstation.
+#
+# This runs automatically on every `devpod up` (via `postStartCommand` in
+# `devcontainer.json`) on DevPod GCP workstations. It is a no-op anywhere
+# without a GCE metadata server / service account, e.g. Codespaces or
+# local Docker. It is safe to re-run by hand to pick up secret changes
+# mid-session, after which you open a new shell:
+#
+# .devcontainer/devpod/fetch-workstation-secrets.sh
+#
+# A secret is injected iff it carries the label `workstation-env=true`,
+# and the secret's NAME becomes the environment variable name, so name
+# secrets as valid shell identifiers (e.g. `OPENAI_API_KEY`). See the
+# "Workstation secrets" section of `.devcontainer/README.md` for the
+# `gcloud` commands to add/list/remove secrets and the one-time setup.
+
+# Intentionally NOT `set -e`: a failure to fetch must never break
+# workstation creation or a shell — secrets are best-effort.
+set -uo pipefail
+
+OUT=/etc/profile.d/10-workstation-secrets.sh
+META="http://metadata.google.internal/computeMetadata/v1"
+MFLAVOR=(-H "Metadata-Flavor: Google")
+
+log() { echo "fetch-workstation-secrets: $*" >&2; }
+
+# Bail quietly unless we're on a GCE VM (the metadata server answers).
+if ! curl -sf -m 5 "${MFLAVOR[@]}" "$META/instance/name" >/dev/null 2>&1; then
+ log "no GCE metadata server; skipping (expected off-GCP)."
+ exit 0
+fi
+
+PROJECT="$(curl -sf -m 5 "${MFLAVOR[@]}" "$META/project/project-id" 2>/dev/null || true)"
+TOKEN="$(curl -sf -m 5 "${MFLAVOR[@]}" \
+ "$META/instance/service-accounts/default/token" 2>/dev/null \
+ | python3 -c 'import sys,json; print(json.load(sys.stdin)["access_token"])' 2>/dev/null || true)"
+if [ -z "${PROJECT:-}" ] || [ -z "${TOKEN:-}" ]; then
+ log "no service-account token (is a service account attached?); skipping."
+ exit 0
+fi
+
+# Do all the Secret Manager REST work in Python (stdlib only); it prints
+# ready-to-source `export` lines. PROJECT/TOKEN come from the env.
+script_dir="$(cd "$(dirname "$0")" && pwd)"
+body="$(PROJECT="$PROJECT" TOKEN="$TOKEN" \
+ python3 "$script_dir/fetch_workstation_secrets.py")"
+if [ $? -ne 0 ]; then
+ log "Secret Manager fetch failed; leaving existing secrets in place."
+ exit 0
+fi
+
+# Write atomically as a profile snippet readable only by us and root.
+tmp="$(mktemp)"
+{
+ echo "# Generated by .devcontainer/devpod/fetch-workstation-secrets.sh."
+ echo "# Secrets in project '$PROJECT' labelled workstation-env=true. Do not edit."
+ echo "$body"
+} >"$tmp"
+sudo install -m 0640 -o root -g "$(id -gn)" "$tmp" "$OUT"
+rm -f "$tmp"
+
+# `/etc/profile.d` is sourced by login shells; make sure interactive
+# bash and zsh shells (e.g. VS Code terminals) pick it up too.
+source_line=". $OUT # workstation secrets"
+for rc in /etc/bash.bashrc /etc/zsh/zshrc; do
+ if [ -f "$rc" ] && ! sudo grep -qF "$OUT" "$rc"; then
+ echo "[ -r $OUT ] && $source_line" | sudo tee -a "$rc" >/dev/null
+ fi
+done
+
+log "wrote $(grep -c '^export ' "$OUT" 2>/dev/null || echo 0) secret(s) to $OUT"
diff --git a/.devcontainer/devpod/fetch_workstation_secrets.py b/.devcontainer/devpod/fetch_workstation_secrets.py
new file mode 100644
index 00000000..013ad49c
--- /dev/null
+++ b/.devcontainer/devpod/fetch_workstation_secrets.py
@@ -0,0 +1,72 @@
+"""Fetches workstation secrets from Google Secret Manager.
+
+Reads PROJECT and TOKEN (a service-account access token) from the
+environment and prints a ready-to-source `export NAME='value'` line for
+every secret labelled `workstation-env=true`.
+"""
+
+import base64
+import json
+import os
+import re
+import sys
+import urllib.error
+import urllib.request
+from typing import Any
+
+_API = "https://secretmanager.googleapis.com/v1"
+_VALID_NAME = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
+
+
+def _get(url: str, token: str) -> Any:
+ request = urllib.request.Request(
+ url,
+ headers={"Authorization": "Bearer " + token},
+ )
+ with urllib.request.urlopen(request, timeout=10) as response:
+ return json.load(response)
+
+
+def main() -> int:
+ project = os.environ["PROJECT"]
+ token = os.environ["TOKEN"]
+
+ try:
+ listing = _get(
+ f"{_API}/projects/{project}/secrets"
+ "?filter=labels.workstation-env%3Dtrue&pageSize=500",
+ token,
+ )
+ except urllib.error.URLError as error:
+ print(f"# listing secrets failed: {error}", file=sys.stderr)
+ return 1
+
+ lines = []
+ for secret in listing.get("secrets", []):
+ name = secret["name"].rsplit("/", 1)[1]
+ if not _VALID_NAME.match(name):
+ print(
+ f"# skipped '{name}': not a valid env var name",
+ file=sys.stderr,
+ )
+ continue
+ try:
+ version = _get(
+ f"{_API}/projects/{project}/secrets/{name}"
+ "/versions/latest:access",
+ token,
+ )
+ value = base64.b64decode(version["payload"]["data"]).decode()
+ except Exception as error: # noqa: BLE001 (skip unreadable secret)
+ print(f"# skipped '{name}': {error}", file=sys.stderr)
+ continue
+ # Single-quote the value, escaping any embedded single quotes.
+ escaped = value.replace("'", "'\\''")
+ lines.append(f"export {name}='{escaped}'")
+
+ print("\n".join(lines))
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/.devcontainer/devpod/gcloud-poweroff.provider.yaml b/.devcontainer/devpod/gcloud-poweroff.provider.yaml
new file mode 100644
index 00000000..14e6eba3
--- /dev/null
+++ b/.devcontainer/devpod/gcloud-poweroff.provider.yaml
@@ -0,0 +1,224 @@
+# Patched copy of the DevPod gcloud provider (v0.0.12). The ONLY change
+# from upstream is `agent.exec.shutdown` (more details below). When bumping
+# the provider version, re-sync this file from the upstream release and
+# re-apply that one change:
+# https://github.com/loft-sh/devpod-provider-gcloud/releases
+name: gcloud
+version: v0.0.12
+description: |-
+ DevPod on Google Cloud
+icon: https://devpod.sh/assets/gcp.svg
+optionGroups:
+ - options:
+ - DISK_SIZE
+ - DISK_IMAGE
+ - MACHINE_TYPE
+ name: "GCloud options"
+ - options:
+ - AGENT_PATH
+ - INACTIVITY_TIMEOUT
+ - INJECT_DOCKER_CREDENTIALS
+ - INJECT_GIT_CREDENTIALS
+ name: "Agent options"
+options:
+ PROJECT:
+ description: The project id to use.
+ required: true
+ command: gcloud config list --quiet --verbosity=error --format "value(core.project)" 2>/dev/null || true
+ ZONE:
+ description: The google cloud zone to create the VM in. E.g. europe-west1-d
+ required: true
+ command: |-
+ GCLOUD_ZONE=$(gcloud config list --quiet --verbosity=error --format "value(compute.zone)" 2>/dev/null || true)
+ if [ -z "$GCLOUD_ZONE" ]; then
+ echo "europe-west2-b"
+ else
+ echo $GCLOUD_ZONE
+ fi
+ suggestions:
+ - asia-east1-a
+ - asia-east1-b
+ - asia-east1-c
+ - asia-east2-a
+ - asia-east2-b
+ - asia-east2-c
+ - asia-northeast1-a
+ - asia-northeast1-c
+ - asia-northeast2-b
+ - asia-northeast3-b
+ - asia-south1-a
+ - asia-south1-b
+ - asia-southeast1-a
+ - europe-north1-a
+ - europe-north1-b
+ - europe-north1-c
+ - europe-west1-b
+ - europe-west1-c
+ - europe-west1-d
+ - europe-west2-a
+ - europe-west2-b
+ - europe-west2-c
+ - europe-west3-a
+ - europe-west3-b
+ - europe-west3-c
+ - europe-west4-a
+ - europe-west4-b
+ - europe-west4-c
+ - europe-west9-a
+ - europe-west9-b
+ - europe-west9-c
+ - me-central1-a
+ - me-central1-b
+ - me-central1-c
+ - me-west1-a
+ - me-west1-b
+ - me-west1-c
+ - northamerica-northeast1-a
+ - northamerica-northeast1-b
+ - northamerica-northeast1-c
+ - southamerica-east1-a
+ - southamerica-east1-b
+ - southamerica-east1-c
+ - southamerica-west1-a
+ - southamerica-west1-b
+ - southamerica-west1-c
+ - us-central1-a
+ - us-central1-b
+ - us-central1-f
+ - us-east1-b
+ - us-east1-c
+ - us-east1-d
+ - us-east4-a
+ - us-east4-b
+ - us-east4-c
+ - us-south1-a
+ - us-south1-b
+ - us-south1-c
+ - us-west1-a
+ - us-west1-b
+ - us-west1-c
+ - us-west2-a
+ - us-west2-b
+ - us-west2-c
+ - us-west4-a
+ - us-west4-b
+ - us-west4-c
+ NETWORK:
+ description: The network id to use.
+ SUBNETWORK:
+ description: The subnetwork id to use.
+ TAG:
+ description: A tag to attach to the instance.
+ default: "devpod"
+ DISK_SIZE:
+ description: The disk size to use (GB).
+ default: "40"
+ DISK_IMAGE:
+ description: The disk image to use.
+ default: projects/cos-cloud/global/images/cos-101-17162-127-5
+ SERVICE_ACCOUNT:
+ description: A service account to attach
+ default: ""
+ PUBLIC_IP_ENABLED:
+ description: Use a public ip to access the instance
+ default: "true"
+ MACHINE_TYPE:
+ description: The machine type to use.
+ default: c2-standard-4
+ suggestions:
+ - f1-micro
+ - e2-small
+ - e2-medium
+ - n2-standard-2
+ - n2-standard-4
+ - n2-standard-8
+ - n2-standard-16
+ - n2-highcpu-8
+ - n2-highcpu-16
+ - c2-standard-4
+ - c2-standard-8
+ - c2-standard-16
+ - c2-standard-30
+ - g2-standard-4
+ - g2-standard-8
+ - g2-standard-12
+ - g2-standard-16
+ - a2-highgpu-1g
+ - a2-highgpu-2g
+ INACTIVITY_TIMEOUT:
+ description: If defined, will automatically stop the VM after the inactivity period.
+ default: 5m
+ INJECT_GIT_CREDENTIALS:
+ description: "If DevPod should inject git credentials into the remote host."
+ default: "true"
+ INJECT_DOCKER_CREDENTIALS:
+ description: "If DevPod should inject docker credentials into the remote host."
+ default: "true"
+ AGENT_PATH:
+ description: The path where to inject the DevPod agent to.
+ default: /var/lib/toolbox/devpod
+ GCLOUD_PROVIDER_TOKEN:
+ local: true
+ hidden: true
+ cache: 5m
+ description: "The Google Cloud auth token to use"
+ command: |-
+ ${GCLOUD_PROVIDER} token
+agent:
+ path: ${AGENT_PATH}
+ inactivityTimeout: ${INACTIVITY_TIMEOUT}
+ injectGitCredentials: ${INJECT_GIT_CREDENTIALS}
+ injectDockerCredentials: ${INJECT_DOCKER_CREDENTIALS}
+ binaries:
+ GCLOUD_PROVIDER:
+ - os: linux
+ arch: amd64
+ path: https://github.com/loft-sh/devpod-provider-gcloud/releases/download/v0.0.12/devpod-provider-gcloud-linux-amd64
+ checksum: ebc38f3ce8f74f1ea4d79f7ff7de2c6fafb7cceb252422b106013c5ceca402bd
+ - os: linux
+ arch: arm64
+ path: https://github.com/loft-sh/devpod-provider-gcloud/releases/download/v0.0.12/devpod-provider-gcloud-linux-arm64
+ checksum: b333374e0f97c514e4ce7c3433a7f1fd6aa46922ca18856d470203145ffc2f0f
+ exec:
+ # PATCHED. The stock provider runs `${GCLOUD_PROVIDER} stop --raw`,
+ # which calls the GCP stop API with a token minted at `up` time. That
+ # token expires after ~1 hour, so an inactivity timeout longer than
+ # ~1 h fires its stop after the token is dead — and the provider's
+ # `rawStop` swallows the resulting auth error, so the VM never shuts
+ # down.
+ #
+ # Our replacement is a guest-side `poweroff` which needs no token;
+ # GCP turns a guest shutdown into TERMINATED (compute billing stops).
+ # The daemon runs as root, so no sudo is needed.
+ shutdown: |-
+ /sbin/poweroff
+binaries:
+ GCLOUD_PROVIDER:
+ - os: linux
+ arch: amd64
+ path: https://github.com/loft-sh/devpod-provider-gcloud/releases/download/v0.0.12/devpod-provider-gcloud-linux-amd64
+ checksum: ebc38f3ce8f74f1ea4d79f7ff7de2c6fafb7cceb252422b106013c5ceca402bd
+ - os: linux
+ arch: arm64
+ path: https://github.com/loft-sh/devpod-provider-gcloud/releases/download/v0.0.12/devpod-provider-gcloud-linux-arm64
+ checksum: b333374e0f97c514e4ce7c3433a7f1fd6aa46922ca18856d470203145ffc2f0f
+ - os: darwin
+ arch: amd64
+ path: https://github.com/loft-sh/devpod-provider-gcloud/releases/download/v0.0.12/devpod-provider-gcloud-darwin-amd64
+ checksum: 37e7a73ebb1be6961695320d54fcb142c021774ff7f5b339a2dec5bbbc317e54
+ - os: darwin
+ arch: arm64
+ path: https://github.com/loft-sh/devpod-provider-gcloud/releases/download/v0.0.12/devpod-provider-gcloud-darwin-arm64
+ checksum: 0eb3862c7e5a07a71a6decfe499320664a57925dda2817d1c4a59e4d598f4f82
+ - os: windows
+ arch: amd64
+ path: https://github.com/loft-sh/devpod-provider-gcloud/releases/download/v0.0.12/devpod-provider-gcloud-windows-amd64.exe
+ checksum: c5140711e5a5bac0219a9efd35c8690eb0fbfdc7b6f28e2afe2c05ebf0a17eaa
+exec:
+ init: ${GCLOUD_PROVIDER} init
+ command: ${GCLOUD_PROVIDER} command
+ create: ${GCLOUD_PROVIDER} create
+ delete: ${GCLOUD_PROVIDER} delete
+ start: ${GCLOUD_PROVIDER} start
+ stop: ${GCLOUD_PROVIDER} stop
+ status: ${GCLOUD_PROVIDER} status
diff --git a/.devcontainer/git_config.sh b/.devcontainer/git_config.sh
index a045077a..f2090521 100755
--- a/.devcontainer/git_config.sh
+++ b/.devcontainer/git_config.sh
@@ -30,8 +30,12 @@ git config --global submodule.recurse true
# https://stackoverflow.com/questions/27417656/should-diff3-be-default-conflictstyle-on-git
git config --global merge.conflictstyle diff3
-# Do some extra work to pre-configure GitHub authentication when running
-# in a codespace. Skip this for a local devcontainer.
+# Codespaces-specific GitHub auth: Codespaces provides HTTPS-only
+# credentials and a `GITHUB_TOKEN` scoped to a single repo, so here we
+# rewrite SSH remotes to HTTPS and register `gh` as the git credential
+# helper. Gated on `CODESPACES` alone because other environments bring
+# their own git credentials — DevPod injects them, and a local
+# devcontainer uses the host's.
if [[ "${CODESPACES:-}" == "true" ]]; then
# Use HTTPS instead of SSH for git operations on this workstation: in
# Codespaces we have credentials ONLY for HTTPS. See:
@@ -39,21 +43,29 @@ if [[ "${CODESPACES:-}" == "true" ]]; then
# https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token#using-a-token-on-the-command-line
git config --global url."https://github.com/".insteadOf "git@github.com:"
- # Make it possible for us to push commits to all repos, not just to
- # `reboot-dev/mono`. To do so, we...
- # 1. Set the `gh` (GitHub CLI) tool as a `git` credential helper.
+ # Register `gh` as a git credential helper so we can push to every
+ # repo our GitHub user can access, not only the one this workstation
+ # was created for.
gh auth setup-git
+fi
- # 2. Set up a script that on every terminal start:
- # * Unsets `GITHUB_TOKEN`, so it uses the credential helper instead.
- # * Tells the user to run `gh auth login` if they haven't yet.
+# A separate, broader block: on any managed workstation (Codespaces or
+# DevPod), install a per-terminal reminder to run `gh auth login` when
+# the `gh` CLI isn't authenticated. It's its own `if` because `gh` CLI
+# auth is useful on DevPod too (for `gh`/agent commands), whereas the
+# HTTPS rewrite and credential helper above are Codespaces-only. On
+# DevPod plain `git` is authenticated by DevPod's injected credentials,
+# so this is purely a `gh`-CLI reminder — the sourced script's
+# `unset GITHUB_TOKEN` is a no-op there (that variable exists only in
+# Codespaces). Gated to managed workstations so a plain local
+# devcontainer isn't nagged.
+if [[ "${CODESPACES:-}" == "true" || "${DEVPOD:-}" == "true" ]]; then
grep -q "gh_auth_for_all_repos.sh" ~/.bashrc \
|| { \
echo "" >> ~/.bashrc \
&& echo "# Installed by .devcontainer/git_config.sh" >> ~/.bashrc \
&& echo "source .devcontainer/gh_auth_for_all_repos.sh" >> ~/.bashrc \
;}
-
fi
# Install a script that on every terminal start checks if the precommit
diff --git a/.devcontainer/install_precommit_hook.sh b/.devcontainer/install_precommit_hook.sh
index 28f47a11..11c81758 100755
--- a/.devcontainer/install_precommit_hook.sh
+++ b/.devcontainer/install_precommit_hook.sh
@@ -1,49 +1,58 @@
#!/bin/bash
#
+# Installs a git pre-commit hook that runs the dev-tools and documentation
+# pre-commit hooks. The generated hook resolves the repository location at
+# RUNTIME (via `git rev-parse`), so it works wherever the repo is checked
+# out — a devcontainer, a local clone, or a git worktree — instead of
+# baking in the absolute path of wherever it happened to be installed.
+
function install_precommit_hook() {
- local repo_top_level="$(git rev-parse --show-toplevel)"
+ local repo_top_level
+ repo_top_level="$(git rev-parse --show-toplevel)"
+
+ # Hooks live in the common git dir, which is shared across worktrees.
+ local hooks_dir
+ hooks_dir="$(cd "$(git rev-parse --git-common-dir)" && pwd)/hooks"
+
# In the mono repo, submodules live under `public/`; in the standalone
# public repo they live directly at the top level.
local prefix=""
if [[ -d "${repo_top_level}/public" ]]; then
prefix="public/"
fi
- local dev_tools_commit_hook_path="${repo_top_level}/${prefix}submodules/dev-tools/pre-commit";
- local local_dev_tools_commit_hook_path="${repo_top_level}/.git/hooks/dev-tools-pre-commit";
- local rbt_documentation_commit_hook_path="${repo_top_level}/${prefix}documentation/pre-commit";
- local local_rbt_documentation_commit_hook_path="${repo_top_level}/.git/hooks/rbt-documentation-pre-commit";
- local local_combined_commit_hook_path="${repo_top_level}/.git/hooks/pre-commit";
- # Check that the dev-tools hook file exists.
- if [[ ! -f "${dev_tools_commit_hook_path}" ]]; then
- echo "Commit hook from dev-tools not found at '${dev_tools_commit_hook_path}' Aborting.";
+ # Sanity-check that the source hooks exist (submodules must be checked
+ # out) before installing.
+ if [[ ! -f "${repo_top_level}/${prefix}submodules/dev-tools/pre-commit" ]]; then
+ echo "Commit hook from dev-tools not found under '${prefix}submodules/dev-tools/'. Aborting."
return 1
fi
-
- # Check that the rbt documentation hook file exists.
- if [[ ! -f "${rbt_documentation_commit_hook_path}" ]]; then
- echo "Commit hook from documentation not found at '${rbt_documentation_commit_hook_path}' Aborting.";
+ if [[ ! -f "${repo_top_level}/${prefix}documentation/pre-commit" ]]; then
+ echo "Commit hook from documentation not found under '${prefix}documentation/'. Aborting."
return 1
fi
- # Create a local symlink for the dev-tools hook. Remove any old ones first,
- # in case the paths we're working with have changed.
- rm -f "${local_dev_tools_commit_hook_path}"
- ln -s -f "${dev_tools_commit_hook_path}" "${local_dev_tools_commit_hook_path}"
+ # Remove any previously-installed hooks, including the old symlink-based
+ # layout that baked in absolute paths.
+ rm -f "${hooks_dir}/dev-tools-pre-commit" \
+ "${hooks_dir}/rbt-documentation-pre-commit" \
+ "${hooks_dir}/pre-commit"
- # Create a local symlink for the rbt documentation hook. Remove any old
- # ones first, in case the paths we're working with have changed.
- rm -f "${local_rbt_documentation_commit_hook_path}"
- ln -s -f "${rbt_documentation_commit_hook_path}" "${local_rbt_documentation_commit_hook_path}"
-
- # Delete any old precommit hook. It's important to explicitly delete (rather
- # than just overwriting) in case the old version was a symlink pointing to a
- # file that we don't actually want to overwrite).
- rm -f "${local_combined_commit_hook_path}"
-
- # Create a top-level precommit hook that calls the pulled-in files.
- echo "${local_dev_tools_commit_hook_path}; if [ ! \$? -eq 0 ]; then exit 1; fi; ${local_rbt_documentation_commit_hook_path}; exit \$?" > "${local_combined_commit_hook_path}"
- chmod +x "${local_combined_commit_hook_path}"
+ # Write a self-contained hook that resolves the repo root (and thus the
+ # submodule hook paths) at runtime. The single-quoted heredoc keeps the
+ # `$(...)` and `${...}` literal so they are evaluated when the hook runs,
+ # not now.
+ cat >"${hooks_dir}/pre-commit" <<'HOOK'
+#!/bin/bash
+# Generated by .devcontainer/install_precommit_hook.sh — do not edit.
+set -e
+repo_top_level="$(git rev-parse --show-toplevel)"
+prefix=""
+[[ -d "${repo_top_level}/public" ]] && prefix="public/"
+"${repo_top_level}/${prefix}submodules/dev-tools/pre-commit"
+"${repo_top_level}/${prefix}documentation/pre-commit"
+HOOK
+ chmod +x "${hooks_dir}/pre-commit"
}
install_precommit_hook
diff --git a/Dockerfile b/Dockerfile
index 95dc2ed9..55635d0c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -514,6 +514,20 @@ RUN curl -LsSf https://astral.sh/uv/0.11.13/install.sh | sh \
# `uv` there, and ours (pinned) must win.
RUN sudo ln -sf "$HOME/.local/bin/uv" /usr/local/bin/uv
RUN echo "export PATH=\"$HOME/.local/bin:\$PATH\"" >> "$HOME/.bashrc"
+
+# Install Claude Code as the target user so it lands in the user's home
+# (where its auto-updater writes), and symlink it onto the system PATH
+# like `uv` above so non-interactive shells and other users find it too.
+# It's a human-driven tool, so we don't pin the version and leave
+# auto-update on.
+RUN curl -fsSL https://claude.ai/install.sh | bash \
+ && sudo ln -sf "$HOME/.local/bin/claude" /usr/local/bin/claude
+
+# Give Claude Code the headless Chrome DevTools MCP server in the user's
+# config, so it can drive a browser out of the box.
+RUN claude mcp add -s user chrome-devtools \
+ -- npx --yes chrome-devtools-mcp@latest --headless=true
+
# Then return to root.
USER root
@@ -564,11 +578,17 @@ RUN set -e; \
&& chmod -R a+rX "${KREW_ROOT}" \
&& rm -rf "${TMPDIR}"
-# Install Claude Code. We're not worried about breaking changes in this
-# tool (we don't use its API or CLI, it's human-driven) so we don't need
-# to pin its version, and in fact leave its default auto-update function
-# enabled.
-RUN curl -fsSL https://claude.ai/install.sh | bash
+# Install the Codex CLI globally via npm so it's on PATH for every user.
+# Like Claude Code (installed in the user's home above) it's a
+# human-driven tool, so we don't pin the version.
+RUN npm install -g @openai/codex
+
+# Give Codex the same headless Chrome DevTools MCP server in the target
+# user's config, mirroring the Claude Code setup above.
+USER $UNAME
+RUN codex mcp add chrome-devtools \
+ -- npx --yes chrome-devtools-mcp@latest --headless=true
+USER root
###############################################################################
# The following is a partial copy-paste from
diff --git a/charts/reboot/Chart.yaml b/charts/reboot/Chart.yaml
index e955c0d1..0b344b8c 100644
--- a/charts/reboot/Chart.yaml
+++ b/charts/reboot/Chart.yaml
@@ -1,6 +1,6 @@
apiVersion: 3.3.2
name: reboot
-version: "1.2.0"
+version: "1.2.1"
description: Reboot is a programming framework that enables transactional microservices built with the developer in mind.
type: application
keywords:
@@ -10,4 +10,4 @@ keywords:
- scalable
- reactive
home: https://docs.reboot.dev/
-appVersion: "1.2.0"
+appVersion: "1.2.1"
diff --git a/documentation/docs/ai_chat_apps/get_started.mdx b/documentation/docs/ai_chat_apps/get_started.mdx
index e1b7ec96..828adb71 100644
--- a/documentation/docs/ai_chat_apps/get_started.mdx
+++ b/documentation/docs/ai_chat_apps/get_started.mdx
@@ -800,33 +800,12 @@ uv run rbt dev run
title="Test with MCPJam Inspector"
description={`
-Create an \`mcp_servers.json\` file that tells MCPJam where
-your app is running, then launch the inspector.
+Run MCPJam Inspector to interact with your app.
`}>
```sh
-touch mcp_servers.json
-```
-
-
-
-
-```json
-{
- "mcpServers": {
- "counter-server": {
- "url": "http://localhost:9991/mcp",
- "useOAuth": true
- }
- }
-}
-```
-
-
-
-```sh
-npx @mcpjam/inspector@2.9.3 --config mcp_servers.json --server counter-server
+npx @mcpjam/inspector@2.18.1 --url http://localhost:9991/mcp --oauth
```
@@ -858,8 +837,7 @@ including `session_create_counter`, `counter_get`,
Under the hood, your app communicates via MCP (Model Context Protocol),
the standard that AI clients like ChatGPT, Claude, and VS Code use to
-discover and interact with apps. The `mcp_servers.json` format works
-with any MCP-compatible client.
+discover and interact with apps.
Next steps
diff --git a/documentation/docs/ai_chat_apps/get_started_claude_code.mdx b/documentation/docs/ai_chat_apps/get_started_claude_code.mdx
index ec0f3da4..2d3b7c30 100644
--- a/documentation/docs/ai_chat_apps/get_started_claude_code.mdx
+++ b/documentation/docs/ai_chat_apps/get_started_claude_code.mdx
@@ -103,7 +103,6 @@ files are:
```
my-app/
├── .rbtrc # Reboot CLI config
-├── mcp_servers.json # MCP client connection config
├── pyproject.toml # Python deps (uv)
├── api/
│ └── my_app/v1/
diff --git a/documentation/docs/ai_chat_apps/get_started_codex.mdx b/documentation/docs/ai_chat_apps/get_started_codex.mdx
index 3d71292b..840696af 100644
--- a/documentation/docs/ai_chat_apps/get_started_codex.mdx
+++ b/documentation/docs/ai_chat_apps/get_started_codex.mdx
@@ -113,7 +113,6 @@ files are:
```
my-app/
├── .rbtrc # Reboot CLI config
-├── mcp_servers.json # MCP client connection config
├── pyproject.toml # Python deps (uv)
├── api/
│ └── my_app/v1/
diff --git a/documentation/docs/learn_more/call/from_mcp_client.mdx b/documentation/docs/learn_more/call/from_mcp_client.mdx
index 6cc6ed2b..b74348a9 100644
--- a/documentation/docs/learn_more/call/from_mcp_client.mdx
+++ b/documentation/docs/learn_more/call/from_mcp_client.mdx
@@ -22,25 +22,9 @@ MCPJam picks up changes immediately.
[MCPJam Inspector](https://mcpjam.com) is a browser-based MCP client
ideal for testing during development.
-Add an `mcp_servers.json` file in your project root, and fill it in:
-```json
-{
- "mcpServers": {
- "my-app": {
- "url": "http://localhost:9991/mcp",
- "useOAuth": true
- }
- }
-}
-```
-
-Replace `my-app` with a name for your server, and update the URL if your
-app runs on a different port or host.
-
-Then run MCPJam:
+Run MCPJam:
```sh
-npx @mcpjam/inspector@2.9.3 \
- --config mcp_servers.json --server my-app
+npx @mcpjam/inspector@2.18.1 --url http://localhost:9991/mcp --oauth
```
This opens an interface where you can browse your app's tools, call
diff --git a/documentation/package-lock.json b/documentation/package-lock.json
index 1582fbcd..07cef2bf 100644
--- a/documentation/package-lock.json
+++ b/documentation/package-lock.json
@@ -35,7 +35,7 @@
"typescript": "~5.5.2"
},
"engines": {
- "node": ">=16.14"
+ "node": ">=20.0"
}
},
"node_modules/@algolia/abtesting": {
diff --git a/package.json b/package.json
index 7fed691b..3ddd8ba0 100644
--- a/package.json
+++ b/package.json
@@ -1,13 +1,13 @@
{
"private": true,
"dependencies": {
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-react": "1.2.0",
- "@reboot-dev/reboot-std-api": "1.2.0",
- "@reboot-dev/reboot-std-react": "1.2.0",
- "@reboot-dev/reboot-std": "1.2.0",
- "@reboot-dev/reboot-web": "1.2.0",
- "@reboot-dev/reboot": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-react": "1.2.1",
+ "@reboot-dev/reboot-std-api": "1.2.1",
+ "@reboot-dev/reboot-std-react": "1.2.1",
+ "@reboot-dev/reboot-std": "1.2.1",
+ "@reboot-dev/reboot-web": "1.2.1",
+ "@reboot-dev/reboot": "1.2.1",
"@modelcontextprotocol/ext-apps": "1.5.0",
"@modelcontextprotocol/sdk": "1.29.0",
"@bufbuild/protobuf": "1.10.1",
diff --git a/rbt/std/package.json b/rbt/std/package.json
index d31deb0b..fc72871f 100644
--- a/rbt/std/package.json
+++ b/rbt/std/package.json
@@ -1,6 +1,6 @@
{
"name": "@reboot-dev/reboot-std-api",
- "version": "1.2.0",
+ "version": "1.2.1",
"description": "Reboot standard library API.",
"main": "index.js",
"type": "module",
diff --git a/rbt/v1alpha1/errors.proto b/rbt/v1alpha1/errors.proto
index 84b77e78..340b36da 100644
--- a/rbt/v1alpha1/errors.proto
+++ b/rbt/v1alpha1/errors.proto
@@ -34,6 +34,14 @@ message TransactionParticipantFailedToPrepare {}
message TransactionParticipantFailedToCommit {}
+////////////////////////////////////////////////////////////////////////
+
+// Raised by a transaction participant that detects that the current
+// transaction should be retried from scratch for any reason. As
+// opposed to `Unavailable` (which also results in a retry), this error
+// signals that no backoff is required before the retry.
+message TransactionShouldRetryWithoutBackoff {}
+
////////////////////////////////////////////////////////////////////////
// gRPC:
////////////////////////////////////////////////////////////////////////
diff --git a/rbt/v1alpha1/index.ts b/rbt/v1alpha1/index.ts
index 25071110..5fdda02d 100644
--- a/rbt/v1alpha1/index.ts
+++ b/rbt/v1alpha1/index.ts
@@ -525,6 +525,9 @@ export const ZOD_ERRORS = z.discriminatedUnion("type", [
z.object({
type: z.literal("TransactionParticipantFailedToCommit"),
}),
+ z.object({
+ type: z.literal("TransactionShouldRetryWithoutBackoff"),
+ }),
z.object({
type: z.literal("Cancelled"),
}),
@@ -619,6 +622,7 @@ export const REBOOT_ERROR_TYPES = [
errors_pb.StateNotConstructed,
errors_pb.TransactionParticipantFailedToPrepare,
errors_pb.TransactionParticipantFailedToCommit,
+ errors_pb.TransactionShouldRetryWithoutBackoff,
errors_pb.UnknownService,
errors_pb.UnknownTask,
] as const; // Need `as const` to ensure TypeScript infers this as a tuple!
@@ -690,6 +694,13 @@ export function grpcStatusCodeFromError(
return StatusCode.UNAVAILABLE;
}
+ // Behaves like `Unavailable` (the call is retried); the distinct type
+ // lets the runtime recognize the restart and refresh its timestamp
+ // before retrying.
+ if (error instanceof errors_pb.TransactionShouldRetryWithoutBackoff) {
+ return StatusCode.UNAVAILABLE;
+ }
+
if (error instanceof errors_pb.DataLoss) {
return StatusCode.DATA_LOSS;
}
@@ -869,6 +880,8 @@ export function errorFromZodError(
return new errors_pb.TransactionParticipantFailedToPrepare();
case "TransactionParticipantFailedToCommit":
return new errors_pb.TransactionParticipantFailedToCommit();
+ case "TransactionShouldRetryWithoutBackoff":
+ return new errors_pb.TransactionShouldRetryWithoutBackoff();
}
}
@@ -924,6 +937,8 @@ export function zodErrorFromError(
return { type: "TransactionParticipantFailedToPrepare" };
} else if (error instanceof errors_pb.TransactionParticipantFailedToCommit) {
return { type: "TransactionParticipantFailedToCommit" };
+ } else if (error instanceof errors_pb.TransactionShouldRetryWithoutBackoff) {
+ return { type: "TransactionShouldRetryWithoutBackoff" };
}
throw new Error(`Unknown error type '${error.getType().typeName}'`);
}
diff --git a/rbt/v1alpha1/package.json b/rbt/v1alpha1/package.json
index 544ca09e..e77db399 100644
--- a/rbt/v1alpha1/package.json
+++ b/rbt/v1alpha1/package.json
@@ -1,6 +1,6 @@
{
"name": "@reboot-dev/reboot-api",
- "version": "1.2.0",
+ "version": "1.2.1",
"type": "module",
"description": "npm package for Reboot API",
"main": "index.js",
diff --git a/reboot/aio/aborted.py b/reboot/aio/aborted.py
index 6412f4a8..b38c90d9 100644
--- a/reboot/aio/aborted.py
+++ b/reboot/aio/aborted.py
@@ -59,6 +59,7 @@ def is_retryable(aborted: Aborted):
rbt.v1alpha1.errors_pb2.StateNotConstructed,
rbt.v1alpha1.errors_pb2.TransactionParticipantFailedToPrepare,
rbt.v1alpha1.errors_pb2.TransactionParticipantFailedToCommit,
+ rbt.v1alpha1.errors_pb2.TransactionShouldRetryWithoutBackoff,
rbt.v1alpha1.errors_pb2.UnknownService,
rbt.v1alpha1.errors_pb2.UnknownTask,
rbt.v1alpha1.errors_pb2.InvalidMethod,
@@ -88,6 +89,7 @@ def is_retryable(aborted: Aborted):
rbt.v1alpha1.errors_pb2.StateNotConstructed,
rbt.v1alpha1.errors_pb2.TransactionParticipantFailedToPrepare,
rbt.v1alpha1.errors_pb2.TransactionParticipantFailedToCommit,
+ rbt.v1alpha1.errors_pb2.TransactionShouldRetryWithoutBackoff,
rbt.v1alpha1.errors_pb2.UnknownService,
rbt.v1alpha1.errors_pb2.UnknownTask,
rbt.v1alpha1.errors_pb2.InvalidMethod,
@@ -304,6 +306,16 @@ def grpc_status_code_from_error(
elif isinstance(error, rbt.v1alpha1.errors_pb2.Unavailable):
return grpc.StatusCode.UNAVAILABLE
+ elif isinstance(
+ error,
+ rbt.v1alpha1.errors_pb2.TransactionShouldRetryWithoutBackoff,
+ ):
+ # Behaves like `Unavailable` (it is retryable), but its
+ # distinct type lets the runtime recognize the restart,
+ # refresh the coordinator's timestamp, and skip the
+ # backoff before the first retry.
+ return grpc.StatusCode.UNAVAILABLE
+
elif isinstance(error, rbt.v1alpha1.errors_pb2.DataLoss):
return grpc.StatusCode.DATA_LOSS
diff --git a/reboot/aio/applications.py b/reboot/aio/applications.py
index 5ceef126..455ccea8 100644
--- a/reboot/aio/applications.py
+++ b/reboot/aio/applications.py
@@ -393,7 +393,7 @@ def __init__(
self._token_verifier = token_verifier
self._initialize_bearer_token = initialize_bearer_token
self._oauth = oauth
- self._title = title
+ self._title = title or application_name()
self._description = description
self._example_prompts = example_prompts or []
@@ -642,6 +642,7 @@ async def record_connection(
oauth_server = OAuthServer(
provider=provider,
protected_resources=[_MCP_PATH],
+ application_title=self._title,
)
self._token_verifier = oauth_server.token_verifier
mcp_sdk_token_verifier = oauth_server.mcp_sdk_token_verifier
@@ -719,7 +720,7 @@ async def initialize(context: InitializeContext) -> None:
# authorizer requires.
await reboot.application.ref().always().initialize(
context,
- title=self._title or application_name(),
+ title=self._title,
description=self._description,
port=local_envoy_port,
mcp=any(
diff --git a/reboot/aio/auth/BUILD.bazel b/reboot/aio/auth/BUILD.bazel
index 31c2a3b3..39c7d066 100644
--- a/reboot/aio/auth/BUILD.bazel
+++ b/reboot/aio/auth/BUILD.bazel
@@ -73,6 +73,7 @@ py_library(
py_library(
name = "oauth_server_py",
srcs = ["oauth_server.py"],
+ data = ["consent_page.html.j2"],
srcs_version = "PY3",
visibility = ["//visibility:public"],
deps = [
@@ -83,6 +84,7 @@ py_library(
"//reboot/aio:http_py",
"//reboot/crypto:root_keys_py",
"@com_github_reboot_dev_reboot//rbt/std/oauth/v1:oauth_py_reboot",
+ requirement("jinja2"),
requirement("pyjwt"),
requirement("starlette"),
],
diff --git a/reboot/aio/auth/consent_page.html.j2 b/reboot/aio/auth/consent_page.html.j2
new file mode 100644
index 00000000..c4f7eae8
--- /dev/null
+++ b/reboot/aio/auth/consent_page.html.j2
@@ -0,0 +1,226 @@
+
+
+
+
+
+Authorize application
+
+
+
+
+
+
+
+
{{ application_title }}
+
Do you trust this AI?
+
+ An AI wants to use {{ application_title }} on your behalf
+
+
+ {% if client_uri %}
+
+ {{ client_name or "An application" }}
+
+ {% if client_uri_safe %}
+
{{ client_uri }}
+ {% else %}
+ {{ client_uri }}
+ {% endif %}
+
+
+ {% else %}
+
{{ client_name or "An application" }}
+ {% endif %}
+
hosted at
+
+ {{ redirect_origin }}
+ {{ redirect_uri }}
+
+
+
+ Only continue if you started this sign-in and recognize this
+ address. If anything looks unfamiliar, do not approve —
+ approving lets this AI act as you.
+
+
+
+
+
+
diff --git a/reboot/aio/auth/oauth_server.py b/reboot/aio/auth/oauth_server.py
index 7c88591b..39aa8a3f 100644
--- a/reboot/aio/auth/oauth_server.py
+++ b/reboot/aio/auth/oauth_server.py
@@ -12,8 +12,11 @@
import json
import jwt
import logging
+import os
import rbt.v1alpha1.errors_pb2
+import secrets
import time
+from jinja2 import Template
from mcp.server.auth.provider import AccessToken
from reboot.aio.auth import Auth
from reboot.aio.auth.oauth_providers import (
@@ -27,9 +30,9 @@
from reboot.aio.http import PythonWebFramework, external_context
from reboot.crypto import root_keys
from starlette.requests import Request
-from starlette.responses import JSONResponse, RedirectResponse
+from starlette.responses import HTMLResponse, JSONResponse, RedirectResponse
from typing import Any, Optional
-from urllib.parse import urlencode
+from urllib.parse import urlencode, urlparse
logger = logging.getLogger(__name__)
@@ -53,10 +56,27 @@
# with developer-specified routes. The `/.well-known/` discovery paths
# (mandated by RFC 8414 / RFC 9728) stay at their standard locations.
_AUTHORIZE_PATH = "/__/oauth/authorize"
+_CONSENT_PATH = "/__/oauth/consent"
_TOKEN_PATH = "/__/oauth/token"
_REGISTER_PATH = "/__/oauth/register"
_CALLBACK_PATH = "/__/oauth/callback"
+# Name of the CSRF cookie set when the consent screen is rendered. See
+# below for more details on how this cookie is used.
+_CONSENT_CSRF_COOKIE = "rbt_oauth_consent"
+
+_CONSENT_PAGE_TEMPLATE_PATH = os.path.join(
+ os.path.dirname(__file__),
+ "consent_page.html.j2",
+)
+
+# Cap on the optional RFC 7591 display metadata (`client_name`,
+# `client_uri`) we copy into the signed `client_id`. These are
+# attacker-controlled and purely cosmetic on the consent screen, so a
+# generous-but-finite limit keeps a hostile registration from bloating
+# the JWT (and the `/authorize` URLs that carry it).
+_MAX_CLIENT_METADATA_LENGTH = 256
+
# CORS headers for browser-based MCP clients (e.g. MCPJam, MCP
# Inspector). Allow any origin since the server is an OAuth
# Authorization Server that public clients talk to.
@@ -218,7 +238,15 @@ class OAuthServer:
JWT encoding the registered URIs).
3. **Authorization.** The client redirects the user to `GET
- /authorize` with PKCE parameters. We redirect to the identity
+ /authorize` with PKCE parameters. Because clients register
+ dynamically (step 2), we don't redirect to the identity provider
+ straight away: we first show a **consent screen** naming the
+ client and, prominently, the `redirect_uri` host its tokens will
+ be sent to, so the user can catch an unknown client trying to
+ harvest their identity (an "open client" / confused-deputy
+ attack — see
+ https://github.com/reboot-dev/mono/issues/5560). Only after the
+ user approves (`POST /consent`) do we redirect to the identity
provider (Google, GitHub, or straight back for Anonymous).
4. **identity provider callback.** The identity provider redirects
@@ -249,15 +277,21 @@ def __init__(
*,
provider: OAuthProvider,
protected_resources: list[str],
+ # The application's human-readable title, e.g. "Hipster Chat".
+ application_title: str,
):
self._provider = provider
self._protected_resources = protected_resources
+ self._application_title = application_title
self._access_token_ttl_seconds = provider.access_token_ttl_seconds
# Derived from the Reboot-managed cryptographic root keys; raises
# if `REBOOT_CRYPTO_ROOT_KEYS` is unset/malformed (fail fast at
# construction rather than per-request).
self._signing_secret = signing_secret()
self._token_verifier = OAuthTokenVerifier(self._signing_secret)
+ # The consent page template, compiled lazily on first render so
+ # only apps that actually serve an OAuth flow pay for it.
+ self._consent_page_template: Optional[Template] = None
@property
def token_verifier(self) -> OAuthTokenVerifier:
@@ -299,6 +333,10 @@ def mount_routes(self, http: PythonWebFramework.HTTP) -> None:
# Authorization and token endpoints.
http.get(_AUTHORIZE_PATH)(self.authorize)
+ # The consent screen `/authorize` renders POSTs the user's
+ # decision here (same-origin form submission, so no CORS
+ # preflight is needed).
+ http.post(_CONSENT_PATH)(self.consent)
# The callback persists the provider's tokens (when
# `store_tokens=True`) via the app-internal-only `Ciphertext` /
# `OrderedMap` servicers, so it opts in to an app-internal context
@@ -354,6 +392,71 @@ def _verify_jwt(
except jwt.exceptions.PyJWTError:
return None
+ def _render_consent_page(self, **context: Any) -> str:
+ """Render the consent screen HTML, compiling the template on
+ first use. `autoescape` is on because every interpolated value
+ (client name/URI, redirect URI, the app origin) is
+ attacker-influenced — anyone can register a client.
+ """
+ if self._consent_page_template is None:
+ with open(_CONSENT_PAGE_TEMPLATE_PATH) as template_file:
+ self._consent_page_template = Template(
+ template_file.read(),
+ autoescape=True,
+ )
+ return self._consent_page_template.render(**context)
+
+ def _redirect_to_idp(
+ self,
+ *,
+ request: Request,
+ client_id_token: str,
+ redirect_uri: str,
+ code_challenge: str,
+ code_challenge_method: str,
+ mcp_state: str,
+ ) -> RedirectResponse:
+ """Mint the `pending` state JWT and redirect the user to the
+ identity provider to sign in. Reached only after the user
+ approves on the consent screen; the `pending` token carries
+ everything needed to resume in `/callback` once the provider
+ redirects back.
+
+ OAuth's `state` parameter is an opaque string that the identity
+ provider passes back unchanged in the callback. We use it to
+ carry a signed JWT with everything we need to resume:
+ `client_id`, `redirect_uri`, PKCE challenge, and the MCP
+ client's own state. This avoids server-side session storage, and
+ is safe because...
+ 1. The communication with the identity provider is over TLS
+ (required by the OAuth spec), so it won't be observed in
+ transit.
+ 2. None of the fields are secret to either the client or the
+ identity provider.
+ 3. Since the token is signed, the identity provider can't alter
+ it to e.g. misdirect the redirect.
+ """
+ pending = self._make_jwt(
+ {
+ "type": "pending",
+ "client_id": client_id_token,
+ "redirect_uri": redirect_uri,
+ "code_challenge": code_challenge,
+ "code_challenge_method": code_challenge_method,
+ "mcp_state": mcp_state,
+ },
+ ttl_seconds=_PENDING_STATE_TTL_SECONDS,
+ )
+
+ # Our own callback URL.
+ callback_uri = f"{origin_from_request(request)}{_CALLBACK_PATH}"
+
+ idp_url = self._provider.authorization_url(
+ state=pending,
+ redirect_uri=callback_uri,
+ )
+ return RedirectResponse(url=idp_url, status_code=302)
+
async def _store_oauth_tokens(
self,
request: Request,
@@ -506,11 +609,30 @@ async def register(self, request: Request) -> JSONResponse:
# in `/authorize` without needing a database. Client
# registrations are normally permanent, but JWTs require an
# `exp`, so we use an effectively-forever TTL.
+ client_metadata: dict[str, Any] = {
+ "type": "client",
+ "redirect_uris": redirect_uris,
+ }
+ # RFC 7591 client metadata we surface on the consent screen so a
+ # user can recognize who's asking. Optional and
+ # attacker-controlled (anyone can register), so they're shown
+ # only as hints next to the authoritative `redirect_uri` host,
+ # never trusted. Carried in the signed `client_id` so they're
+ # available statelessly at `/authorize`.
+ # Over-limit values are dropped (not truncated), leaving the
+ # consent screen to fall back to the authoritative
+ # `redirect_uri` host.
+ client_name = body.get("client_name")
+ if isinstance(client_name, str
+ ) and (len(client_name) <= _MAX_CLIENT_METADATA_LENGTH):
+ client_metadata["client_name"] = client_name
+ client_uri = body.get("client_uri")
+ if isinstance(client_uri, str
+ ) and (len(client_uri) <= _MAX_CLIENT_METADATA_LENGTH):
+ client_metadata["client_uri"] = client_uri
+
client_id = self._make_jwt(
- {
- "type": "client",
- "redirect_uris": redirect_uris,
- },
+ client_metadata,
ttl_seconds=1000 * 365 * 24 * 3600, # ~1000 years.
)
@@ -527,7 +649,9 @@ async def register(self, request: Request) -> JSONResponse:
async def authorize(self, request: Request):
"""GET /authorize
- Validates the request, then redirects to the identity provider.
+ Validates the request, then renders a consent screen naming the
+ client and its `redirect_uri` host. The flow only continues to
+ the identity provider once the user approves via `POST /consent`.
"""
params = request.query_params
@@ -584,40 +708,170 @@ async def authorize(self, request: Request):
mcp_state = params.get("state", "")
- # OAuth's `state` parameter is an opaque string that the
- # identity provider passes back unchanged in the callback. We
- # use it to carry a signed JWT with everything we need to resume
- # after the identity provider redirects back: `client_id`,
- # `redirect_uri`, PKCE challenge, and the MCP client's own
- # state. This avoids server-side session storage, and is safe
- # because...
- # 1. The communication with the identity provider is over TLS
- # (required by the OAuth spec), so it won't be observed in
- # transit.
- # 2. None of the fields are secret to either the client or the
- # identity provider.
- # 3. Since the token is signed, the identity provider can't
- # alter it to e.g. misdirect the redirect.
- pending = self._make_jwt(
+ # Don't redirect to the identity provider yet: show a consent
+ # screen first. Clients register dynamically (RFC 7591), so the
+ # `client_id` and `redirect_uri` are whatever some caller asked
+ # for — absent this checkpoint an attacker can register a client
+ # with their own `redirect_uri`, send a victim an `/authorize`
+ # link on this trusted origin, and harvest an access token for
+ # the victim's identity once they sign in (an "open client" /
+ # confused-deputy attack;
+ # https://github.com/reboot-dev/mono/issues/5560). PKCE doesn't
+ # help — the attacker is the registered client, so they hold the
+ # verifier. The consent screen gives the user a chance to notice
+ # the unfamiliar `redirect_uri` host before signing in.
+ #
+ # See the discussion on `_CONSENT_CSRF_COOKIE` below for info on
+ # how the `nonce` helps prevent CSRF attacks.
+ nonce = secrets.token_urlsafe(32)
+ consent_token = self._make_jwt(
{
- "type": "pending",
+ "type": "consent",
"client_id": client_id_token,
"redirect_uri": redirect_uri,
"code_challenge": code_challenge,
"code_challenge_method": code_challenge_method,
"mcp_state": mcp_state,
+ "nonce": nonce,
},
ttl_seconds=_PENDING_STATE_TTL_SECONDS,
)
- # Our own callback URL.
- callback_uri = f"{origin_from_request(request)}{_CALLBACK_PATH}"
+ origin = origin_from_request(request)
+ parsed_redirect = urlparse(redirect_uri)
+ # The prominent host on the consent screen is what the user
+ # checks, so strip any `userinfo@` from the netloc: a
+ # `redirect_uri` like `https://trusted.example@evil.com/cb`
+ # would otherwise show the trusted-looking left side while the
+ # tokens actually go to `evil.com`.
+ redirect_host = parsed_redirect.netloc.rsplit("@", 1)[-1]
+ client_uri = client_data.get("client_uri")
+ # Only render `client_uri` as a clickable link when it's an
+ # http(s) URL; a `javascript:`/`data:` scheme would be an XSS
+ # vector even with autoescaping, which doesn't neutralize a
+ # dangerous URL scheme.
+ client_uri_safe = (
+ isinstance(client_uri, str) and
+ urlparse(client_uri).scheme in ("http", "https")
+ )
+ html = self._render_consent_page(
+ application_title=self._application_title,
+ client_name=client_data.get("client_name") or None,
+ client_uri=client_uri if isinstance(client_uri, str) else None,
+ client_uri_safe=client_uri_safe,
+ redirect_uri=redirect_uri,
+ redirect_origin=f"{parsed_redirect.scheme}://{redirect_host}",
+ consent_token=consent_token,
+ consent_path=_CONSENT_PATH,
+ )
+ response = HTMLResponse(html)
+ response.set_cookie(
+ _CONSENT_CSRF_COOKIE,
+ nonce,
+ max_age=_PENDING_STATE_TTL_SECONDS,
+ path=_CONSENT_PATH,
+ httponly=True,
+ # `Strict`: the only request that ever consumes this cookie
+ # is the same-site `POST /consent` triggered by a user
+ # click; our CSRF protection relies on this cookie never
+ # getting delivered cross-site.
+ samesite="strict",
+ # Only mark `Secure` over https; in local `http://` dev a
+ # `Secure` cookie would be silently dropped by the browser,
+ # breaking the double-submit check.
+ secure=origin.startswith("https://"),
+ )
+ return response
- idp_url = self._provider.authorization_url(
- state=pending,
- redirect_uri=callback_uri,
+ async def consent(self, request: Request):
+ """POST /consent
+
+ Receives the user's decision from the consent screen rendered by
+ `/authorize`. On approval, resumes the flow by redirecting to the
+ identity provider; on denial (or any non-approval), redirects
+ back to the client with an `access_denied` error per RFC 6749
+ 4.1.2.1.
+ """
+ form = await request.form()
+
+ consent_token = form.get("consent")
+ if consent_token is None:
+ return _oauth_error(
+ error="invalid_request",
+ description="The 'consent' parameter is required.",
+ status_code=400,
+ )
+
+ # `_verify_jwt` checks the HS256 signature (and `type`/`exp`)
+ # with our signing secret, so from here the token's `nonce` is
+ # known to be one we issued, not an attacker's.
+ consent_data = self._verify_jwt(str(consent_token), "consent")
+ if consent_data is None:
+ return _oauth_error(
+ error="invalid_request",
+ description="Invalid or expired consent request.",
+ status_code=400,
+ )
+
+ # CSRF defense — the "double-submit cookie" pattern. When the
+ # consent page was rendered (in `authorize`) we generated one
+ # random `nonce` and put it in two places: the
+ # `rbt_oauth_consent` cookie (set on that GET response) and,
+ # signed, inside the consent token in the form. `/consent`
+ # requires the two to match — the same secret arrives once via
+ # the cookie and once via the form body.
+ #
+ # This protects against a cross-site attacker (a different
+ # site): they can mint a valid consent token for their own
+ # client (so they control the form half), but they can't read
+ # our cookie or make the browser send it. `SameSite=Strict`
+ # keeps the cookie off every cross-site request, so a forged
+ # POST from their page carries no cookie of ours to pair with
+ # their token — reject.
+ #
+ # Putting our nonce into our _signed_ token additionally covers
+ # a *same-site* attacker — e.g. a compromised sibling subdomain,
+ # which can write a parent-domain cookie. In that case they
+ # could set both the cookie and the form field, but they still
+ # wouldn't be able to forge our signature. We already validated
+ # the token signature above, so here we just need to check that
+ # the cookie matches.
+ cookie_nonce = request.cookies.get(_CONSENT_CSRF_COOKIE)
+ if cookie_nonce is None or not secrets.compare_digest(
+ cookie_nonce, str(consent_data.get("nonce", ""))
+ ):
+ return _oauth_error(
+ error="access_denied",
+ description="Consent could not be verified; please restart "
+ "the sign-in.",
+ status_code=403,
+ )
+
+ redirect_uri = consent_data["redirect_uri"]
+ mcp_state = consent_data.get("mcp_state", "")
+
+ if form.get("action") != "approve":
+ # Anything that isn't an explicit approval — the "Cancel"
+ # button, a malformed submission — is reported to the client
+ # as a denial.
+ query = urlencode({"error": "access_denied", "state": mcp_state})
+ denied = RedirectResponse(
+ url=f"{redirect_uri}?{query}",
+ status_code=302,
+ )
+ denied.delete_cookie(_CONSENT_CSRF_COOKIE, path=_CONSENT_PATH)
+ return denied
+
+ response = self._redirect_to_idp(
+ request=request,
+ client_id_token=consent_data["client_id"],
+ redirect_uri=redirect_uri,
+ code_challenge=consent_data["code_challenge"],
+ code_challenge_method=consent_data["code_challenge_method"],
+ mcp_state=mcp_state,
)
- return RedirectResponse(url=idp_url, status_code=302)
+ response.delete_cookie(_CONSENT_CSRF_COOKIE, path=_CONSENT_PATH)
+ return response
async def callback(self, request: Request):
"""GET /oauth/callback
diff --git a/reboot/aio/state_managers.py b/reboot/aio/state_managers.py
index 9d28074d..793de8cd 100644
--- a/reboot/aio/state_managers.py
+++ b/reboot/aio/state_managers.py
@@ -37,6 +37,7 @@
StateNotConstructed,
TransactionParticipantFailedToCommit,
TransactionParticipantFailedToPrepare,
+ TransactionShouldRetryWithoutBackoff,
Unavailable,
)
from reboot.admin.export_import_converters import ExportImportItemConverters
@@ -105,6 +106,21 @@
logger.setLevel(logging.WARNING)
+def _should_retry_without_backoff(exception: BaseException) -> bool:
+ """Whether `exception` is an `Aborted` carrying a
+ `TransactionShouldRetryWithoutBackoff` (raised by a participant
+ that recovered after the transaction began). The error may be
+ wrapped in any method's generated `Aborted` subtype, so we inspect
+ `.error` rather than the exception type."""
+ if not isinstance(exception, Aborted):
+ return False
+ try:
+ error = exception.error
+ except NotImplementedError:
+ return False
+ return isinstance(error, TransactionShouldRetryWithoutBackoff)
+
+
def check_idempotency_key_not_expired(idempotency_key: uuid.UUID) -> None:
"""Check if the idempotency key is an expired UUIDv7.
@@ -3666,9 +3682,12 @@ async def transactionally(
):
# Server recovered after this transaction started,
# which means it may have already participated and
- # lost in-memory state when it restarted. Return
- # UNAVAILABLE so the caller retries with a fresh
- # transaction.
+ # lost in-memory state when it restarted. Abort with
+ # `TransactionShouldRetryWithoutBackoff` (retried
+ # like `Unavailable`) so the caller retries with a
+ # fresh transaction; the distinct type lets the
+ # coordinator refresh its timestamp and skip the
+ # first retry's backoff.
transaction_time = datetime.fromtimestamp(
transaction_timestamp_ms / 1000,
tz=timezone.utc,
@@ -3678,7 +3697,7 @@ async def transactionally(
tz=timezone.utc,
).isoformat()
raise SystemAborted(
- Unavailable(),
+ TransactionShouldRetryWithoutBackoff(),
message=(
f"Transaction {root_transaction_id} was "
f"created at {transaction_time} but this "
@@ -4666,12 +4685,30 @@ async def complete(effects: Effects) -> None:
coordinator_state_ref=state_ref,
participants=context.participants,
)
- except:
+ except BaseException as exception:
if context.nested:
# A nested transaction just re-raises and lets the
# abort propagate back to the caller.
raise
else:
+ # We are the coordinator, so we are the one who stamps
+ # the transaction with a timestamp. If a participant
+ # aborted with `TransactionShouldRetryWithoutBackoff`,
+ # advance our clock so the upcoming retry is stamped
+ # with a newer timestamp - that addresses a reason why
+ # `TransactionShouldRetryWithoutBackoff` might have been
+ # raised (the participant might have restarted after the
+ # transaction started).
+ if _should_retry_without_backoff(exception):
+ try:
+ self._update_latest_timestamp(
+ await self._database_client.refresh_timestamp()
+ )
+ except Exception:
+ # Best-effort: on failure we fall back to the
+ # periodic refresh loop.
+ pass
+
# Drive Abort RPCs so participants release their
# locks and forget the transaction. Covers
# failures from `_load` / the body / validation /
diff --git a/reboot/aio/stubs.py b/reboot/aio/stubs.py
index 39450aae..a8c4b20d 100644
--- a/reboot/aio/stubs.py
+++ b/reboot/aio/stubs.py
@@ -112,8 +112,26 @@ def _should_retry(self, error: grpc.aio.AioRpcError) -> bool:
# For now, the only retriable error is UNAVAILABLE.
return error.code() == grpc.StatusCode.UNAVAILABLE
+ async def _should_retry_without_backoff(self) -> bool:
+ """Whether the failed call aborted with
+ `TransactionShouldRetryWithoutBackoff`."""
+ if self._call is None:
+ return False
+ status = await rpc_status.from_call(self._call)
+ if status is None:
+ return False
+ return Aborted.error_from_google_rpc_status_details(
+ status,
+ [errors_pb2.TransactionShouldRetryWithoutBackoff],
+ ) is not None
+
async def _call_with_retries(self) -> ResponseT:
backoff = Backoff()
+ # A `TransactionShouldRetryWithoutBackoff` asks us to retry
+ # immediately, but we elide the backoff only once: a transaction
+ # that keeps restarting should still back off rather than hammer
+ # the cluster with immediate retries.
+ backoff_elided = False
while True:
if self._call is None:
new_call = self._stub_method(
@@ -125,12 +143,17 @@ async def _call_with_retries(self) -> ResponseT:
return await self._call
except grpc.aio.AioRpcError as error:
if self._should_retry(error):
- # Retry this, with some backoff.
logger.debug(
f"Unary call to '{self._method_name}' encountered "
f"retryable error: {error}; will retry..."
)
- await backoff()
+ apply_backoff = backoff_elided or not (
+ await self._should_retry_without_backoff()
+ )
+ if apply_backoff:
+ await backoff()
+ else:
+ backoff_elided = True
# We need to create a fresh call object for the
# retry.
self._call = None
diff --git a/reboot/api.py b/reboot/api.py
index b113abeb..89660b14 100644
--- a/reboot/api.py
+++ b/reboot/api.py
@@ -1223,19 +1223,23 @@ def __init__(self, **types: Optional[Type]):
f"{type_name} types. If you want "
"custom initialization logic, "
"override the default "
- f"'{AUTO_CONSTRUCT_METHOD}' Writer "
- "in your servicer implementation "
- "instead of declaring it in the API "
- "definition. You may also declare "
- "alternative factory methods for "
- "your own use, but note the system "
- "will always use "
+ f"'{AUTO_CONSTRUCT_METHOD}' "
+ "Transaction in your servicer "
+ "implementation instead of declaring "
+ "it in the API definition. You may "
+ "also declare alternative factory "
+ "methods for your own use, but note "
+ "the system will always use "
f"'{AUTO_CONSTRUCT_METHOD}' when "
"automatically constructing a "
f"'{type_name}' for a new AI "
"session."
)
- data_type.methods[AUTO_CONSTRUCT_METHOD] = Writer(
+ # The auto-constructed `create` is a `Transaction`, so
+ # that a servicer overriding it can call other state
+ # machines while constructing a `User`, e.g. "when a
+ # `User` is created, also create a `Cart`".
+ data_type.methods[AUTO_CONSTRUCT_METHOD] = Transaction(
request=None,
response=None,
factory=True,
@@ -1253,8 +1257,13 @@ def __init__(self, **types: Optional[Type]):
def get_types(self) -> Dict[str, Type]:
"""Get all Reboot data types defined in this API."""
types = {}
- for field_name in self.model_fields_set:
- field_value = getattr(self, field_name, None)
+ # Iterate `__pydantic_extra__`, which preserves the order in
+ # which the types were passed to `API(...)`, rather than
+ # `model_fields_set`, which is a `set` whose iteration order
+ # is randomized per process. The types' order flows through
+ # to the synthesized `.proto` and thus the generated code, so
+ # a stable order keeps generated output deterministic.
+ for field_name, field_value in (self.__pydantic_extra__ or {}).items():
if isinstance(field_value, Type):
types[field_name] = field_value
diff --git a/reboot/benchmarks/construct/package-lock.json b/reboot/benchmarks/construct/package-lock.json
index e6db9829..adaf4320 100644
--- a/reboot/benchmarks/construct/package-lock.json
+++ b/reboot/benchmarks/construct/package-lock.json
@@ -10,7 +10,7 @@
"license": "ISC",
"dependencies": {
"@bufbuild/protobuf": "1.10.1",
- "@reboot-dev/reboot": "1.2.0",
+ "@reboot-dev/reboot": "1.2.1",
"@supercharge/promise-pool": "^3.2.0",
"parse-duration": "2.1.3",
"uuid": "11.1.0"
@@ -532,15 +532,15 @@
}
},
"node_modules/@reboot-dev/reboot": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.2.0.tgz",
- "integrity": "sha512-VPjg8N0ab2iZzQ8PeVnATye5iPjEfmT7digqxxCKBYndOB/W8CjKg4YUVREzg+hNTrILSDmgJex2lpPd5yo4TQ==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.2.1.tgz",
+ "integrity": "sha512-1x35ldNqzFLUbRtjXrAfd27YDVX43gGYA+4O9BOx14yBu4y3OC4JZHT4Am7pVRuHJs4qNWSj0dCBFnEtqL3Rvg==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@bufbuild/protoc-gen-es": "1.10.1",
"@bufbuild/protoplugin": "1.10.1",
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
"@scarf/scarf": "1.4.0",
"@standard-schema/spec": "1.0.0",
"chalk": "^4.1.2",
@@ -569,9 +569,9 @@
}
},
"node_modules/@reboot-dev/reboot-api": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.0.tgz",
- "integrity": "sha512-6k0OhzPX3fcUurmxQY9ITJCX3LrMnimUYIdkPTEWWIpzV1jirpijx5ovTFG08MACic8mu1UMvCLEv7wq3/w1pg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.1.tgz",
+ "integrity": "sha512-CmrL7P/dtm+DBMWLWijs+NIlt0PKmdelwmP3Efmsz9x1dkICn6iq/MOh7FLVe2gcHubKL6RuiwO3vOOBVzObPg==",
"license": "Apache-2.0",
"dependencies": {
"@scarf/scarf": "1.4.0",
@@ -3148,13 +3148,13 @@
"optional": true
},
"@reboot-dev/reboot": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.2.0.tgz",
- "integrity": "sha512-VPjg8N0ab2iZzQ8PeVnATye5iPjEfmT7digqxxCKBYndOB/W8CjKg4YUVREzg+hNTrILSDmgJex2lpPd5yo4TQ==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.2.1.tgz",
+ "integrity": "sha512-1x35ldNqzFLUbRtjXrAfd27YDVX43gGYA+4O9BOx14yBu4y3OC4JZHT4Am7pVRuHJs4qNWSj0dCBFnEtqL3Rvg==",
"requires": {
"@bufbuild/protoc-gen-es": "1.10.1",
"@bufbuild/protoplugin": "1.10.1",
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
"@scarf/scarf": "1.4.0",
"@standard-schema/spec": "1.0.0",
"chalk": "^4.1.2",
@@ -3178,9 +3178,9 @@
}
},
"@reboot-dev/reboot-api": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.0.tgz",
- "integrity": "sha512-6k0OhzPX3fcUurmxQY9ITJCX3LrMnimUYIdkPTEWWIpzV1jirpijx5ovTFG08MACic8mu1UMvCLEv7wq3/w1pg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.1.tgz",
+ "integrity": "sha512-CmrL7P/dtm+DBMWLWijs+NIlt0PKmdelwmP3Efmsz9x1dkICn6iq/MOh7FLVe2gcHubKL6RuiwO3vOOBVzObPg==",
"requires": {
"@scarf/scarf": "1.4.0",
"typescript": "5.4.5",
diff --git a/reboot/benchmarks/construct/package.json b/reboot/benchmarks/construct/package.json
index 3b8cb220..f1784a3f 100644
--- a/reboot/benchmarks/construct/package.json
+++ b/reboot/benchmarks/construct/package.json
@@ -11,7 +11,7 @@
"type": "module",
"dependencies": {
"@bufbuild/protobuf": "1.10.1",
- "@reboot-dev/reboot": "1.2.0",
+ "@reboot-dev/reboot": "1.2.1",
"@supercharge/promise-pool": "^3.2.0",
"uuid": "11.1.0",
"parse-duration": "2.1.3"
diff --git a/reboot/cli/init/templates/backend_package.json.j2 b/reboot/cli/init/templates/backend_package.json.j2
index 962f670c..f16d14d8 100644
--- a/reboot/cli/init/templates/backend_package.json.j2
+++ b/reboot/cli/init/templates/backend_package.json.j2
@@ -4,7 +4,7 @@
"private": true,
"type": "module",
"dependencies": {
- "@reboot-dev/reboot": "1.2.0",
+ "@reboot-dev/reboot": "1.2.1",
"typescript": "^5.5.2"
}
}
diff --git a/reboot/cli/init/templates/package.json.j2 b/reboot/cli/init/templates/package.json.j2
index 946e2697..61a3ce92 100644
--- a/reboot/cli/init/templates/package.json.j2
+++ b/reboot/cli/init/templates/package.json.j2
@@ -3,7 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
- "@reboot-dev/reboot-react": "1.2.0",
+ "@reboot-dev/reboot-react": "1.2.1",
"@types/jest": "^27.5.2",
"@types/node": "^20.11.5",
"@types/react": "^19.2.1",
diff --git a/reboot/create-ui/package.json b/reboot/create-ui/package.json
index 0f510557..ae26ba44 100644
--- a/reboot/create-ui/package.json
+++ b/reboot/create-ui/package.json
@@ -1,6 +1,6 @@
{
"name": "@reboot-dev/create-ui",
- "version": "1.2.0",
+ "version": "1.2.1",
"description": "Scaffold React implementation for Reboot AI Chat App UIs",
"type": "commonjs",
"bin": {
diff --git a/reboot/create-ui/src/templates.ts b/reboot/create-ui/src/templates.ts
index 641ecf97..3d40beb8 100644
--- a/reboot/create-ui/src/templates.ts
+++ b/reboot/create-ui/src/templates.ts
@@ -70,8 +70,8 @@ export function packageJson(
dependencies: {
"@modelcontextprotocol/ext-apps": "1.5.0",
"@modelcontextprotocol/sdk": "1.29.0",
- "@reboot-dev/reboot-react": "1.2.0",
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-react": "1.2.1",
+ "@reboot-dev/reboot-api": "1.2.1",
react: "^18.2.0",
"react-dom": "^18.2.0",
zod: "^4.0.0",
diff --git a/reboot/demos/fig/package-lock.json b/reboot/demos/fig/package-lock.json
index bbf9c8c2..a1ffcf5d 100644
--- a/reboot/demos/fig/package-lock.json
+++ b/reboot/demos/fig/package-lock.json
@@ -10,11 +10,11 @@
"dependencies": {
"@bufbuild/protobuf": "1.10.1",
"@radix-ui/react-icons": "^1.3.0",
- "@reboot-dev/reboot": "1.2.0",
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-std": "1.2.0",
- "@reboot-dev/reboot-std-api": "1.2.0",
- "@reboot-dev/reboot-std-react": "1.2.0",
+ "@reboot-dev/reboot": "1.2.1",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-std": "1.2.1",
+ "@reboot-dev/reboot-std-api": "1.2.1",
+ "@reboot-dev/reboot-std-react": "1.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-draggable": "^4.4.6",
@@ -1357,15 +1357,15 @@
}
},
"node_modules/@reboot-dev/reboot": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.2.0.tgz",
- "integrity": "sha512-VPjg8N0ab2iZzQ8PeVnATye5iPjEfmT7digqxxCKBYndOB/W8CjKg4YUVREzg+hNTrILSDmgJex2lpPd5yo4TQ==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.2.1.tgz",
+ "integrity": "sha512-1x35ldNqzFLUbRtjXrAfd27YDVX43gGYA+4O9BOx14yBu4y3OC4JZHT4Am7pVRuHJs4qNWSj0dCBFnEtqL3Rvg==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@bufbuild/protoc-gen-es": "1.10.1",
"@bufbuild/protoplugin": "1.10.1",
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
"@scarf/scarf": "1.4.0",
"@standard-schema/spec": "1.0.0",
"chalk": "^4.1.2",
@@ -1394,9 +1394,9 @@
}
},
"node_modules/@reboot-dev/reboot-api": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.0.tgz",
- "integrity": "sha512-6k0OhzPX3fcUurmxQY9ITJCX3LrMnimUYIdkPTEWWIpzV1jirpijx5ovTFG08MACic8mu1UMvCLEv7wq3/w1pg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.1.tgz",
+ "integrity": "sha512-CmrL7P/dtm+DBMWLWijs+NIlt0PKmdelwmP3Efmsz9x1dkICn6iq/MOh7FLVe2gcHubKL6RuiwO3vOOBVzObPg==",
"license": "Apache-2.0",
"dependencies": {
"@scarf/scarf": "1.4.0",
@@ -1420,15 +1420,15 @@
}
},
"node_modules/@reboot-dev/reboot-react": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.0.tgz",
- "integrity": "sha512-gNkSLFRgWDl40SbW7VNO+xD16DaFdPBZ95E2bULEHpAcyd8lsg5qiHHCPYThwCkDou/HqWbb/2WH5iiQaBFmRg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.1.tgz",
+ "integrity": "sha512-ImVCLLaOTZoDDtAnXdXufScZWOz2DVomVuhs6WwEgjW99yTtwTx1OMsC/wbU37pSmadmdfdlTYP9vy/V9EcFMg==",
"license": "Apache-2.0",
"dependencies": {
"@modelcontextprotocol/ext-apps": "1.5.0",
"@modelcontextprotocol/sdk": "1.29.0",
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-web": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-web": "1.2.1",
"@scarf/scarf": "1.4.0",
"@types/uuid": "^9.0.4",
"js-sha1": "0.7.0",
@@ -1469,45 +1469,45 @@
}
},
"node_modules/@reboot-dev/reboot-std": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std/-/reboot-std-1.2.0.tgz",
- "integrity": "sha512-Jblt3IM3hFHZdf78zv27/FL4IZL3l/ULnTFKwB4LloIlHVnhNegisHISvHn2fAkM8VzDy64s174tA/jHPHUyrw==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std/-/reboot-std-1.2.1.tgz",
+ "integrity": "sha512-+4oa3p/om+WsEc5/CVkYP5tTlBpZmIDMaEsMxgDScsa5txDwAxAPEIrYpSFppUZ/WZo2OzbjqDnUyp491Iym/g==",
"license": "Apache-2.0",
"dependencies": {
- "@reboot-dev/reboot": "1.2.0",
- "@reboot-dev/reboot-std-api": "1.2.0",
+ "@reboot-dev/reboot": "1.2.1",
+ "@reboot-dev/reboot-std-api": "1.2.1",
"@scarf/scarf": "1.4.0"
}
},
"node_modules/@reboot-dev/reboot-std-api": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std-api/-/reboot-std-api-1.2.0.tgz",
- "integrity": "sha512-8EI8+Ys4gWlVNiq/5f0ZU2W76CkddqLygdDrZTqgLNsI/Y4wa/ZOFMiEGvayULTzQu0Og5pZ0jXbXrak9iYTqw==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std-api/-/reboot-std-api-1.2.1.tgz",
+ "integrity": "sha512-evFzB6oQDURxkddknXakyRKcYT53EY1AlyQnNs3nffF84gWMjIbZ6nb3C5yDNnra+pDjsiiz7DsBhMvr9qHX2g==",
"license": "Apache-2.0",
"dependencies": {
"@scarf/scarf": "1.4.0"
}
},
"node_modules/@reboot-dev/reboot-std-react": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std-react/-/reboot-std-react-1.2.0.tgz",
- "integrity": "sha512-h77eSxkbER7BnzBEuZjfIdOwVq+a21EvKqUpwD7kEouJGiGIWxWmTvtlnKkrUr+G2YZnXyb/eVLzKpoq6aGnJQ==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std-react/-/reboot-std-react-1.2.1.tgz",
+ "integrity": "sha512-CCjS7MYLmwXGJ4u1gcsRZ1EwarJtnFTezukujuQs8xS80m6sc9UJ0SgCkgvNkLfv0rWwxnRP2sP4FFQ5R4hWVw==",
"license": "Apache-2.0",
"dependencies": {
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-react": "1.2.0",
- "@reboot-dev/reboot-std-api": "1.2.0",
- "@reboot-dev/reboot-web": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-react": "1.2.1",
+ "@reboot-dev/reboot-std-api": "1.2.1",
+ "@reboot-dev/reboot-web": "1.2.1",
"@scarf/scarf": "1.4.0"
}
},
"node_modules/@reboot-dev/reboot-web": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.0.tgz",
- "integrity": "sha512-ziBVJ5WLlnR57nkCUAJc9OAdqnl4PjoIYPdzMMkEYh3wpf10LPT/souGQT9c5BHUSWOx81azdQqU5lCqYXKWsg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.1.tgz",
+ "integrity": "sha512-/jk3OdG6pF+McvIP6yEuJKSWmDpHSR4+nVdbvUIHDdJh8cQkFj2QnzQJ+pRbql207LvwUNJ3T9YwZDSYz8rB9A==",
"license": "Apache-2.0",
"dependencies": {
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
"@scarf/scarf": "1.4.0",
"js-sha1": "0.7.0",
"lru-cache-idb": "^0.5.2",
@@ -4184,9 +4184,9 @@
}
},
"node_modules/hono": {
- "version": "4.12.26",
- "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.26.tgz",
- "integrity": "sha512-uyZtpnYxM9CmQ7QsQknM4zN8EftNqhON1qYeIKM0Se67CCEe2c44xyGURwB0axX2fBDu1dqHrHAc1hmNT8ITkw==",
+ "version": "4.12.27",
+ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.27.tgz",
+ "integrity": "sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==",
"license": "MIT",
"engines": {
"node": ">=16.9.0"
@@ -8098,13 +8098,13 @@
"requires": {}
},
"@reboot-dev/reboot": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.2.0.tgz",
- "integrity": "sha512-VPjg8N0ab2iZzQ8PeVnATye5iPjEfmT7digqxxCKBYndOB/W8CjKg4YUVREzg+hNTrILSDmgJex2lpPd5yo4TQ==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.2.1.tgz",
+ "integrity": "sha512-1x35ldNqzFLUbRtjXrAfd27YDVX43gGYA+4O9BOx14yBu4y3OC4JZHT4Am7pVRuHJs4qNWSj0dCBFnEtqL3Rvg==",
"requires": {
"@bufbuild/protoc-gen-es": "1.10.1",
"@bufbuild/protoplugin": "1.10.1",
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
"@scarf/scarf": "1.4.0",
"@standard-schema/spec": "1.0.0",
"chalk": "^4.1.2",
@@ -8311,9 +8311,9 @@
}
},
"@reboot-dev/reboot-api": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.0.tgz",
- "integrity": "sha512-6k0OhzPX3fcUurmxQY9ITJCX3LrMnimUYIdkPTEWWIpzV1jirpijx5ovTFG08MACic8mu1UMvCLEv7wq3/w1pg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.1.tgz",
+ "integrity": "sha512-CmrL7P/dtm+DBMWLWijs+NIlt0PKmdelwmP3Efmsz9x1dkICn6iq/MOh7FLVe2gcHubKL6RuiwO3vOOBVzObPg==",
"requires": {
"@scarf/scarf": "1.4.0",
"typescript": "5.4.5",
@@ -8328,14 +8328,14 @@
}
},
"@reboot-dev/reboot-react": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.0.tgz",
- "integrity": "sha512-gNkSLFRgWDl40SbW7VNO+xD16DaFdPBZ95E2bULEHpAcyd8lsg5qiHHCPYThwCkDou/HqWbb/2WH5iiQaBFmRg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.1.tgz",
+ "integrity": "sha512-ImVCLLaOTZoDDtAnXdXufScZWOz2DVomVuhs6WwEgjW99yTtwTx1OMsC/wbU37pSmadmdfdlTYP9vy/V9EcFMg==",
"requires": {
"@modelcontextprotocol/ext-apps": "1.5.0",
"@modelcontextprotocol/sdk": "1.29.0",
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-web": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-web": "1.2.1",
"@scarf/scarf": "1.4.0",
"@types/uuid": "^9.0.4",
"js-sha1": "0.7.0",
@@ -8357,41 +8357,41 @@
}
},
"@reboot-dev/reboot-std": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std/-/reboot-std-1.2.0.tgz",
- "integrity": "sha512-Jblt3IM3hFHZdf78zv27/FL4IZL3l/ULnTFKwB4LloIlHVnhNegisHISvHn2fAkM8VzDy64s174tA/jHPHUyrw==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std/-/reboot-std-1.2.1.tgz",
+ "integrity": "sha512-+4oa3p/om+WsEc5/CVkYP5tTlBpZmIDMaEsMxgDScsa5txDwAxAPEIrYpSFppUZ/WZo2OzbjqDnUyp491Iym/g==",
"requires": {
- "@reboot-dev/reboot": "1.2.0",
- "@reboot-dev/reboot-std-api": "1.2.0",
+ "@reboot-dev/reboot": "1.2.1",
+ "@reboot-dev/reboot-std-api": "1.2.1",
"@scarf/scarf": "1.4.0"
}
},
"@reboot-dev/reboot-std-api": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std-api/-/reboot-std-api-1.2.0.tgz",
- "integrity": "sha512-8EI8+Ys4gWlVNiq/5f0ZU2W76CkddqLygdDrZTqgLNsI/Y4wa/ZOFMiEGvayULTzQu0Og5pZ0jXbXrak9iYTqw==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std-api/-/reboot-std-api-1.2.1.tgz",
+ "integrity": "sha512-evFzB6oQDURxkddknXakyRKcYT53EY1AlyQnNs3nffF84gWMjIbZ6nb3C5yDNnra+pDjsiiz7DsBhMvr9qHX2g==",
"requires": {
"@scarf/scarf": "1.4.0"
}
},
"@reboot-dev/reboot-std-react": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std-react/-/reboot-std-react-1.2.0.tgz",
- "integrity": "sha512-h77eSxkbER7BnzBEuZjfIdOwVq+a21EvKqUpwD7kEouJGiGIWxWmTvtlnKkrUr+G2YZnXyb/eVLzKpoq6aGnJQ==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std-react/-/reboot-std-react-1.2.1.tgz",
+ "integrity": "sha512-CCjS7MYLmwXGJ4u1gcsRZ1EwarJtnFTezukujuQs8xS80m6sc9UJ0SgCkgvNkLfv0rWwxnRP2sP4FFQ5R4hWVw==",
"requires": {
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-react": "1.2.0",
- "@reboot-dev/reboot-std-api": "1.2.0",
- "@reboot-dev/reboot-web": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-react": "1.2.1",
+ "@reboot-dev/reboot-std-api": "1.2.1",
+ "@reboot-dev/reboot-web": "1.2.1",
"@scarf/scarf": "1.4.0"
}
},
"@reboot-dev/reboot-web": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.0.tgz",
- "integrity": "sha512-ziBVJ5WLlnR57nkCUAJc9OAdqnl4PjoIYPdzMMkEYh3wpf10LPT/souGQT9c5BHUSWOx81azdQqU5lCqYXKWsg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.1.tgz",
+ "integrity": "sha512-/jk3OdG6pF+McvIP6yEuJKSWmDpHSR4+nVdbvUIHDdJh8cQkFj2QnzQJ+pRbql207LvwUNJ3T9YwZDSYz8rB9A==",
"requires": {
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
"@scarf/scarf": "1.4.0",
"js-sha1": "0.7.0",
"lru-cache-idb": "^0.5.2",
@@ -9919,9 +9919,9 @@
}
},
"hono": {
- "version": "4.12.26",
- "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.26.tgz",
- "integrity": "sha512-uyZtpnYxM9CmQ7QsQknM4zN8EftNqhON1qYeIKM0Se67CCEe2c44xyGURwB0axX2fBDu1dqHrHAc1hmNT8ITkw=="
+ "version": "4.12.27",
+ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.27.tgz",
+ "integrity": "sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q=="
},
"http-cache-semantics": {
"version": "4.1.1",
diff --git a/reboot/demos/fig/package.json b/reboot/demos/fig/package.json
index fbd88c94..b753e192 100644
--- a/reboot/demos/fig/package.json
+++ b/reboot/demos/fig/package.json
@@ -11,11 +11,11 @@
"dependencies": {
"@bufbuild/protobuf": "1.10.1",
"@radix-ui/react-icons": "^1.3.0",
- "@reboot-dev/reboot": "1.2.0",
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-std": "1.2.0",
- "@reboot-dev/reboot-std-api": "1.2.0",
- "@reboot-dev/reboot-std-react": "1.2.0",
+ "@reboot-dev/reboot": "1.2.1",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-std": "1.2.1",
+ "@reboot-dev/reboot-std-api": "1.2.1",
+ "@reboot-dev/reboot-std-react": "1.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-draggable": "^4.4.6",
diff --git a/reboot/examples/agent-wiki/.dockerignore b/reboot/examples/agent-wiki/.dockerignore
index 2d96968d..a236b55e 100644
--- a/reboot/examples/agent-wiki/.dockerignore
+++ b/reboot/examples/agent-wiki/.dockerignore
@@ -35,5 +35,4 @@ web/index.css
.DS_Store
# Local-only files.
-mcp_servers.json
README.md
diff --git a/reboot/examples/agent-wiki/.tests/test.sh b/reboot/examples/agent-wiki/.tests/test.sh
index ddf490ef..c8207fad 100755
--- a/reboot/examples/agent-wiki/.tests/test.sh
+++ b/reboot/examples/agent-wiki/.tests/test.sh
@@ -51,24 +51,8 @@ pytest backend/
if [ -n "$EXPECTED_RBT_DEV_OUTPUT_FILE" ]; then
actual_output_file=$(mktemp)
- # `--config=dist` overrides `.rbtrc`'s default `hmr` config,
- # whose `--mcp-frontend-host=http://localhost:4444` would
- # have Envoy proxy `/__/web/**` to a Vite dev server. There
- # is no Vite running in CI, and on the macOS executable-Envoy
- # path that proxy target makes cluster init hang
- # indefinitely, so `--terminate-after-health-check` never
- # fires. The `dist` config sets `--mcp-frontend-host=""`,
- # which skips the proxy entirely. `web/dist/` doesn't need
- # to actually exist; the health check only probes gRPC and
- # Envoy listeners.
- #
- # The librarian agent fails fast if `ANTHROPIC_API_KEY` is
- # unset, so we provide a dummy value. No real Anthropic calls
- # are made: the health check terminates the process before any
- # transcript ingestion happens.
ANTHROPIC_API_KEY="dummy-for-health-check" \
rbt $RBT_FLAGS dev run \
- --config=dist \
--terminate-after-health-check \
> "$actual_output_file"
diff --git a/reboot/examples/agent-wiki/Dockerfile b/reboot/examples/agent-wiki/Dockerfile
index f2348998..442307c2 100644
--- a/reboot/examples/agent-wiki/Dockerfile
+++ b/reboot/examples/agent-wiki/Dockerfile
@@ -4,7 +4,7 @@
# locally before `docker build` so that `web/dist/` contains the
# bundled UIs. This image copies that prebuilt bundle rather
# than installing Node and rebuilding it here.
-FROM ghcr.io/reboot-dev/reboot-base:1.2.0
+FROM ghcr.io/reboot-dev/reboot-base:1.2.1
WORKDIR /app
diff --git a/reboot/examples/agent-wiki/mcp_servers.json b/reboot/examples/agent-wiki/mcp_servers.json
deleted file mode 100644
index 667a4b7c..00000000
--- a/reboot/examples/agent-wiki/mcp_servers.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "mcpServers": {
- "agent-wiki": {
- "url": "http://localhost:9991/mcp",
- "useOAuth": true
- }
- }
-}
diff --git a/reboot/examples/agent-wiki/pyproject.toml b/reboot/examples/agent-wiki/pyproject.toml
index 68bb872a..489d97e8 100644
--- a/reboot/examples/agent-wiki/pyproject.toml
+++ b/reboot/examples/agent-wiki/pyproject.toml
@@ -7,7 +7,7 @@ dependencies = [
"uuid7>=0.1.0",
"anyio>=4.0.0",
"pydantic-ai-slim[anthropic]>=1.0.0",
- "reboot==1.2.0",
+ "reboot==1.2.1",
]
[dependency-groups]
diff --git a/reboot/examples/agent-wiki/uv.lock b/reboot/examples/agent-wiki/uv.lock
index 752327ec..b40b8f6c 100644
--- a/reboot/examples/agent-wiki/uv.lock
+++ b/reboot/examples/agent-wiki/uv.lock
@@ -28,7 +28,7 @@ requires-dist = [
{ name = "anyio", specifier = ">=4.0.0" },
{ name = "httpx", specifier = ">=0.27,<1.0" },
{ name = "pydantic-ai-slim", extras = ["anthropic"], specifier = ">=1.0.0" },
- { name = "reboot", specifier = "==1.2.0" },
+ { name = "reboot", specifier = "==1.2.1" },
{ name = "uuid7", specifier = ">=0.1.0" },
]
@@ -2020,7 +2020,7 @@ wheels = [
[[package]]
name = "reboot"
-version = "1.2.0"
+version = "1.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiofiles" },
@@ -2070,9 +2070,9 @@ dependencies = [
{ name = "websockets" },
]
wheels = [
- { url = "https://files.pythonhosted.org/packages/ae/01/d67f4c2cfa6cd6605f25f50d8d6dd962cda3dbb1a3be5a413b27cecfe5ba/reboot-1.2.0-py3-none-macosx_14_0_arm64.whl", hash = "sha256:45c74d870833b0a092c451c117006623363fc559d1224522859d94993d0f6c1e", size = 20440626, upload-time = "2026-06-18T15:42:03.995Z" },
- { url = "https://files.pythonhosted.org/packages/62/fb/32f27dc3d16d5446ca8987cd81961ebc70c4d9d8e8b0cea71257f1a6b4b1/reboot-1.2.0-py3-none-manylinux_2_34_aarch64.whl", hash = "sha256:e3d026f55c1b084f50e417c70fe211d33ac1a796f42eefddd3e288ceb8ea2fd1", size = 24124117, upload-time = "2026-06-18T15:27:54.363Z" },
- { url = "https://files.pythonhosted.org/packages/74/e0/51021d59e6bc3802b1726aee7503d28d7ea0eb507896f0a4b7cac81fccf2/reboot-1.2.0-py3-none-manylinux_2_34_x86_64.whl", hash = "sha256:fb27f3b851e25b797d5cb6e9f93e606ac8ba0c3a138d83f126a2ca26bdfbbd57", size = 24254127, upload-time = "2026-06-18T15:29:00.942Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/85/082fea1aaf65904b2f732c893469cb1d2534dff96ba527ca5575c5afbf55/reboot-1.2.1-py3-none-macosx_14_0_arm64.whl", hash = "sha256:b54cbf0233f297926ce516d39578efaea47953d113eca7095d4d0f07a1e4b192", size = 20452221, upload-time = "2026-06-23T19:41:33.886Z" },
+ { url = "https://files.pythonhosted.org/packages/64/3a/e004b65f1bbef40d8da6051fcff73925fae358a4c3ccf7b00d00bd2c7421/reboot-1.2.1-py3-none-manylinux_2_34_aarch64.whl", hash = "sha256:bb91ee132940d66c6779c2e5038f2fbf70fd567351c912bf0acf33ba60111860", size = 24135744, upload-time = "2026-06-23T19:28:20.962Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/a3/7c8b5caf625a3f1b87b075b5bf23040a6f7761c797c245822144900999ce/reboot-1.2.1-py3-none-manylinux_2_34_x86_64.whl", hash = "sha256:988cb8d6e84981d35c1bf4c9040eda4181d4d221d0ff1e9a48922b7f50c123de", size = 24265726, upload-time = "2026-06-23T19:27:45.447Z" },
]
[[package]]
diff --git a/reboot/examples/agent-wiki/web/package-lock.json b/reboot/examples/agent-wiki/web/package-lock.json
index d6b52f6e..aaa47c61 100644
--- a/reboot/examples/agent-wiki/web/package-lock.json
+++ b/reboot/examples/agent-wiki/web/package-lock.json
@@ -10,8 +10,8 @@
"dependencies": {
"@modelcontextprotocol/ext-apps": "1.5.0",
"@modelcontextprotocol/sdk": "1.29.0",
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-react": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-react": "1.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^10.1.0",
@@ -888,9 +888,9 @@
}
},
"node_modules/@reboot-dev/reboot-api": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.0.tgz",
- "integrity": "sha512-6k0OhzPX3fcUurmxQY9ITJCX3LrMnimUYIdkPTEWWIpzV1jirpijx5ovTFG08MACic8mu1UMvCLEv7wq3/w1pg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.1.tgz",
+ "integrity": "sha512-CmrL7P/dtm+DBMWLWijs+NIlt0PKmdelwmP3Efmsz9x1dkICn6iq/MOh7FLVe2gcHubKL6RuiwO3vOOBVzObPg==",
"license": "Apache-2.0",
"dependencies": {
"@scarf/scarf": "1.4.0",
@@ -915,15 +915,15 @@
}
},
"node_modules/@reboot-dev/reboot-react": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.0.tgz",
- "integrity": "sha512-gNkSLFRgWDl40SbW7VNO+xD16DaFdPBZ95E2bULEHpAcyd8lsg5qiHHCPYThwCkDou/HqWbb/2WH5iiQaBFmRg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.1.tgz",
+ "integrity": "sha512-ImVCLLaOTZoDDtAnXdXufScZWOz2DVomVuhs6WwEgjW99yTtwTx1OMsC/wbU37pSmadmdfdlTYP9vy/V9EcFMg==",
"license": "Apache-2.0",
"dependencies": {
"@modelcontextprotocol/ext-apps": "1.5.0",
"@modelcontextprotocol/sdk": "1.29.0",
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-web": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-web": "1.2.1",
"@scarf/scarf": "1.4.0",
"@types/uuid": "^9.0.4",
"js-sha1": "0.7.0",
@@ -951,12 +951,12 @@
}
},
"node_modules/@reboot-dev/reboot-web": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.0.tgz",
- "integrity": "sha512-ziBVJ5WLlnR57nkCUAJc9OAdqnl4PjoIYPdzMMkEYh3wpf10LPT/souGQT9c5BHUSWOx81azdQqU5lCqYXKWsg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.1.tgz",
+ "integrity": "sha512-/jk3OdG6pF+McvIP6yEuJKSWmDpHSR4+nVdbvUIHDdJh8cQkFj2QnzQJ+pRbql207LvwUNJ3T9YwZDSYz8rB9A==",
"license": "Apache-2.0",
"dependencies": {
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
"@scarf/scarf": "1.4.0",
"js-sha1": "0.7.0",
"lru-cache-idb": "^0.5.2",
diff --git a/reboot/examples/agent-wiki/web/package.json b/reboot/examples/agent-wiki/web/package.json
index 4a8d0298..811023f2 100644
--- a/reboot/examples/agent-wiki/web/package.json
+++ b/reboot/examples/agent-wiki/web/package.json
@@ -17,8 +17,8 @@
"dependencies": {
"@modelcontextprotocol/ext-apps": "1.5.0",
"@modelcontextprotocol/sdk": "1.29.0",
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-react": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-react": "1.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^10.1.0",
diff --git a/reboot/examples/ai-chat-counter-dashboard/README.md b/reboot/examples/ai-chat-counter-dashboard/README.md
index 9dcb7a8a..e16b2f72 100644
--- a/reboot/examples/ai-chat-counter-dashboard/README.md
+++ b/reboot/examples/ai-chat-counter-dashboard/README.md
@@ -25,11 +25,11 @@ uv run rbt dev run
## Testing with MCPJam Inspector
-The project includes `mcp_servers.json` for testing with MCPJam Inspector:
+For testing with MCPJam Inspector:
```bash
# In another terminal, run MCPJam:
-npx @mcpjam/inspector@2.9.3 --config mcp_servers.json --server counter-server
+npx @mcpjam/inspector@2.18.1 --url http://localhost:9991/mcp --oauth
```
This opens a browser-based inspector where you can test tools.
diff --git a/reboot/examples/ai-chat-counter-dashboard/mcp_servers.json b/reboot/examples/ai-chat-counter-dashboard/mcp_servers.json
deleted file mode 100644
index 7ea8c989..00000000
--- a/reboot/examples/ai-chat-counter-dashboard/mcp_servers.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "mcpServers": {
- "counter-server": {
- "url": "http://localhost:9991/mcp",
- "useOAuth": true
- }
- }
-}
diff --git a/reboot/examples/ai-chat-counter-dashboard/pyproject.toml b/reboot/examples/ai-chat-counter-dashboard/pyproject.toml
index 20753ed5..e64ff576 100644
--- a/reboot/examples/ai-chat-counter-dashboard/pyproject.toml
+++ b/reboot/examples/ai-chat-counter-dashboard/pyproject.toml
@@ -6,7 +6,7 @@ dependencies = [
"httpx>=0.27,<1.0",
"uuid7>=0.1.0",
"anyio>=4.0.0",
- "reboot==1.2.0",
+ "reboot==1.2.1",
]
[dependency-groups]
diff --git a/reboot/examples/ai-chat-counter-dashboard/uv.lock b/reboot/examples/ai-chat-counter-dashboard/uv.lock
index 2f9853c2..ce53847c 100644
--- a/reboot/examples/ai-chat-counter-dashboard/uv.lock
+++ b/reboot/examples/ai-chat-counter-dashboard/uv.lock
@@ -27,7 +27,7 @@ dev = [
requires-dist = [
{ name = "anyio", specifier = ">=4.0.0" },
{ name = "httpx", specifier = ">=0.27,<1.0" },
- { name = "reboot", specifier = "==1.2.0" },
+ { name = "reboot", specifier = "==1.2.1" },
{ name = "uuid7", specifier = ">=0.1.0" },
]
@@ -1886,7 +1886,7 @@ wheels = [
[[package]]
name = "reboot"
-version = "1.2.0"
+version = "1.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiofiles" },
@@ -1936,9 +1936,9 @@ dependencies = [
{ name = "websockets" },
]
wheels = [
- { url = "https://files.pythonhosted.org/packages/ae/01/d67f4c2cfa6cd6605f25f50d8d6dd962cda3dbb1a3be5a413b27cecfe5ba/reboot-1.2.0-py3-none-macosx_14_0_arm64.whl", hash = "sha256:45c74d870833b0a092c451c117006623363fc559d1224522859d94993d0f6c1e", size = 20440626, upload-time = "2026-06-18T15:42:03.995Z" },
- { url = "https://files.pythonhosted.org/packages/62/fb/32f27dc3d16d5446ca8987cd81961ebc70c4d9d8e8b0cea71257f1a6b4b1/reboot-1.2.0-py3-none-manylinux_2_34_aarch64.whl", hash = "sha256:e3d026f55c1b084f50e417c70fe211d33ac1a796f42eefddd3e288ceb8ea2fd1", size = 24124117, upload-time = "2026-06-18T15:27:54.363Z" },
- { url = "https://files.pythonhosted.org/packages/74/e0/51021d59e6bc3802b1726aee7503d28d7ea0eb507896f0a4b7cac81fccf2/reboot-1.2.0-py3-none-manylinux_2_34_x86_64.whl", hash = "sha256:fb27f3b851e25b797d5cb6e9f93e606ac8ba0c3a138d83f126a2ca26bdfbbd57", size = 24254127, upload-time = "2026-06-18T15:29:00.942Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/85/082fea1aaf65904b2f732c893469cb1d2534dff96ba527ca5575c5afbf55/reboot-1.2.1-py3-none-macosx_14_0_arm64.whl", hash = "sha256:b54cbf0233f297926ce516d39578efaea47953d113eca7095d4d0f07a1e4b192", size = 20452221, upload-time = "2026-06-23T19:41:33.886Z" },
+ { url = "https://files.pythonhosted.org/packages/64/3a/e004b65f1bbef40d8da6051fcff73925fae358a4c3ccf7b00d00bd2c7421/reboot-1.2.1-py3-none-manylinux_2_34_aarch64.whl", hash = "sha256:bb91ee132940d66c6779c2e5038f2fbf70fd567351c912bf0acf33ba60111860", size = 24135744, upload-time = "2026-06-23T19:28:20.962Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/a3/7c8b5caf625a3f1b87b075b5bf23040a6f7761c797c245822144900999ce/reboot-1.2.1-py3-none-manylinux_2_34_x86_64.whl", hash = "sha256:988cb8d6e84981d35c1bf4c9040eda4181d4d221d0ff1e9a48922b7f50c123de", size = 24265726, upload-time = "2026-06-23T19:27:45.447Z" },
]
[[package]]
diff --git a/reboot/examples/ai-chat-counter-dashboard/web/package-lock.json b/reboot/examples/ai-chat-counter-dashboard/web/package-lock.json
index f1924af7..cf09b1cd 100644
--- a/reboot/examples/ai-chat-counter-dashboard/web/package-lock.json
+++ b/reboot/examples/ai-chat-counter-dashboard/web/package-lock.json
@@ -10,8 +10,8 @@
"dependencies": {
"@modelcontextprotocol/ext-apps": "1.5.0",
"@modelcontextprotocol/sdk": "1.29.0",
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-react": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-react": "1.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"zod": "^4.0.0"
@@ -886,9 +886,9 @@
}
},
"node_modules/@reboot-dev/reboot-api": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.0.tgz",
- "integrity": "sha512-6k0OhzPX3fcUurmxQY9ITJCX3LrMnimUYIdkPTEWWIpzV1jirpijx5ovTFG08MACic8mu1UMvCLEv7wq3/w1pg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.1.tgz",
+ "integrity": "sha512-CmrL7P/dtm+DBMWLWijs+NIlt0PKmdelwmP3Efmsz9x1dkICn6iq/MOh7FLVe2gcHubKL6RuiwO3vOOBVzObPg==",
"license": "Apache-2.0",
"dependencies": {
"@scarf/scarf": "1.4.0",
@@ -913,15 +913,15 @@
}
},
"node_modules/@reboot-dev/reboot-react": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.0.tgz",
- "integrity": "sha512-gNkSLFRgWDl40SbW7VNO+xD16DaFdPBZ95E2bULEHpAcyd8lsg5qiHHCPYThwCkDou/HqWbb/2WH5iiQaBFmRg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.1.tgz",
+ "integrity": "sha512-ImVCLLaOTZoDDtAnXdXufScZWOz2DVomVuhs6WwEgjW99yTtwTx1OMsC/wbU37pSmadmdfdlTYP9vy/V9EcFMg==",
"license": "Apache-2.0",
"dependencies": {
"@modelcontextprotocol/ext-apps": "1.5.0",
"@modelcontextprotocol/sdk": "1.29.0",
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-web": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-web": "1.2.1",
"@scarf/scarf": "1.4.0",
"@types/uuid": "^9.0.4",
"js-sha1": "0.7.0",
@@ -949,12 +949,12 @@
}
},
"node_modules/@reboot-dev/reboot-web": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.0.tgz",
- "integrity": "sha512-ziBVJ5WLlnR57nkCUAJc9OAdqnl4PjoIYPdzMMkEYh3wpf10LPT/souGQT9c5BHUSWOx81azdQqU5lCqYXKWsg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.1.tgz",
+ "integrity": "sha512-/jk3OdG6pF+McvIP6yEuJKSWmDpHSR4+nVdbvUIHDdJh8cQkFj2QnzQJ+pRbql207LvwUNJ3T9YwZDSYz8rB9A==",
"license": "Apache-2.0",
"dependencies": {
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
"@scarf/scarf": "1.4.0",
"js-sha1": "0.7.0",
"lru-cache-idb": "^0.5.2",
diff --git a/reboot/examples/ai-chat-counter-dashboard/web/package.json b/reboot/examples/ai-chat-counter-dashboard/web/package.json
index 55ce0798..3801a807 100644
--- a/reboot/examples/ai-chat-counter-dashboard/web/package.json
+++ b/reboot/examples/ai-chat-counter-dashboard/web/package.json
@@ -15,8 +15,8 @@
"dependencies": {
"@modelcontextprotocol/ext-apps": "1.5.0",
"@modelcontextprotocol/sdk": "1.29.0",
- "@reboot-dev/reboot-react": "1.2.0",
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-react": "1.2.1",
+ "@reboot-dev/reboot-api": "1.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"zod": "^4.0.0"
diff --git a/reboot/examples/ai-chat-counter/Dockerfile b/reboot/examples/ai-chat-counter/Dockerfile
index b63d4ec9..b7a1774f 100644
--- a/reboot/examples/ai-chat-counter/Dockerfile
+++ b/reboot/examples/ai-chat-counter/Dockerfile
@@ -1,4 +1,4 @@
-FROM ghcr.io/reboot-dev/reboot-base:1.2.0
+FROM ghcr.io/reboot-dev/reboot-base:1.2.1
WORKDIR /app
diff --git a/reboot/examples/ai-chat-counter/README.md b/reboot/examples/ai-chat-counter/README.md
index 34813b1e..7a7d6091 100644
--- a/reboot/examples/ai-chat-counter/README.md
+++ b/reboot/examples/ai-chat-counter/README.md
@@ -24,11 +24,11 @@ uv run rbt dev run
## Testing with MCPJam Inspector
-The project includes `mcp_servers.json` for testing with MCPJam Inspector:
+For testing with MCPJam Inspector:
```bash
# In another terminal, run MCPJam:
-npx @mcpjam/inspector@2.9.3 --config mcp_servers.json --server counter-server
+npx @mcpjam/inspector@2.18.1 --url http://localhost:9991/mcp --oauth
```
This opens a browser-based inspector where you can test tools.
diff --git a/reboot/examples/ai-chat-counter/mcp_servers.json b/reboot/examples/ai-chat-counter/mcp_servers.json
deleted file mode 100644
index 7ea8c989..00000000
--- a/reboot/examples/ai-chat-counter/mcp_servers.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "mcpServers": {
- "counter-server": {
- "url": "http://localhost:9991/mcp",
- "useOAuth": true
- }
- }
-}
diff --git a/reboot/examples/ai-chat-counter/pyproject.toml b/reboot/examples/ai-chat-counter/pyproject.toml
index c443e09c..2d9d944b 100644
--- a/reboot/examples/ai-chat-counter/pyproject.toml
+++ b/reboot/examples/ai-chat-counter/pyproject.toml
@@ -6,7 +6,7 @@ dependencies = [
"httpx>=0.27,<1.0",
"uuid7>=0.1.0",
"anyio>=4.0.0",
- "reboot==1.2.0",
+ "reboot==1.2.1",
]
[dependency-groups]
diff --git a/reboot/examples/ai-chat-counter/uv.lock b/reboot/examples/ai-chat-counter/uv.lock
index 20090da3..04d57adc 100644
--- a/reboot/examples/ai-chat-counter/uv.lock
+++ b/reboot/examples/ai-chat-counter/uv.lock
@@ -27,7 +27,7 @@ dev = [
requires-dist = [
{ name = "anyio", specifier = ">=4.0.0" },
{ name = "httpx", specifier = ">=0.27,<1.0" },
- { name = "reboot", specifier = "==1.2.0" },
+ { name = "reboot", specifier = "==1.2.1" },
{ name = "uuid7", specifier = ">=0.1.0" },
]
@@ -1886,7 +1886,7 @@ wheels = [
[[package]]
name = "reboot"
-version = "1.2.0"
+version = "1.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiofiles" },
@@ -1936,9 +1936,9 @@ dependencies = [
{ name = "websockets" },
]
wheels = [
- { url = "https://files.pythonhosted.org/packages/ae/01/d67f4c2cfa6cd6605f25f50d8d6dd962cda3dbb1a3be5a413b27cecfe5ba/reboot-1.2.0-py3-none-macosx_14_0_arm64.whl", hash = "sha256:45c74d870833b0a092c451c117006623363fc559d1224522859d94993d0f6c1e", size = 20440626, upload-time = "2026-06-18T15:42:03.995Z" },
- { url = "https://files.pythonhosted.org/packages/62/fb/32f27dc3d16d5446ca8987cd81961ebc70c4d9d8e8b0cea71257f1a6b4b1/reboot-1.2.0-py3-none-manylinux_2_34_aarch64.whl", hash = "sha256:e3d026f55c1b084f50e417c70fe211d33ac1a796f42eefddd3e288ceb8ea2fd1", size = 24124117, upload-time = "2026-06-18T15:27:54.363Z" },
- { url = "https://files.pythonhosted.org/packages/74/e0/51021d59e6bc3802b1726aee7503d28d7ea0eb507896f0a4b7cac81fccf2/reboot-1.2.0-py3-none-manylinux_2_34_x86_64.whl", hash = "sha256:fb27f3b851e25b797d5cb6e9f93e606ac8ba0c3a138d83f126a2ca26bdfbbd57", size = 24254127, upload-time = "2026-06-18T15:29:00.942Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/85/082fea1aaf65904b2f732c893469cb1d2534dff96ba527ca5575c5afbf55/reboot-1.2.1-py3-none-macosx_14_0_arm64.whl", hash = "sha256:b54cbf0233f297926ce516d39578efaea47953d113eca7095d4d0f07a1e4b192", size = 20452221, upload-time = "2026-06-23T19:41:33.886Z" },
+ { url = "https://files.pythonhosted.org/packages/64/3a/e004b65f1bbef40d8da6051fcff73925fae358a4c3ccf7b00d00bd2c7421/reboot-1.2.1-py3-none-manylinux_2_34_aarch64.whl", hash = "sha256:bb91ee132940d66c6779c2e5038f2fbf70fd567351c912bf0acf33ba60111860", size = 24135744, upload-time = "2026-06-23T19:28:20.962Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/a3/7c8b5caf625a3f1b87b075b5bf23040a6f7761c797c245822144900999ce/reboot-1.2.1-py3-none-manylinux_2_34_x86_64.whl", hash = "sha256:988cb8d6e84981d35c1bf4c9040eda4181d4d221d0ff1e9a48922b7f50c123de", size = 24265726, upload-time = "2026-06-23T19:27:45.447Z" },
]
[[package]]
diff --git a/reboot/examples/ai-chat-counter/web/package-lock.json b/reboot/examples/ai-chat-counter/web/package-lock.json
index 3b786e70..efd71528 100644
--- a/reboot/examples/ai-chat-counter/web/package-lock.json
+++ b/reboot/examples/ai-chat-counter/web/package-lock.json
@@ -10,8 +10,8 @@
"dependencies": {
"@modelcontextprotocol/ext-apps": "1.5.0",
"@modelcontextprotocol/sdk": "1.29.0",
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-react": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-react": "1.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"zod": "^4.0.0"
@@ -405,9 +405,9 @@
}
},
"node_modules/@reboot-dev/reboot-api": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.0.tgz",
- "integrity": "sha512-6k0OhzPX3fcUurmxQY9ITJCX3LrMnimUYIdkPTEWWIpzV1jirpijx5ovTFG08MACic8mu1UMvCLEv7wq3/w1pg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.1.tgz",
+ "integrity": "sha512-CmrL7P/dtm+DBMWLWijs+NIlt0PKmdelwmP3Efmsz9x1dkICn6iq/MOh7FLVe2gcHubKL6RuiwO3vOOBVzObPg==",
"license": "Apache-2.0",
"dependencies": {
"@scarf/scarf": "1.4.0",
@@ -430,15 +430,15 @@
}
},
"node_modules/@reboot-dev/reboot-react": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.0.tgz",
- "integrity": "sha512-gNkSLFRgWDl40SbW7VNO+xD16DaFdPBZ95E2bULEHpAcyd8lsg5qiHHCPYThwCkDou/HqWbb/2WH5iiQaBFmRg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.1.tgz",
+ "integrity": "sha512-ImVCLLaOTZoDDtAnXdXufScZWOz2DVomVuhs6WwEgjW99yTtwTx1OMsC/wbU37pSmadmdfdlTYP9vy/V9EcFMg==",
"license": "Apache-2.0",
"dependencies": {
"@modelcontextprotocol/ext-apps": "1.5.0",
"@modelcontextprotocol/sdk": "1.29.0",
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-web": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-web": "1.2.1",
"@scarf/scarf": "1.4.0",
"@types/uuid": "^9.0.4",
"js-sha1": "0.7.0",
@@ -464,12 +464,12 @@
}
},
"node_modules/@reboot-dev/reboot-web": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.0.tgz",
- "integrity": "sha512-ziBVJ5WLlnR57nkCUAJc9OAdqnl4PjoIYPdzMMkEYh3wpf10LPT/souGQT9c5BHUSWOx81azdQqU5lCqYXKWsg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.1.tgz",
+ "integrity": "sha512-/jk3OdG6pF+McvIP6yEuJKSWmDpHSR4+nVdbvUIHDdJh8cQkFj2QnzQJ+pRbql207LvwUNJ3T9YwZDSYz8rB9A==",
"license": "Apache-2.0",
"dependencies": {
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
"@scarf/scarf": "1.4.0",
"js-sha1": "0.7.0",
"lru-cache-idb": "^0.5.2",
diff --git a/reboot/examples/ai-chat-counter/web/package.json b/reboot/examples/ai-chat-counter/web/package.json
index 5e449ffe..b3d910ff 100644
--- a/reboot/examples/ai-chat-counter/web/package.json
+++ b/reboot/examples/ai-chat-counter/web/package.json
@@ -13,8 +13,8 @@
"dependencies": {
"@modelcontextprotocol/ext-apps": "1.5.0",
"@modelcontextprotocol/sdk": "1.29.0",
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-react": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-react": "1.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"zod": "^4.0.0"
diff --git a/reboot/examples/bank-nodejs/package-lock.json b/reboot/examples/bank-nodejs/package-lock.json
index 92fd8ecc..f1460d02 100644
--- a/reboot/examples/bank-nodejs/package-lock.json
+++ b/reboot/examples/bank-nodejs/package-lock.json
@@ -9,8 +9,8 @@
"version": "0.1.0",
"dependencies": {
"@bufbuild/protobuf": "1.10.1",
- "@reboot-dev/reboot": "1.2.0",
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot": "1.2.1",
+ "@reboot-dev/reboot-api": "1.2.1",
"@types/node": "20.11.5",
"typescript": "5.4.5"
},
@@ -495,15 +495,15 @@
}
},
"node_modules/@reboot-dev/reboot": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.2.0.tgz",
- "integrity": "sha512-VPjg8N0ab2iZzQ8PeVnATye5iPjEfmT7digqxxCKBYndOB/W8CjKg4YUVREzg+hNTrILSDmgJex2lpPd5yo4TQ==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.2.1.tgz",
+ "integrity": "sha512-1x35ldNqzFLUbRtjXrAfd27YDVX43gGYA+4O9BOx14yBu4y3OC4JZHT4Am7pVRuHJs4qNWSj0dCBFnEtqL3Rvg==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@bufbuild/protoc-gen-es": "1.10.1",
"@bufbuild/protoplugin": "1.10.1",
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
"@scarf/scarf": "1.4.0",
"@standard-schema/spec": "1.0.0",
"chalk": "^4.1.2",
@@ -532,9 +532,9 @@
}
},
"node_modules/@reboot-dev/reboot-api": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.0.tgz",
- "integrity": "sha512-6k0OhzPX3fcUurmxQY9ITJCX3LrMnimUYIdkPTEWWIpzV1jirpijx5ovTFG08MACic8mu1UMvCLEv7wq3/w1pg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.1.tgz",
+ "integrity": "sha512-CmrL7P/dtm+DBMWLWijs+NIlt0PKmdelwmP3Efmsz9x1dkICn6iq/MOh7FLVe2gcHubKL6RuiwO3vOOBVzObPg==",
"license": "Apache-2.0",
"dependencies": {
"@scarf/scarf": "1.4.0",
@@ -2404,13 +2404,13 @@
}
},
"@reboot-dev/reboot": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.2.0.tgz",
- "integrity": "sha512-VPjg8N0ab2iZzQ8PeVnATye5iPjEfmT7digqxxCKBYndOB/W8CjKg4YUVREzg+hNTrILSDmgJex2lpPd5yo4TQ==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.2.1.tgz",
+ "integrity": "sha512-1x35ldNqzFLUbRtjXrAfd27YDVX43gGYA+4O9BOx14yBu4y3OC4JZHT4Am7pVRuHJs4qNWSj0dCBFnEtqL3Rvg==",
"requires": {
"@bufbuild/protoc-gen-es": "1.10.1",
"@bufbuild/protoplugin": "1.10.1",
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
"@scarf/scarf": "1.4.0",
"@standard-schema/spec": "1.0.0",
"chalk": "^4.1.2",
@@ -2427,9 +2427,9 @@
}
},
"@reboot-dev/reboot-api": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.0.tgz",
- "integrity": "sha512-6k0OhzPX3fcUurmxQY9ITJCX3LrMnimUYIdkPTEWWIpzV1jirpijx5ovTFG08MACic8mu1UMvCLEv7wq3/w1pg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.1.tgz",
+ "integrity": "sha512-CmrL7P/dtm+DBMWLWijs+NIlt0PKmdelwmP3Efmsz9x1dkICn6iq/MOh7FLVe2gcHubKL6RuiwO3vOOBVzObPg==",
"requires": {
"@scarf/scarf": "1.4.0",
"typescript": "5.4.5",
diff --git a/reboot/examples/bank-nodejs/package.json b/reboot/examples/bank-nodejs/package.json
index 5db98881..6f73277e 100644
--- a/reboot/examples/bank-nodejs/package.json
+++ b/reboot/examples/bank-nodejs/package.json
@@ -5,8 +5,8 @@
"type": "module",
"dependencies": {
"@bufbuild/protobuf": "1.10.1",
- "@reboot-dev/reboot": "1.2.0",
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot": "1.2.1",
+ "@reboot-dev/reboot-api": "1.2.1",
"typescript": "5.4.5",
"@types/node": "20.11.5"
},
diff --git a/reboot/examples/bank-pydantic/pyproject.toml b/reboot/examples/bank-pydantic/pyproject.toml
index 280c4dc1..42e1b913 100644
--- a/reboot/examples/bank-pydantic/pyproject.toml
+++ b/reboot/examples/bank-pydantic/pyproject.toml
@@ -3,7 +3,7 @@ name = "bank-pydantic"
version = "0.1.0"
requires-python = ">= 3.10"
dependencies = [
- "reboot==1.2.0",
+ "reboot==1.2.1",
]
[dependency-groups]
diff --git a/reboot/examples/bank-pydantic/uv.lock b/reboot/examples/bank-pydantic/uv.lock
index 1cb25bfb..38ee7886 100644
--- a/reboot/examples/bank-pydantic/uv.lock
+++ b/reboot/examples/bank-pydantic/uv.lock
@@ -214,7 +214,7 @@ dev = [
]
[package.metadata]
-requires-dist = [{ name = "reboot", specifier = "==1.2.0" }]
+requires-dist = [{ name = "reboot", specifier = "==1.2.1" }]
[package.metadata.requires-dev]
dev = [
@@ -1925,7 +1925,7 @@ wheels = [
[[package]]
name = "reboot"
-version = "1.2.0"
+version = "1.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiofiles" },
@@ -1975,9 +1975,9 @@ dependencies = [
{ name = "websockets" },
]
wheels = [
- { url = "https://files.pythonhosted.org/packages/ae/01/d67f4c2cfa6cd6605f25f50d8d6dd962cda3dbb1a3be5a413b27cecfe5ba/reboot-1.2.0-py3-none-macosx_14_0_arm64.whl", hash = "sha256:45c74d870833b0a092c451c117006623363fc559d1224522859d94993d0f6c1e", size = 20440626, upload-time = "2026-06-18T15:42:03.995Z" },
- { url = "https://files.pythonhosted.org/packages/62/fb/32f27dc3d16d5446ca8987cd81961ebc70c4d9d8e8b0cea71257f1a6b4b1/reboot-1.2.0-py3-none-manylinux_2_34_aarch64.whl", hash = "sha256:e3d026f55c1b084f50e417c70fe211d33ac1a796f42eefddd3e288ceb8ea2fd1", size = 24124117, upload-time = "2026-06-18T15:27:54.363Z" },
- { url = "https://files.pythonhosted.org/packages/74/e0/51021d59e6bc3802b1726aee7503d28d7ea0eb507896f0a4b7cac81fccf2/reboot-1.2.0-py3-none-manylinux_2_34_x86_64.whl", hash = "sha256:fb27f3b851e25b797d5cb6e9f93e606ac8ba0c3a138d83f126a2ca26bdfbbd57", size = 24254127, upload-time = "2026-06-18T15:29:00.942Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/85/082fea1aaf65904b2f732c893469cb1d2534dff96ba527ca5575c5afbf55/reboot-1.2.1-py3-none-macosx_14_0_arm64.whl", hash = "sha256:b54cbf0233f297926ce516d39578efaea47953d113eca7095d4d0f07a1e4b192", size = 20452221, upload-time = "2026-06-23T19:41:33.886Z" },
+ { url = "https://files.pythonhosted.org/packages/64/3a/e004b65f1bbef40d8da6051fcff73925fae358a4c3ccf7b00d00bd2c7421/reboot-1.2.1-py3-none-manylinux_2_34_aarch64.whl", hash = "sha256:bb91ee132940d66c6779c2e5038f2fbf70fd567351c912bf0acf33ba60111860", size = 24135744, upload-time = "2026-06-23T19:28:20.962Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/a3/7c8b5caf625a3f1b87b075b5bf23040a6f7761c797c245822144900999ce/reboot-1.2.1-py3-none-manylinux_2_34_x86_64.whl", hash = "sha256:988cb8d6e84981d35c1bf4c9040eda4181d4d221d0ff1e9a48922b7f50c123de", size = 24265726, upload-time = "2026-06-23T19:27:45.447Z" },
]
[[package]]
diff --git a/reboot/examples/bank-pydantic/web/package-lock.json b/reboot/examples/bank-pydantic/web/package-lock.json
index df114cc8..d154e610 100644
--- a/reboot/examples/bank-pydantic/web/package-lock.json
+++ b/reboot/examples/bank-pydantic/web/package-lock.json
@@ -9,9 +9,9 @@
"version": "0.1.0",
"dependencies": {
"@eslint/js": "^9.34.0",
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-react": "1.2.0",
- "@reboot-dev/reboot-std": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-react": "1.2.1",
+ "@reboot-dev/reboot-std": "1.2.1",
"@tailwindcss/vite": "^4.1.11",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
@@ -1200,15 +1200,15 @@
}
},
"node_modules/@reboot-dev/reboot": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.2.0.tgz",
- "integrity": "sha512-VPjg8N0ab2iZzQ8PeVnATye5iPjEfmT7digqxxCKBYndOB/W8CjKg4YUVREzg+hNTrILSDmgJex2lpPd5yo4TQ==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.2.1.tgz",
+ "integrity": "sha512-1x35ldNqzFLUbRtjXrAfd27YDVX43gGYA+4O9BOx14yBu4y3OC4JZHT4Am7pVRuHJs4qNWSj0dCBFnEtqL3Rvg==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@bufbuild/protoc-gen-es": "1.10.1",
"@bufbuild/protoplugin": "1.10.1",
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
"@scarf/scarf": "1.4.0",
"@standard-schema/spec": "1.0.0",
"chalk": "^4.1.2",
@@ -1237,9 +1237,9 @@
}
},
"node_modules/@reboot-dev/reboot-api": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.0.tgz",
- "integrity": "sha512-6k0OhzPX3fcUurmxQY9ITJCX3LrMnimUYIdkPTEWWIpzV1jirpijx5ovTFG08MACic8mu1UMvCLEv7wq3/w1pg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.1.tgz",
+ "integrity": "sha512-CmrL7P/dtm+DBMWLWijs+NIlt0PKmdelwmP3Efmsz9x1dkICn6iq/MOh7FLVe2gcHubKL6RuiwO3vOOBVzObPg==",
"license": "Apache-2.0",
"dependencies": {
"@scarf/scarf": "1.4.0",
@@ -1263,15 +1263,15 @@
}
},
"node_modules/@reboot-dev/reboot-react": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.0.tgz",
- "integrity": "sha512-gNkSLFRgWDl40SbW7VNO+xD16DaFdPBZ95E2bULEHpAcyd8lsg5qiHHCPYThwCkDou/HqWbb/2WH5iiQaBFmRg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.1.tgz",
+ "integrity": "sha512-ImVCLLaOTZoDDtAnXdXufScZWOz2DVomVuhs6WwEgjW99yTtwTx1OMsC/wbU37pSmadmdfdlTYP9vy/V9EcFMg==",
"license": "Apache-2.0",
"dependencies": {
"@modelcontextprotocol/ext-apps": "1.5.0",
"@modelcontextprotocol/sdk": "1.29.0",
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-web": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-web": "1.2.1",
"@scarf/scarf": "1.4.0",
"@types/uuid": "^9.0.4",
"js-sha1": "0.7.0",
@@ -1298,32 +1298,32 @@
}
},
"node_modules/@reboot-dev/reboot-std": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std/-/reboot-std-1.2.0.tgz",
- "integrity": "sha512-Jblt3IM3hFHZdf78zv27/FL4IZL3l/ULnTFKwB4LloIlHVnhNegisHISvHn2fAkM8VzDy64s174tA/jHPHUyrw==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std/-/reboot-std-1.2.1.tgz",
+ "integrity": "sha512-+4oa3p/om+WsEc5/CVkYP5tTlBpZmIDMaEsMxgDScsa5txDwAxAPEIrYpSFppUZ/WZo2OzbjqDnUyp491Iym/g==",
"license": "Apache-2.0",
"dependencies": {
- "@reboot-dev/reboot": "1.2.0",
- "@reboot-dev/reboot-std-api": "1.2.0",
+ "@reboot-dev/reboot": "1.2.1",
+ "@reboot-dev/reboot-std-api": "1.2.1",
"@scarf/scarf": "1.4.0"
}
},
"node_modules/@reboot-dev/reboot-std-api": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std-api/-/reboot-std-api-1.2.0.tgz",
- "integrity": "sha512-8EI8+Ys4gWlVNiq/5f0ZU2W76CkddqLygdDrZTqgLNsI/Y4wa/ZOFMiEGvayULTzQu0Og5pZ0jXbXrak9iYTqw==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std-api/-/reboot-std-api-1.2.1.tgz",
+ "integrity": "sha512-evFzB6oQDURxkddknXakyRKcYT53EY1AlyQnNs3nffF84gWMjIbZ6nb3C5yDNnra+pDjsiiz7DsBhMvr9qHX2g==",
"license": "Apache-2.0",
"dependencies": {
"@scarf/scarf": "1.4.0"
}
},
"node_modules/@reboot-dev/reboot-web": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.0.tgz",
- "integrity": "sha512-ziBVJ5WLlnR57nkCUAJc9OAdqnl4PjoIYPdzMMkEYh3wpf10LPT/souGQT9c5BHUSWOx81azdQqU5lCqYXKWsg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.1.tgz",
+ "integrity": "sha512-/jk3OdG6pF+McvIP6yEuJKSWmDpHSR4+nVdbvUIHDdJh8cQkFj2QnzQJ+pRbql207LvwUNJ3T9YwZDSYz8rB9A==",
"license": "Apache-2.0",
"dependencies": {
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
"@scarf/scarf": "1.4.0",
"js-sha1": "0.7.0",
"lru-cache-idb": "^0.5.2",
@@ -5528,9 +5528,9 @@
}
},
"node_modules/node-gyp/node_modules/semver": {
- "version": "7.8.4",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz",
- "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==",
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -7888,13 +7888,13 @@
}
},
"@reboot-dev/reboot": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.2.0.tgz",
- "integrity": "sha512-VPjg8N0ab2iZzQ8PeVnATye5iPjEfmT7digqxxCKBYndOB/W8CjKg4YUVREzg+hNTrILSDmgJex2lpPd5yo4TQ==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.2.1.tgz",
+ "integrity": "sha512-1x35ldNqzFLUbRtjXrAfd27YDVX43gGYA+4O9BOx14yBu4y3OC4JZHT4Am7pVRuHJs4qNWSj0dCBFnEtqL3Rvg==",
"requires": {
"@bufbuild/protoc-gen-es": "1.10.1",
"@bufbuild/protoplugin": "1.10.1",
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
"@scarf/scarf": "1.4.0",
"@standard-schema/spec": "1.0.0",
"chalk": "^4.1.2",
@@ -8102,9 +8102,9 @@
}
},
"@reboot-dev/reboot-api": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.0.tgz",
- "integrity": "sha512-6k0OhzPX3fcUurmxQY9ITJCX3LrMnimUYIdkPTEWWIpzV1jirpijx5ovTFG08MACic8mu1UMvCLEv7wq3/w1pg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.1.tgz",
+ "integrity": "sha512-CmrL7P/dtm+DBMWLWijs+NIlt0PKmdelwmP3Efmsz9x1dkICn6iq/MOh7FLVe2gcHubKL6RuiwO3vOOBVzObPg==",
"requires": {
"@scarf/scarf": "1.4.0",
"typescript": "5.4.5",
@@ -8119,14 +8119,14 @@
}
},
"@reboot-dev/reboot-react": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.0.tgz",
- "integrity": "sha512-gNkSLFRgWDl40SbW7VNO+xD16DaFdPBZ95E2bULEHpAcyd8lsg5qiHHCPYThwCkDou/HqWbb/2WH5iiQaBFmRg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.1.tgz",
+ "integrity": "sha512-ImVCLLaOTZoDDtAnXdXufScZWOz2DVomVuhs6WwEgjW99yTtwTx1OMsC/wbU37pSmadmdfdlTYP9vy/V9EcFMg==",
"requires": {
"@modelcontextprotocol/ext-apps": "1.5.0",
"@modelcontextprotocol/sdk": "1.29.0",
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-web": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-web": "1.2.1",
"@scarf/scarf": "1.4.0",
"@types/uuid": "^9.0.4",
"js-sha1": "0.7.0",
@@ -8143,29 +8143,29 @@
}
},
"@reboot-dev/reboot-std": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std/-/reboot-std-1.2.0.tgz",
- "integrity": "sha512-Jblt3IM3hFHZdf78zv27/FL4IZL3l/ULnTFKwB4LloIlHVnhNegisHISvHn2fAkM8VzDy64s174tA/jHPHUyrw==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std/-/reboot-std-1.2.1.tgz",
+ "integrity": "sha512-+4oa3p/om+WsEc5/CVkYP5tTlBpZmIDMaEsMxgDScsa5txDwAxAPEIrYpSFppUZ/WZo2OzbjqDnUyp491Iym/g==",
"requires": {
- "@reboot-dev/reboot": "1.2.0",
- "@reboot-dev/reboot-std-api": "1.2.0",
+ "@reboot-dev/reboot": "1.2.1",
+ "@reboot-dev/reboot-std-api": "1.2.1",
"@scarf/scarf": "1.4.0"
}
},
"@reboot-dev/reboot-std-api": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std-api/-/reboot-std-api-1.2.0.tgz",
- "integrity": "sha512-8EI8+Ys4gWlVNiq/5f0ZU2W76CkddqLygdDrZTqgLNsI/Y4wa/ZOFMiEGvayULTzQu0Og5pZ0jXbXrak9iYTqw==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std-api/-/reboot-std-api-1.2.1.tgz",
+ "integrity": "sha512-evFzB6oQDURxkddknXakyRKcYT53EY1AlyQnNs3nffF84gWMjIbZ6nb3C5yDNnra+pDjsiiz7DsBhMvr9qHX2g==",
"requires": {
"@scarf/scarf": "1.4.0"
}
},
"@reboot-dev/reboot-web": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.0.tgz",
- "integrity": "sha512-ziBVJ5WLlnR57nkCUAJc9OAdqnl4PjoIYPdzMMkEYh3wpf10LPT/souGQT9c5BHUSWOx81azdQqU5lCqYXKWsg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.1.tgz",
+ "integrity": "sha512-/jk3OdG6pF+McvIP6yEuJKSWmDpHSR4+nVdbvUIHDdJh8cQkFj2QnzQJ+pRbql207LvwUNJ3T9YwZDSYz8rB9A==",
"requires": {
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
"@scarf/scarf": "1.4.0",
"js-sha1": "0.7.0",
"lru-cache-idb": "^0.5.2",
@@ -10501,9 +10501,9 @@
"integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="
},
"semver": {
- "version": "7.8.4",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz",
- "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA=="
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="
},
"which": {
"version": "6.0.1",
diff --git a/reboot/examples/bank-pydantic/web/package.json b/reboot/examples/bank-pydantic/web/package.json
index e734722d..d6a3d11c 100644
--- a/reboot/examples/bank-pydantic/web/package.json
+++ b/reboot/examples/bank-pydantic/web/package.json
@@ -5,9 +5,9 @@
"type": "module",
"dependencies": {
"@eslint/js": "^9.34.0",
- "@reboot-dev/reboot-react": "1.2.0",
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-std": "1.2.0",
+ "@reboot-dev/reboot-react": "1.2.1",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-std": "1.2.1",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/eslint__js": "^8.42.3",
diff --git a/reboot/examples/bank-zod/package-lock.json b/reboot/examples/bank-zod/package-lock.json
index f9151a98..225bdd85 100644
--- a/reboot/examples/bank-zod/package-lock.json
+++ b/reboot/examples/bank-zod/package-lock.json
@@ -9,10 +9,10 @@
"version": "0.1.0",
"dependencies": {
"@bufbuild/protobuf": "1.10.1",
- "@reboot-dev/reboot": "1.2.0",
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-react": "1.2.0",
- "@reboot-dev/reboot-std": "1.2.0",
+ "@reboot-dev/reboot": "1.2.1",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-react": "1.2.1",
+ "@reboot-dev/reboot-std": "1.2.1",
"@tailwindcss/vite": "^4.1.11",
"@types/node": "20.11.5",
"lucide-react": "^0.525.0",
@@ -1242,15 +1242,15 @@
}
},
"node_modules/@reboot-dev/reboot": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.2.0.tgz",
- "integrity": "sha512-VPjg8N0ab2iZzQ8PeVnATye5iPjEfmT7digqxxCKBYndOB/W8CjKg4YUVREzg+hNTrILSDmgJex2lpPd5yo4TQ==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.2.1.tgz",
+ "integrity": "sha512-1x35ldNqzFLUbRtjXrAfd27YDVX43gGYA+4O9BOx14yBu4y3OC4JZHT4Am7pVRuHJs4qNWSj0dCBFnEtqL3Rvg==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@bufbuild/protoc-gen-es": "1.10.1",
"@bufbuild/protoplugin": "1.10.1",
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
"@scarf/scarf": "1.4.0",
"@standard-schema/spec": "1.0.0",
"chalk": "^4.1.2",
@@ -1279,9 +1279,9 @@
}
},
"node_modules/@reboot-dev/reboot-api": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.0.tgz",
- "integrity": "sha512-6k0OhzPX3fcUurmxQY9ITJCX3LrMnimUYIdkPTEWWIpzV1jirpijx5ovTFG08MACic8mu1UMvCLEv7wq3/w1pg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.1.tgz",
+ "integrity": "sha512-CmrL7P/dtm+DBMWLWijs+NIlt0PKmdelwmP3Efmsz9x1dkICn6iq/MOh7FLVe2gcHubKL6RuiwO3vOOBVzObPg==",
"license": "Apache-2.0",
"dependencies": {
"@scarf/scarf": "1.4.0",
@@ -1305,15 +1305,15 @@
}
},
"node_modules/@reboot-dev/reboot-react": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.0.tgz",
- "integrity": "sha512-gNkSLFRgWDl40SbW7VNO+xD16DaFdPBZ95E2bULEHpAcyd8lsg5qiHHCPYThwCkDou/HqWbb/2WH5iiQaBFmRg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.1.tgz",
+ "integrity": "sha512-ImVCLLaOTZoDDtAnXdXufScZWOz2DVomVuhs6WwEgjW99yTtwTx1OMsC/wbU37pSmadmdfdlTYP9vy/V9EcFMg==",
"license": "Apache-2.0",
"dependencies": {
"@modelcontextprotocol/ext-apps": "1.5.0",
"@modelcontextprotocol/sdk": "1.29.0",
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-web": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-web": "1.2.1",
"@scarf/scarf": "1.4.0",
"@types/uuid": "^9.0.4",
"js-sha1": "0.7.0",
@@ -1340,32 +1340,32 @@
}
},
"node_modules/@reboot-dev/reboot-std": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std/-/reboot-std-1.2.0.tgz",
- "integrity": "sha512-Jblt3IM3hFHZdf78zv27/FL4IZL3l/ULnTFKwB4LloIlHVnhNegisHISvHn2fAkM8VzDy64s174tA/jHPHUyrw==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std/-/reboot-std-1.2.1.tgz",
+ "integrity": "sha512-+4oa3p/om+WsEc5/CVkYP5tTlBpZmIDMaEsMxgDScsa5txDwAxAPEIrYpSFppUZ/WZo2OzbjqDnUyp491Iym/g==",
"license": "Apache-2.0",
"dependencies": {
- "@reboot-dev/reboot": "1.2.0",
- "@reboot-dev/reboot-std-api": "1.2.0",
+ "@reboot-dev/reboot": "1.2.1",
+ "@reboot-dev/reboot-std-api": "1.2.1",
"@scarf/scarf": "1.4.0"
}
},
"node_modules/@reboot-dev/reboot-std-api": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std-api/-/reboot-std-api-1.2.0.tgz",
- "integrity": "sha512-8EI8+Ys4gWlVNiq/5f0ZU2W76CkddqLygdDrZTqgLNsI/Y4wa/ZOFMiEGvayULTzQu0Og5pZ0jXbXrak9iYTqw==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std-api/-/reboot-std-api-1.2.1.tgz",
+ "integrity": "sha512-evFzB6oQDURxkddknXakyRKcYT53EY1AlyQnNs3nffF84gWMjIbZ6nb3C5yDNnra+pDjsiiz7DsBhMvr9qHX2g==",
"license": "Apache-2.0",
"dependencies": {
"@scarf/scarf": "1.4.0"
}
},
"node_modules/@reboot-dev/reboot-web": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.0.tgz",
- "integrity": "sha512-ziBVJ5WLlnR57nkCUAJc9OAdqnl4PjoIYPdzMMkEYh3wpf10LPT/souGQT9c5BHUSWOx81azdQqU5lCqYXKWsg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.1.tgz",
+ "integrity": "sha512-/jk3OdG6pF+McvIP6yEuJKSWmDpHSR4+nVdbvUIHDdJh8cQkFj2QnzQJ+pRbql207LvwUNJ3T9YwZDSYz8rB9A==",
"license": "Apache-2.0",
"dependencies": {
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
"@scarf/scarf": "1.4.0",
"js-sha1": "0.7.0",
"lru-cache-idb": "^0.5.2",
@@ -7520,13 +7520,13 @@
"optional": true
},
"@reboot-dev/reboot": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.2.0.tgz",
- "integrity": "sha512-VPjg8N0ab2iZzQ8PeVnATye5iPjEfmT7digqxxCKBYndOB/W8CjKg4YUVREzg+hNTrILSDmgJex2lpPd5yo4TQ==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.2.1.tgz",
+ "integrity": "sha512-1x35ldNqzFLUbRtjXrAfd27YDVX43gGYA+4O9BOx14yBu4y3OC4JZHT4Am7pVRuHJs4qNWSj0dCBFnEtqL3Rvg==",
"requires": {
"@bufbuild/protoc-gen-es": "1.10.1",
"@bufbuild/protoplugin": "1.10.1",
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
"@scarf/scarf": "1.4.0",
"@standard-schema/spec": "1.0.0",
"chalk": "^4.1.2",
@@ -7543,9 +7543,9 @@
}
},
"@reboot-dev/reboot-api": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.0.tgz",
- "integrity": "sha512-6k0OhzPX3fcUurmxQY9ITJCX3LrMnimUYIdkPTEWWIpzV1jirpijx5ovTFG08MACic8mu1UMvCLEv7wq3/w1pg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.1.tgz",
+ "integrity": "sha512-CmrL7P/dtm+DBMWLWijs+NIlt0PKmdelwmP3Efmsz9x1dkICn6iq/MOh7FLVe2gcHubKL6RuiwO3vOOBVzObPg==",
"requires": {
"@scarf/scarf": "1.4.0",
"typescript": "5.4.5",
@@ -7560,14 +7560,14 @@
}
},
"@reboot-dev/reboot-react": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.0.tgz",
- "integrity": "sha512-gNkSLFRgWDl40SbW7VNO+xD16DaFdPBZ95E2bULEHpAcyd8lsg5qiHHCPYThwCkDou/HqWbb/2WH5iiQaBFmRg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.1.tgz",
+ "integrity": "sha512-ImVCLLaOTZoDDtAnXdXufScZWOz2DVomVuhs6WwEgjW99yTtwTx1OMsC/wbU37pSmadmdfdlTYP9vy/V9EcFMg==",
"requires": {
"@modelcontextprotocol/ext-apps": "1.5.0",
"@modelcontextprotocol/sdk": "1.29.0",
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-web": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-web": "1.2.1",
"@scarf/scarf": "1.4.0",
"@types/uuid": "^9.0.4",
"js-sha1": "0.7.0",
@@ -7584,29 +7584,29 @@
}
},
"@reboot-dev/reboot-std": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std/-/reboot-std-1.2.0.tgz",
- "integrity": "sha512-Jblt3IM3hFHZdf78zv27/FL4IZL3l/ULnTFKwB4LloIlHVnhNegisHISvHn2fAkM8VzDy64s174tA/jHPHUyrw==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std/-/reboot-std-1.2.1.tgz",
+ "integrity": "sha512-+4oa3p/om+WsEc5/CVkYP5tTlBpZmIDMaEsMxgDScsa5txDwAxAPEIrYpSFppUZ/WZo2OzbjqDnUyp491Iym/g==",
"requires": {
- "@reboot-dev/reboot": "1.2.0",
- "@reboot-dev/reboot-std-api": "1.2.0",
+ "@reboot-dev/reboot": "1.2.1",
+ "@reboot-dev/reboot-std-api": "1.2.1",
"@scarf/scarf": "1.4.0"
}
},
"@reboot-dev/reboot-std-api": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std-api/-/reboot-std-api-1.2.0.tgz",
- "integrity": "sha512-8EI8+Ys4gWlVNiq/5f0ZU2W76CkddqLygdDrZTqgLNsI/Y4wa/ZOFMiEGvayULTzQu0Og5pZ0jXbXrak9iYTqw==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std-api/-/reboot-std-api-1.2.1.tgz",
+ "integrity": "sha512-evFzB6oQDURxkddknXakyRKcYT53EY1AlyQnNs3nffF84gWMjIbZ6nb3C5yDNnra+pDjsiiz7DsBhMvr9qHX2g==",
"requires": {
"@scarf/scarf": "1.4.0"
}
},
"@reboot-dev/reboot-web": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.0.tgz",
- "integrity": "sha512-ziBVJ5WLlnR57nkCUAJc9OAdqnl4PjoIYPdzMMkEYh3wpf10LPT/souGQT9c5BHUSWOx81azdQqU5lCqYXKWsg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.1.tgz",
+ "integrity": "sha512-/jk3OdG6pF+McvIP6yEuJKSWmDpHSR4+nVdbvUIHDdJh8cQkFj2QnzQJ+pRbql207LvwUNJ3T9YwZDSYz8rB9A==",
"requires": {
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
"@scarf/scarf": "1.4.0",
"js-sha1": "0.7.0",
"lru-cache-idb": "^0.5.2",
diff --git a/reboot/examples/bank-zod/package.json b/reboot/examples/bank-zod/package.json
index 445e249f..996a3b43 100644
--- a/reboot/examples/bank-zod/package.json
+++ b/reboot/examples/bank-zod/package.json
@@ -9,10 +9,10 @@
},
"dependencies": {
"@bufbuild/protobuf": "1.10.1",
- "@reboot-dev/reboot": "1.2.0",
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-std": "1.2.0",
- "@reboot-dev/reboot-react": "1.2.0",
+ "@reboot-dev/reboot": "1.2.1",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-std": "1.2.1",
+ "@reboot-dev/reboot-react": "1.2.1",
"@tailwindcss/vite": "^4.1.11",
"@types/node": "20.11.5",
"lucide-react": "^0.525.0",
diff --git a/reboot/examples/bank/pyproject.toml b/reboot/examples/bank/pyproject.toml
index e8f504b3..764d99c8 100644
--- a/reboot/examples/bank/pyproject.toml
+++ b/reboot/examples/bank/pyproject.toml
@@ -3,7 +3,7 @@ name = "bank"
version = "0.1.0"
requires-python = ">= 3.10"
dependencies = [
- "reboot==1.2.0",
+ "reboot==1.2.1",
]
[dependency-groups]
diff --git a/reboot/examples/bank/uv.lock b/reboot/examples/bank/uv.lock
index 2071f8dd..73b83c22 100644
--- a/reboot/examples/bank/uv.lock
+++ b/reboot/examples/bank/uv.lock
@@ -213,7 +213,7 @@ dev = [
]
[package.metadata]
-requires-dist = [{ name = "reboot", specifier = "==1.2.0" }]
+requires-dist = [{ name = "reboot", specifier = "==1.2.1" }]
[package.metadata.requires-dev]
dev = [
@@ -1878,7 +1878,7 @@ wheels = [
[[package]]
name = "reboot"
-version = "1.2.0"
+version = "1.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiofiles" },
@@ -1928,9 +1928,9 @@ dependencies = [
{ name = "websockets" },
]
wheels = [
- { url = "https://files.pythonhosted.org/packages/ae/01/d67f4c2cfa6cd6605f25f50d8d6dd962cda3dbb1a3be5a413b27cecfe5ba/reboot-1.2.0-py3-none-macosx_14_0_arm64.whl", hash = "sha256:45c74d870833b0a092c451c117006623363fc559d1224522859d94993d0f6c1e", size = 20440626, upload-time = "2026-06-18T15:42:03.995Z" },
- { url = "https://files.pythonhosted.org/packages/62/fb/32f27dc3d16d5446ca8987cd81961ebc70c4d9d8e8b0cea71257f1a6b4b1/reboot-1.2.0-py3-none-manylinux_2_34_aarch64.whl", hash = "sha256:e3d026f55c1b084f50e417c70fe211d33ac1a796f42eefddd3e288ceb8ea2fd1", size = 24124117, upload-time = "2026-06-18T15:27:54.363Z" },
- { url = "https://files.pythonhosted.org/packages/74/e0/51021d59e6bc3802b1726aee7503d28d7ea0eb507896f0a4b7cac81fccf2/reboot-1.2.0-py3-none-manylinux_2_34_x86_64.whl", hash = "sha256:fb27f3b851e25b797d5cb6e9f93e606ac8ba0c3a138d83f126a2ca26bdfbbd57", size = 24254127, upload-time = "2026-06-18T15:29:00.942Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/85/082fea1aaf65904b2f732c893469cb1d2534dff96ba527ca5575c5afbf55/reboot-1.2.1-py3-none-macosx_14_0_arm64.whl", hash = "sha256:b54cbf0233f297926ce516d39578efaea47953d113eca7095d4d0f07a1e4b192", size = 20452221, upload-time = "2026-06-23T19:41:33.886Z" },
+ { url = "https://files.pythonhosted.org/packages/64/3a/e004b65f1bbef40d8da6051fcff73925fae358a4c3ccf7b00d00bd2c7421/reboot-1.2.1-py3-none-manylinux_2_34_aarch64.whl", hash = "sha256:bb91ee132940d66c6779c2e5038f2fbf70fd567351c912bf0acf33ba60111860", size = 24135744, upload-time = "2026-06-23T19:28:20.962Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/a3/7c8b5caf625a3f1b87b075b5bf23040a6f7761c797c245822144900999ce/reboot-1.2.1-py3-none-manylinux_2_34_x86_64.whl", hash = "sha256:988cb8d6e84981d35c1bf4c9040eda4181d4d221d0ff1e9a48922b7f50c123de", size = 24265726, upload-time = "2026-06-23T19:27:45.447Z" },
]
[[package]]
diff --git a/reboot/examples/bank/web/package-lock.json b/reboot/examples/bank/web/package-lock.json
index 59ddf8c1..c8352a68 100644
--- a/reboot/examples/bank/web/package-lock.json
+++ b/reboot/examples/bank/web/package-lock.json
@@ -10,7 +10,7 @@
"dependencies": {
"@bufbuild/protobuf": "1.10.1",
"@eslint/js": "^9.34.0",
- "@reboot-dev/reboot-react": "1.2.0",
+ "@reboot-dev/reboot-react": "1.2.1",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/eslint__js": "^8.42.3",
@@ -1225,9 +1225,9 @@
}
},
"node_modules/@reboot-dev/reboot-api": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.0.tgz",
- "integrity": "sha512-6k0OhzPX3fcUurmxQY9ITJCX3LrMnimUYIdkPTEWWIpzV1jirpijx5ovTFG08MACic8mu1UMvCLEv7wq3/w1pg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.1.tgz",
+ "integrity": "sha512-CmrL7P/dtm+DBMWLWijs+NIlt0PKmdelwmP3Efmsz9x1dkICn6iq/MOh7FLVe2gcHubKL6RuiwO3vOOBVzObPg==",
"license": "Apache-2.0",
"dependencies": {
"@scarf/scarf": "1.4.0",
@@ -1252,15 +1252,15 @@
}
},
"node_modules/@reboot-dev/reboot-react": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.0.tgz",
- "integrity": "sha512-gNkSLFRgWDl40SbW7VNO+xD16DaFdPBZ95E2bULEHpAcyd8lsg5qiHHCPYThwCkDou/HqWbb/2WH5iiQaBFmRg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.1.tgz",
+ "integrity": "sha512-ImVCLLaOTZoDDtAnXdXufScZWOz2DVomVuhs6WwEgjW99yTtwTx1OMsC/wbU37pSmadmdfdlTYP9vy/V9EcFMg==",
"license": "Apache-2.0",
"dependencies": {
"@modelcontextprotocol/ext-apps": "1.5.0",
"@modelcontextprotocol/sdk": "1.29.0",
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-web": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-web": "1.2.1",
"@scarf/scarf": "1.4.0",
"@types/uuid": "^9.0.4",
"js-sha1": "0.7.0",
@@ -1287,12 +1287,12 @@
}
},
"node_modules/@reboot-dev/reboot-web": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.0.tgz",
- "integrity": "sha512-ziBVJ5WLlnR57nkCUAJc9OAdqnl4PjoIYPdzMMkEYh3wpf10LPT/souGQT9c5BHUSWOx81azdQqU5lCqYXKWsg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.1.tgz",
+ "integrity": "sha512-/jk3OdG6pF+McvIP6yEuJKSWmDpHSR4+nVdbvUIHDdJh8cQkFj2QnzQJ+pRbql207LvwUNJ3T9YwZDSYz8rB9A==",
"license": "Apache-2.0",
"dependencies": {
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
"@scarf/scarf": "1.4.0",
"js-sha1": "0.7.0",
"lru-cache-idb": "^0.5.2",
@@ -6620,9 +6620,9 @@
}
},
"@reboot-dev/reboot-api": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.0.tgz",
- "integrity": "sha512-6k0OhzPX3fcUurmxQY9ITJCX3LrMnimUYIdkPTEWWIpzV1jirpijx5ovTFG08MACic8mu1UMvCLEv7wq3/w1pg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.1.tgz",
+ "integrity": "sha512-CmrL7P/dtm+DBMWLWijs+NIlt0PKmdelwmP3Efmsz9x1dkICn6iq/MOh7FLVe2gcHubKL6RuiwO3vOOBVzObPg==",
"requires": {
"@scarf/scarf": "1.4.0",
"typescript": "5.4.5",
@@ -6637,14 +6637,14 @@
}
},
"@reboot-dev/reboot-react": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.0.tgz",
- "integrity": "sha512-gNkSLFRgWDl40SbW7VNO+xD16DaFdPBZ95E2bULEHpAcyd8lsg5qiHHCPYThwCkDou/HqWbb/2WH5iiQaBFmRg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.1.tgz",
+ "integrity": "sha512-ImVCLLaOTZoDDtAnXdXufScZWOz2DVomVuhs6WwEgjW99yTtwTx1OMsC/wbU37pSmadmdfdlTYP9vy/V9EcFMg==",
"requires": {
"@modelcontextprotocol/ext-apps": "1.5.0",
"@modelcontextprotocol/sdk": "1.29.0",
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-web": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-web": "1.2.1",
"@scarf/scarf": "1.4.0",
"@types/uuid": "^9.0.4",
"js-sha1": "0.7.0",
@@ -6661,11 +6661,11 @@
}
},
"@reboot-dev/reboot-web": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.0.tgz",
- "integrity": "sha512-ziBVJ5WLlnR57nkCUAJc9OAdqnl4PjoIYPdzMMkEYh3wpf10LPT/souGQT9c5BHUSWOx81azdQqU5lCqYXKWsg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.1.tgz",
+ "integrity": "sha512-/jk3OdG6pF+McvIP6yEuJKSWmDpHSR4+nVdbvUIHDdJh8cQkFj2QnzQJ+pRbql207LvwUNJ3T9YwZDSYz8rB9A==",
"requires": {
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
"@scarf/scarf": "1.4.0",
"js-sha1": "0.7.0",
"lru-cache-idb": "^0.5.2",
diff --git a/reboot/examples/bank/web/package.json b/reboot/examples/bank/web/package.json
index 85b94437..934cbc5b 100644
--- a/reboot/examples/bank/web/package.json
+++ b/reboot/examples/bank/web/package.json
@@ -6,7 +6,7 @@
"dependencies": {
"@bufbuild/protobuf": "1.10.1",
"@eslint/js": "^9.34.0",
- "@reboot-dev/reboot-react": "1.2.0",
+ "@reboot-dev/reboot-react": "1.2.1",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/eslint__js": "^8.42.3",
diff --git a/reboot/examples/boutique/Dockerfile b/reboot/examples/boutique/Dockerfile
index 498213a7..1777e12c 100644
--- a/reboot/examples/boutique/Dockerfile
+++ b/reboot/examples/boutique/Dockerfile
@@ -1,4 +1,4 @@
-FROM ghcr.io/reboot-dev/reboot-base:1.2.0
+FROM ghcr.io/reboot-dev/reboot-base:1.2.1
WORKDIR /app
diff --git a/reboot/examples/boutique/pyproject.toml b/reboot/examples/boutique/pyproject.toml
index 1752bfcb..d091b843 100644
--- a/reboot/examples/boutique/pyproject.toml
+++ b/reboot/examples/boutique/pyproject.toml
@@ -3,7 +3,7 @@ name = "boutique"
version = "0.1.0"
requires-python = ">= 3.10"
dependencies = [
- "reboot==1.2.0",
+ "reboot==1.2.1",
]
[dependency-groups]
diff --git a/reboot/examples/boutique/uv.lock b/reboot/examples/boutique/uv.lock
index 07c590c5..43512288 100644
--- a/reboot/examples/boutique/uv.lock
+++ b/reboot/examples/boutique/uv.lock
@@ -300,7 +300,7 @@ dev = [
]
[package.metadata]
-requires-dist = [{ name = "reboot", specifier = "==1.2.0" }]
+requires-dist = [{ name = "reboot", specifier = "==1.2.1" }]
[package.metadata.requires-dev]
dev = [
@@ -1925,7 +1925,7 @@ wheels = [
[[package]]
name = "reboot"
-version = "1.2.0"
+version = "1.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiofiles" },
@@ -1975,9 +1975,9 @@ dependencies = [
{ name = "websockets" },
]
wheels = [
- { url = "https://files.pythonhosted.org/packages/ae/01/d67f4c2cfa6cd6605f25f50d8d6dd962cda3dbb1a3be5a413b27cecfe5ba/reboot-1.2.0-py3-none-macosx_14_0_arm64.whl", hash = "sha256:45c74d870833b0a092c451c117006623363fc559d1224522859d94993d0f6c1e", size = 20440626, upload-time = "2026-06-18T15:42:03.995Z" },
- { url = "https://files.pythonhosted.org/packages/62/fb/32f27dc3d16d5446ca8987cd81961ebc70c4d9d8e8b0cea71257f1a6b4b1/reboot-1.2.0-py3-none-manylinux_2_34_aarch64.whl", hash = "sha256:e3d026f55c1b084f50e417c70fe211d33ac1a796f42eefddd3e288ceb8ea2fd1", size = 24124117, upload-time = "2026-06-18T15:27:54.363Z" },
- { url = "https://files.pythonhosted.org/packages/74/e0/51021d59e6bc3802b1726aee7503d28d7ea0eb507896f0a4b7cac81fccf2/reboot-1.2.0-py3-none-manylinux_2_34_x86_64.whl", hash = "sha256:fb27f3b851e25b797d5cb6e9f93e606ac8ba0c3a138d83f126a2ca26bdfbbd57", size = 24254127, upload-time = "2026-06-18T15:29:00.942Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/85/082fea1aaf65904b2f732c893469cb1d2534dff96ba527ca5575c5afbf55/reboot-1.2.1-py3-none-macosx_14_0_arm64.whl", hash = "sha256:b54cbf0233f297926ce516d39578efaea47953d113eca7095d4d0f07a1e4b192", size = 20452221, upload-time = "2026-06-23T19:41:33.886Z" },
+ { url = "https://files.pythonhosted.org/packages/64/3a/e004b65f1bbef40d8da6051fcff73925fae358a4c3ccf7b00d00bd2c7421/reboot-1.2.1-py3-none-manylinux_2_34_aarch64.whl", hash = "sha256:bb91ee132940d66c6779c2e5038f2fbf70fd567351c912bf0acf33ba60111860", size = 24135744, upload-time = "2026-06-23T19:28:20.962Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/a3/7c8b5caf625a3f1b87b075b5bf23040a6f7761c797c245822144900999ce/reboot-1.2.1-py3-none-manylinux_2_34_x86_64.whl", hash = "sha256:988cb8d6e84981d35c1bf4c9040eda4181d4d221d0ff1e9a48922b7f50c123de", size = 24265726, upload-time = "2026-06-23T19:27:45.447Z" },
]
[[package]]
diff --git a/reboot/examples/boutique/web/package-lock.json b/reboot/examples/boutique/web/package-lock.json
index 99532ed2..92255b78 100644
--- a/reboot/examples/boutique/web/package-lock.json
+++ b/reboot/examples/boutique/web/package-lock.json
@@ -10,7 +10,7 @@
"dependencies": {
"@bufbuild/protobuf": "1.10.1",
"@eslint/js": "^9.34.0",
- "@reboot-dev/reboot-react": "1.2.0",
+ "@reboot-dev/reboot-react": "1.2.1",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/eslint__js": "^8.42.3",
@@ -1225,9 +1225,9 @@
}
},
"node_modules/@reboot-dev/reboot-api": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.0.tgz",
- "integrity": "sha512-6k0OhzPX3fcUurmxQY9ITJCX3LrMnimUYIdkPTEWWIpzV1jirpijx5ovTFG08MACic8mu1UMvCLEv7wq3/w1pg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.1.tgz",
+ "integrity": "sha512-CmrL7P/dtm+DBMWLWijs+NIlt0PKmdelwmP3Efmsz9x1dkICn6iq/MOh7FLVe2gcHubKL6RuiwO3vOOBVzObPg==",
"license": "Apache-2.0",
"dependencies": {
"@scarf/scarf": "1.4.0",
@@ -1252,15 +1252,15 @@
}
},
"node_modules/@reboot-dev/reboot-react": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.0.tgz",
- "integrity": "sha512-gNkSLFRgWDl40SbW7VNO+xD16DaFdPBZ95E2bULEHpAcyd8lsg5qiHHCPYThwCkDou/HqWbb/2WH5iiQaBFmRg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.1.tgz",
+ "integrity": "sha512-ImVCLLaOTZoDDtAnXdXufScZWOz2DVomVuhs6WwEgjW99yTtwTx1OMsC/wbU37pSmadmdfdlTYP9vy/V9EcFMg==",
"license": "Apache-2.0",
"dependencies": {
"@modelcontextprotocol/ext-apps": "1.5.0",
"@modelcontextprotocol/sdk": "1.29.0",
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-web": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-web": "1.2.1",
"@scarf/scarf": "1.4.0",
"@types/uuid": "^9.0.4",
"js-sha1": "0.7.0",
@@ -1300,12 +1300,12 @@
}
},
"node_modules/@reboot-dev/reboot-web": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.0.tgz",
- "integrity": "sha512-ziBVJ5WLlnR57nkCUAJc9OAdqnl4PjoIYPdzMMkEYh3wpf10LPT/souGQT9c5BHUSWOx81azdQqU5lCqYXKWsg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.1.tgz",
+ "integrity": "sha512-/jk3OdG6pF+McvIP6yEuJKSWmDpHSR4+nVdbvUIHDdJh8cQkFj2QnzQJ+pRbql207LvwUNJ3T9YwZDSYz8rB9A==",
"license": "Apache-2.0",
"dependencies": {
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
"@scarf/scarf": "1.4.0",
"js-sha1": "0.7.0",
"lru-cache-idb": "^0.5.2",
@@ -6729,9 +6729,9 @@
}
},
"@reboot-dev/reboot-api": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.0.tgz",
- "integrity": "sha512-6k0OhzPX3fcUurmxQY9ITJCX3LrMnimUYIdkPTEWWIpzV1jirpijx5ovTFG08MACic8mu1UMvCLEv7wq3/w1pg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.1.tgz",
+ "integrity": "sha512-CmrL7P/dtm+DBMWLWijs+NIlt0PKmdelwmP3Efmsz9x1dkICn6iq/MOh7FLVe2gcHubKL6RuiwO3vOOBVzObPg==",
"requires": {
"@scarf/scarf": "1.4.0",
"typescript": "5.4.5",
@@ -6746,14 +6746,14 @@
}
},
"@reboot-dev/reboot-react": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.0.tgz",
- "integrity": "sha512-gNkSLFRgWDl40SbW7VNO+xD16DaFdPBZ95E2bULEHpAcyd8lsg5qiHHCPYThwCkDou/HqWbb/2WH5iiQaBFmRg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.1.tgz",
+ "integrity": "sha512-ImVCLLaOTZoDDtAnXdXufScZWOz2DVomVuhs6WwEgjW99yTtwTx1OMsC/wbU37pSmadmdfdlTYP9vy/V9EcFMg==",
"requires": {
"@modelcontextprotocol/ext-apps": "1.5.0",
"@modelcontextprotocol/sdk": "1.29.0",
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-web": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-web": "1.2.1",
"@scarf/scarf": "1.4.0",
"@types/uuid": "^9.0.4",
"js-sha1": "0.7.0",
@@ -6775,11 +6775,11 @@
}
},
"@reboot-dev/reboot-web": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.0.tgz",
- "integrity": "sha512-ziBVJ5WLlnR57nkCUAJc9OAdqnl4PjoIYPdzMMkEYh3wpf10LPT/souGQT9c5BHUSWOx81azdQqU5lCqYXKWsg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.1.tgz",
+ "integrity": "sha512-/jk3OdG6pF+McvIP6yEuJKSWmDpHSR4+nVdbvUIHDdJh8cQkFj2QnzQJ+pRbql207LvwUNJ3T9YwZDSYz8rB9A==",
"requires": {
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
"@scarf/scarf": "1.4.0",
"js-sha1": "0.7.0",
"lru-cache-idb": "^0.5.2",
diff --git a/reboot/examples/boutique/web/package.json b/reboot/examples/boutique/web/package.json
index e44d8a2d..f98277f1 100644
--- a/reboot/examples/boutique/web/package.json
+++ b/reboot/examples/boutique/web/package.json
@@ -6,7 +6,7 @@
"dependencies": {
"@bufbuild/protobuf": "1.10.1",
"@eslint/js": "^9.34.0",
- "@reboot-dev/reboot-react": "1.2.0",
+ "@reboot-dev/reboot-react": "1.2.1",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/eslint__js": "^8.42.3",
diff --git a/reboot/examples/chat-room-nodejs/Dockerfile b/reboot/examples/chat-room-nodejs/Dockerfile
index 40b295d0..1aa8cb10 100644
--- a/reboot/examples/chat-room-nodejs/Dockerfile
+++ b/reboot/examples/chat-room-nodejs/Dockerfile
@@ -1,4 +1,4 @@
-FROM ghcr.io/reboot-dev/reboot-base:1.2.0
+FROM ghcr.io/reboot-dev/reboot-base:1.2.1
WORKDIR /app
diff --git a/reboot/examples/chat-room-nodejs/package-lock.json b/reboot/examples/chat-room-nodejs/package-lock.json
index 1c2955d4..de62d6cb 100644
--- a/reboot/examples/chat-room-nodejs/package-lock.json
+++ b/reboot/examples/chat-room-nodejs/package-lock.json
@@ -9,7 +9,7 @@
"version": "0.1.0",
"dependencies": {
"@bufbuild/protobuf": "1.10.1",
- "@reboot-dev/reboot": "1.2.0",
+ "@reboot-dev/reboot": "1.2.1",
"@types/node": "20.11.5",
"typescript": "5.4.5"
},
@@ -494,15 +494,15 @@
}
},
"node_modules/@reboot-dev/reboot": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.2.0.tgz",
- "integrity": "sha512-VPjg8N0ab2iZzQ8PeVnATye5iPjEfmT7digqxxCKBYndOB/W8CjKg4YUVREzg+hNTrILSDmgJex2lpPd5yo4TQ==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.2.1.tgz",
+ "integrity": "sha512-1x35ldNqzFLUbRtjXrAfd27YDVX43gGYA+4O9BOx14yBu4y3OC4JZHT4Am7pVRuHJs4qNWSj0dCBFnEtqL3Rvg==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@bufbuild/protoc-gen-es": "1.10.1",
"@bufbuild/protoplugin": "1.10.1",
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
"@scarf/scarf": "1.4.0",
"@standard-schema/spec": "1.0.0",
"chalk": "^4.1.2",
@@ -531,9 +531,9 @@
}
},
"node_modules/@reboot-dev/reboot-api": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.0.tgz",
- "integrity": "sha512-6k0OhzPX3fcUurmxQY9ITJCX3LrMnimUYIdkPTEWWIpzV1jirpijx5ovTFG08MACic8mu1UMvCLEv7wq3/w1pg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.1.tgz",
+ "integrity": "sha512-CmrL7P/dtm+DBMWLWijs+NIlt0PKmdelwmP3Efmsz9x1dkICn6iq/MOh7FLVe2gcHubKL6RuiwO3vOOBVzObPg==",
"license": "Apache-2.0",
"dependencies": {
"@scarf/scarf": "1.4.0",
@@ -2403,13 +2403,13 @@
}
},
"@reboot-dev/reboot": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.2.0.tgz",
- "integrity": "sha512-VPjg8N0ab2iZzQ8PeVnATye5iPjEfmT7digqxxCKBYndOB/W8CjKg4YUVREzg+hNTrILSDmgJex2lpPd5yo4TQ==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.2.1.tgz",
+ "integrity": "sha512-1x35ldNqzFLUbRtjXrAfd27YDVX43gGYA+4O9BOx14yBu4y3OC4JZHT4Am7pVRuHJs4qNWSj0dCBFnEtqL3Rvg==",
"requires": {
"@bufbuild/protoc-gen-es": "1.10.1",
"@bufbuild/protoplugin": "1.10.1",
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
"@scarf/scarf": "1.4.0",
"@standard-schema/spec": "1.0.0",
"chalk": "^4.1.2",
@@ -2426,9 +2426,9 @@
}
},
"@reboot-dev/reboot-api": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.0.tgz",
- "integrity": "sha512-6k0OhzPX3fcUurmxQY9ITJCX3LrMnimUYIdkPTEWWIpzV1jirpijx5ovTFG08MACic8mu1UMvCLEv7wq3/w1pg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.1.tgz",
+ "integrity": "sha512-CmrL7P/dtm+DBMWLWijs+NIlt0PKmdelwmP3Efmsz9x1dkICn6iq/MOh7FLVe2gcHubKL6RuiwO3vOOBVzObPg==",
"requires": {
"@scarf/scarf": "1.4.0",
"typescript": "5.4.5",
diff --git a/reboot/examples/chat-room-nodejs/package.json b/reboot/examples/chat-room-nodejs/package.json
index 08ef965f..a928052a 100644
--- a/reboot/examples/chat-room-nodejs/package.json
+++ b/reboot/examples/chat-room-nodejs/package.json
@@ -5,7 +5,7 @@
"type": "module",
"dependencies": {
"@bufbuild/protobuf": "1.10.1",
- "@reboot-dev/reboot": "1.2.0",
+ "@reboot-dev/reboot": "1.2.1",
"typescript": "5.4.5",
"@types/node": "20.11.5"
},
diff --git a/reboot/examples/chat-room/Dockerfile b/reboot/examples/chat-room/Dockerfile
index 4c1bfd61..ce9d11ed 100644
--- a/reboot/examples/chat-room/Dockerfile
+++ b/reboot/examples/chat-room/Dockerfile
@@ -1,4 +1,4 @@
-FROM ghcr.io/reboot-dev/reboot-base:1.2.0
+FROM ghcr.io/reboot-dev/reboot-base:1.2.1
WORKDIR /app
diff --git a/reboot/examples/chat-room/pyproject.toml b/reboot/examples/chat-room/pyproject.toml
index a39891bb..c3a22481 100644
--- a/reboot/examples/chat-room/pyproject.toml
+++ b/reboot/examples/chat-room/pyproject.toml
@@ -3,7 +3,7 @@ name = "chat-room"
version = "0.1.0"
requires-python = ">= 3.10"
dependencies = [
- "reboot==1.2.0",
+ "reboot==1.2.1",
]
[dependency-groups]
diff --git a/reboot/examples/chat-room/reboot-non-react-web/package-lock.json b/reboot/examples/chat-room/reboot-non-react-web/package-lock.json
index 53382d16..840dcd77 100644
--- a/reboot/examples/chat-room/reboot-non-react-web/package-lock.json
+++ b/reboot/examples/chat-room/reboot-non-react-web/package-lock.json
@@ -9,7 +9,7 @@
"version": "0.0.0",
"dependencies": {
"@bufbuild/protobuf": "1.10.1",
- "@reboot-dev/reboot-web": "1.2.0",
+ "@reboot-dev/reboot-web": "1.2.1",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
@@ -71,9 +71,9 @@
}
},
"node_modules/@reboot-dev/reboot-api": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.0.tgz",
- "integrity": "sha512-6k0OhzPX3fcUurmxQY9ITJCX3LrMnimUYIdkPTEWWIpzV1jirpijx5ovTFG08MACic8mu1UMvCLEv7wq3/w1pg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.1.tgz",
+ "integrity": "sha512-CmrL7P/dtm+DBMWLWijs+NIlt0PKmdelwmP3Efmsz9x1dkICn6iq/MOh7FLVe2gcHubKL6RuiwO3vOOBVzObPg==",
"license": "Apache-2.0",
"dependencies": {
"@scarf/scarf": "1.4.0",
@@ -98,12 +98,12 @@
}
},
"node_modules/@reboot-dev/reboot-web": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.0.tgz",
- "integrity": "sha512-ziBVJ5WLlnR57nkCUAJc9OAdqnl4PjoIYPdzMMkEYh3wpf10LPT/souGQT9c5BHUSWOx81azdQqU5lCqYXKWsg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.1.tgz",
+ "integrity": "sha512-/jk3OdG6pF+McvIP6yEuJKSWmDpHSR4+nVdbvUIHDdJh8cQkFj2QnzQJ+pRbql207LvwUNJ3T9YwZDSYz8rB9A==",
"license": "Apache-2.0",
"dependencies": {
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
"@scarf/scarf": "1.4.0",
"js-sha1": "0.7.0",
"lru-cache-idb": "^0.5.2",
diff --git a/reboot/examples/chat-room/reboot-non-react-web/package.json b/reboot/examples/chat-room/reboot-non-react-web/package.json
index 5b4054cf..8f771422 100644
--- a/reboot/examples/chat-room/reboot-non-react-web/package.json
+++ b/reboot/examples/chat-room/reboot-non-react-web/package.json
@@ -10,7 +10,7 @@
},
"dependencies": {
"@bufbuild/protobuf": "1.10.1",
- "@reboot-dev/reboot-web": "1.2.0",
+ "@reboot-dev/reboot-web": "1.2.1",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
diff --git a/reboot/examples/chat-room/uv.lock b/reboot/examples/chat-room/uv.lock
index 96f8839d..4beb650d 100644
--- a/reboot/examples/chat-room/uv.lock
+++ b/reboot/examples/chat-room/uv.lock
@@ -366,7 +366,7 @@ dev = [
]
[package.metadata]
-requires-dist = [{ name = "reboot", specifier = "==1.2.0" }]
+requires-dist = [{ name = "reboot", specifier = "==1.2.1" }]
[package.metadata.requires-dev]
dev = [
@@ -1925,7 +1925,7 @@ wheels = [
[[package]]
name = "reboot"
-version = "1.2.0"
+version = "1.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiofiles" },
@@ -1975,9 +1975,9 @@ dependencies = [
{ name = "websockets" },
]
wheels = [
- { url = "https://files.pythonhosted.org/packages/ae/01/d67f4c2cfa6cd6605f25f50d8d6dd962cda3dbb1a3be5a413b27cecfe5ba/reboot-1.2.0-py3-none-macosx_14_0_arm64.whl", hash = "sha256:45c74d870833b0a092c451c117006623363fc559d1224522859d94993d0f6c1e", size = 20440626, upload-time = "2026-06-18T15:42:03.995Z" },
- { url = "https://files.pythonhosted.org/packages/62/fb/32f27dc3d16d5446ca8987cd81961ebc70c4d9d8e8b0cea71257f1a6b4b1/reboot-1.2.0-py3-none-manylinux_2_34_aarch64.whl", hash = "sha256:e3d026f55c1b084f50e417c70fe211d33ac1a796f42eefddd3e288ceb8ea2fd1", size = 24124117, upload-time = "2026-06-18T15:27:54.363Z" },
- { url = "https://files.pythonhosted.org/packages/74/e0/51021d59e6bc3802b1726aee7503d28d7ea0eb507896f0a4b7cac81fccf2/reboot-1.2.0-py3-none-manylinux_2_34_x86_64.whl", hash = "sha256:fb27f3b851e25b797d5cb6e9f93e606ac8ba0c3a138d83f126a2ca26bdfbbd57", size = 24254127, upload-time = "2026-06-18T15:29:00.942Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/85/082fea1aaf65904b2f732c893469cb1d2534dff96ba527ca5575c5afbf55/reboot-1.2.1-py3-none-macosx_14_0_arm64.whl", hash = "sha256:b54cbf0233f297926ce516d39578efaea47953d113eca7095d4d0f07a1e4b192", size = 20452221, upload-time = "2026-06-23T19:41:33.886Z" },
+ { url = "https://files.pythonhosted.org/packages/64/3a/e004b65f1bbef40d8da6051fcff73925fae358a4c3ccf7b00d00bd2c7421/reboot-1.2.1-py3-none-manylinux_2_34_aarch64.whl", hash = "sha256:bb91ee132940d66c6779c2e5038f2fbf70fd567351c912bf0acf33ba60111860", size = 24135744, upload-time = "2026-06-23T19:28:20.962Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/a3/7c8b5caf625a3f1b87b075b5bf23040a6f7761c797c245822144900999ce/reboot-1.2.1-py3-none-manylinux_2_34_x86_64.whl", hash = "sha256:988cb8d6e84981d35c1bf4c9040eda4181d4d221d0ff1e9a48922b7f50c123de", size = 24265726, upload-time = "2026-06-23T19:27:45.447Z" },
]
[[package]]
diff --git a/reboot/examples/chat-room/web/package-lock.json b/reboot/examples/chat-room/web/package-lock.json
index fe25936c..3dc890b5 100644
--- a/reboot/examples/chat-room/web/package-lock.json
+++ b/reboot/examples/chat-room/web/package-lock.json
@@ -10,7 +10,7 @@
"dependencies": {
"@bufbuild/protobuf": "1.10.1",
"@eslint/js": "^9.34.0",
- "@reboot-dev/reboot-react": "1.2.0",
+ "@reboot-dev/reboot-react": "1.2.1",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/eslint__js": "^8.42.3",
@@ -1225,9 +1225,9 @@
}
},
"node_modules/@reboot-dev/reboot-api": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.0.tgz",
- "integrity": "sha512-6k0OhzPX3fcUurmxQY9ITJCX3LrMnimUYIdkPTEWWIpzV1jirpijx5ovTFG08MACic8mu1UMvCLEv7wq3/w1pg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.1.tgz",
+ "integrity": "sha512-CmrL7P/dtm+DBMWLWijs+NIlt0PKmdelwmP3Efmsz9x1dkICn6iq/MOh7FLVe2gcHubKL6RuiwO3vOOBVzObPg==",
"license": "Apache-2.0",
"dependencies": {
"@scarf/scarf": "1.4.0",
@@ -1252,15 +1252,15 @@
}
},
"node_modules/@reboot-dev/reboot-react": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.0.tgz",
- "integrity": "sha512-gNkSLFRgWDl40SbW7VNO+xD16DaFdPBZ95E2bULEHpAcyd8lsg5qiHHCPYThwCkDou/HqWbb/2WH5iiQaBFmRg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.1.tgz",
+ "integrity": "sha512-ImVCLLaOTZoDDtAnXdXufScZWOz2DVomVuhs6WwEgjW99yTtwTx1OMsC/wbU37pSmadmdfdlTYP9vy/V9EcFMg==",
"license": "Apache-2.0",
"dependencies": {
"@modelcontextprotocol/ext-apps": "1.5.0",
"@modelcontextprotocol/sdk": "1.29.0",
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-web": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-web": "1.2.1",
"@scarf/scarf": "1.4.0",
"@types/uuid": "^9.0.4",
"js-sha1": "0.7.0",
@@ -1286,12 +1286,12 @@
}
},
"node_modules/@reboot-dev/reboot-web": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.0.tgz",
- "integrity": "sha512-ziBVJ5WLlnR57nkCUAJc9OAdqnl4PjoIYPdzMMkEYh3wpf10LPT/souGQT9c5BHUSWOx81azdQqU5lCqYXKWsg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.1.tgz",
+ "integrity": "sha512-/jk3OdG6pF+McvIP6yEuJKSWmDpHSR4+nVdbvUIHDdJh8cQkFj2QnzQJ+pRbql207LvwUNJ3T9YwZDSYz8rB9A==",
"license": "Apache-2.0",
"dependencies": {
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
"@scarf/scarf": "1.4.0",
"js-sha1": "0.7.0",
"lru-cache-idb": "^0.5.2",
@@ -6564,9 +6564,9 @@
}
},
"@reboot-dev/reboot-api": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.0.tgz",
- "integrity": "sha512-6k0OhzPX3fcUurmxQY9ITJCX3LrMnimUYIdkPTEWWIpzV1jirpijx5ovTFG08MACic8mu1UMvCLEv7wq3/w1pg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.1.tgz",
+ "integrity": "sha512-CmrL7P/dtm+DBMWLWijs+NIlt0PKmdelwmP3Efmsz9x1dkICn6iq/MOh7FLVe2gcHubKL6RuiwO3vOOBVzObPg==",
"requires": {
"@scarf/scarf": "1.4.0",
"typescript": "5.4.5",
@@ -6581,14 +6581,14 @@
}
},
"@reboot-dev/reboot-react": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.0.tgz",
- "integrity": "sha512-gNkSLFRgWDl40SbW7VNO+xD16DaFdPBZ95E2bULEHpAcyd8lsg5qiHHCPYThwCkDou/HqWbb/2WH5iiQaBFmRg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.1.tgz",
+ "integrity": "sha512-ImVCLLaOTZoDDtAnXdXufScZWOz2DVomVuhs6WwEgjW99yTtwTx1OMsC/wbU37pSmadmdfdlTYP9vy/V9EcFMg==",
"requires": {
"@modelcontextprotocol/ext-apps": "1.5.0",
"@modelcontextprotocol/sdk": "1.29.0",
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-web": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-web": "1.2.1",
"@scarf/scarf": "1.4.0",
"@types/uuid": "^9.0.4",
"js-sha1": "0.7.0",
@@ -6603,11 +6603,11 @@
}
},
"@reboot-dev/reboot-web": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.0.tgz",
- "integrity": "sha512-ziBVJ5WLlnR57nkCUAJc9OAdqnl4PjoIYPdzMMkEYh3wpf10LPT/souGQT9c5BHUSWOx81azdQqU5lCqYXKWsg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.1.tgz",
+ "integrity": "sha512-/jk3OdG6pF+McvIP6yEuJKSWmDpHSR4+nVdbvUIHDdJh8cQkFj2QnzQJ+pRbql207LvwUNJ3T9YwZDSYz8rB9A==",
"requires": {
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
"@scarf/scarf": "1.4.0",
"js-sha1": "0.7.0",
"lru-cache-idb": "^0.5.2",
diff --git a/reboot/examples/chat-room/web/package.json b/reboot/examples/chat-room/web/package.json
index b16de060..decf0096 100644
--- a/reboot/examples/chat-room/web/package.json
+++ b/reboot/examples/chat-room/web/package.json
@@ -6,7 +6,7 @@
"dependencies": {
"@bufbuild/protobuf": "1.10.1",
"@eslint/js": "^9.34.0",
- "@reboot-dev/reboot-react": "1.2.0",
+ "@reboot-dev/reboot-react": "1.2.1",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/eslint__js": "^8.42.3",
diff --git a/reboot/examples/chick-potle/.dockerignore b/reboot/examples/chick-potle/.dockerignore
index 2d96968d..a236b55e 100644
--- a/reboot/examples/chick-potle/.dockerignore
+++ b/reboot/examples/chick-potle/.dockerignore
@@ -35,5 +35,4 @@ web/index.css
.DS_Store
# Local-only files.
-mcp_servers.json
README.md
diff --git a/reboot/examples/chick-potle/.tests/test.sh b/reboot/examples/chick-potle/.tests/test.sh
index 5c71c18d..35fa002c 100755
--- a/reboot/examples/chick-potle/.tests/test.sh
+++ b/reboot/examples/chick-potle/.tests/test.sh
@@ -51,18 +51,7 @@ pytest backend/
if [ -n "$EXPECTED_RBT_DEV_OUTPUT_FILE" ]; then
actual_output_file=$(mktemp)
- # `--config=dist` overrides `.rbtrc`'s default `hmr` config,
- # whose `--mcp-frontend-host=http://localhost:4444` would
- # have Envoy proxy `/__/web/**` to a Vite dev server. There
- # is no Vite running in CI, and on the macOS executable-Envoy
- # path that proxy target makes cluster init hang
- # indefinitely, so `--terminate-after-health-check` never
- # fires. The `dist` config sets `--mcp-frontend-host=""`,
- # which skips the proxy entirely. `web/dist/` doesn't need
- # to actually exist; the health check only probes gRPC and
- # Envoy listeners.
rbt $RBT_FLAGS dev run \
- --config=dist \
--terminate-after-health-check \
> "$actual_output_file"
diff --git a/reboot/examples/chick-potle/Dockerfile b/reboot/examples/chick-potle/Dockerfile
index 8dcdfe89..171b7703 100644
--- a/reboot/examples/chick-potle/Dockerfile
+++ b/reboot/examples/chick-potle/Dockerfile
@@ -4,7 +4,7 @@
# locally before `docker build` so that `web/dist/` contains the
# bundled UIs. This image copies that prebuilt bundle rather
# than installing Node and rebuilding it here.
-FROM ghcr.io/reboot-dev/reboot-base:1.2.0
+FROM ghcr.io/reboot-dev/reboot-base:1.2.1
WORKDIR /app
diff --git a/reboot/examples/chick-potle/mcp_servers.json b/reboot/examples/chick-potle/mcp_servers.json
deleted file mode 100644
index 6b2fa362..00000000
--- a/reboot/examples/chick-potle/mcp_servers.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "mcpServers": {
- "chick-potle": {
- "url": "http://localhost:9991/mcp",
- "useOAuth": true
- }
- }
-}
diff --git a/reboot/examples/chick-potle/pyproject.toml b/reboot/examples/chick-potle/pyproject.toml
index 28b6ceb8..35afea72 100644
--- a/reboot/examples/chick-potle/pyproject.toml
+++ b/reboot/examples/chick-potle/pyproject.toml
@@ -6,7 +6,7 @@ dependencies = [
"httpx>=0.27,<1.0",
"uuid7>=0.1.0",
"anyio>=4.0.0",
- "reboot==1.2.0",
+ "reboot==1.2.1",
]
[dependency-groups]
diff --git a/reboot/examples/chick-potle/uv.lock b/reboot/examples/chick-potle/uv.lock
index d0fa436e..de6af061 100644
--- a/reboot/examples/chick-potle/uv.lock
+++ b/reboot/examples/chick-potle/uv.lock
@@ -370,7 +370,7 @@ dev = [
requires-dist = [
{ name = "anyio", specifier = ">=4.0.0" },
{ name = "httpx", specifier = ">=0.27,<1.0" },
- { name = "reboot", specifier = "==1.2.0" },
+ { name = "reboot", specifier = "==1.2.1" },
{ name = "uuid7", specifier = ">=0.1.0" },
]
@@ -1873,7 +1873,7 @@ wheels = [
[[package]]
name = "reboot"
-version = "1.2.0"
+version = "1.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiofiles" },
@@ -1923,9 +1923,9 @@ dependencies = [
{ name = "websockets" },
]
wheels = [
- { url = "https://files.pythonhosted.org/packages/ae/01/d67f4c2cfa6cd6605f25f50d8d6dd962cda3dbb1a3be5a413b27cecfe5ba/reboot-1.2.0-py3-none-macosx_14_0_arm64.whl", hash = "sha256:45c74d870833b0a092c451c117006623363fc559d1224522859d94993d0f6c1e", size = 20440626, upload-time = "2026-06-18T15:42:03.995Z" },
- { url = "https://files.pythonhosted.org/packages/62/fb/32f27dc3d16d5446ca8987cd81961ebc70c4d9d8e8b0cea71257f1a6b4b1/reboot-1.2.0-py3-none-manylinux_2_34_aarch64.whl", hash = "sha256:e3d026f55c1b084f50e417c70fe211d33ac1a796f42eefddd3e288ceb8ea2fd1", size = 24124117, upload-time = "2026-06-18T15:27:54.363Z" },
- { url = "https://files.pythonhosted.org/packages/74/e0/51021d59e6bc3802b1726aee7503d28d7ea0eb507896f0a4b7cac81fccf2/reboot-1.2.0-py3-none-manylinux_2_34_x86_64.whl", hash = "sha256:fb27f3b851e25b797d5cb6e9f93e606ac8ba0c3a138d83f126a2ca26bdfbbd57", size = 24254127, upload-time = "2026-06-18T15:29:00.942Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/85/082fea1aaf65904b2f732c893469cb1d2534dff96ba527ca5575c5afbf55/reboot-1.2.1-py3-none-macosx_14_0_arm64.whl", hash = "sha256:b54cbf0233f297926ce516d39578efaea47953d113eca7095d4d0f07a1e4b192", size = 20452221, upload-time = "2026-06-23T19:41:33.886Z" },
+ { url = "https://files.pythonhosted.org/packages/64/3a/e004b65f1bbef40d8da6051fcff73925fae358a4c3ccf7b00d00bd2c7421/reboot-1.2.1-py3-none-manylinux_2_34_aarch64.whl", hash = "sha256:bb91ee132940d66c6779c2e5038f2fbf70fd567351c912bf0acf33ba60111860", size = 24135744, upload-time = "2026-06-23T19:28:20.962Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/a3/7c8b5caf625a3f1b87b075b5bf23040a6f7761c797c245822144900999ce/reboot-1.2.1-py3-none-manylinux_2_34_x86_64.whl", hash = "sha256:988cb8d6e84981d35c1bf4c9040eda4181d4d221d0ff1e9a48922b7f50c123de", size = 24265726, upload-time = "2026-06-23T19:27:45.447Z" },
]
[[package]]
diff --git a/reboot/examples/chick-potle/web/package-lock.json b/reboot/examples/chick-potle/web/package-lock.json
index 0c8c4b6b..6ae7dbfc 100644
--- a/reboot/examples/chick-potle/web/package-lock.json
+++ b/reboot/examples/chick-potle/web/package-lock.json
@@ -10,8 +10,8 @@
"dependencies": {
"@modelcontextprotocol/ext-apps": "1.5.0",
"@modelcontextprotocol/sdk": "1.29.0",
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-react": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-react": "1.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"zod": "^4.0.0"
@@ -886,9 +886,9 @@
}
},
"node_modules/@reboot-dev/reboot-api": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.0.tgz",
- "integrity": "sha512-6k0OhzPX3fcUurmxQY9ITJCX3LrMnimUYIdkPTEWWIpzV1jirpijx5ovTFG08MACic8mu1UMvCLEv7wq3/w1pg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.1.tgz",
+ "integrity": "sha512-CmrL7P/dtm+DBMWLWijs+NIlt0PKmdelwmP3Efmsz9x1dkICn6iq/MOh7FLVe2gcHubKL6RuiwO3vOOBVzObPg==",
"license": "Apache-2.0",
"dependencies": {
"@scarf/scarf": "1.4.0",
@@ -913,15 +913,15 @@
}
},
"node_modules/@reboot-dev/reboot-react": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.0.tgz",
- "integrity": "sha512-gNkSLFRgWDl40SbW7VNO+xD16DaFdPBZ95E2bULEHpAcyd8lsg5qiHHCPYThwCkDou/HqWbb/2WH5iiQaBFmRg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.1.tgz",
+ "integrity": "sha512-ImVCLLaOTZoDDtAnXdXufScZWOz2DVomVuhs6WwEgjW99yTtwTx1OMsC/wbU37pSmadmdfdlTYP9vy/V9EcFMg==",
"license": "Apache-2.0",
"dependencies": {
"@modelcontextprotocol/ext-apps": "1.5.0",
"@modelcontextprotocol/sdk": "1.29.0",
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-web": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-web": "1.2.1",
"@scarf/scarf": "1.4.0",
"@types/uuid": "^9.0.4",
"js-sha1": "0.7.0",
@@ -949,12 +949,12 @@
}
},
"node_modules/@reboot-dev/reboot-web": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.0.tgz",
- "integrity": "sha512-ziBVJ5WLlnR57nkCUAJc9OAdqnl4PjoIYPdzMMkEYh3wpf10LPT/souGQT9c5BHUSWOx81azdQqU5lCqYXKWsg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.1.tgz",
+ "integrity": "sha512-/jk3OdG6pF+McvIP6yEuJKSWmDpHSR4+nVdbvUIHDdJh8cQkFj2QnzQJ+pRbql207LvwUNJ3T9YwZDSYz8rB9A==",
"license": "Apache-2.0",
"dependencies": {
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
"@scarf/scarf": "1.4.0",
"js-sha1": "0.7.0",
"lru-cache-idb": "^0.5.2",
diff --git a/reboot/examples/chick-potle/web/package.json b/reboot/examples/chick-potle/web/package.json
index 1e51d6e5..a6007e17 100644
--- a/reboot/examples/chick-potle/web/package.json
+++ b/reboot/examples/chick-potle/web/package.json
@@ -15,8 +15,8 @@
"dependencies": {
"@modelcontextprotocol/ext-apps": "1.5.0",
"@modelcontextprotocol/sdk": "1.29.0",
- "@reboot-dev/reboot-react": "1.2.0",
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-react": "1.2.1",
+ "@reboot-dev/reboot-api": "1.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"zod": "^4.0.0"
diff --git a/reboot/examples/counter/package-lock.json b/reboot/examples/counter/package-lock.json
index f4c79b1f..df8012ef 100644
--- a/reboot/examples/counter/package-lock.json
+++ b/reboot/examples/counter/package-lock.json
@@ -9,8 +9,8 @@
"version": "0.0.0",
"dependencies": {
"@bufbuild/protobuf": "1.10.1",
- "@reboot-dev/reboot": "1.2.0",
- "@reboot-dev/reboot-react": "1.2.0",
+ "@reboot-dev/reboot": "1.2.1",
+ "@reboot-dev/reboot-react": "1.2.1",
"next": "14.2.13",
"react": "^18.3.1",
"react-dom": "^18.3.1"
@@ -1366,15 +1366,15 @@
}
},
"node_modules/@reboot-dev/reboot": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.2.0.tgz",
- "integrity": "sha512-VPjg8N0ab2iZzQ8PeVnATye5iPjEfmT7digqxxCKBYndOB/W8CjKg4YUVREzg+hNTrILSDmgJex2lpPd5yo4TQ==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.2.1.tgz",
+ "integrity": "sha512-1x35ldNqzFLUbRtjXrAfd27YDVX43gGYA+4O9BOx14yBu4y3OC4JZHT4Am7pVRuHJs4qNWSj0dCBFnEtqL3Rvg==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@bufbuild/protoc-gen-es": "1.10.1",
"@bufbuild/protoplugin": "1.10.1",
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
"@scarf/scarf": "1.4.0",
"@standard-schema/spec": "1.0.0",
"chalk": "^4.1.2",
@@ -1403,9 +1403,9 @@
}
},
"node_modules/@reboot-dev/reboot-api": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.0.tgz",
- "integrity": "sha512-6k0OhzPX3fcUurmxQY9ITJCX3LrMnimUYIdkPTEWWIpzV1jirpijx5ovTFG08MACic8mu1UMvCLEv7wq3/w1pg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.1.tgz",
+ "integrity": "sha512-CmrL7P/dtm+DBMWLWijs+NIlt0PKmdelwmP3Efmsz9x1dkICn6iq/MOh7FLVe2gcHubKL6RuiwO3vOOBVzObPg==",
"license": "Apache-2.0",
"dependencies": {
"@scarf/scarf": "1.4.0",
@@ -1430,15 +1430,15 @@
}
},
"node_modules/@reboot-dev/reboot-react": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.0.tgz",
- "integrity": "sha512-gNkSLFRgWDl40SbW7VNO+xD16DaFdPBZ95E2bULEHpAcyd8lsg5qiHHCPYThwCkDou/HqWbb/2WH5iiQaBFmRg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.1.tgz",
+ "integrity": "sha512-ImVCLLaOTZoDDtAnXdXufScZWOz2DVomVuhs6WwEgjW99yTtwTx1OMsC/wbU37pSmadmdfdlTYP9vy/V9EcFMg==",
"license": "Apache-2.0",
"dependencies": {
"@modelcontextprotocol/ext-apps": "1.5.0",
"@modelcontextprotocol/sdk": "1.29.0",
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-web": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-web": "1.2.1",
"@scarf/scarf": "1.4.0",
"@types/uuid": "^9.0.4",
"js-sha1": "0.7.0",
@@ -1465,12 +1465,12 @@
}
},
"node_modules/@reboot-dev/reboot-web": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.0.tgz",
- "integrity": "sha512-ziBVJ5WLlnR57nkCUAJc9OAdqnl4PjoIYPdzMMkEYh3wpf10LPT/souGQT9c5BHUSWOx81azdQqU5lCqYXKWsg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.1.tgz",
+ "integrity": "sha512-/jk3OdG6pF+McvIP6yEuJKSWmDpHSR4+nVdbvUIHDdJh8cQkFj2QnzQJ+pRbql207LvwUNJ3T9YwZDSYz8rB9A==",
"license": "Apache-2.0",
"dependencies": {
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
"@scarf/scarf": "1.4.0",
"js-sha1": "0.7.0",
"lru-cache-idb": "^0.5.2",
@@ -9909,13 +9909,13 @@
"optional": true
},
"@reboot-dev/reboot": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.2.0.tgz",
- "integrity": "sha512-VPjg8N0ab2iZzQ8PeVnATye5iPjEfmT7digqxxCKBYndOB/W8CjKg4YUVREzg+hNTrILSDmgJex2lpPd5yo4TQ==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.2.1.tgz",
+ "integrity": "sha512-1x35ldNqzFLUbRtjXrAfd27YDVX43gGYA+4O9BOx14yBu4y3OC4JZHT4Am7pVRuHJs4qNWSj0dCBFnEtqL3Rvg==",
"requires": {
"@bufbuild/protoc-gen-es": "1.10.1",
"@bufbuild/protoplugin": "1.10.1",
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
"@scarf/scarf": "1.4.0",
"@standard-schema/spec": "1.0.0",
"chalk": "^4.1.2",
@@ -10123,9 +10123,9 @@
}
},
"@reboot-dev/reboot-api": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.0.tgz",
- "integrity": "sha512-6k0OhzPX3fcUurmxQY9ITJCX3LrMnimUYIdkPTEWWIpzV1jirpijx5ovTFG08MACic8mu1UMvCLEv7wq3/w1pg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.1.tgz",
+ "integrity": "sha512-CmrL7P/dtm+DBMWLWijs+NIlt0PKmdelwmP3Efmsz9x1dkICn6iq/MOh7FLVe2gcHubKL6RuiwO3vOOBVzObPg==",
"requires": {
"@scarf/scarf": "1.4.0",
"typescript": "5.4.5",
@@ -10140,14 +10140,14 @@
}
},
"@reboot-dev/reboot-react": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.0.tgz",
- "integrity": "sha512-gNkSLFRgWDl40SbW7VNO+xD16DaFdPBZ95E2bULEHpAcyd8lsg5qiHHCPYThwCkDou/HqWbb/2WH5iiQaBFmRg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.1.tgz",
+ "integrity": "sha512-ImVCLLaOTZoDDtAnXdXufScZWOz2DVomVuhs6WwEgjW99yTtwTx1OMsC/wbU37pSmadmdfdlTYP9vy/V9EcFMg==",
"requires": {
"@modelcontextprotocol/ext-apps": "1.5.0",
"@modelcontextprotocol/sdk": "1.29.0",
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-web": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-web": "1.2.1",
"@scarf/scarf": "1.4.0",
"@types/uuid": "^9.0.4",
"js-sha1": "0.7.0",
@@ -10164,11 +10164,11 @@
}
},
"@reboot-dev/reboot-web": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.0.tgz",
- "integrity": "sha512-ziBVJ5WLlnR57nkCUAJc9OAdqnl4PjoIYPdzMMkEYh3wpf10LPT/souGQT9c5BHUSWOx81azdQqU5lCqYXKWsg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.1.tgz",
+ "integrity": "sha512-/jk3OdG6pF+McvIP6yEuJKSWmDpHSR4+nVdbvUIHDdJh8cQkFj2QnzQJ+pRbql207LvwUNJ3T9YwZDSYz8rB9A==",
"requires": {
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
"@scarf/scarf": "1.4.0",
"js-sha1": "0.7.0",
"lru-cache-idb": "^0.5.2",
diff --git a/reboot/examples/counter/package.json b/reboot/examples/counter/package.json
index ab2d78ac..e03e13fb 100644
--- a/reboot/examples/counter/package.json
+++ b/reboot/examples/counter/package.json
@@ -11,8 +11,8 @@
},
"dependencies": {
"@bufbuild/protobuf": "1.10.1",
- "@reboot-dev/reboot": "1.2.0",
- "@reboot-dev/reboot-react": "1.2.0",
+ "@reboot-dev/reboot": "1.2.1",
+ "@reboot-dev/reboot-react": "1.2.1",
"next": "14.2.13",
"react": "^18.3.1",
"react-dom": "^18.3.1"
diff --git a/reboot/examples/docubot/api/package.json b/reboot/examples/docubot/api/package.json
index f02a1465..3c360e21 100644
--- a/reboot/examples/docubot/api/package.json
+++ b/reboot/examples/docubot/api/package.json
@@ -7,7 +7,7 @@
"prepack": "rbt generate && tsc"
},
"dependencies": {
- "@reboot-dev/reboot": "1.2.0",
+ "@reboot-dev/reboot": "1.2.1",
"typescript": "^5.2.2"
},
"files": [
diff --git a/reboot/examples/docubot/docubot/package.json b/reboot/examples/docubot/docubot/package.json
index 62021bfd..d8b2a318 100644
--- a/reboot/examples/docubot/docubot/package.json
+++ b/reboot/examples/docubot/docubot/package.json
@@ -8,8 +8,8 @@
},
"dependencies": {
"@reboot-dev/docubot-api": "0.1.0",
- "@reboot-dev/reboot": "1.2.0",
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot": "1.2.1",
+ "@reboot-dev/reboot-api": "1.2.1",
"create-temp-directory": "^2.4.0",
"openai": "^4.52.7",
"puppeteer": "^22.14.0",
diff --git a/reboot/examples/docubot/package-lock.json b/reboot/examples/docubot/package-lock.json
index ef3f02e6..8882d0a4 100644
--- a/reboot/examples/docubot/package-lock.json
+++ b/reboot/examples/docubot/package-lock.json
@@ -18,9 +18,9 @@
"@radix-ui/react-slot": "^1.0.2",
"@reboot-dev/docubot": "workspace:*",
"@reboot-dev/docubot-api": "workspace:*",
- "@reboot-dev/reboot": "1.2.0",
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-react": "1.2.0",
+ "@reboot-dev/reboot": "1.2.1",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-react": "1.2.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"framer-motion": "^11.0.24",
@@ -54,7 +54,7 @@
"name": "@reboot-dev/docubot-api",
"version": "0.1.0",
"dependencies": {
- "@reboot-dev/reboot": "1.2.0",
+ "@reboot-dev/reboot": "1.2.1",
"typescript": "^5.2.2"
}
},
@@ -63,8 +63,8 @@
"version": "0.1.0",
"dependencies": {
"@reboot-dev/docubot-api": "0.1.0",
- "@reboot-dev/reboot": "1.2.0",
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot": "1.2.1",
+ "@reboot-dev/reboot-api": "1.2.1",
"create-temp-directory": "^2.4.0",
"openai": "^4.52.7",
"puppeteer": "^22.14.0",
@@ -1665,15 +1665,15 @@
"link": true
},
"node_modules/@reboot-dev/reboot": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.2.0.tgz",
- "integrity": "sha512-VPjg8N0ab2iZzQ8PeVnATye5iPjEfmT7digqxxCKBYndOB/W8CjKg4YUVREzg+hNTrILSDmgJex2lpPd5yo4TQ==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.2.1.tgz",
+ "integrity": "sha512-1x35ldNqzFLUbRtjXrAfd27YDVX43gGYA+4O9BOx14yBu4y3OC4JZHT4Am7pVRuHJs4qNWSj0dCBFnEtqL3Rvg==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@bufbuild/protoc-gen-es": "1.10.1",
"@bufbuild/protoplugin": "1.10.1",
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
"@scarf/scarf": "1.4.0",
"@standard-schema/spec": "1.0.0",
"chalk": "^4.1.2",
@@ -1702,9 +1702,9 @@
}
},
"node_modules/@reboot-dev/reboot-api": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.0.tgz",
- "integrity": "sha512-6k0OhzPX3fcUurmxQY9ITJCX3LrMnimUYIdkPTEWWIpzV1jirpijx5ovTFG08MACic8mu1UMvCLEv7wq3/w1pg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.1.tgz",
+ "integrity": "sha512-CmrL7P/dtm+DBMWLWijs+NIlt0PKmdelwmP3Efmsz9x1dkICn6iq/MOh7FLVe2gcHubKL6RuiwO3vOOBVzObPg==",
"license": "Apache-2.0",
"dependencies": {
"@scarf/scarf": "1.4.0",
@@ -1736,15 +1736,15 @@
}
},
"node_modules/@reboot-dev/reboot-react": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.0.tgz",
- "integrity": "sha512-gNkSLFRgWDl40SbW7VNO+xD16DaFdPBZ95E2bULEHpAcyd8lsg5qiHHCPYThwCkDou/HqWbb/2WH5iiQaBFmRg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.1.tgz",
+ "integrity": "sha512-ImVCLLaOTZoDDtAnXdXufScZWOz2DVomVuhs6WwEgjW99yTtwTx1OMsC/wbU37pSmadmdfdlTYP9vy/V9EcFMg==",
"license": "Apache-2.0",
"dependencies": {
"@modelcontextprotocol/ext-apps": "1.5.0",
"@modelcontextprotocol/sdk": "1.29.0",
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-web": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-web": "1.2.1",
"@scarf/scarf": "1.4.0",
"@types/uuid": "^9.0.4",
"js-sha1": "0.7.0",
@@ -1807,12 +1807,12 @@
}
},
"node_modules/@reboot-dev/reboot-web": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.0.tgz",
- "integrity": "sha512-ziBVJ5WLlnR57nkCUAJc9OAdqnl4PjoIYPdzMMkEYh3wpf10LPT/souGQT9c5BHUSWOx81azdQqU5lCqYXKWsg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.1.tgz",
+ "integrity": "sha512-/jk3OdG6pF+McvIP6yEuJKSWmDpHSR4+nVdbvUIHDdJh8cQkFj2QnzQJ+pRbql207LvwUNJ3T9YwZDSYz8rB9A==",
"license": "Apache-2.0",
"dependencies": {
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
"@scarf/scarf": "1.4.0",
"js-sha1": "0.7.0",
"lru-cache-idb": "^0.5.2",
@@ -11508,8 +11508,8 @@
"version": "file:docubot",
"requires": {
"@reboot-dev/docubot-api": "0.1.0",
- "@reboot-dev/reboot": "1.2.0",
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot": "1.2.1",
+ "@reboot-dev/reboot-api": "1.2.1",
"@types/node": "^20.12.4",
"create-temp-directory": "^2.4.0",
"openai": "^4.52.7",
@@ -11529,18 +11529,18 @@
"@reboot-dev/docubot-api": {
"version": "file:api",
"requires": {
- "@reboot-dev/reboot": "1.2.0",
+ "@reboot-dev/reboot": "1.2.1",
"typescript": "^5.2.2"
}
},
"@reboot-dev/reboot": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.2.0.tgz",
- "integrity": "sha512-VPjg8N0ab2iZzQ8PeVnATye5iPjEfmT7digqxxCKBYndOB/W8CjKg4YUVREzg+hNTrILSDmgJex2lpPd5yo4TQ==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.2.1.tgz",
+ "integrity": "sha512-1x35ldNqzFLUbRtjXrAfd27YDVX43gGYA+4O9BOx14yBu4y3OC4JZHT4Am7pVRuHJs4qNWSj0dCBFnEtqL3Rvg==",
"requires": {
"@bufbuild/protoc-gen-es": "1.10.1",
"@bufbuild/protoplugin": "1.10.1",
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
"@scarf/scarf": "1.4.0",
"@standard-schema/spec": "1.0.0",
"chalk": "^4.1.2",
@@ -11564,9 +11564,9 @@
}
},
"@reboot-dev/reboot-api": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.0.tgz",
- "integrity": "sha512-6k0OhzPX3fcUurmxQY9ITJCX3LrMnimUYIdkPTEWWIpzV1jirpijx5ovTFG08MACic8mu1UMvCLEv7wq3/w1pg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.1.tgz",
+ "integrity": "sha512-CmrL7P/dtm+DBMWLWijs+NIlt0PKmdelwmP3Efmsz9x1dkICn6iq/MOh7FLVe2gcHubKL6RuiwO3vOOBVzObPg==",
"requires": {
"@scarf/scarf": "1.4.0",
"typescript": "5.4.5",
@@ -11586,14 +11586,14 @@
}
},
"@reboot-dev/reboot-react": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.0.tgz",
- "integrity": "sha512-gNkSLFRgWDl40SbW7VNO+xD16DaFdPBZ95E2bULEHpAcyd8lsg5qiHHCPYThwCkDou/HqWbb/2WH5iiQaBFmRg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.1.tgz",
+ "integrity": "sha512-ImVCLLaOTZoDDtAnXdXufScZWOz2DVomVuhs6WwEgjW99yTtwTx1OMsC/wbU37pSmadmdfdlTYP9vy/V9EcFMg==",
"requires": {
"@modelcontextprotocol/ext-apps": "1.5.0",
"@modelcontextprotocol/sdk": "1.29.0",
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-web": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-web": "1.2.1",
"@scarf/scarf": "1.4.0",
"@types/uuid": "^9.0.4",
"js-sha1": "0.7.0",
@@ -11622,11 +11622,11 @@
}
},
"@reboot-dev/reboot-web": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.0.tgz",
- "integrity": "sha512-ziBVJ5WLlnR57nkCUAJc9OAdqnl4PjoIYPdzMMkEYh3wpf10LPT/souGQT9c5BHUSWOx81azdQqU5lCqYXKWsg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.1.tgz",
+ "integrity": "sha512-/jk3OdG6pF+McvIP6yEuJKSWmDpHSR4+nVdbvUIHDdJh8cQkFj2QnzQJ+pRbql207LvwUNJ3T9YwZDSYz8rB9A==",
"requires": {
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
"@scarf/scarf": "1.4.0",
"js-sha1": "0.7.0",
"lru-cache-idb": "^0.5.2",
diff --git a/reboot/examples/docubot/package.json b/reboot/examples/docubot/package.json
index 0728689b..e454a5f1 100644
--- a/reboot/examples/docubot/package.json
+++ b/reboot/examples/docubot/package.json
@@ -19,9 +19,9 @@
"@radix-ui/react-slot": "^1.0.2",
"@reboot-dev/docubot": "workspace:*",
"@reboot-dev/docubot-api": "workspace:*",
- "@reboot-dev/reboot": "1.2.0",
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-react": "1.2.0",
+ "@reboot-dev/reboot": "1.2.1",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-react": "1.2.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"framer-motion": "^11.0.24",
diff --git a/reboot/examples/kcdc-2025/pyproject.toml b/reboot/examples/kcdc-2025/pyproject.toml
index 897aed2c..3e5c9d92 100644
--- a/reboot/examples/kcdc-2025/pyproject.toml
+++ b/reboot/examples/kcdc-2025/pyproject.toml
@@ -5,7 +5,7 @@ requires-python = ">= 3.10"
dependencies = [
"langchain>=0.3.27",
"langchain-anthropic>=0.3.18",
- "reboot==1.2.0",
+ "reboot==1.2.1",
]
[dependency-groups]
dev = [
diff --git a/reboot/examples/kcdc-2025/uv.lock b/reboot/examples/kcdc-2025/uv.lock
index 57c52173..c27f2ea5 100644
--- a/reboot/examples/kcdc-2025/uv.lock
+++ b/reboot/examples/kcdc-2025/uv.lock
@@ -1127,7 +1127,7 @@ dev = [
requires-dist = [
{ name = "langchain", specifier = ">=0.3.27" },
{ name = "langchain-anthropic", specifier = ">=0.3.18" },
- { name = "reboot", specifier = "==1.2.0" },
+ { name = "reboot", specifier = "==1.2.1" },
]
[package.metadata.requires-dev]
@@ -2141,7 +2141,7 @@ wheels = [
[[package]]
name = "reboot"
-version = "1.2.0"
+version = "1.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiofiles" },
@@ -2191,9 +2191,9 @@ dependencies = [
{ name = "websockets" },
]
wheels = [
- { url = "https://files.pythonhosted.org/packages/ae/01/d67f4c2cfa6cd6605f25f50d8d6dd962cda3dbb1a3be5a413b27cecfe5ba/reboot-1.2.0-py3-none-macosx_14_0_arm64.whl", hash = "sha256:45c74d870833b0a092c451c117006623363fc559d1224522859d94993d0f6c1e", size = 20440626, upload-time = "2026-06-18T15:42:03.995Z" },
- { url = "https://files.pythonhosted.org/packages/62/fb/32f27dc3d16d5446ca8987cd81961ebc70c4d9d8e8b0cea71257f1a6b4b1/reboot-1.2.0-py3-none-manylinux_2_34_aarch64.whl", hash = "sha256:e3d026f55c1b084f50e417c70fe211d33ac1a796f42eefddd3e288ceb8ea2fd1", size = 24124117, upload-time = "2026-06-18T15:27:54.363Z" },
- { url = "https://files.pythonhosted.org/packages/74/e0/51021d59e6bc3802b1726aee7503d28d7ea0eb507896f0a4b7cac81fccf2/reboot-1.2.0-py3-none-manylinux_2_34_x86_64.whl", hash = "sha256:fb27f3b851e25b797d5cb6e9f93e606ac8ba0c3a138d83f126a2ca26bdfbbd57", size = 24254127, upload-time = "2026-06-18T15:29:00.942Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/85/082fea1aaf65904b2f732c893469cb1d2534dff96ba527ca5575c5afbf55/reboot-1.2.1-py3-none-macosx_14_0_arm64.whl", hash = "sha256:b54cbf0233f297926ce516d39578efaea47953d113eca7095d4d0f07a1e4b192", size = 20452221, upload-time = "2026-06-23T19:41:33.886Z" },
+ { url = "https://files.pythonhosted.org/packages/64/3a/e004b65f1bbef40d8da6051fcff73925fae358a4c3ccf7b00d00bd2c7421/reboot-1.2.1-py3-none-manylinux_2_34_aarch64.whl", hash = "sha256:bb91ee132940d66c6779c2e5038f2fbf70fd567351c912bf0acf33ba60111860", size = 24135744, upload-time = "2026-06-23T19:28:20.962Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/a3/7c8b5caf625a3f1b87b075b5bf23040a6f7761c797c245822144900999ce/reboot-1.2.1-py3-none-manylinux_2_34_x86_64.whl", hash = "sha256:988cb8d6e84981d35c1bf4c9040eda4181d4d221d0ff1e9a48922b7f50c123de", size = 24265726, upload-time = "2026-06-23T19:27:45.447Z" },
]
[[package]]
diff --git a/reboot/examples/kcdc-2025/web/package-lock.json b/reboot/examples/kcdc-2025/web/package-lock.json
index 964edc2d..a8e7fc4e 100644
--- a/reboot/examples/kcdc-2025/web/package-lock.json
+++ b/reboot/examples/kcdc-2025/web/package-lock.json
@@ -16,9 +16,9 @@
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tooltip": "^1.0.7",
- "@reboot-dev/reboot-react": "1.2.0",
- "@reboot-dev/reboot-std-api": "1.2.0",
- "@reboot-dev/reboot-std-react": "1.2.0",
+ "@reboot-dev/reboot-react": "1.2.1",
+ "@reboot-dev/reboot-std-api": "1.2.1",
+ "@reboot-dev/reboot-std-react": "1.2.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"emoji-picker-react": "^4.9.2",
@@ -1753,9 +1753,9 @@
"license": "MIT"
},
"node_modules/@reboot-dev/reboot-api": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.0.tgz",
- "integrity": "sha512-6k0OhzPX3fcUurmxQY9ITJCX3LrMnimUYIdkPTEWWIpzV1jirpijx5ovTFG08MACic8mu1UMvCLEv7wq3/w1pg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.1.tgz",
+ "integrity": "sha512-CmrL7P/dtm+DBMWLWijs+NIlt0PKmdelwmP3Efmsz9x1dkICn6iq/MOh7FLVe2gcHubKL6RuiwO3vOOBVzObPg==",
"license": "Apache-2.0",
"dependencies": {
"@scarf/scarf": "1.4.0",
@@ -1780,15 +1780,15 @@
}
},
"node_modules/@reboot-dev/reboot-react": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.0.tgz",
- "integrity": "sha512-gNkSLFRgWDl40SbW7VNO+xD16DaFdPBZ95E2bULEHpAcyd8lsg5qiHHCPYThwCkDou/HqWbb/2WH5iiQaBFmRg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.1.tgz",
+ "integrity": "sha512-ImVCLLaOTZoDDtAnXdXufScZWOz2DVomVuhs6WwEgjW99yTtwTx1OMsC/wbU37pSmadmdfdlTYP9vy/V9EcFMg==",
"license": "Apache-2.0",
"dependencies": {
"@modelcontextprotocol/ext-apps": "1.5.0",
"@modelcontextprotocol/sdk": "1.29.0",
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-web": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-web": "1.2.1",
"@scarf/scarf": "1.4.0",
"@types/uuid": "^9.0.4",
"js-sha1": "0.7.0",
@@ -1816,34 +1816,34 @@
}
},
"node_modules/@reboot-dev/reboot-std-api": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std-api/-/reboot-std-api-1.2.0.tgz",
- "integrity": "sha512-8EI8+Ys4gWlVNiq/5f0ZU2W76CkddqLygdDrZTqgLNsI/Y4wa/ZOFMiEGvayULTzQu0Og5pZ0jXbXrak9iYTqw==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std-api/-/reboot-std-api-1.2.1.tgz",
+ "integrity": "sha512-evFzB6oQDURxkddknXakyRKcYT53EY1AlyQnNs3nffF84gWMjIbZ6nb3C5yDNnra+pDjsiiz7DsBhMvr9qHX2g==",
"license": "Apache-2.0",
"dependencies": {
"@scarf/scarf": "1.4.0"
}
},
"node_modules/@reboot-dev/reboot-std-react": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std-react/-/reboot-std-react-1.2.0.tgz",
- "integrity": "sha512-h77eSxkbER7BnzBEuZjfIdOwVq+a21EvKqUpwD7kEouJGiGIWxWmTvtlnKkrUr+G2YZnXyb/eVLzKpoq6aGnJQ==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std-react/-/reboot-std-react-1.2.1.tgz",
+ "integrity": "sha512-CCjS7MYLmwXGJ4u1gcsRZ1EwarJtnFTezukujuQs8xS80m6sc9UJ0SgCkgvNkLfv0rWwxnRP2sP4FFQ5R4hWVw==",
"license": "Apache-2.0",
"dependencies": {
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-react": "1.2.0",
- "@reboot-dev/reboot-std-api": "1.2.0",
- "@reboot-dev/reboot-web": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-react": "1.2.1",
+ "@reboot-dev/reboot-std-api": "1.2.1",
+ "@reboot-dev/reboot-web": "1.2.1",
"@scarf/scarf": "1.4.0"
}
},
"node_modules/@reboot-dev/reboot-web": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.0.tgz",
- "integrity": "sha512-ziBVJ5WLlnR57nkCUAJc9OAdqnl4PjoIYPdzMMkEYh3wpf10LPT/souGQT9c5BHUSWOx81azdQqU5lCqYXKWsg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.1.tgz",
+ "integrity": "sha512-/jk3OdG6pF+McvIP6yEuJKSWmDpHSR4+nVdbvUIHDdJh8cQkFj2QnzQJ+pRbql207LvwUNJ3T9YwZDSYz8rB9A==",
"license": "Apache-2.0",
"dependencies": {
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
"@scarf/scarf": "1.4.0",
"js-sha1": "0.7.0",
"lru-cache-idb": "^0.5.2",
diff --git a/reboot/examples/kcdc-2025/web/package.json b/reboot/examples/kcdc-2025/web/package.json
index 5c9cc3f7..11bb4dc9 100644
--- a/reboot/examples/kcdc-2025/web/package.json
+++ b/reboot/examples/kcdc-2025/web/package.json
@@ -18,9 +18,9 @@
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tooltip": "^1.0.7",
- "@reboot-dev/reboot-react": "1.2.0",
- "@reboot-dev/reboot-std-api": "1.2.0",
- "@reboot-dev/reboot-std-react": "1.2.0",
+ "@reboot-dev/reboot-react": "1.2.1",
+ "@reboot-dev/reboot-std-api": "1.2.1",
+ "@reboot-dev/reboot-std-react": "1.2.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"emoji-picker-react": "^4.9.2",
diff --git a/reboot/examples/monorepo/pyproject.toml b/reboot/examples/monorepo/pyproject.toml
index 14965b06..8764c9d4 100644
--- a/reboot/examples/monorepo/pyproject.toml
+++ b/reboot/examples/monorepo/pyproject.toml
@@ -3,7 +3,7 @@ name = "monorepo"
version = "0.1.0"
requires-python = ">= 3.10"
dependencies = [
- "reboot==1.2.0",
+ "reboot==1.2.1",
]
[dependency-groups]
diff --git a/reboot/examples/monorepo/uv.lock b/reboot/examples/monorepo/uv.lock
index f6db8662..234bf52c 100644
--- a/reboot/examples/monorepo/uv.lock
+++ b/reboot/examples/monorepo/uv.lock
@@ -1067,7 +1067,7 @@ dev = [
]
[package.metadata]
-requires-dist = [{ name = "reboot", specifier = "==1.2.0" }]
+requires-dist = [{ name = "reboot", specifier = "==1.2.1" }]
[package.metadata.requires-dev]
dev = [
@@ -1925,7 +1925,7 @@ wheels = [
[[package]]
name = "reboot"
-version = "1.2.0"
+version = "1.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiofiles" },
@@ -1975,9 +1975,9 @@ dependencies = [
{ name = "websockets" },
]
wheels = [
- { url = "https://files.pythonhosted.org/packages/ae/01/d67f4c2cfa6cd6605f25f50d8d6dd962cda3dbb1a3be5a413b27cecfe5ba/reboot-1.2.0-py3-none-macosx_14_0_arm64.whl", hash = "sha256:45c74d870833b0a092c451c117006623363fc559d1224522859d94993d0f6c1e", size = 20440626, upload-time = "2026-06-18T15:42:03.995Z" },
- { url = "https://files.pythonhosted.org/packages/62/fb/32f27dc3d16d5446ca8987cd81961ebc70c4d9d8e8b0cea71257f1a6b4b1/reboot-1.2.0-py3-none-manylinux_2_34_aarch64.whl", hash = "sha256:e3d026f55c1b084f50e417c70fe211d33ac1a796f42eefddd3e288ceb8ea2fd1", size = 24124117, upload-time = "2026-06-18T15:27:54.363Z" },
- { url = "https://files.pythonhosted.org/packages/74/e0/51021d59e6bc3802b1726aee7503d28d7ea0eb507896f0a4b7cac81fccf2/reboot-1.2.0-py3-none-manylinux_2_34_x86_64.whl", hash = "sha256:fb27f3b851e25b797d5cb6e9f93e606ac8ba0c3a138d83f126a2ca26bdfbbd57", size = 24254127, upload-time = "2026-06-18T15:29:00.942Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/85/082fea1aaf65904b2f732c893469cb1d2534dff96ba527ca5575c5afbf55/reboot-1.2.1-py3-none-macosx_14_0_arm64.whl", hash = "sha256:b54cbf0233f297926ce516d39578efaea47953d113eca7095d4d0f07a1e4b192", size = 20452221, upload-time = "2026-06-23T19:41:33.886Z" },
+ { url = "https://files.pythonhosted.org/packages/64/3a/e004b65f1bbef40d8da6051fcff73925fae358a4c3ccf7b00d00bd2c7421/reboot-1.2.1-py3-none-manylinux_2_34_aarch64.whl", hash = "sha256:bb91ee132940d66c6779c2e5038f2fbf70fd567351c912bf0acf33ba60111860", size = 24135744, upload-time = "2026-06-23T19:28:20.962Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/a3/7c8b5caf625a3f1b87b075b5bf23040a6f7761c797c245822144900999ce/reboot-1.2.1-py3-none-manylinux_2_34_x86_64.whl", hash = "sha256:988cb8d6e84981d35c1bf4c9040eda4181d4d221d0ff1e9a48922b7f50c123de", size = 24265726, upload-time = "2026-06-23T19:27:45.447Z" },
]
[[package]]
diff --git a/reboot/examples/prosemirror-zod/Dockerfile b/reboot/examples/prosemirror-zod/Dockerfile
index 98dea835..e52281b6 100644
--- a/reboot/examples/prosemirror-zod/Dockerfile
+++ b/reboot/examples/prosemirror-zod/Dockerfile
@@ -1,4 +1,4 @@
-FROM ghcr.io/reboot-dev/reboot-base:1.2.0
+FROM ghcr.io/reboot-dev/reboot-base:1.2.1
WORKDIR /app
diff --git a/reboot/examples/prosemirror-zod/backend/package.json b/reboot/examples/prosemirror-zod/backend/package.json
index 057cee78..0c9956b2 100644
--- a/reboot/examples/prosemirror-zod/backend/package.json
+++ b/reboot/examples/prosemirror-zod/backend/package.json
@@ -10,8 +10,8 @@
"@bufbuild/protobuf": "1.10.1",
"@monorepo/api": "workspace:*",
"@monorepo/common": "workspace:*",
- "@reboot-dev/reboot": "1.2.0",
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot": "1.2.1",
+ "@reboot-dev/reboot-api": "1.2.1",
"prosemirror-model": "^1.23.0",
"prosemirror-transform": "^1.1.0"
}
diff --git a/reboot/examples/prosemirror-zod/web/package.json b/reboot/examples/prosemirror-zod/web/package.json
index b7dddb80..9727d17e 100644
--- a/reboot/examples/prosemirror-zod/web/package.json
+++ b/reboot/examples/prosemirror-zod/web/package.json
@@ -14,7 +14,7 @@
"@monorepo/api": "workspace:*",
"@monorepo/common": "workspace:*",
"@nytimes/react-prosemirror": "^0.6.2",
- "@reboot-dev/reboot-react": "1.2.0",
+ "@reboot-dev/reboot-react": "1.2.1",
"next": "15.0.5",
"prosemirror-collab": "^1.3.1",
"prosemirror-history": "^1.4.1",
diff --git a/reboot/examples/prosemirror-zod/yarn.lock b/reboot/examples/prosemirror-zod/yarn.lock
index c48c0900..845793b2 100644
--- a/reboot/examples/prosemirror-zod/yarn.lock
+++ b/reboot/examples/prosemirror-zod/yarn.lock
@@ -812,8 +812,8 @@ __metadata:
"@bufbuild/protobuf": "npm:1.10.1"
"@monorepo/api": "workspace:*"
"@monorepo/common": "workspace:*"
- "@reboot-dev/reboot": "npm:1.2.0"
- "@reboot-dev/reboot-api": "npm:1.2.0"
+ "@reboot-dev/reboot": "npm:1.2.1"
+ "@reboot-dev/reboot-api": "npm:1.2.1"
prosemirror-model: "npm:^1.23.0"
prosemirror-transform: "npm:^1.1.0"
languageName: unknown
@@ -838,7 +838,7 @@ __metadata:
"@monorepo/api": "workspace:*"
"@monorepo/common": "workspace:*"
"@nytimes/react-prosemirror": "npm:^0.6.2"
- "@reboot-dev/reboot-react": "npm:1.2.0"
+ "@reboot-dev/reboot-react": "npm:1.2.1"
"@types/node": "npm:^20"
"@types/react": "npm:^18"
"@types/react-dom": "npm:^18"
@@ -1018,27 +1018,27 @@ __metadata:
languageName: node
linkType: hard
-"@reboot-dev/reboot-api@npm:1.2.0":
- version: 1.2.0
- resolution: "@reboot-dev/reboot-api@npm:1.2.0"
+"@reboot-dev/reboot-api@npm:1.2.1":
+ version: 1.2.1
+ resolution: "@reboot-dev/reboot-api@npm:1.2.1"
dependencies:
"@scarf/scarf": "npm:1.4.0"
typescript: "npm:5.4.5"
zod: "npm:^3.25.51 || ^4.0.0"
peerDependencies:
"@bufbuild/protobuf": 1.10.1
- checksum: 10c0/e0fcf33eccf948c2a2ea25421e61ab8311a39a44b40efa9280d658314efc7ad964b30db308cdadfaa386114bb065e8734eaa1e5741851893dd00803d067a1c64
+ checksum: 10c0/a7073cf1646a4eb54af72ec5f4ee4529471dd7a98ab2333a0a9d72bee08d725cae22a8d234cace4e9fdd71d7027d7618ce205b626dde5c94d705750bb32f2e56
languageName: node
linkType: hard
-"@reboot-dev/reboot-react@npm:1.2.0":
- version: 1.2.0
- resolution: "@reboot-dev/reboot-react@npm:1.2.0"
+"@reboot-dev/reboot-react@npm:1.2.1":
+ version: 1.2.1
+ resolution: "@reboot-dev/reboot-react@npm:1.2.1"
dependencies:
"@modelcontextprotocol/ext-apps": "npm:1.5.0"
"@modelcontextprotocol/sdk": "npm:1.29.0"
- "@reboot-dev/reboot-api": "npm:1.2.0"
- "@reboot-dev/reboot-web": "npm:1.2.0"
+ "@reboot-dev/reboot-api": "npm:1.2.1"
+ "@reboot-dev/reboot-web": "npm:1.2.1"
"@scarf/scarf": "npm:1.4.0"
"@types/uuid": "npm:^9.0.4"
js-sha1: "npm:0.7.0"
@@ -1049,15 +1049,15 @@ __metadata:
"@bufbuild/protobuf": 1.10.1
react: ">=18.0.0"
react-dom: ">=18.0.0"
- checksum: 10c0/a836ffa7c9255892148b692ae521018e543afa2b76d6f1326fc194f543aead2c418c4d3e56f4cdaa25243c3e9fb431faa409fcf52975a9f2a07b8059005ab7e8
+ checksum: 10c0/6a8f8cc44fa794a00d9f4923794d6376fb9fd6f42d303850fc8f05becd3e03cdad595c35e7f51025cf44cbdbca0343227bfec16540ee62154b5e00829d2a8141
languageName: node
linkType: hard
-"@reboot-dev/reboot-web@npm:1.2.0":
- version: 1.2.0
- resolution: "@reboot-dev/reboot-web@npm:1.2.0"
+"@reboot-dev/reboot-web@npm:1.2.1":
+ version: 1.2.1
+ resolution: "@reboot-dev/reboot-web@npm:1.2.1"
dependencies:
- "@reboot-dev/reboot-api": "npm:1.2.0"
+ "@reboot-dev/reboot-api": "npm:1.2.1"
"@scarf/scarf": "npm:1.4.0"
js-sha1: "npm:0.7.0"
lru-cache-idb: "npm:^0.5.2"
@@ -1066,17 +1066,17 @@ __metadata:
uuid: "npm:^11.1.1"
peerDependencies:
"@bufbuild/protobuf": 1.10.1
- checksum: 10c0/0db0171c368858ce5f848b9eea55b93baad1bf47556b08d6a9c3c2c0a80c10e2840caa3d584e29b73bab88c99a182a338084ace24b77501b14232996409c06df
+ checksum: 10c0/48884cb1043c4c5570eb19966c12bd914e5259a8929d4210f9adedbcf32356aeeaa4baaef496b56288df9efa61226a4b315d55e43e6dcb9eb20fa21aee38adcc
languageName: node
linkType: hard
-"@reboot-dev/reboot@npm:1.2.0":
- version: 1.2.0
- resolution: "@reboot-dev/reboot@npm:1.2.0"
+"@reboot-dev/reboot@npm:1.2.1":
+ version: 1.2.1
+ resolution: "@reboot-dev/reboot@npm:1.2.1"
dependencies:
"@bufbuild/protoc-gen-es": "npm:1.10.1"
"@bufbuild/protoplugin": "npm:1.10.1"
- "@reboot-dev/reboot-api": "npm:1.2.0"
+ "@reboot-dev/reboot-api": "npm:1.2.1"
"@scarf/scarf": "npm:1.4.0"
"@standard-schema/spec": "npm:1.0.0"
chalk: "npm:^4.1.2"
@@ -1097,7 +1097,7 @@ __metadata:
rbt: rbt.js
rbt-esbuild: rbt-esbuild.js
zod-to-proto: zod-to-proto.js
- checksum: 10c0/511decf69a51481bdd45d889530201ed647cd59cb8229c587b27de791d990bcac9059dcbaf2b5a60c31909182b705ffe6b7bb8e343ff0b36a3bf991460bbd7f0
+ checksum: 10c0/50983c3ef4a46ccf80d325b9cfbac69e5b2d9987062eaefa98903b785bc53a1f8edaafece647fd4e68907a6952f7c9dd1d83b47461e8161fedf68efb829485d1
languageName: node
linkType: hard
diff --git a/reboot/examples/prosemirror/Dockerfile b/reboot/examples/prosemirror/Dockerfile
index 98dea835..e52281b6 100644
--- a/reboot/examples/prosemirror/Dockerfile
+++ b/reboot/examples/prosemirror/Dockerfile
@@ -1,4 +1,4 @@
-FROM ghcr.io/reboot-dev/reboot-base:1.2.0
+FROM ghcr.io/reboot-dev/reboot-base:1.2.1
WORKDIR /app
diff --git a/reboot/examples/prosemirror/backend/package.json b/reboot/examples/prosemirror/backend/package.json
index 057cee78..0c9956b2 100644
--- a/reboot/examples/prosemirror/backend/package.json
+++ b/reboot/examples/prosemirror/backend/package.json
@@ -10,8 +10,8 @@
"@bufbuild/protobuf": "1.10.1",
"@monorepo/api": "workspace:*",
"@monorepo/common": "workspace:*",
- "@reboot-dev/reboot": "1.2.0",
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot": "1.2.1",
+ "@reboot-dev/reboot-api": "1.2.1",
"prosemirror-model": "^1.23.0",
"prosemirror-transform": "^1.1.0"
}
diff --git a/reboot/examples/prosemirror/web/package.json b/reboot/examples/prosemirror/web/package.json
index b7dddb80..9727d17e 100644
--- a/reboot/examples/prosemirror/web/package.json
+++ b/reboot/examples/prosemirror/web/package.json
@@ -14,7 +14,7 @@
"@monorepo/api": "workspace:*",
"@monorepo/common": "workspace:*",
"@nytimes/react-prosemirror": "^0.6.2",
- "@reboot-dev/reboot-react": "1.2.0",
+ "@reboot-dev/reboot-react": "1.2.1",
"next": "15.0.5",
"prosemirror-collab": "^1.3.1",
"prosemirror-history": "^1.4.1",
diff --git a/reboot/examples/prosemirror/yarn.lock b/reboot/examples/prosemirror/yarn.lock
index ca02176d..db79a94e 100644
--- a/reboot/examples/prosemirror/yarn.lock
+++ b/reboot/examples/prosemirror/yarn.lock
@@ -790,8 +790,8 @@ __metadata:
"@bufbuild/protobuf": "npm:1.10.1"
"@monorepo/api": "workspace:*"
"@monorepo/common": "workspace:*"
- "@reboot-dev/reboot": "npm:1.2.0"
- "@reboot-dev/reboot-api": "npm:1.2.0"
+ "@reboot-dev/reboot": "npm:1.2.1"
+ "@reboot-dev/reboot-api": "npm:1.2.1"
prosemirror-model: "npm:^1.23.0"
prosemirror-transform: "npm:^1.1.0"
languageName: unknown
@@ -816,7 +816,7 @@ __metadata:
"@monorepo/api": "workspace:*"
"@monorepo/common": "workspace:*"
"@nytimes/react-prosemirror": "npm:^0.6.2"
- "@reboot-dev/reboot-react": "npm:1.2.0"
+ "@reboot-dev/reboot-react": "npm:1.2.1"
"@types/node": "npm:^20"
"@types/react": "npm:^18"
"@types/react-dom": "npm:^18"
@@ -985,27 +985,27 @@ __metadata:
languageName: node
linkType: hard
-"@reboot-dev/reboot-api@npm:1.2.0":
- version: 1.2.0
- resolution: "@reboot-dev/reboot-api@npm:1.2.0"
+"@reboot-dev/reboot-api@npm:1.2.1":
+ version: 1.2.1
+ resolution: "@reboot-dev/reboot-api@npm:1.2.1"
dependencies:
"@scarf/scarf": "npm:1.4.0"
typescript: "npm:5.4.5"
zod: "npm:^3.25.51 || ^4.0.0"
peerDependencies:
"@bufbuild/protobuf": 1.10.1
- checksum: 10c0/e0fcf33eccf948c2a2ea25421e61ab8311a39a44b40efa9280d658314efc7ad964b30db308cdadfaa386114bb065e8734eaa1e5741851893dd00803d067a1c64
+ checksum: 10c0/a7073cf1646a4eb54af72ec5f4ee4529471dd7a98ab2333a0a9d72bee08d725cae22a8d234cace4e9fdd71d7027d7618ce205b626dde5c94d705750bb32f2e56
languageName: node
linkType: hard
-"@reboot-dev/reboot-react@npm:1.2.0":
- version: 1.2.0
- resolution: "@reboot-dev/reboot-react@npm:1.2.0"
+"@reboot-dev/reboot-react@npm:1.2.1":
+ version: 1.2.1
+ resolution: "@reboot-dev/reboot-react@npm:1.2.1"
dependencies:
"@modelcontextprotocol/ext-apps": "npm:1.5.0"
"@modelcontextprotocol/sdk": "npm:1.29.0"
- "@reboot-dev/reboot-api": "npm:1.2.0"
- "@reboot-dev/reboot-web": "npm:1.2.0"
+ "@reboot-dev/reboot-api": "npm:1.2.1"
+ "@reboot-dev/reboot-web": "npm:1.2.1"
"@scarf/scarf": "npm:1.4.0"
"@types/uuid": "npm:^9.0.4"
js-sha1: "npm:0.7.0"
@@ -1016,15 +1016,15 @@ __metadata:
"@bufbuild/protobuf": 1.10.1
react: ">=18.0.0"
react-dom: ">=18.0.0"
- checksum: 10c0/a836ffa7c9255892148b692ae521018e543afa2b76d6f1326fc194f543aead2c418c4d3e56f4cdaa25243c3e9fb431faa409fcf52975a9f2a07b8059005ab7e8
+ checksum: 10c0/6a8f8cc44fa794a00d9f4923794d6376fb9fd6f42d303850fc8f05becd3e03cdad595c35e7f51025cf44cbdbca0343227bfec16540ee62154b5e00829d2a8141
languageName: node
linkType: hard
-"@reboot-dev/reboot-web@npm:1.2.0":
- version: 1.2.0
- resolution: "@reboot-dev/reboot-web@npm:1.2.0"
+"@reboot-dev/reboot-web@npm:1.2.1":
+ version: 1.2.1
+ resolution: "@reboot-dev/reboot-web@npm:1.2.1"
dependencies:
- "@reboot-dev/reboot-api": "npm:1.2.0"
+ "@reboot-dev/reboot-api": "npm:1.2.1"
"@scarf/scarf": "npm:1.4.0"
js-sha1: "npm:0.7.0"
lru-cache-idb: "npm:^0.5.2"
@@ -1033,17 +1033,17 @@ __metadata:
uuid: "npm:^11.1.1"
peerDependencies:
"@bufbuild/protobuf": 1.10.1
- checksum: 10c0/0db0171c368858ce5f848b9eea55b93baad1bf47556b08d6a9c3c2c0a80c10e2840caa3d584e29b73bab88c99a182a338084ace24b77501b14232996409c06df
+ checksum: 10c0/48884cb1043c4c5570eb19966c12bd914e5259a8929d4210f9adedbcf32356aeeaa4baaef496b56288df9efa61226a4b315d55e43e6dcb9eb20fa21aee38adcc
languageName: node
linkType: hard
-"@reboot-dev/reboot@npm:1.2.0":
- version: 1.2.0
- resolution: "@reboot-dev/reboot@npm:1.2.0"
+"@reboot-dev/reboot@npm:1.2.1":
+ version: 1.2.1
+ resolution: "@reboot-dev/reboot@npm:1.2.1"
dependencies:
"@bufbuild/protoc-gen-es": "npm:1.10.1"
"@bufbuild/protoplugin": "npm:1.10.1"
- "@reboot-dev/reboot-api": "npm:1.2.0"
+ "@reboot-dev/reboot-api": "npm:1.2.1"
"@scarf/scarf": "npm:1.4.0"
"@standard-schema/spec": "npm:1.0.0"
chalk: "npm:^4.1.2"
@@ -1064,7 +1064,7 @@ __metadata:
rbt: rbt.js
rbt-esbuild: rbt-esbuild.js
zod-to-proto: zod-to-proto.js
- checksum: 10c0/511decf69a51481bdd45d889530201ed647cd59cb8229c587b27de791d990bcac9059dcbaf2b5a60c31909182b705ffe6b7bb8e343ff0b36a3bf991460bbd7f0
+ checksum: 10c0/50983c3ef4a46ccf80d325b9cfbac69e5b2d9987062eaefa98903b785bc53a1f8edaafece647fd4e68907a6952f7c9dd1d83b47461e8161fedf68efb829485d1
languageName: node
linkType: hard
diff --git a/reboot/examples/reboot-swag-store/.tests/test.sh b/reboot/examples/reboot-swag-store/.tests/test.sh
index 5c71c18d..35fa002c 100755
--- a/reboot/examples/reboot-swag-store/.tests/test.sh
+++ b/reboot/examples/reboot-swag-store/.tests/test.sh
@@ -51,18 +51,7 @@ pytest backend/
if [ -n "$EXPECTED_RBT_DEV_OUTPUT_FILE" ]; then
actual_output_file=$(mktemp)
- # `--config=dist` overrides `.rbtrc`'s default `hmr` config,
- # whose `--mcp-frontend-host=http://localhost:4444` would
- # have Envoy proxy `/__/web/**` to a Vite dev server. There
- # is no Vite running in CI, and on the macOS executable-Envoy
- # path that proxy target makes cluster init hang
- # indefinitely, so `--terminate-after-health-check` never
- # fires. The `dist` config sets `--mcp-frontend-host=""`,
- # which skips the proxy entirely. `web/dist/` doesn't need
- # to actually exist; the health check only probes gRPC and
- # Envoy listeners.
rbt $RBT_FLAGS dev run \
- --config=dist \
--terminate-after-health-check \
> "$actual_output_file"
diff --git a/reboot/examples/reboot-swag-store/Dockerfile b/reboot/examples/reboot-swag-store/Dockerfile
index 71245e57..bae893e6 100644
--- a/reboot/examples/reboot-swag-store/Dockerfile
+++ b/reboot/examples/reboot-swag-store/Dockerfile
@@ -8,7 +8,7 @@
# - `/app/backend/api/` and `/app/web/api/` — generated Python and
# TypeScript bindings from `api/reboot_swag_store/v1/store.py`.
# ---------------------------------------------------------------------------
-FROM ghcr.io/reboot-dev/reboot-base:1.2.0 AS backend
+FROM ghcr.io/reboot-dev/reboot-base:1.2.1 AS backend
WORKDIR /app
diff --git a/reboot/examples/reboot-swag-store/backend/tests/store_servicer_test.py b/reboot/examples/reboot-swag-store/backend/tests/store_servicer_test.py
index 2937b195..0c5871ca 100644
--- a/reboot/examples/reboot-swag-store/backend/tests/store_servicer_test.py
+++ b/reboot/examples/reboot-swag-store/backend/tests/store_servicer_test.py
@@ -93,7 +93,7 @@ async def asyncSetUp(self) -> None:
)
)
# Authenticated context for a "guest" user. With
- # `useOAuth: true` enabled in `mcp_servers.json`, every
+ # `--oauth` flag for MCPJam, every
# session — including anonymous ones — gets a stable
# OAuth user-id, which our authorizers rely on. Tests
# bypass the MCP session hook that auto-constructs
diff --git a/reboot/examples/reboot-swag-store/mcp_servers.json b/reboot/examples/reboot-swag-store/mcp_servers.json
deleted file mode 100644
index 3d7b0a58..00000000
--- a/reboot/examples/reboot-swag-store/mcp_servers.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "mcpServers": {
- "reboot-swag-store": {
- "url": "http://localhost:9991/mcp",
- "useOAuth": true
- }
- }
-}
diff --git a/reboot/examples/reboot-swag-store/pyproject.toml b/reboot/examples/reboot-swag-store/pyproject.toml
index a94bed8a..0fab44ac 100644
--- a/reboot/examples/reboot-swag-store/pyproject.toml
+++ b/reboot/examples/reboot-swag-store/pyproject.toml
@@ -7,7 +7,7 @@ dependencies = [
"httpx>=0.27,<1.0",
"python-dotenv>=1.0.0",
"uuid7>=0.1.0",
- "reboot==1.2.0",
+ "reboot==1.2.1",
]
[dependency-groups]
diff --git a/reboot/examples/reboot-swag-store/uv.lock b/reboot/examples/reboot-swag-store/uv.lock
index db6d2154..42fceb0f 100644
--- a/reboot/examples/reboot-swag-store/uv.lock
+++ b/reboot/examples/reboot-swag-store/uv.lock
@@ -1796,7 +1796,7 @@ wheels = [
[[package]]
name = "reboot"
-version = "1.2.0"
+version = "1.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiofiles" },
@@ -1846,9 +1846,9 @@ dependencies = [
{ name = "websockets" },
]
wheels = [
- { url = "https://files.pythonhosted.org/packages/ae/01/d67f4c2cfa6cd6605f25f50d8d6dd962cda3dbb1a3be5a413b27cecfe5ba/reboot-1.2.0-py3-none-macosx_14_0_arm64.whl", hash = "sha256:45c74d870833b0a092c451c117006623363fc559d1224522859d94993d0f6c1e", size = 20440626, upload-time = "2026-06-18T15:42:03.995Z" },
- { url = "https://files.pythonhosted.org/packages/62/fb/32f27dc3d16d5446ca8987cd81961ebc70c4d9d8e8b0cea71257f1a6b4b1/reboot-1.2.0-py3-none-manylinux_2_34_aarch64.whl", hash = "sha256:e3d026f55c1b084f50e417c70fe211d33ac1a796f42eefddd3e288ceb8ea2fd1", size = 24124117, upload-time = "2026-06-18T15:27:54.363Z" },
- { url = "https://files.pythonhosted.org/packages/74/e0/51021d59e6bc3802b1726aee7503d28d7ea0eb507896f0a4b7cac81fccf2/reboot-1.2.0-py3-none-manylinux_2_34_x86_64.whl", hash = "sha256:fb27f3b851e25b797d5cb6e9f93e606ac8ba0c3a138d83f126a2ca26bdfbbd57", size = 24254127, upload-time = "2026-06-18T15:29:00.942Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/85/082fea1aaf65904b2f732c893469cb1d2534dff96ba527ca5575c5afbf55/reboot-1.2.1-py3-none-macosx_14_0_arm64.whl", hash = "sha256:b54cbf0233f297926ce516d39578efaea47953d113eca7095d4d0f07a1e4b192", size = 20452221, upload-time = "2026-06-23T19:41:33.886Z" },
+ { url = "https://files.pythonhosted.org/packages/64/3a/e004b65f1bbef40d8da6051fcff73925fae358a4c3ccf7b00d00bd2c7421/reboot-1.2.1-py3-none-manylinux_2_34_aarch64.whl", hash = "sha256:bb91ee132940d66c6779c2e5038f2fbf70fd567351c912bf0acf33ba60111860", size = 24135744, upload-time = "2026-06-23T19:28:20.962Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/a3/7c8b5caf625a3f1b87b075b5bf23040a6f7761c797c245822144900999ce/reboot-1.2.1-py3-none-manylinux_2_34_x86_64.whl", hash = "sha256:988cb8d6e84981d35c1bf4c9040eda4181d4d221d0ff1e9a48922b7f50c123de", size = 24265726, upload-time = "2026-06-23T19:27:45.447Z" },
]
[[package]]
@@ -1873,7 +1873,7 @@ requires-dist = [
{ name = "anyio", specifier = ">=4.0.0" },
{ name = "httpx", specifier = ">=0.27,<1.0" },
{ name = "python-dotenv", specifier = ">=1.0.0" },
- { name = "reboot", specifier = "==1.2.0" },
+ { name = "reboot", specifier = "==1.2.1" },
{ name = "uuid7", specifier = ">=0.1.0" },
]
diff --git a/reboot/examples/reboot-swag-store/web/package-lock.json b/reboot/examples/reboot-swag-store/web/package-lock.json
index fff16146..f0a09663 100644
--- a/reboot/examples/reboot-swag-store/web/package-lock.json
+++ b/reboot/examples/reboot-swag-store/web/package-lock.json
@@ -10,8 +10,8 @@
"dependencies": {
"@modelcontextprotocol/ext-apps": "1.5.0",
"@modelcontextprotocol/sdk": "1.29.0",
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-react": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-react": "1.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"zod": "^4.0.0"
@@ -886,9 +886,9 @@
}
},
"node_modules/@reboot-dev/reboot-api": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.0.tgz",
- "integrity": "sha512-6k0OhzPX3fcUurmxQY9ITJCX3LrMnimUYIdkPTEWWIpzV1jirpijx5ovTFG08MACic8mu1UMvCLEv7wq3/w1pg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.2.1.tgz",
+ "integrity": "sha512-CmrL7P/dtm+DBMWLWijs+NIlt0PKmdelwmP3Efmsz9x1dkICn6iq/MOh7FLVe2gcHubKL6RuiwO3vOOBVzObPg==",
"license": "Apache-2.0",
"dependencies": {
"@scarf/scarf": "1.4.0",
@@ -913,15 +913,15 @@
}
},
"node_modules/@reboot-dev/reboot-react": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.0.tgz",
- "integrity": "sha512-gNkSLFRgWDl40SbW7VNO+xD16DaFdPBZ95E2bULEHpAcyd8lsg5qiHHCPYThwCkDou/HqWbb/2WH5iiQaBFmRg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.2.1.tgz",
+ "integrity": "sha512-ImVCLLaOTZoDDtAnXdXufScZWOz2DVomVuhs6WwEgjW99yTtwTx1OMsC/wbU37pSmadmdfdlTYP9vy/V9EcFMg==",
"license": "Apache-2.0",
"dependencies": {
"@modelcontextprotocol/ext-apps": "1.5.0",
"@modelcontextprotocol/sdk": "1.29.0",
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-web": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-web": "1.2.1",
"@scarf/scarf": "1.4.0",
"@types/uuid": "^9.0.4",
"js-sha1": "0.7.0",
@@ -949,12 +949,12 @@
}
},
"node_modules/@reboot-dev/reboot-web": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.0.tgz",
- "integrity": "sha512-ziBVJ5WLlnR57nkCUAJc9OAdqnl4PjoIYPdzMMkEYh3wpf10LPT/souGQT9c5BHUSWOx81azdQqU5lCqYXKWsg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.2.1.tgz",
+ "integrity": "sha512-/jk3OdG6pF+McvIP6yEuJKSWmDpHSR4+nVdbvUIHDdJh8cQkFj2QnzQJ+pRbql207LvwUNJ3T9YwZDSYz8rB9A==",
"license": "Apache-2.0",
"dependencies": {
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
"@scarf/scarf": "1.4.0",
"js-sha1": "0.7.0",
"lru-cache-idb": "^0.5.2",
diff --git a/reboot/examples/reboot-swag-store/web/package.json b/reboot/examples/reboot-swag-store/web/package.json
index 3349c574..ab476c3a 100644
--- a/reboot/examples/reboot-swag-store/web/package.json
+++ b/reboot/examples/reboot-swag-store/web/package.json
@@ -17,8 +17,8 @@
"dependencies": {
"@modelcontextprotocol/ext-apps": "1.5.0",
"@modelcontextprotocol/sdk": "1.29.0",
- "@reboot-dev/reboot-react": "1.2.0",
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-react": "1.2.1",
+ "@reboot-dev/reboot-api": "1.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"zod": "^4.0.0"
diff --git a/reboot/mcp/ui.py b/reboot/mcp/ui.py
index 161b6cf1..0d42a360 100644
--- a/reboot/mcp/ui.py
+++ b/reboot/mcp/ui.py
@@ -53,6 +53,7 @@
import urllib.request
import uuid
from pathlib import Path
+from reboot.api import snake_to_camel
from reboot.mcp.iframe import (
cache_busting_iframe_html,
dev_iframe_html,
@@ -802,3 +803,28 @@ def _read_and_inject(
del cache_bust # Intentionally unused in the body.
html = dist_path.read_text()
return _inject_globals(html, reboot_url, ui_name)
+
+
+def camelize_request_payload(value: Any) -> Any:
+ """Recursively convert snake_case dict keys to camelCase.
+
+ Produces the protobuf-canonical JSON shape for a validated
+ `request: ` payload — i.e., `primary_color` becomes
+ `primaryColor`, matching the protobuf-es generated TypeScript
+ field names that customer components are typed against.
+ Pydantic's `model_dump(mode='json')` defaults to Python
+ attribute names (snake_case), so without this step a UI request
+ field named `personalized_message` would land as
+ `personalized_message` on the React side while the TS type
+ expected `personalizedMessage`, and the prop would silently be
+ undefined. Recurses through lists and nested dicts so nested
+ Models in the request payload convert too.
+ """
+ if isinstance(value, dict):
+ return {
+ snake_to_camel(key): camelize_request_payload(inner)
+ for key, inner in value.items()
+ }
+ if isinstance(value, list):
+ return [camelize_request_payload(item) for item in value]
+ return value
diff --git a/reboot/nodejs/package.json b/reboot/nodejs/package.json
index fd861d5b..63bb9828 100644
--- a/reboot/nodejs/package.json
+++ b/reboot/nodejs/package.json
@@ -2,7 +2,7 @@
"dependencies": {
"@bufbuild/protoplugin": "1.10.1",
"@bufbuild/protoc-gen-es": "1.10.1",
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
"chalk": "^4.1.2",
"node-addon-api": "^7.0.0",
"node-gyp": ">=10.2.0",
@@ -22,7 +22,7 @@
},
"type": "module",
"name": "@reboot-dev/reboot",
- "version": "1.2.0",
+ "version": "1.2.1",
"description": "npm package for Reboot",
"scripts": {
"preinstall": "node preinstall.cjs",
diff --git a/reboot/ping/README.md b/reboot/ping/README.md
index a930a562..568d61af 100644
--- a/reboot/ping/README.md
+++ b/reboot/ping/README.md
@@ -13,11 +13,11 @@ features:
## Using MCPJam
-The application contains MCP functionality. Test it using MCPJam:
+For testing with MCPJam Inspector:
-```
-# From the root of the Bazel repo:
-npx @mcpjam/inspector@2.9.3 --config reboot/ping/mcp_servers.json --server ping-server
+```bash
+# In another terminal, run MCPJam:
+npx @mcpjam/inspector@2.18.1 --url http://localhost:9991/mcp --oauth
```
## On the local cluster
diff --git a/reboot/ping/mcp_servers.json b/reboot/ping/mcp_servers.json
deleted file mode 100644
index a83edcc1..00000000
--- a/reboot/ping/mcp_servers.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "mcpServers": {
- "ping-server": {
- "url": "http://localhost:9991/mcp",
- "useOAuth": true
- }
- }
-}
diff --git a/reboot/ping/ping_api.py b/reboot/ping/ping_api.py
index 77eb13e6..d63fbfd6 100644
--- a/reboot/ping/ping_api.py
+++ b/reboot/ping/ping_api.py
@@ -100,6 +100,16 @@ class CounterState(Model):
description: str = Field(tag=2, default="")
+class ShowClickerProps(Model):
+ # Visual style for the clicker UI; the AI picks a primary
+ # color to match the user's mood or the counter's purpose.
+ # The snake_case field name is intentional — it exercises the
+ # Python→TS camelCase conversion path that any real Reboot
+ # app needs for multi-word UI request fields. Empty string
+ # means use the app's default style.
+ primary_color: str = Field(tag=1, default="")
+
+
api = API(
Ping=Type(
state=PingState,
@@ -182,7 +192,7 @@ class CounterState(Model):
state=CounterState,
methods=Methods(
show_clicker=UI(
- request=None,
+ request=ShowClickerProps,
path="web/ui/clicker",
title="Counter Clicker",
description=("Interactive clicker for the Counter."),
diff --git a/reboot/ping/web/ui/clicker/App.tsx b/reboot/ping/web/ui/clicker/App.tsx
index 1684b079..9a21c4fc 100644
--- a/reboot/ping/web/ui/clicker/App.tsx
+++ b/reboot/ping/web/ui/clicker/App.tsx
@@ -1,12 +1,21 @@
import { useState, type FC } from "react";
-import { useCounter } from "../../../ping_api_zod_rbt_react";
+import {
+ type ShowClickerProps,
+ useCounter,
+} from "../../../ping_api_zod_rbt_react";
import css from "./App.module.css";
/**
* Compact clicker widget. Uses generated MCP-aware hook:
- * WebSocket for reads, MCP tools for writes.
+ * WebSocket for reads, MCP tools for writes. The `primaryColor`
+ * prop comes from the `ShowClickerProps` request the AI passes
+ * to the `show_clicker` MCP tool; `main.tsx` renders a bare
+ * ` `, and `RebootClientProvider` (via its internal
+ * `McpConnector`) auto-injects the request fields as props onto
+ * it with `React.cloneElement`. Python `primary_color` is
+ * generated as TypeScript `primaryColor` by protobuf-es.
*/
-export const ClickerApp: FC = () => {
+export const ClickerApp: FC = ({ primaryColor }) => {
const [isPending, setIsPending] = useState(false);
const counter = useCounter();
@@ -32,7 +41,10 @@ export const ClickerApp: FC = () => {
}
return (
-
+
{count}
":{"url":"http://localhost:9991/mcp","useOAuth":true}}}`.
-14. Run the app — load the [`run` skill](../run/SKILL.md) and
+13. Run the app — load the [`run` skill](../run/SKILL.md) and
follow it. It is the single canonical "start the app"
procedure: it detects the app type, makes sure dependencies
and secrets are in place, and starts the backend and
diff --git a/reboot/plugin/skills/chat-app/references/auth-oauth-providers.md b/reboot/plugin/skills/chat-app/references/auth-oauth-providers.md
index 1e78161f..b4cff2ff 100644
--- a/reboot/plugin/skills/chat-app/references/auth-oauth-providers.md
+++ b/reboot/plugin/skills/chat-app/references/auth-oauth-providers.md
@@ -97,6 +97,50 @@ host-agnostic recipe (custom endpoints + the in-`Workflow` call) is in
[`python/references/auth-external-api-calls.md`](../../python/references/auth-external-api-calls.md).
The rest of this reference is only about **identity**.
+## How MCP Clients Sign In: The Consent Screen
+
+MCP clients register with your app **dynamically** (RFC 7591 Dynamic
+Client Registration): any MCP host — claude.ai, ChatGPT, Cursor, Codex,
+or one that doesn't exist yet — can `POST /__/oauth/register` and get a
+`client_id`, with no prior coordination with your deployment. That open
+registration is the whole point: it's what lets new MCP hosts integrate
+with your app without you shipping an allow-list of every client.
+
+The flip side is that **anyone** can register a client, including an
+attacker. They can register one whose `redirect_uri` points at a server
+_they_ control, then send a victim a link to your app's own
+`/__/oauth/authorize` endpoint. The victim is on your real origin —
+nothing in the URL bar looks wrong — so if they sign in, the
+authorization code (and the access token it exchanges for) would be
+delivered to the attacker's `redirect_uri`, handing the attacker a
+session as the victim. PKCE doesn't help: the attacker is the
+registered client, so they hold the verifier. This is the classic
+OAuth "open client" / confused-deputy attack.
+
+Reboot closes this with a **consent screen**. Before redirecting to
+your identity provider, `/__/oauth/authorize` shows the user who is
+asking — the client's self-reported name, and **prominently the
+`redirect_uri` host** the tokens would be sent to — and requires an
+explicit approval (a CSRF-protected `POST /__/oauth/consent`) to
+continue. A victim handed an attacker's link gets a clear chance to
+notice an unfamiliar destination (`evil.example` next to a host they
+recognize) and cancel before any sign-in happens.
+
+This is automatic — there's nothing to configure, and it applies to
+every dynamically-registered client (i.e. every MCP host). Two things
+worth knowing:
+
+- **The user is the last line of defense.** The consent screen makes
+ the unknown client _visible_; it can't tell a careful "yes" from a
+ careless one. The client name is attacker-controlled, so the
+ `redirect_uri` host shown beneath it — not the name — is the signal
+ to trust.
+- **Custom-IdP apps are unaffected.** An app that sets
+ `Application(token_verifier=...)` to validate tokens from its own
+ external IdP doesn't run Reboot's OAuth server at all — there's no
+ Reboot `/__/oauth/*` flow, so there's no consent screen and nothing
+ here applies.
+
## Replace `Development` Before Production
This part is obvious: `Development` is a local-development convenience.
diff --git a/reboot/plugin/skills/chat-app/references/react-scaffolding.md b/reboot/plugin/skills/chat-app/references/react-scaffolding.md
index 91955238..be37ee9c 100644
--- a/reboot/plugin/skills/chat-app/references/react-scaffolding.md
+++ b/reboot/plugin/skills/chat-app/references/react-scaffolding.md
@@ -32,8 +32,8 @@ scripts directly.**
"dependencies": {
"@modelcontextprotocol/ext-apps": "1.5.0",
"@modelcontextprotocol/sdk": "1.29.0",
- "@reboot-dev/reboot-react": "1.2.0",
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-react": "1.2.1",
+ "@reboot-dev/reboot-api": "1.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"zod": "^4.0.0"
diff --git a/reboot/plugin/skills/python/references/lifecycle-dockerfile.md b/reboot/plugin/skills/python/references/lifecycle-dockerfile.md
index fa53cadd..b07a8c6b 100644
--- a/reboot/plugin/skills/python/references/lifecycle-dockerfile.md
+++ b/reboot/plugin/skills/python/references/lifecycle-dockerfile.md
@@ -27,7 +27,7 @@ backend is the API server; any UI is deployed separately). Matches
verbatim:
```dockerfile
-FROM ghcr.io/reboot-dev/reboot-base:1.2.0
+FROM ghcr.io/reboot-dev/reboot-base:1.2.1
WORKDIR /app
@@ -160,7 +160,6 @@ web/
.DS_Store
# Local-only files.
-mcp_servers.json
README.md
```
@@ -197,7 +196,6 @@ web/dist/
.DS_Store
# Local-only files.
-mcp_servers.json
README.md
```
diff --git a/reboot/plugin/skills/python/references/lifecycle-project-setup.md b/reboot/plugin/skills/python/references/lifecycle-project-setup.md
index e37e06f3..ef7eab3b 100644
--- a/reboot/plugin/skills/python/references/lifecycle-project-setup.md
+++ b/reboot/plugin/skills/python/references/lifecycle-project-setup.md
@@ -55,7 +55,7 @@ name = "my-app"
version = "0.1.0"
requires-python = ">= 3.10"
dependencies = [
- "reboot==1.2.0",
+ "reboot==1.2.1",
]
[dependency-groups]
diff --git a/reboot/plugin/skills/run/SKILL.md b/reboot/plugin/skills/run/SKILL.md
index 29dfd330..949c2663 100644
--- a/reboot/plugin/skills/run/SKILL.md
+++ b/reboot/plugin/skills/run/SKILL.md
@@ -39,7 +39,6 @@ strongest first.
**MCP Chat App** — any of:
-- `mcp_servers.json` exists in the project root.
- An API file under `api/` uses `mcp=Tool()`, `mcp=None`, or `UI(`
(the `UI()` method type).
- `.rbtrc` has a `dev run --default-config=hmr` line together with
@@ -48,7 +47,6 @@ strongest first.
**Web App** — all of:
-- No `mcp_servers.json`.
- No `mcp=` / `UI(` anywhere under `api/`.
- A single SPA entry at `web/index.html` (top of `web/`, not under
`web/ui/`).
@@ -189,7 +187,7 @@ the app's `/mcp` endpoint with OAuth — for the user to run from
their own terminal:
```sh
-npx @mcpjam/inspector@2.9.3 --url http://localhost:9991/mcp --oauth
+npx @mcpjam/inspector@2.18.1 --url http://localhost:9991/mcp --oauth
```
Leave the launch to the wizard. Only run it yourself if the user
diff --git a/reboot/plugin/skills/upgrade/migrations/1.2.1/remove-mcp-servers-json.md b/reboot/plugin/skills/upgrade/migrations/1.2.1/remove-mcp-servers-json.md
new file mode 100644
index 00000000..e9a336ad
--- /dev/null
+++ b/reboot/plugin/skills/upgrade/migrations/1.2.1/remove-mcp-servers-json.md
@@ -0,0 +1,14 @@
+## `mcp_servers.json` is no longer used
+
+MCPJam Inspector is now launched with explicit flags rather than a
+config file, so the `mcp_servers.json` file that older Chat Apps kept
+in their project root is unused.
+
+If a `mcp_servers.json` file exists at the project root, delete it.
+Then remove any leftover references to it:
+
+- In `.dockerignore`, delete a line that is exactly `mcp_servers.json`.
+- Anywhere (e.g. `README.md`) that launches MCPJam as
+ `npx @mcpjam/inspector@ --config mcp_servers.json --server `, replace it with
+ `npx @mcpjam/inspector@ --url http://localhost:9991/mcp --oauth`
+ (use the port the app's backend serves on if it isn't `9991`).
diff --git a/reboot/plugin/skills/upgrade/migrations/1.2.1/user-create-transaction.md b/reboot/plugin/skills/upgrade/migrations/1.2.1/user-create-transaction.md
new file mode 100644
index 00000000..5de2ffbb
--- /dev/null
+++ b/reboot/plugin/skills/upgrade/migrations/1.2.1/user-create-transaction.md
@@ -0,0 +1,26 @@
+## The auto-constructed `User.create` is now a `Transaction`
+
+The reserved `create` factory method on an auto-constructed `User`
+state used to be a `Writer`; it is now a `Transaction`. An override of
+it can now construct other state machines (call other Reboot services)
+while a `User` is being created.
+
+If a servicer that subclasses `User.Servicer` overrides `create` with
+a `WriterContext` parameter, e.g.:
+
+```python
+async def create(self, context: WriterContext) -> None:
+ ...
+```
+
+change the parameter type to `TransactionContext`:
+
+```python
+async def create(self, context: TransactionContext) -> None:
+ ...
+```
+
+and import `TransactionContext` in place of the now-unused
+`WriterContext` (drop the `WriterContext` import only if nothing else
+in the file still needs it). Apps that do not override `create` need
+no changes.
diff --git a/reboot/plugin/skills/upgrade/migrations/next/mcp-ui-single-child.md b/reboot/plugin/skills/upgrade/migrations/next/mcp-ui-single-child.md
new file mode 100644
index 00000000..bc51a626
--- /dev/null
+++ b/reboot/plugin/skills/upgrade/migrations/next/mcp-ui-single-child.md
@@ -0,0 +1,32 @@
+## `RebootClientProvider` must wrap exactly one element in MCP UIs
+
+Reboot now auto-injects the AI-supplied UI request props (from
+`UI(request=)` declarations) onto the single child element of
+`RebootClientProvider`, using `React.cloneElement`. As a result, in an
+MCP UI, `RebootClientProvider` must wrap exactly one React element —
+your UI App component. A misconfigured tree now throws at render time
+with a message explaining the fix.
+
+In each UI's `main.tsx` (typically under a `web/ui//` directory),
+find any `RebootClientProvider` that wraps more than one child, a
+fragment (`<>...>`), an array, or a non-element child, e.g.:
+
+```tsx
+
+
+
+
+```
+
+Wrap those children in a single parent component and render that one
+component instead:
+
+```tsx
+
+
+
+```
+
+where `AppShell` is a component you define that renders ``
+and ` `. UIs that already render a single component need no
+changes.
diff --git a/reboot/plugin/skills/web-app/SKILL.md b/reboot/plugin/skills/web-app/SKILL.md
index 4086a6d2..d8182700 100644
--- a/reboot/plugin/skills/web-app/SKILL.md
+++ b/reboot/plugin/skills/web-app/SKILL.md
@@ -30,8 +30,7 @@ backend behind a standalone React frontend served at a normal URL.
> MCP host (ChatGPT, Claude, VSCode, Goose, …) with MCP tools and
> embedded UIs, use the [chat-app skill](../chat-app/SKILL.md) instead.
> Signals you're in the wrong place: `mcp=Tool()`, `UI()`, `User`
-> auto-construct from the MCP host, `mcp_servers.json`, MCPJam
-> inspector.
+> auto-construct from the MCP host, MCPJam inspector.
## When to Use
@@ -52,7 +51,7 @@ The Reboot backend is identical. The deltas are all on the surface:
| API exposure | `mcp=Tool()` on writer/transaction methods. | Methods exposed only through the generated React client. |
| UI shape | `UI()` methods → artifacts embedded in the MCP host. | A normal SPA at `web/` opened at a URL. |
| Vite config | Special — nested `dist//index.html` for MCP. | Stock single-page Vite output. |
-| Test surface | MCPJam inspector + `mcp_servers.json`. | Browser + the standard React devtools / Playwright. |
+| Test surface | MCPJam inspector. | Browser + the standard React devtools / Playwright. |
| `User` type | Required — the MCP entry point. | Optional — only if your app needs per-user state. |
Backend mechanics (state, methods, Servicers, workflows, refs,
@@ -412,7 +411,7 @@ Key differences from a `chat-app` layout:
- `vite.config.ts` is the **stock** Vite config — no nested-output
override, no `viteSingleFile` plugin. There's no MCP host
resolving artifacts by path.
-- No `mcp_servers.json`. No MCPJam inspector.
+- No MCPJam inspector.
## Step-by-Step Build Flow
diff --git a/reboot/protoc_gen_reboot_generic.py b/reboot/protoc_gen_reboot_generic.py
index 08f27a5c..4b128f89 100644
--- a/reboot/protoc_gen_reboot_generic.py
+++ b/reboot/protoc_gen_reboot_generic.py
@@ -918,13 +918,13 @@ def _base_services_for_state(
if not has_auto_construct:
raise UserProtoError(
f"State type '{proto_state.name}' requires "
- f"a '{AUTO_CONSTRUCT_PROTO_METHOD}' Writer "
- "method. Add to your service:\n"
+ f"a '{AUTO_CONSTRUCT_PROTO_METHOD}' "
+ "Transaction method. Add to your service:\n"
f" rpc {AUTO_CONSTRUCT_PROTO_METHOD}"
"(google.protobuf.Empty) returns "
"(google.protobuf.Empty) {\n"
" option (rbt.v1alpha1.method) = "
- "{ writer: {} };\n"
+ "{ transaction: {} };\n"
" }"
)
diff --git a/reboot/react/internal/McpConnector.tsx b/reboot/react/internal/McpConnector.tsx
index 7cd51627..0bad6d83 100644
--- a/reboot/react/internal/McpConnector.tsx
+++ b/reboot/react/internal/McpConnector.tsx
@@ -14,7 +14,17 @@ import {
useHostStyleVariables,
type App as McpApp,
} from "@modelcontextprotocol/ext-apps/react";
-import { useCallback, useMemo, useRef, useState, type ReactNode } from "react";
+import {
+ Children,
+ cloneElement,
+ isValidElement,
+ useCallback,
+ useMemo,
+ useRef,
+ useState,
+ type ReactElement,
+ type ReactNode,
+} from "react";
import { McpAppContext, type McpAppContextValue } from "./index.js";
import { useAppSafe } from "./useAppSafe.js";
@@ -27,6 +37,41 @@ export default function McpConnector({
setBearerToken: (token?: string) => void;
children: ReactNode;
}) {
+ // Reboot auto-injects the AI-supplied `UI(request=...)` props onto
+ // the single child element via `React.cloneElement` (see the render
+ // at the bottom of this component), so `RebootClientProvider` in MCP
+ // mode must wrap exactly one React element — the customer's UI App.
+ // Validate eagerly here, before any other render or state setup, so a
+ // misconfigured provider tree fails fast with a Reboot-specific
+ // message that points at the fix, rather than silently injecting the
+ // props onto just one of several siblings while the rest render
+ // untouched. Common mistakes this catches:
+ // - multiple siblings:
+ // - fragment wrapper: <>...>
+ // - text/null/array: "loading", null, [ , ]
+ // - empty provider:
+ const childArray = Children.toArray(children);
+ if (childArray.length !== 1 || !isValidElement(childArray[0])) {
+ const count = childArray.length;
+ throw new Error(
+ `RebootClientProvider must wrap exactly one React ` +
+ `element (your UI App component); got ${count} ` +
+ `${count === 1 ? "child" : "children"}. Reboot ` +
+ `auto-injects UI request props (from ` +
+ `UI(request=) declarations) onto that single ` +
+ `child via React.cloneElement, so a fragment, an ` +
+ `array of siblings, or a non-element child would ` +
+ `silently drop the props. Wrap multiple top-level ` +
+ `components in a single parent component (e.g. ` +
+ ` ) and ` +
+ `render that one component here.`
+ );
+ }
+ // The validated single child — used below in place of the
+ // raw `children` prop so the cloneElement path and the
+ // pass-through path share the same element.
+ const onlyChild = childArray[0] as ReactElement;
+
// Merged tool data from ontoolinput + ontoolresult.
const [toolData, setToolData] = useState | null>(
null
@@ -214,9 +259,28 @@ export default function McpConnector({
refreshMCPBearerToken,
};
+ // Auto-inject the AI-supplied request props onto the single
+ // child component, so customer `main.tsx` can write a plain
+ // ` ` and the App's typed `FC<>` props get
+ // populated automatically. The MCP tool stub puts the
+ // validated, camelCased request payload under
+ // `toolData.request`. UIs declared with `UI(request=None)`
+ // have no `request` field and the child renders unchanged.
+ // `onlyChild` is the validated single element from the top of
+ // this component.
+ const requestProps =
+ toolData?.request != null &&
+ typeof toolData.request === "object" &&
+ !Array.isArray(toolData.request)
+ ? (toolData.request as Record)
+ : null;
+
+ const child =
+ requestProps !== null ? cloneElement(onlyChild, requestProps) : onlyChild;
+
return (
- {children}
+ {child}
);
}
diff --git a/reboot/react/package.json b/reboot/react/package.json
index 488d360d..2c28cd18 100644
--- a/reboot/react/package.json
+++ b/reboot/react/package.json
@@ -1,6 +1,6 @@
{
"name": "@reboot-dev/reboot-react",
- "version": "1.2.0",
+ "version": "1.2.1",
"description": "npm package for Reboot React",
"main": "index.js",
"type": "module",
@@ -20,8 +20,8 @@
},
"author": "reboot-dev",
"dependencies": {
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-web": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-web": "1.2.1",
"@types/uuid": "^9.0.4",
"js-sha1": "0.7.0",
"tslib": "^2.6.2",
diff --git a/reboot/rootpage/tunnel_step.tsx b/reboot/rootpage/tunnel_step.tsx
index 6b68da76..eb99603e 100644
--- a/reboot/rootpage/tunnel_step.tsx
+++ b/reboot/rootpage/tunnel_step.tsx
@@ -115,7 +115,7 @@ const MCPJAM_LOCAL_URL = "http://localhost:6274";
// MCPJam inspector version the launch command pins to. Kept in
// sync with the plugin's `bin/mcpjam-inspector` shim so a
// hand-launched inspector matches the one an agent would start.
-const MCPJAM_VERSION = "2.9.3";
+const MCPJAM_VERSION = "2.18.1";
// Step 01 variant for MCPJam — no tunnel needed, and MCPJam runs
// locally so there's no separate "install" step either. The
diff --git a/reboot/routing/xds_server.py b/reboot/routing/xds_server.py
index 8d44fb3b..1532153c 100644
--- a/reboot/routing/xds_server.py
+++ b/reboot/routing/xds_server.py
@@ -16,6 +16,40 @@
TYPE_LISTENER = "type.googleapis.com/envoy.config.listener.v3.Listener"
+def _kind_for_type_url(type_url: str) -> str:
+ if type_url == TYPE_LISTENER:
+ return "listener"
+ if type_url == TYPE_CLUSTER:
+ return "cluster"
+ return type_url
+
+
+class ServerError(Exception):
+ """A fatal error in the xDS server. Once one is recorded the server
+ can no longer serve correct configuration, so owners surface it to
+ bring the whole process down with a helpful message rather than
+ continue running in a broken state."""
+
+
+class ConfigRejected(ServerError):
+ """Raised when a connected Envoy rejects (NACKs) the xDS config we
+ served it. A NACK means Envoy will not serve traffic with this
+ config, and since the config is generated by Reboot (not the user),
+ re-serving it would just spin in an endless NACK loop."""
+
+ def __init__(self, type_url: str, message: str):
+ self.type_url = type_url
+ self.envoy_message = message
+ super().__init__(
+ f"Envoy rejected the xDS {_kind_for_type_url(type_url)} "
+ "configuration and will not serve traffic:\n\n"
+ f" {message}\n\n"
+ "This is a bug in the Envoy configuration Reboot generated.\n"
+ "Reboot cannot route traffic and is shutting down; please "
+ "report this bug to the maintainers."
+ )
+
+
class AggregatedDiscoveryServiceServicer(
ads_pb2_grpc.AggregatedDiscoveryServiceServicer
):
@@ -65,6 +99,11 @@ class ClientEntry:
# of the latest version.
requested_types: set[str]
+ # The config version of the most recent cluster update we've sent
+ # this client (empty before any). Used to keep listeners from
+ # being sent ahead of the clusters they route to.
+ last_cluster_version_sent: str = ""
+
def set_version_info(self, type_url: str, version_info: str):
self.version_info_by_type[type_url] = version_info
self.changed.set()
@@ -88,6 +127,31 @@ def __init__(
self._clients: list[AggregatedDiscoveryServiceServicer.ClientEntry
] = []
self._client_added_events: list[asyncio.Event] = []
+ # Set to the first fatal error the server hit (a NACK, an
+ # unsupported Envoy version, an unexpected exception, ...).
+ # Once set, the server can no longer serve correct config;
+ # owners poll `fatal_error` to bring the process down instead
+ # of running in a broken state.
+ self._fatal_error: Optional[ServerError] = None
+
+ @property
+ def fatal_error(self) -> Optional[ServerError]:
+ """The first fatal error the server hit, if any."""
+ return self._fatal_error
+
+ def _record_fatal_error(self, error: ServerError):
+ """Record the first fatal error and wake everything waiting on
+ the server so owners can bring the process down. Idempotent:
+ only the first error is kept."""
+ if self._fatal_error is not None:
+ return
+ self._fatal_error = error
+ logger.error("Fatal xDS server error; shutting down:\n%s", error)
+ # Wake every client's `_wait_until_*` waiter, since a waiter may
+ # be blocked on a different client than the one that failed.
+ for client in self._clients:
+ client.changed.set()
+ client.changed.clear()
async def _mark_config_changed(self):
# The version number is a timestamp, to ensure that version numbers are
@@ -102,7 +166,7 @@ def _version_info(self) -> str:
async def _wait_until_all_clients_at_version(self, version_info: str):
for client in self._clients:
- while not client.disconnected and (
+ while not client.disconnected and self._fatal_error is None and (
# ASSUMPTION: all clients will (eventually) ask for both the
# cluster and listeners configs. This holds, since
# we know our Envoys request both.
@@ -118,6 +182,12 @@ async def _wait_until_all_clients_at_version(self, version_info: str):
):
await client.changed.wait()
+ # If the server hit a fatal error while we were waiting, the
+ # config will never be applied; surface it rather than returning
+ # as if the update succeeded.
+ if self._fatal_error is not None:
+ raise self._fatal_error
+
async def _wait_until_at_least_one_client(self):
if len(self._clients) > 0:
return
@@ -218,16 +288,43 @@ async def _one_request():
request.node.user_agent_build_version.version.patch
)
)
+ next_request_task = None
if envoy_version < ENVOY_VERSION:
- logger.error(
- f"Envoy version '{envoy_version}' is less than "
- f"the minimum required version '{ENVOY_VERSION}'"
+ # We pin Envoy's version and control both it and
+ # this xDS server, so an older Envoy is a bug we
+ # treat as fatal rather than limp along with.
+ self._record_fatal_error(
+ ServerError(
+ f"Envoy version '{envoy_version}' is less "
+ "than the minimum required version "
+ f"'{ENVOY_VERSION}'."
+ )
)
- next_request_task = None
+ continue
if request.type_url not in [TYPE_CLUSTER, TYPE_LISTENER]:
- raise ValueError(
- f"Request for unexpected 'type_url': '{request.type_url}'"
+ # Likewise, an unexpected resource type is a bug
+ # in Envoy (it asked for something we never
+ # serve); treat it as fatal and crash rather than
+ # limp along.
+ self._record_fatal_error(
+ ServerError(
+ "Request for unexpected 'type_url': "
+ f"'{request.type_url}'."
+ )
)
+ continue
+ if request.HasField("error_detail"):
+ # Envoy NACK'd the version we last served for this
+ # type. Record it as fatal; do not fall through to
+ # re-serve the rejected config, which would just
+ # spin in an endless NACK loop.
+ self._record_fatal_error(
+ ConfigRejected(
+ request.type_url,
+ request.error_detail.message,
+ )
+ )
+ continue
client_entry.set_version_info(
request.type_url, request.version_info
)
@@ -236,20 +333,49 @@ async def _one_request():
await config_changed_task # So we don't get "never awaited" warnings.
config_changed_task = None # Mark this config change as consumed.
+ if self._fatal_error is not None:
+ # The server hit a fatal error; serving more config
+ # can't help (and re-serving a rejected config would
+ # just provoke another NACK). Stop serving and let the
+ # owner bring the process down.
+ continue
+
latest_version = self._version_info()
for type_url in [
- # Always send the clusters first, then the listeners.
- # Listeners will get rejected if they reference a cluster
- # that doesn't exist.
+ # Always send the clusters before the listeners: Envoy
+ # rejects a listener that routes to a cluster it
+ # doesn't have yet.
TYPE_CLUSTER,
TYPE_LISTENER,
]:
if type_url not in client_entry.requested_types:
- # The client hasn't sent us a request for this type, or
- # if it did so previously we've answered it previously.
- # Don't send any further updates until it asks for them.
+ # Only answer types the client has asked for and
+ # that we haven't already answered.
continue
+
+ if (
+ type_url == TYPE_LISTENER and
+ client_entry.last_cluster_version_sent < latest_version
+ ):
+ # Don't release a listener until we've sent this
+ # client clusters at >= the listener's version. A
+ # config update can add a cluster a new listener
+ # routes to, and Envoy rejects a listener whose
+ # cluster it doesn't have yet ("unknown cluster
+ # ..."). Clusters are sent earlier in this same
+ # loop and travel ahead of the listener on the one
+ # (FIFO) ADS stream, so a listener referencing the
+ # current clusters is released as soon as those
+ # clusters go out — but a listener whose clusters
+ # haven't been sent yet waits. We gate on what we
+ # have *sent*, not on what the client has acked, so
+ # we don't stall on the ack; and we never withhold
+ # clusters this way, since with ADS Envoy waits for
+ # the cluster response before requesting listeners,
+ # so withholding clusters would deadlock.
+ continue
+
client_version = client_entry.version_info_by_type[type_url
]
if client_version >= latest_version:
@@ -275,6 +401,11 @@ async def _one_request():
yield response
+ if type_url == TYPE_CLUSTER:
+ # The client now has clusters at this version, so
+ # listeners that reference them can be released.
+ client_entry.last_cluster_version_sent = latest_version
+
# The request has been satisfied. Don't send this type again
# until the client explicitly requests it again.
client_entry.requested_types.remove(type_url)
@@ -287,9 +418,15 @@ async def _one_request():
# The server is shutting down. That's fine.
pass
- except:
- logger.error("There was an error in the ADS server:")
+ except Exception as error:
+ # Any unexpected error means we can no longer serve correct
+ # xDS config; record it as fatal so owners bring the process
+ # down rather than limp along with a dead ADS stream.
traceback.print_exc()
+ self._record_fatal_error(
+ error if isinstance(error, ServerError) else
+ ServerError(f"Unexpected error in the ADS server: {error}")
+ )
raise
finally:
diff --git a/reboot/server/executable_local_envoy.py b/reboot/server/executable_local_envoy.py
index 8afd88bd..3b5ed99d 100644
--- a/reboot/server/executable_local_envoy.py
+++ b/reboot/server/executable_local_envoy.py
@@ -107,6 +107,15 @@ def trusted_port(self) -> int:
)
return self._trusted_port
+ async def _raise_if_fatal_error(self):
+ """If the xDS server has hit a fatal error (e.g. the connected
+ Envoy rejected our config), tear down the Envoy process and
+ raise it."""
+ fatal_error = self._servicer.fatal_error
+ if fatal_error is not None:
+ await self._stop()
+ raise fatal_error
+
async def _start(self):
# We have Envoy write its logs to a file. This has two benefits:
# 1. Unlike when writing to a stream like `stdout`, we can't
@@ -187,6 +196,10 @@ async def _start(self):
# ready.
logger.debug("Waiting for Envoy admin address file...")
while True:
+ # If Envoy rejected our xDS config it won't come up, so bail
+ # out of this poll instead of spinning until the timeout.
+ await self._raise_if_fatal_error()
+
# Check if process has exited.
if self._process.returncode is not None:
raise RuntimeError(
@@ -227,6 +240,11 @@ def _fetch_listeners(url: str) -> dict:
listeners_url = f"http://127.0.0.1:{self._admin_port}/listeners?format=json"
while True:
+ # If Envoy rejected our xDS config the listeners never come
+ # up, so this poll would hang forever; surface the error and
+ # bail instead.
+ await self._raise_if_fatal_error()
+
try:
listeners_data = await asyncio.to_thread(
_fetch_listeners, listeners_url
@@ -293,6 +311,8 @@ async def _output_logs():
)
async def _stop(self):
+ """Terminate the Envoy process, escalating to a kill if it
+ doesn't exit promptly. Safe to call if it has already exited."""
assert self._process is not None
try:
self._process.terminate()
diff --git a/reboot/server/local_envoy.py b/reboot/server/local_envoy.py
index b6177960..c3b29f45 100644
--- a/reboot/server/local_envoy.py
+++ b/reboot/server/local_envoy.py
@@ -90,11 +90,15 @@ def __init__(
self._register_xds_port()
self._servicer = AggregatedDiscoveryServiceServicer(
- clusters=[],
- # Initialize our xDS servicer with `listeners` so that our
- # connecting Envoy at least gets a port it should listen
- # to, otherwise any Envoy connecting will have no
+ # Initialize our xDS servicer with `clusters` and `listeners`
+ # so that our connecting Envoy at least gets a port it can
+ # listen to, otherwise any Envoy connecting will have no
# configuration at all.
+ clusters=envoy_config.clusters(
+ application_id=self._application_id,
+ servers=[],
+ local_envoy_mode=self._mode,
+ ),
listeners=self._get_listeners_from_servers([]),
)
ads_pb2_grpc.add_AggregatedDiscoveryServiceServicer_to_server(
diff --git a/reboot/std/package.json b/reboot/std/package.json
index 55f8ef19..840dfe09 100644
--- a/reboot/std/package.json
+++ b/reboot/std/package.json
@@ -1,6 +1,6 @@
{
"name": "@reboot-dev/reboot-std",
- "version": "1.2.0",
+ "version": "1.2.1",
"description": "Reboot standard library.",
"main": "index.js",
"type": "module",
@@ -10,8 +10,8 @@
},
"author": "reboot-dev",
"dependencies": {
- "@reboot-dev/reboot-std-api": "1.2.0",
- "@reboot-dev/reboot": "1.2.0",
+ "@reboot-dev/reboot-std-api": "1.2.1",
+ "@reboot-dev/reboot": "1.2.1",
"@scarf/scarf": "1.4.0"
},
"license": "Apache-2.0",
diff --git a/reboot/std/react/package.json b/reboot/std/react/package.json
index 4930e1b4..26943027 100644
--- a/reboot/std/react/package.json
+++ b/reboot/std/react/package.json
@@ -1,6 +1,6 @@
{
"name": "@reboot-dev/reboot-std-react",
- "version": "1.2.0",
+ "version": "1.2.1",
"description": "Reboot standard library for React.",
"main": "index.js",
"type": "module",
@@ -10,10 +10,10 @@
},
"author": "reboot-dev",
"dependencies": {
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-react": "1.2.0",
- "@reboot-dev/reboot-std-api": "1.2.0",
- "@reboot-dev/reboot-web": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-react": "1.2.1",
+ "@reboot-dev/reboot-std-api": "1.2.1",
+ "@reboot-dev/reboot-web": "1.2.1",
"@scarf/scarf": "1.4.0"
},
"license": "Apache-2.0",
diff --git a/reboot/templates/reboot.py.j2 b/reboot/templates/reboot.py.j2
index 1de0aa5c..106948d6 100644
--- a/reboot/templates/reboot.py.j2
+++ b/reboot/templates/reboot.py.j2
@@ -2888,6 +2888,10 @@ class {{ state.proto.name }}BaseServicer(IMPORT_reboot_aio_servicers.Servicer):
{% if state.proto.auto_construct != AUTO_CONSTRUCT_UNSPECIFIED %}
async def {{ mcp_name_prefix }}{{ ui.name }}_tool(
ctx: IMPORT_mcp_server_fastmcp.Context,
+{% if ui.request_message %}
+ *,
+ request: {{ state.proto.name }}.{{ ui.request_message }},
+{% endif %}
) -> dict:
_ids: dict[IMPORT_reboot_aio_types.StateTypeName, str | None] = {
IMPORT_reboot_aio_types.StateTypeName("{{ state.proto.full_name }}"):
@@ -2898,7 +2902,31 @@ class {{ state.proto.name }}BaseServicer(IMPORT_reboot_aio_servicers.Servicer):
if _auto_construct_full_name not in _ids:
_ids[_auto_construct_full_name] = (
IMPORT_reboot_mcp_context.get_mcp_user_id(ctx))
+{% if ui.request_message %}
+ # Echo the validated request under `request` so
+ # `McpConnector.ontoolresult` carries it across to the
+ # React side without colliding with framework-reserved
+ # keys (`ids`, `bearer_token`). `McpConnector` injects
+ # these fields as props onto the single UI component via
+ # `React.cloneElement` (reading them from
+ # `toolData.request`), so customers write a plain
+ # ` ` and don't read `useMcpToolData()?.request`
+ # themselves. The payload is camelCased to match the
+ # protobuf-es generated TypeScript field names —
+ # Pydantic's default `model_dump` returns snake_case, but
+ # the customer's generated `...Props` interface (the type
+ # parameter of their App's `FC<...>`) is camelCase, so a
+ # field named `primary_color` here must land as
+ # `primaryColor` in the prop.
+ result: dict = {
+ "ids": _ids,
+ "request": IMPORT_reboot_mcp_ui.camelize_request_payload(
+ request.model_dump(mode="json")
+ ),
+ }
+{% else %}
result: dict = {"ids": _ids}
+{% endif %}
# Invariant: the MCP server has a bearer token for this tool
# call because opening a UI requires an authenticated
@@ -2918,6 +2946,10 @@ class {{ state.proto.name }}BaseServicer(IMPORT_reboot_aio_servicers.Servicer):
async def {{ mcp_name_prefix }}{{ ui.name }}_tool(
ctx: IMPORT_mcp_server_fastmcp.Context,
{{ mcp_name_prefix }}id: str,
+{% if ui.request_message %}
+ *,
+ request: {{ state.proto.name }}.{{ ui.request_message }},
+{% endif %}
) -> dict:
_ids: dict[IMPORT_reboot_aio_types.StateTypeName, str | None] = {
IMPORT_reboot_aio_types.StateTypeName("{{ state.proto.full_name }}"): {{ mcp_name_prefix }}id,
@@ -2926,7 +2958,31 @@ class {{ state.proto.name }}BaseServicer(IMPORT_reboot_aio_servicers.Servicer):
for _auto_construct_full_name in auto_construct_state_type_full_names:
_ids[_auto_construct_full_name] = (
IMPORT_reboot_mcp_context.get_mcp_user_id(ctx))
+{% if ui.request_message %}
+ # Echo the validated request under `request` so
+ # `McpConnector.ontoolresult` carries it across to the
+ # React side without colliding with framework-reserved
+ # keys (`ids`, `bearer_token`). `McpConnector` injects
+ # these fields as props onto the single UI component via
+ # `React.cloneElement` (reading them from
+ # `toolData.request`), so customers write a plain
+ # ` ` and don't read `useMcpToolData()?.request`
+ # themselves. The payload is camelCased to match the
+ # protobuf-es generated TypeScript field names —
+ # Pydantic's default `model_dump` returns snake_case, but
+ # the customer's generated `...Props` interface (the type
+ # parameter of their App's `FC<...>`) is camelCase, so a
+ # field named `primary_color` here must land as
+ # `primaryColor` in the prop.
+ result: dict = {
+ "ids": _ids,
+ "request": IMPORT_reboot_mcp_ui.camelize_request_payload(
+ request.model_dump(mode="json")
+ ),
+ }
+{% else %}
result: dict = {"ids": _ids}
+{% endif %}
# Invariant: the MCP server has a bearer token for this tool
# call because opening a UI requires an authenticated
@@ -3558,18 +3614,6 @@ class {{ state.proto.name }}BaseServicer(IMPORT_reboot_aio_servicers.Servicer):
yield # Necessary for type checking.
{% endif %}
{% elif method.options.proto.kind == 'writer' %}
- {% if method.proto.name == AUTO_CONSTRUCT_PROTO_METHOD and state.proto.auto_construct != AUTO_CONSTRUCT_UNSPECIFIED %}
- # Concrete no-op default for `{{ AUTO_CONSTRUCT_METHOD }}`.
- # Override in your Servicer to run custom initialization
- # on new {{ state.proto.name }} instances.
- async def _{{ method.proto.name }}(
- self,
- context: IMPORT_reboot_aio_contexts.WriterContext,
- state: {{ state.proto.name }}.State,
- request: {{ method.input_type }},
- ) -> {{ method.output_type }}:
- pass
- {% else %}
@IMPORT_abc_abstractmethod
async def _{{ method.proto.name }}(
self,
@@ -3578,7 +3622,6 @@ class {{ state.proto.name }}BaseServicer(IMPORT_reboot_aio_servicers.Servicer):
request: {{ method.input_type }},
) -> {{ method.output_type }}:
raise NotImplementedError
- {% endif %}
{% elif method.options.proto.kind == 'transaction' %}
@IMPORT_abc_abstractmethod
async def _{{ method.proto.name }}(
@@ -3692,30 +3735,6 @@ class {{ state.proto.name }}SingletonServicer({{ state.proto.name }}BaseServicer
yield # Necessary for type checking.
{% endif %}
{% elif method.options.proto.kind == 'writer' %}
- {% if method.proto.name == AUTO_CONSTRUCT_PROTO_METHOD and state.proto.auto_construct != AUTO_CONSTRUCT_UNSPECIFIED %}
- # Concrete no-op default for `{{ AUTO_CONSTRUCT_METHOD }}`.
- # Override in your Servicer to run custom initialization
- # on new {{ state.proto.name }} instances.
- async def {{ method.proto.name }}(
- self,
- context: IMPORT_reboot_aio_contexts.WriterContext,
- state: {{ state.proto.name }}.State,
- {% if method.has_non_none_request %}
- request: {{ state.proto.name}}.{{ method.proto.name }}Request,
- {% endif %}
- ) -> {% if method.has_non_none_response %}{{ state.proto.name }}.{{ method.proto.name }}Response{% else %}None{% endif %}:
- pass
-
- async def {{ method.proto.name | to_snake }}(
- self,
- context: IMPORT_reboot_aio_contexts.WriterContext,
- state: {{ state.pb2_name }}.{{ state.proto.name }},
- {% if method.has_non_none_request %}
- request: {{ state.proto.name}}.{{ method.proto.name }}Request,
- {% endif %}
- ) -> {% if method.has_non_none_response %}{{ state.proto.name }}.{{ method.proto.name }}Response{% else %}None{% endif %}:
- pass
- {% else %}
# To be backwards compatible during the renaming don't make this
# method to be 'abstractmethod', so that new code that
# doesn't implement it continues to work.
@@ -3754,8 +3773,31 @@ class {{ state.proto.name }}SingletonServicer({{ state.proto.name }}BaseServicer
{% endif %}
) -> {% if method.has_non_none_response %}{{ state.proto.name }}.{{ method.proto.name }}Response{% else %}None{% endif %}:
raise NotImplementedError
- {% endif %}
{% elif method.options.proto.kind == 'transaction' %}
+ {% if method.proto.name == AUTO_CONSTRUCT_PROTO_METHOD and state.proto.auto_construct != AUTO_CONSTRUCT_UNSPECIFIED %}
+ # Concrete no-op default for `{{ AUTO_CONSTRUCT_METHOD }}`.
+ # Override in your Servicer to run custom initialization
+ # on new {{ state.proto.name }} instances.
+ async def {{ method.proto.name }}(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext,
+ state: {{ state.proto.name }}.State,
+ {% if method.has_non_none_request %}
+ request: {{ state.proto.name}}.{{ method.proto.name }}Request,
+ {% endif %}
+ ) -> {% if method.has_non_none_response %}{{ state.proto.name }}.{{ method.proto.name }}Response{% else %}None{% endif %}:
+ pass
+
+ async def {{ method.proto.name | to_snake }}(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext,
+ state: {{ state.pb2_name }}.{{ state.proto.name }},
+ {% if method.has_non_none_request %}
+ request: {{ state.proto.name}}.{{ method.proto.name }}Request,
+ {% endif %}
+ ) -> {% if method.has_non_none_response %}{{ state.proto.name }}.{{ method.proto.name }}Response{% else %}None{% endif %}:
+ pass
+ {% else %}
# To be backwards compatible during the renaming don't make this
# method to be 'abstractmethod', so that new code that
# doesn't implement it continues to work.
@@ -3794,6 +3836,7 @@ class {{ state.proto.name }}SingletonServicer({{ state.proto.name }}BaseServicer
{% endif %}
) -> {% if method.has_non_none_response %}{{ state.proto.name }}.{{ method.proto.name }}Response{% else %}None{% endif %}:
raise NotImplementedError
+ {% endif %}
{% elif method.options.proto.kind == 'workflow' %}
# To be backwards compatible during the renaming don't make this
# method to be 'abstractmethod', so that new code that
@@ -4027,12 +4070,14 @@ class {{ state.proto.name }}Servicer({{ state.proto.name }}BaseServicer):
{% for method in service.methods %}
# For '{{ method.proto.full_name }}'.
{% if method.proto.name == AUTO_CONSTRUCT_PROTO_METHOD and state.proto.auto_construct != AUTO_CONSTRUCT_UNSPECIFIED %}
- # Concrete no-op default for `{{ AUTO_CONSTRUCT_METHOD }}`.
+ # Concrete no-op default for `{{ AUTO_CONSTRUCT_METHOD }}`. The
+ # auto-constructed `{{ AUTO_CONSTRUCT_METHOD }}` is always a
+ # `Transaction` (see `api.py`), so it takes a `TransactionContext`.
# Override in your Servicer to run custom initialization
# on new {{ state.proto.name }} instances.
async def {{ method.proto.name }}(
self,
- context: IMPORT_reboot_aio_contexts.WriterContext,
+ context: IMPORT_reboot_aio_contexts.TransactionContext,
{% if method.has_non_none_request %}
request: {{ state.proto.name }}.{{ method.proto.name }}Request,
{% endif %}
@@ -4046,7 +4091,7 @@ class {{ state.proto.name }}Servicer({{ state.proto.name }}BaseServicer):
async def {{ method.proto.name | to_snake }}(
self,
- context: IMPORT_reboot_aio_contexts.WriterContext,
+ context: IMPORT_reboot_aio_contexts.TransactionContext,
{% if method.has_non_none_request %}
request: {{ state.proto.name }}.{{ method.proto.name }}Request,
{% endif %}
@@ -4368,6 +4413,21 @@ class {{ client.proto.state_name }}:
{% endfor %}
{% endfor %}
+{%- if client.state is not none %}
+{%- for ui in client.state.proto.uis %}
+{%- if ui.request_message %}
+
+ {# UI request type alias. The generated MCP tool annotates its
+ `request:` parameter with `.` (see the UI
+ tool bodies above); FastMCP reads that annotation to derive the
+ tool's parameter schema, so without this alias the AI would see
+ no parameters to pass. Mirrors the per-method Request alias
+ above, and matches the TS-side re-export in
+ `reboot_react.ts.j2`. #}
+ {{ ui.request_message }}: IMPORT_typing.TypeAlias = IMPORT_typing.cast(type, IMPORT_api.{{ client.proto.state_name }}.methods['{{ ui.name }}'].request)
+{%- endif %}
+{%- endfor %}
+{%- endif %}
__state_type_name__ = IMPORT_reboot_aio_types.StateTypeName("{{ client.proto.state_full_name }}")
diff --git a/reboot/templates/reboot_react.ts.j2 b/reboot/templates/reboot_react.ts.j2
index b7bf8998..162680ef 100644
--- a/reboot/templates/reboot_react.ts.j2
+++ b/reboot/templates/reboot_react.ts.j2
@@ -136,6 +136,7 @@ const ERROR_TYPES = [
reboot_api.errors_pb.StateNotConstructed,
reboot_api.errors_pb.TransactionParticipantFailedToPrepare,
reboot_api.errors_pb.TransactionParticipantFailedToCommit,
+ reboot_api.errors_pb.TransactionShouldRetryWithoutBackoff,
reboot_api.errors_pb.UnknownService,
reboot_api.errors_pb.UnknownTask,
] as const; // Need `as const` to ensure TypeScript infers this as a tuple!
diff --git a/reboot/versions.bzl b/reboot/versions.bzl
index 3cc2d7a6..3a20cc0d 100644
--- a/reboot/versions.bzl
+++ b/reboot/versions.bzl
@@ -20,4 +20,4 @@ packages in multiple BUILD.bazel files.
#
# NOTE: if this variable name is ever changed, it must also be updated in
# tests/reboot/versions_test.py and in bazel/release_scripts/update_versions.py.
-REBOOT_VERSION = "1.2.0"
+REBOOT_VERSION = "1.2.1"
diff --git a/reboot/web/package.json b/reboot/web/package.json
index 18712422..fee3ddab 100644
--- a/reboot/web/package.json
+++ b/reboot/web/package.json
@@ -1,6 +1,6 @@
{
"name": "@reboot-dev/reboot-web",
- "version": "1.2.0",
+ "version": "1.2.1",
"description": "npm package for Reboot Web",
"main": "index.js",
"type": "module",
@@ -10,7 +10,7 @@
},
"author": "reboot-dev",
"dependencies": {
- "@reboot-dev/reboot-api": "1.2.0",
+ "@reboot-dev/reboot-api": "1.2.1",
"@scarf/scarf": "1.4.0",
"js-sha1": "0.7.0",
"lru-cache-idb": "^0.5.2",
diff --git a/tests/reboot/BUILD.bazel b/tests/reboot/BUILD.bazel
index cbe20acb..f4fa2747 100644
--- a/tests/reboot/BUILD.bazel
+++ b/tests/reboot/BUILD.bazel
@@ -187,6 +187,27 @@ py_test(
],
)
+# The `ping` example is valuable as a golden since it uses Pydantic APIs
+# with MCP tools and UIs.
+py_test(
+ name = "ping_generation_tests_py",
+ srcs = [
+ "golden_test.py",
+ ],
+ args = [
+ "--golden_file=ping_api_rbt.golden.py",
+ "--generated_file=../../reboot/ping/ping_api_rbt.py",
+ ],
+ data = [
+ ":ping_api_rbt.golden.py",
+ ],
+ main = "golden_test.py",
+ deps = [
+ ":test_helpers_py",
+ "//reboot/ping:ping_py_reboot",
+ ],
+)
+
py_boilerplate_reboot_library(
name = "echo_boilerplate_py_reboot",
proto = "echo.proto",
diff --git a/tests/reboot/aio/auth/oauth_providers_test.py b/tests/reboot/aio/auth/oauth_providers_test.py
index cf7bd9c8..9ebbed9b 100644
--- a/tests/reboot/aio/auth/oauth_providers_test.py
+++ b/tests/reboot/aio/auth/oauth_providers_test.py
@@ -35,6 +35,48 @@
from unittest import mock
from urllib.parse import parse_qs, urlencode, urlparse
+# Generous per-request HTTP timeout for the in-process OAuth flows
+# below. Each request hits a full Reboot cluster plus a local Envoy,
+# and some — notably the OAuth callback, which runs a distributed
+# token-store transaction — can take several seconds on a loaded or
+# degraded CI runner. `httpx`'s 5-second default is tight enough that
+# such a request occasionally trips it and fails as a `ReadTimeout`;
+# this headroom keeps that latency from reading as a failure, while
+# Bazel's test timeout stays the real backstop against a genuine hang.
+_HTTP_TIMEOUT_SECONDS = 30.0
+
+# Path of the consent endpoint the `/authorize` consent screen POSTs to.
+_CONSENT_PATH = "/__/oauth/consent"
+
+
+def _extract_consent_token(consent_page_html: str) -> str:
+ """Pull the signed consent token out of the consent page's hidden
+ form field. The page HTML-escapes attribute values, so unescape what
+ we scrape back to its raw form."""
+ match = re.search(r'name="consent" value="([^"]+)"', consent_page_html)
+ assert match is not None, "consent page is missing its consent token"
+ return html.unescape(match.group(1))
+
+
+async def _approve_consent(
+ client: httpx.AsyncClient,
+ consent_url: str,
+ consent_page_html: str,
+) -> httpx.Response:
+ """Approve the consent screen: extract its token and POST an approval
+ to `/__/oauth/consent`, returning the resulting (302) response.
+
+ `client` must be the same cookie-aware client that fetched the
+ consent page, so the CSRF cookie set on `/authorize` is sent back
+ with the approval (the server requires the two to match)."""
+ return await client.post(
+ consent_url,
+ data={
+ "consent": _extract_consent_token(consent_page_html),
+ "action": "approve",
+ },
+ )
+
class DevelopmentOAuthProviderTest(unittest.IsolatedAsyncioTestCase):
@@ -64,7 +106,7 @@ async def test_development_login_flow(self):
# Discover the OAuth endpoints the way a real client does, via
# the RFC 8414 authorization server metadata.
- async with httpx.AsyncClient() as client:
+ async with httpx.AsyncClient(timeout=_HTTP_TIMEOUT_SECONDS) as client:
response = await client.get(
self.rbt.
http_localhost_url("/.well-known/oauth-authorization-server"),
@@ -74,6 +116,7 @@ async def test_development_login_flow(self):
register_url = metadata["registration_endpoint"]
authorize_url = metadata["authorization_endpoint"]
token_url = metadata["token_endpoint"]
+ consent_url = self.rbt.http_localhost_url(_CONSENT_PATH)
async def login_as(identity: str) -> str:
"""Run the full OAuth flow picking `identity`; return an
@@ -85,7 +128,9 @@ async def login_as(identity: str) -> str:
hashlib.sha256(code_verifier.encode()).digest()
).rstrip(b"=").decode()
- async with httpx.AsyncClient() as client:
+ async with httpx.AsyncClient(
+ timeout=_HTTP_TIMEOUT_SECONDS
+ ) as client:
# Register a client.
response = await client.post(
register_url,
@@ -94,7 +139,7 @@ async def login_as(identity: str) -> str:
self.assertEqual(response.status_code, 201)
client_id = response.json()["client_id"]
- # GET /authorize → 302 to the dev-login page.
+ # GET /authorize → 200 consent screen.
response = await client.get(
authorize_url,
params={
@@ -106,6 +151,12 @@ async def login_as(identity: str) -> str:
"state": "mcp-state-123",
},
)
+ self.assertEqual(response.status_code, 200)
+
+ # Approving consent → 302 to the dev-login page.
+ response = await _approve_consent(
+ client, consent_url, response.text
+ )
self.assertEqual(response.status_code, 302)
login_location = response.headers["location"]
self.assertIn("/__/oauth/dev-login", login_location)
@@ -154,6 +205,7 @@ async def whoami(access_token: str) -> str:
"Authorization": f"Bearer {access_token}",
},
follow_redirects=True,
+ timeout=_HTTP_TIMEOUT_SECONDS,
) as http_client:
async with streamable_http_client(
mcp_url,
@@ -196,7 +248,7 @@ async def test_dev_login_rejects_untrusted_redirect_uri(self):
dev_login_url = self.rbt.http_localhost_url("/__/oauth/dev-login")
same_origin = self.rbt.http_localhost_url("/__/oauth/callback")
- async with httpx.AsyncClient() as client:
+ async with httpx.AsyncClient(timeout=_HTTP_TIMEOUT_SECONDS) as client:
# Same-origin callback: accepted, page renders.
response = await client.get(
dev_login_url,
@@ -252,6 +304,265 @@ async def test_user_without_oauth_raises_in_prod(self):
)
+class ConsentScreenTest(unittest.IsolatedAsyncioTestCase):
+ """The `/authorize` consent screen is what stands between a
+ dynamically-registered ("open") client and a victim's identity. It
+ surfaces the client's `redirect_uri` host before the user signs in,
+ and only an explicit, same-browser POST to `/__/oauth/consent`
+ resumes the flow — closing the "open client" / confused-deputy
+ token-theft hole. See
+ https://github.com/reboot-dev/mono/issues/5560.
+ """
+
+ async def asyncSetUp(self):
+ self.rbt = Reboot()
+ await self.rbt.start()
+
+ async def asyncTearDown(self):
+ await self.rbt.stop()
+
+ async def _up(
+ self,
+ provider: Optional[OAuthProvider] = None,
+ *,
+ title: Optional[str] = None,
+ ) -> None:
+ await self.rbt.up(
+ Application(
+ servicers=[UserServicer, CounterServicer],
+ oauth=OAuthProviderForTest(provider or Anonymous()),
+ title=title,
+ ),
+ )
+
+ async def _register(
+ self,
+ client: httpx.AsyncClient,
+ *,
+ redirect_uri: str,
+ client_name: Optional[str] = None,
+ client_uri: Optional[str] = None,
+ ) -> str:
+ body: dict = {"redirect_uris": [redirect_uri]}
+ if client_name is not None:
+ body["client_name"] = client_name
+ if client_uri is not None:
+ body["client_uri"] = client_uri
+ response = await client.post(
+ self.rbt.http_localhost_url("/__/oauth/register"),
+ json=body,
+ )
+ self.assertEqual(response.status_code, 201)
+ return response.json()["client_id"]
+
+ async def _authorize(
+ self,
+ client: httpx.AsyncClient,
+ *,
+ client_id: str,
+ redirect_uri: str,
+ state: str = "mcp-state",
+ ) -> httpx.Response:
+ # A fixed PKCE pair (verifier "verifier"); the consent screen
+ # doesn't depend on its value, but `/authorize` requires PKCE.
+ code_challenge = base64.urlsafe_b64encode(
+ hashlib.sha256(b"verifier").digest()
+ ).rstrip(b"=").decode()
+ return await client.get(
+ self.rbt.http_localhost_url("/__/oauth/authorize"),
+ params={
+ "response_type": "code",
+ "client_id": client_id,
+ "redirect_uri": redirect_uri,
+ "code_challenge": code_challenge,
+ "code_challenge_method": "S256",
+ "state": state,
+ },
+ )
+
+ async def test_authorize_shows_consent_screen_not_idp_redirect(self):
+ # The heart of the fix: `/authorize` renders a consent screen
+ # that names the client and, prominently, the `redirect_uri`
+ # host the tokens would be sent to — so a victim handed an
+ # attacker's `/authorize` link on this trusted origin can notice
+ # the unfamiliar destination before signing in.
+ await self._up(title="Reboot Chat")
+ async with httpx.AsyncClient(timeout=_HTTP_TIMEOUT_SECONDS) as client:
+ redirect_uri = "https://evil.example/callback"
+ client_id = await self._register(
+ client,
+ redirect_uri=redirect_uri,
+ client_name="Totally Legit MCP",
+ client_uri="https://evil.example/about",
+ )
+ response = await self._authorize(
+ client,
+ client_id=client_id,
+ redirect_uri=redirect_uri,
+ )
+ # A consent screen, not a redirect to the identity provider.
+ self.assertEqual(response.status_code, 200)
+ self.assertNotIn("location", response.headers)
+ # The screen frames the app (via `Application(title=...)`) as
+ # the thing being acted on behalf of, not "Reboot".
+ self.assertIn("Do you trust this AI?", response.text)
+ self.assertIn("Reboot Chat", response.text)
+ # The redirect host is surfaced, and the client's
+ # self-reported metadata is shown as a hint.
+ self.assertIn("evil.example", response.text)
+ self.assertIn("Totally Legit MCP", response.text)
+ self.assertIn("https://evil.example/about", response.text)
+ # The double-submit CSRF cookie is set for the POST back.
+ self.assertIn(
+ "rbt_oauth_consent",
+ response.headers.get("set-cookie", ""),
+ )
+
+ async def test_authorize_strips_userinfo_from_displayed_host(self):
+ # A `redirect_uri` can smuggle `userinfo@` ahead of the real
+ # host (`https://trusted.example@evil.example/...`). The
+ # prominent host on the consent screen must show the real
+ # destination (`evil.example`), not the trusted-looking left
+ # side, so the user checks the address tokens actually go to.
+ await self._up()
+ async with httpx.AsyncClient(timeout=_HTTP_TIMEOUT_SECONDS) as client:
+ redirect_uri = "https://trusted.example@evil.example/callback"
+ client_id = await self._register(client, redirect_uri=redirect_uri)
+ response = await self._authorize(
+ client, client_id=client_id, redirect_uri=redirect_uri
+ )
+ self.assertEqual(response.status_code, 200)
+ host = re.search(
+ r'([^<]*) ',
+ response.text,
+ )
+ self.assertIsNotNone(host)
+ assert host is not None # Narrow for the type checker.
+ self.assertEqual(host.group(1).strip(), "https://evil.example")
+
+ async def test_consent_approval_completes_flow(self):
+ # Approving on the consent screen resumes the flow all the way to
+ # a usable access token — the screen gates the flow, it doesn't
+ # break it.
+ await self._up(Anonymous())
+ redirect_uri = "http://localhost/callback"
+ async with httpx.AsyncClient(timeout=_HTTP_TIMEOUT_SECONDS) as client:
+ client_id = await self._register(client, redirect_uri=redirect_uri)
+ response = await self._authorize(
+ client, client_id=client_id, redirect_uri=redirect_uri
+ )
+ self.assertEqual(response.status_code, 200)
+
+ # Approve → 302 onward to the (here, `Anonymous`) provider,
+ # which redirects straight into our own callback.
+ response = await _approve_consent(
+ client,
+ self.rbt.http_localhost_url(_CONSENT_PATH),
+ response.text,
+ )
+ self.assertEqual(response.status_code, 302)
+ self.assertIn("/__/oauth/callback", response.headers["location"])
+
+ # Follow into the callback; it 302s back to the client with
+ # an authorization code.
+ response = await client.get(response.headers["location"])
+ self.assertEqual(response.status_code, 302)
+ auth_code = httpx.URL(response.headers["location"]).params["code"]
+
+ # The code exchanges for an access token (PKCE verifier
+ # "verifier", matching the challenge `_authorize` sent).
+ response = await client.post(
+ self.rbt.http_localhost_url("/__/oauth/token"),
+ data={
+ "grant_type": "authorization_code",
+ "code": auth_code,
+ "redirect_uri": redirect_uri,
+ "client_id": client_id,
+ "code_verifier": "verifier",
+ },
+ )
+ self.assertEqual(response.status_code, 200)
+ self.assertIn("access_token", response.json())
+
+ async def test_consent_denial_redirects_with_error(self):
+ # Cancelling is reported back to the client as `access_denied`
+ # per RFC 6749 4.1.2.1, carrying the client's `state` and no
+ # authorization code.
+ await self._up()
+ redirect_uri = "https://evil.example/callback"
+ async with httpx.AsyncClient(timeout=_HTTP_TIMEOUT_SECONDS) as client:
+ client_id = await self._register(client, redirect_uri=redirect_uri)
+ page = await self._authorize(
+ client,
+ client_id=client_id,
+ redirect_uri=redirect_uri,
+ state="client-state-xyz",
+ )
+ response = await client.post(
+ self.rbt.http_localhost_url(_CONSENT_PATH),
+ data={
+ "consent": _extract_consent_token(page.text),
+ "action": "deny",
+ },
+ )
+ self.assertEqual(response.status_code, 302)
+ location = httpx.URL(response.headers["location"])
+ self.assertEqual(
+ f"{location.scheme}://{location.host}{location.path}",
+ redirect_uri,
+ )
+ self.assertEqual(location.params["error"], "access_denied")
+ self.assertEqual(location.params["state"], "client-state-xyz")
+ self.assertNotIn("code", location.params)
+
+ async def test_consent_rejects_cross_site_submit_without_cookie(self):
+ # The CSRF guard: a cross-site auto-submit can't carry the
+ # consent cookie (it isn't sent on a cross-site POST, and an
+ # attacker can't set it on our origin). Model that with a fresh
+ # client that holds a *valid* consent token but not the cookie
+ # the matching `/authorize` set — the server must refuse to act
+ # on it, so the screen can't be silently clicked through.
+ await self._up()
+ redirect_uri = "https://evil.example/callback"
+ async with httpx.AsyncClient(
+ timeout=_HTTP_TIMEOUT_SECONDS
+ ) as victim_browser:
+ client_id = await self._register(
+ victim_browser, redirect_uri=redirect_uri
+ )
+ page = await self._authorize(
+ victim_browser,
+ client_id=client_id,
+ redirect_uri=redirect_uri,
+ )
+ consent_token = _extract_consent_token(page.text)
+
+ async with httpx.AsyncClient(
+ timeout=_HTTP_TIMEOUT_SECONDS
+ ) as no_cookie_client:
+ response = await no_cookie_client.post(
+ self.rbt.http_localhost_url(_CONSENT_PATH),
+ data={
+ "consent": consent_token,
+ "action": "approve"
+ },
+ )
+ self.assertEqual(response.status_code, 403)
+
+ async def test_consent_rejects_invalid_token(self):
+ # A garbage / unsigned consent token never resumes the flow.
+ await self._up()
+ async with httpx.AsyncClient(timeout=_HTTP_TIMEOUT_SECONDS) as client:
+ response = await client.post(
+ self.rbt.http_localhost_url(_CONSENT_PATH),
+ data={
+ "consent": "not-a-real-jwt",
+ "action": "approve"
+ },
+ )
+ self.assertEqual(response.status_code, 400)
+
+
class GoogleValidateTest(unittest.TestCase):
"""
`Google.validate()` (inherited from `RegisteredOAuthProvider`)
@@ -607,7 +918,7 @@ async def _perform_idp_login(self) -> None:
→ follow into the callback), which causes the OAuth server to store
the provider's tokens. The token exchange isn't needed: storage
happens in the callback."""
- async with httpx.AsyncClient() as client:
+ async with httpx.AsyncClient(timeout=_HTTP_TIMEOUT_SECONDS) as client:
metadata = (
await client.get(
self.rbt.http_localhost_url(
@@ -638,6 +949,14 @@ async def _perform_idp_login(self) -> None:
"state": "mcp-state",
},
)
+ self.assertEqual(response.status_code, 200)
+ # Approve consent → 302 to the (fake) identity provider, which
+ # here is just our own callback.
+ response = await _approve_consent(
+ client,
+ self.rbt.http_localhost_url(_CONSENT_PATH),
+ response.text,
+ )
self.assertEqual(response.status_code, 302)
# Follow the redirect into the callback; it stores the tokens.
response = await client.get(response.headers["location"])
@@ -695,7 +1014,7 @@ async def test_tokens_stored_on_login_and_untouched_by_refresh(self):
),
)
- async with httpx.AsyncClient() as client:
+ async with httpx.AsyncClient(timeout=_HTTP_TIMEOUT_SECONDS) as client:
metadata = (
await client.get(
self.rbt.http_localhost_url(
@@ -706,6 +1025,7 @@ async def test_tokens_stored_on_login_and_untouched_by_refresh(self):
register_url = metadata["registration_endpoint"]
authorize_url = metadata["authorization_endpoint"]
token_url = metadata["token_endpoint"]
+ consent_url = self.rbt.http_localhost_url(_CONSENT_PATH)
client_redirect_uri = "http://localhost/callback"
code_verifier = base64.urlsafe_b64encode(os.urandom(32)
@@ -721,8 +1041,7 @@ async def test_tokens_stored_on_login_and_untouched_by_refresh(self):
)
).json()["client_id"]
- # GET /authorize → 302 to the (fake) identity provider, which
- # here is just our own callback.
+ # GET /authorize → 200 consent screen.
response = await client.get(
authorize_url,
params={
@@ -734,6 +1053,13 @@ async def test_tokens_stored_on_login_and_untouched_by_refresh(self):
"state": "mcp-state",
},
)
+ self.assertEqual(response.status_code, 200)
+
+ # Approve consent → 302 to the (fake) identity provider, which
+ # here is just our own callback.
+ response = await _approve_consent(
+ client, consent_url, response.text
+ )
self.assertEqual(response.status_code, 302)
# Follow the redirect into the callback; it stores the tokens
@@ -775,7 +1101,7 @@ async def test_tokens_stored_on_login_and_untouched_by_refresh(self):
# re-authorize at the identity provider: the stored provider
# tokens are left untouched (storage is for the app to use, not
# to drive re-authorization).
- async with httpx.AsyncClient() as client:
+ async with httpx.AsyncClient(timeout=_HTTP_TIMEOUT_SECONDS) as client:
response = await client.post(
token_url,
data={
diff --git a/tests/reboot/echo_rbt.golden.py b/tests/reboot/echo_rbt.golden.py
index 3dc231bd..219286d5 100755
--- a/tests/reboot/echo_rbt.golden.py
+++ b/tests/reboot/echo_rbt.golden.py
@@ -17,7 +17,7 @@
# may be invalid (broken) if the generated code is mismatched with the installed
# libraries.
import reboot.versioning as IMPORT_reboot_versioning
-IMPORT_reboot_versioning.check_generated_code_compatible("1.2.0")
+IMPORT_reboot_versioning.check_generated_code_compatible("1.2.1")
# ATTENTION: no types in this file should be imported with their unqualified
# name (e.g. `from typing import Any`). That would cause clashes
diff --git a/tests/reboot/greeter_rbt.golden.js b/tests/reboot/greeter_rbt.golden.js
index 713960d9..47134768 100755
--- a/tests/reboot/greeter_rbt.golden.js
+++ b/tests/reboot/greeter_rbt.golden.js
@@ -5501,6 +5501,6 @@ Greeter._ConstructIdempotently = (_j = class {
export function importPys() {
reboot_native.importPy("tests.reboot.greeter_pb2", "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");
reboot_native.importPy("tests.reboot.greeter_pb2_grpc", "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");
- reboot_native.importPy("tests.reboot.greeter_rbt", "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");
+ reboot_native.importPy("tests.reboot.greeter_rbt", "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");
}
importPys();
diff --git a/tests/reboot/greeter_rbt.golden.py b/tests/reboot/greeter_rbt.golden.py
index 8d14f6fe..f610b987 100755
--- a/tests/reboot/greeter_rbt.golden.py
+++ b/tests/reboot/greeter_rbt.golden.py
@@ -17,7 +17,7 @@
# may be invalid (broken) if the generated code is mismatched with the installed
# libraries.
import reboot.versioning as IMPORT_reboot_versioning
-IMPORT_reboot_versioning.check_generated_code_compatible("1.2.0")
+IMPORT_reboot_versioning.check_generated_code_compatible("1.2.1")
# ATTENTION: no types in this file should be imported with their unqualified
# name (e.g. `from typing import Any`). That would cause clashes
diff --git a/tests/reboot/greeter_rbt_react.golden.js b/tests/reboot/greeter_rbt_react.golden.js
index 795ddb34..f61d1ec5 100755
--- a/tests/reboot/greeter_rbt_react.golden.js
+++ b/tests/reboot/greeter_rbt_react.golden.js
@@ -67,6 +67,7 @@ const ERROR_TYPES = [
reboot_api.errors_pb.StateNotConstructed,
reboot_api.errors_pb.TransactionParticipantFailedToPrepare,
reboot_api.errors_pb.TransactionParticipantFailedToCommit,
+ reboot_api.errors_pb.TransactionShouldRetryWithoutBackoff,
reboot_api.errors_pb.UnknownService,
reboot_api.errors_pb.UnknownTask,
]; // Need `as const` to ensure TypeScript infers this as a tuple!
diff --git a/tests/reboot/nodejs/auth_integration_test/package.json b/tests/reboot/nodejs/auth_integration_test/package.json
index 31d7099a..4b41cfbc 100644
--- a/tests/reboot/nodejs/auth_integration_test/package.json
+++ b/tests/reboot/nodejs/auth_integration_test/package.json
@@ -4,7 +4,7 @@
"private": true,
"type": "module",
"dependencies": {
- "@reboot-dev/reboot": "1.2.0",
+ "@reboot-dev/reboot": "1.2.1",
"typescript": "5.4.5",
"@types/node": "20.11.5"
}
diff --git a/tests/reboot/nodejs/input_error_integration_test/package.json b/tests/reboot/nodejs/input_error_integration_test/package.json
index 31d7099a..4b41cfbc 100644
--- a/tests/reboot/nodejs/input_error_integration_test/package.json
+++ b/tests/reboot/nodejs/input_error_integration_test/package.json
@@ -4,7 +4,7 @@
"private": true,
"type": "module",
"dependencies": {
- "@reboot-dev/reboot": "1.2.0",
+ "@reboot-dev/reboot": "1.2.1",
"typescript": "5.4.5",
"@types/node": "20.11.5"
}
diff --git a/tests/reboot/nodejs/yarn_zod_test/backend/package.json b/tests/reboot/nodejs/yarn_zod_test/backend/package.json
index b32c4ff1..444b7ff0 100644
--- a/tests/reboot/nodejs/yarn_zod_test/backend/package.json
+++ b/tests/reboot/nodejs/yarn_zod_test/backend/package.json
@@ -8,9 +8,9 @@
},
"dependencies": {
"@monorepo/api": "workspace:*",
- "@reboot-dev/reboot": "1.2.0",
- "@reboot-dev/reboot-api": "1.2.0",
- "@reboot-dev/reboot-std": "1.2.0",
+ "@reboot-dev/reboot": "1.2.1",
+ "@reboot-dev/reboot-api": "1.2.1",
+ "@reboot-dev/reboot-std": "1.2.1",
"tsx": "^4.20.3"
}
}
diff --git a/tests/reboot/nodejs/yarn_zod_test/backend/tests/test.ts b/tests/reboot/nodejs/yarn_zod_test/backend/tests/test.ts
index cdef5378..aa09f198 100644
--- a/tests/reboot/nodejs/yarn_zod_test/backend/tests/test.ts
+++ b/tests/reboot/nodejs/yarn_zod_test/backend/tests/test.ts
@@ -43,8 +43,13 @@ test("Bank test", async (t) => {
const response = await bank.accountBalances(context);
- assert.deepEqual(response, {
- balances: [{ accountId: "test@reboot.dev", balance: 1000 }],
- });
+ // The account's scheduled `interest` task may tick between `signUp`
+ // and this read, so the `balance` can be at or above the initial
+ // deposit. Assert the deposit landed without depending on that
+ // timing.
+ assert.equal(response.balances.length, 1);
+ const [account] = response.balances;
+ assert.equal(account.accountId, "test@reboot.dev");
+ assert.ok(account.balance >= 1000);
});
});
diff --git a/tests/reboot/nodejs/yarn_zod_test/yarn.lock b/tests/reboot/nodejs/yarn_zod_test/yarn.lock
index 92d257e3..25d0fdd6 100644
--- a/tests/reboot/nodejs/yarn_zod_test/yarn.lock
+++ b/tests/reboot/nodejs/yarn_zod_test/yarn.lock
@@ -438,9 +438,9 @@ __metadata:
resolution: "@monorepo/backend@workspace:backend"
dependencies:
"@monorepo/api": "workspace:*"
- "@reboot-dev/reboot": "npm:1.2.0"
- "@reboot-dev/reboot-api": "npm:1.2.0"
- "@reboot-dev/reboot-std": "npm:1.2.0"
+ "@reboot-dev/reboot": "npm:1.2.1"
+ "@reboot-dev/reboot-api": "npm:1.2.1"
+ "@reboot-dev/reboot-std": "npm:1.2.1"
tsx: "npm:^4.20.3"
languageName: unknown
linkType: soft
@@ -474,46 +474,46 @@ __metadata:
languageName: node
linkType: hard
-"@reboot-dev/reboot-api@npm:1.2.0":
- version: 1.2.0
- resolution: "@reboot-dev/reboot-api@npm:1.2.0"
+"@reboot-dev/reboot-api@npm:1.2.1":
+ version: 1.2.1
+ resolution: "@reboot-dev/reboot-api@npm:1.2.1"
dependencies:
"@scarf/scarf": "npm:1.4.0"
typescript: "npm:5.4.5"
zod: "npm:^3.25.51 || ^4.0.0"
peerDependencies:
"@bufbuild/protobuf": 1.10.1
- checksum: 10c0/e0fcf33eccf948c2a2ea25421e61ab8311a39a44b40efa9280d658314efc7ad964b30db308cdadfaa386114bb065e8734eaa1e5741851893dd00803d067a1c64
+ checksum: 10c0/a7073cf1646a4eb54af72ec5f4ee4529471dd7a98ab2333a0a9d72bee08d725cae22a8d234cace4e9fdd71d7027d7618ce205b626dde5c94d705750bb32f2e56
languageName: node
linkType: hard
-"@reboot-dev/reboot-std-api@npm:1.2.0":
- version: 1.2.0
- resolution: "@reboot-dev/reboot-std-api@npm:1.2.0"
+"@reboot-dev/reboot-std-api@npm:1.2.1":
+ version: 1.2.1
+ resolution: "@reboot-dev/reboot-std-api@npm:1.2.1"
dependencies:
"@scarf/scarf": "npm:1.4.0"
- checksum: 10c0/5e953de5b72982f8cf39b5494a92ce1888ddf76405f02256785942a650368afab71b4142c5ebd642c49ff6163db2d59da73c1cd7905786802312481014bfd30f
+ checksum: 10c0/cef72703e7e6add6486d04c1ad5c0043d398317d32bb9b6bffeefeec25f18c1424ff0bd132d5cb6a99391d87e5d3cfa8ba86ed2d8164e6a7057f86ab62376c66
languageName: node
linkType: hard
-"@reboot-dev/reboot-std@npm:1.2.0":
- version: 1.2.0
- resolution: "@reboot-dev/reboot-std@npm:1.2.0"
+"@reboot-dev/reboot-std@npm:1.2.1":
+ version: 1.2.1
+ resolution: "@reboot-dev/reboot-std@npm:1.2.1"
dependencies:
- "@reboot-dev/reboot": "npm:1.2.0"
- "@reboot-dev/reboot-std-api": "npm:1.2.0"
+ "@reboot-dev/reboot": "npm:1.2.1"
+ "@reboot-dev/reboot-std-api": "npm:1.2.1"
"@scarf/scarf": "npm:1.4.0"
- checksum: 10c0/edc6b28e11b02538a14cb89afcdf0798c2cb9976570ecf503cc522385e31a27f4bf1c0fcfe7768f2edab09ed9fcd09c902d0844ce0f23e2cd74abafde835f73f
+ checksum: 10c0/29ae931ea000e1f6c409baf090bb56cc67c38508e5f30c8c7ce1bc191e3605c2f6c9446959b6866477e62188f58291fe543adbc38992a433e471e39e2aa336a3
languageName: node
linkType: hard
-"@reboot-dev/reboot@npm:1.2.0":
- version: 1.2.0
- resolution: "@reboot-dev/reboot@npm:1.2.0"
+"@reboot-dev/reboot@npm:1.2.1":
+ version: 1.2.1
+ resolution: "@reboot-dev/reboot@npm:1.2.1"
dependencies:
"@bufbuild/protoc-gen-es": "npm:1.10.1"
"@bufbuild/protoplugin": "npm:1.10.1"
- "@reboot-dev/reboot-api": "npm:1.2.0"
+ "@reboot-dev/reboot-api": "npm:1.2.1"
"@scarf/scarf": "npm:1.4.0"
"@standard-schema/spec": "npm:1.0.0"
chalk: "npm:^4.1.2"
@@ -534,7 +534,7 @@ __metadata:
rbt: rbt.js
rbt-esbuild: rbt-esbuild.js
zod-to-proto: zod-to-proto.js
- checksum: 10c0/511decf69a51481bdd45d889530201ed647cd59cb8229c587b27de791d990bcac9059dcbaf2b5a60c31909182b705ffe6b7bb8e343ff0b36a3bf991460bbd7f0
+ checksum: 10c0/50983c3ef4a46ccf80d325b9cfbac69e5b2d9987062eaefa98903b785bc53a1f8edaafece647fd4e68907a6952f7c9dd1d83b47461e8161fedf68efb829485d1
languageName: node
linkType: hard
diff --git a/tests/reboot/ping/BUILD.bazel b/tests/reboot/ping/BUILD.bazel
index 4664449c..676f34c9 100644
--- a/tests/reboot/ping/BUILD.bazel
+++ b/tests/reboot/ping/BUILD.bazel
@@ -4,7 +4,7 @@ load("@rules_python//python:defs.bzl", "py_test")
py_test(
name = "ping_test",
size = "small",
- timeout = "short",
+ timeout = "moderate",
srcs = ["ping_test.py"],
deps = [
# The `ping` application is originally defined in
diff --git a/tests/reboot/ping/ping_test.py b/tests/reboot/ping/ping_test.py
index 154a8f7c..93bf3133 100644
--- a/tests/reboot/ping/ping_test.py
+++ b/tests/reboot/ping/ping_test.py
@@ -276,14 +276,163 @@ async def test_ui_tool_ids_mapping(self):
# The `counter_show_clicker` UI tool should
# return an `ids` mapping with the Counter ID.
+ # `counter_show_clicker` declares
+ # `request=ShowClickerProps`, so we need to pass a
+ # `primary_color`.
result = await session.call_tool(
"counter_show_clicker",
- {"counter_id": counter_id},
+ {
+ "counter_id": counter_id,
+ "request": {
+ "primary_color": "green"
+ },
+ },
)
data = json.loads(result.content[0].text)
ids = data["ids"]
self.assertEqual(ids["reboot.ping.Counter"], counter_id)
+ async def test_ui_tool_request_echo(self):
+ """Verify the `UI(request=)` round-trip.
+
+ Three things to check:
+
+ 1. `tools/list` exposes `counter_show_clicker` with a
+ nested `request` parameter in its `inputSchema`,
+ and that nested schema's `properties` contains
+ `primary_color` (the snake_case Pydantic field
+ name — protobuf and Pydantic both use snake_case
+ on the wire to the AI).
+ 2. Calling the tool with
+ `{counter_id, request: {primary_color: "green"}}`
+ succeeds and returns the validated request under a
+ top-level `request` key in the result, so
+ `McpConnector.ontoolresult` will spread it into
+ tool-data context.
+ 3. The echoed result is camelCased (`primary_color`
+ lands as `primaryColor`) so the protobuf-es
+ generated TypeScript field name on the React side
+ matches at runtime. This is the casing path tested
+ by `camelize_request_payload` in `reboot/mcp/ui.py`.
+ """
+ await self.rbt.up(
+ Application(
+ servicers=[
+ UserServicer, CounterServicer, PingServicer, PongServicer
+ ],
+ oauth=OAuthProviderForTest(Anonymous()),
+ ),
+ )
+
+ mcp_url = self.rbt.http_localhost_url("/mcp")
+ access_token = self.rbt.make_valid_oauth_access_token()
+
+ async with httpx.AsyncClient(
+ headers={"Authorization": f"Bearer {access_token}"},
+ follow_redirects=True,
+ ) as http_client:
+ async with streamable_http_client(
+ mcp_url, http_client=http_client
+ ) as (read_stream, write_stream, _):
+ async with ClientSession(read_stream, write_stream) as session:
+ await session.initialize()
+
+ # (1) Schema: `counter_show_clicker` must expose
+ # `request` as a parameter, and `request`'s
+ # nested schema must declare `primary_color`.
+ tools = await session.list_tools()
+ show_clicker = next(
+ t for t in tools.tools
+ if t.name == "counter_show_clicker"
+ )
+ properties = (
+ show_clicker.inputSchema.get("properties", {})
+ )
+ self.assertIn(
+ "request",
+ properties,
+ f"`counter_show_clicker.inputSchema` should "
+ f"expose a `request` parameter, got "
+ f"properties: {list(properties.keys())}",
+ )
+ # The `request` schema is a `$ref` into `$defs`
+ # (Pydantic's default), so resolve it.
+ request_schema = properties["request"]
+ if "$ref" in request_schema:
+ ref = request_schema["$ref"]
+ # e.g., '#/$defs/ShowClickerProps'.
+ def_name = ref.rsplit("/", 1)[-1]
+ request_schema = (
+ show_clicker.inputSchema.get("$defs",
+ {}).get(def_name, {})
+ )
+ self.assertIn(
+ "primary_color",
+ request_schema.get("properties", {}),
+ f"`request` schema for `counter_show_clicker` "
+ f"should declare `primary_color`, got "
+ f"{request_schema}",
+ )
+
+ # Create a counter so we can show its clicker.
+ create_result = await session.call_tool(
+ "create_counter",
+ {"request": {
+ "description": "test counter"
+ }},
+ )
+ counter_id = json.loads(create_result.content[0].text
+ )["counter_id"]
+
+ # (2) and (3): invoke with a request and verify
+ # the echoed payload is camelCased.
+ result = await session.call_tool(
+ "counter_show_clicker",
+ {
+ "counter_id": counter_id,
+ "request": {
+ "primary_color": "green"
+ },
+ },
+ )
+ data = json.loads(result.content[0].text)
+ self.assertIn(
+ "request",
+ data,
+ f"UI tool result should echo `request` so "
+ f"React's `McpConnector` can inject it as "
+ f"props, got keys: {list(data.keys())}",
+ )
+ echoed = data["request"]
+ self.assertEqual(
+ echoed,
+ {"primaryColor": "green"},
+ f"Echoed request should be camelCased "
+ f"(`primary_color` → `primaryColor`), got "
+ f"{echoed}",
+ )
+
+ # A UI tool with `request=None`
+ # (`ping_show_pinger`) should NOT include a
+ # `request` key in its result — the auto-
+ # injection in `McpConnector` keys off that
+ # field, and a stray `null` would clone the
+ # child with `null` props.
+ no_request_result = await session.call_tool(
+ "ping_show_pinger",
+ {"ping_id": "my-ping"},
+ )
+ no_request_data = json.loads(
+ no_request_result.content[0].text
+ )
+ self.assertNotIn(
+ "request",
+ no_request_data,
+ f"`UI(request=None)` tools must not echo a "
+ f"`request` key, got keys: "
+ f"{list(no_request_data.keys())}",
+ )
+
async def test_ui_resource_metadata(self):
"""Verify UI resources include CSP metadata.
diff --git a/tests/reboot/ping_api_rbt.golden.py b/tests/reboot/ping_api_rbt.golden.py
new file mode 100755
index 00000000..874f8989
--- /dev/null
+++ b/tests/reboot/ping_api_rbt.golden.py
@@ -0,0 +1,31484 @@
+# yapf: disable
+# isort: skip_file
+# ruff: noqa
+# mypy: disable-error-code="func-returns-value"
+
+
+
+# To not generate code where imported names might get shadowed when a user
+# specifies some name in their proto file to be the same as one of our imported
+# names, (for example: a request field named `uuid`) we bind all imports to
+# names that are forbidden in 'proto' and therefore can never collide.
+
+# Standard imports.
+from __future__ import annotations as IMPORT_future_annotations
+
+# The following MUST appear before the rest of the imports, since those imports
+# may be invalid (broken) if the generated code is mismatched with the installed
+# libraries.
+import reboot.versioning as IMPORT_reboot_versioning
+IMPORT_reboot_versioning.check_generated_code_compatible("1.2.1")
+
+# ATTENTION: no types in this file should be imported with their unqualified
+# name (e.g. `from typing import Any`). That would cause clashes
+# with user-defined methods that have the same name. Use
+# fully-qualified names (e.g. `IMPORT_typing.Any`) instead.
+import asyncio as IMPORT_asyncio
+import builtins as IMPORT_builtins
+import contextvars as IMPORT_contextvars
+import dataclasses as IMPORT_dataclasses
+import google.protobuf.descriptor as IMPORT_google_protobuf_descriptor
+import google.protobuf.json_format as IMPORT_google_protobuf_json_format
+import google.protobuf.message as IMPORT_google_protobuf_message
+import grpc as IMPORT_grpc
+import grpc_status._async as IMPORT_rpc_status_async
+from grpc_status import rpc_status as IMPORT_rpc_status_sync
+import json as IMPORT_json
+import os as IMPORT_os
+import traceback as IMPORT_traceback
+import uuid as IMPORT_uuid
+import pickle as IMPORT_pickle
+import reboot as IMPORT_reboot
+import log.log as IMPORT_log_log # type: ignore[import]
+import typing as IMPORT_typing
+import reboot.aio.backoff as IMPORT_reboot_aio_backoff
+import functools as IMPORT_functools
+from abc import abstractmethod as IMPORT_abc_abstractmethod
+from datetime import datetime as IMPORT_datetime_datetime
+from datetime import timedelta as IMPORT_datetime_timedelta
+from datetime import timezone as IMPORT_datetime_timezone
+from google.protobuf import timestamp_pb2 as IMPORT_google_protobuf_timestamp_pb2
+from google.protobuf import wrappers_pb2 as IMPORT_google_protobuf_wrappers_pb2
+from google.protobuf.empty_pb2 import Empty as IMPORT_google_protobuf_empty_pb2_Empty
+import reboot.aio.tracing as IMPORT_reboot_aio_tracing
+from google.rpc import status_pb2 as IMPORT_google_rpc_status_pb2
+from tzlocal import get_localzone as IMPORT_tzlocal_get_localzone
+import reboot.aio.call as IMPORT_reboot_aio_call
+import reboot.aio.caller_id as IMPORT_reboot_aio_caller_id
+import reboot.aio.contexts as IMPORT_reboot_aio_contexts
+import reboot.aio.headers as IMPORT_reboot_aio_headers
+import reboot.aio.idempotency as IMPORT_reboot_aio_idempotency
+import reboot.aio.internals.channel_manager as IMPORT_reboot_aio_internals_channel_manager
+import reboot.aio.internals.middleware as IMPORT_reboot_aio_internals_middleware
+import reboot.aio.internals.tasks_cache as IMPORT_reboot_aio_internals_tasks_cache
+import reboot.aio.internals.tasks_dispatcher as IMPORT_reboot_aio_internals_tasks_dispatcher
+import reboot.aio.placement as IMPORT_reboot_aio_placement
+import reboot.aio.servicers as IMPORT_reboot_aio_servicers
+import reboot.aio.state_managers as IMPORT_reboot_aio_state_managers
+import reboot.aio.stubs as IMPORT_reboot_aio_stubs
+import reboot.aio.tasks as IMPORT_reboot_aio_tasks
+import reboot.aio.types as IMPORT_reboot_aio_types
+import reboot.aio.external as IMPORT_reboot_aio_external
+import reboot.aio.workflows as IMPORT_reboot_aio_workflows
+import reboot.aio.auth as IMPORT_reboot_aio_auth
+import reboot.aio.auth.authorizers as IMPORT_reboot_aio_auth_authorizers
+import reboot.aio.auth.token_verifiers as IMPORT_reboot_aio_auth_token_verifiers
+import reboot.aio.aborted as IMPORT_reboot_aio_aborted
+import reboot.settings as IMPORT_reboot_settings
+import reboot.nodejs.python as IMPORT_reboot_nodejs_python
+from reboot.time import DateTimeWithTimeZone as IMPORT_reboot_time_DateTimeWithTimeZone
+import rbt.v1alpha1 as IMPORT_rbt_v1alpha1
+import rbt.v1alpha1.nodejs_pb2 as IMPORT_rbt_v1alpha1_nodejs_pb2
+import google.protobuf.any_pb2 as IMPORT_google_protobuf_any_pb2
+import sys as IMPORT_sys
+import mcp.server.fastmcp as IMPORT_mcp_server_fastmcp
+import reboot.mcp.context as IMPORT_reboot_mcp_context
+import reboot.mcp.ui as IMPORT_reboot_mcp_ui
+
+
+# User defined or referenced imports.
+import google.protobuf.any_pb2
+import google.protobuf.descriptor_pb2
+import google.protobuf.empty_pb2
+import google.protobuf.struct_pb2
+import google.protobuf.timestamp_pb2
+import rbt.v1alpha1.options_pb2
+import rbt.v1alpha1.tasks_pb2
+import rbt.v1alpha1.tasks_pb2_grpc
+import reboot.ping.ping_api_pb2
+import reboot.ping.ping_api_pb2_grpc
+
+logger = IMPORT_log_log.get_logger(__name__)
+
+# We won't validate Pydantic state models while they are under construction.
+states_being_constructed: set[str] = set()
+# To support writers seeing partial updates of transactions,
+# and transactions seeing updates from writers, we need to store
+# a reference to the latest state in an ongoing transaction.
+#
+# Moreover, we need to update that _reference_ after each writer
+# executes within a transaction.
+ongoing_transaction_states: dict[str, IMPORT_reboot_api.Model] = {}
+
+
+# Helper to get the `ongoing_transaction_states` dictionary key.
+# The key should contain both the state ID and the state type name
+# to avoid conflicts when multiple states share the same ID.
+def ongoing_transaction_state_key(
+ context: IMPORT_reboot_aio_contexts.ReaderContext
+ | IMPORT_reboot_aio_contexts.WriterContext
+ | IMPORT_reboot_aio_contexts.TransactionContext,
+) -> str:
+ return f"{context.state_type_name}/{context.state_id}"
+
+class Unset:
+ pass
+
+UNSET = Unset()
+
+
+import pydantic as IMPORT_pydantic
+from reboot.ping.ping_api import api as IMPORT_api
+import reboot.api as IMPORT_reboot_api
+
+
+def PingToProto(state: Ping.State, protobuf_state: reboot.ping.ping_api_pb2.Ping):
+ assert isinstance(state, IMPORT_reboot_api.Model)
+ proto = IMPORT_reboot_api.pydantic_to_proto(
+ state,
+ Ping.State,
+ reboot.ping.ping_api_pb2.Ping,
+ )
+ protobuf_state.CopyFrom(proto)
+
+def PingFromProto(
+ state: reboot.ping.ping_api_pb2.Ping,
+ is_initial_state: bool = False,
+) -> Ping.State:
+ assert isinstance(state, IMPORT_google_protobuf_message.Message)
+ if is_initial_state:
+ # Currently we have a special case when we are creating a state, so
+ # 'is_initial_state=True' is passed. In that case we skip any validation
+ # and create the Pydantic model using 'model_construct', which will
+ # apply all 'default' and 'default_factory' values defined in the
+ # Pydantic model, since we know the Protobuf 'state' will be always
+ # "empty" and later when the constructor is done, we will do
+ # the proper validation and convert that Pydantic model back to
+ # Protobuf.
+ return Ping.State.model_construct()
+
+ return IMPORT_reboot_api.proto_to_pydantic(
+ state,
+ Ping.State,
+ )
+def PingDoPingResponseToProto(
+ response: Ping.DoPingResponse
+) -> reboot.ping.ping_api_pb2.PingDoPingResponse:
+ assert isinstance(response, IMPORT_reboot_api.Model)
+ return IMPORT_reboot_api.pydantic_to_proto(
+ response,
+ Ping.DoPingResponse,
+ reboot.ping.ping_api_pb2.PingDoPingResponse
+ )
+
+def PingDoPingResponseFromProto(
+ response: reboot.ping.ping_api_pb2.PingDoPingResponse
+) -> Ping.DoPingResponse:
+ assert isinstance(response, IMPORT_google_protobuf_message.Message)
+ return IMPORT_reboot_api.proto_to_pydantic(
+ response,
+ Ping.DoPingResponse
+ )
+
+def PingDoPingRequestToProto(
+) -> google.protobuf.empty_pb2.Empty:
+ assert issubclass(google.protobuf.empty_pb2.Empty, IMPORT_google_protobuf_empty_pb2_Empty)
+ return google.protobuf.empty_pb2.Empty()
+
+
+def PingDoPingPeriodicallyResponseToProto(
+ response: Ping.DoPingPeriodicallyResponse
+) -> reboot.ping.ping_api_pb2.PingDoPingPeriodicallyResponse:
+ assert isinstance(response, IMPORT_reboot_api.Model)
+ return IMPORT_reboot_api.pydantic_to_proto(
+ response,
+ Ping.DoPingPeriodicallyResponse,
+ reboot.ping.ping_api_pb2.PingDoPingPeriodicallyResponse
+ )
+
+def PingDoPingPeriodicallyResponseFromProto(
+ response: reboot.ping.ping_api_pb2.PingDoPingPeriodicallyResponse
+) -> Ping.DoPingPeriodicallyResponse:
+ assert isinstance(response, IMPORT_google_protobuf_message.Message)
+ return IMPORT_reboot_api.proto_to_pydantic(
+ response,
+ Ping.DoPingPeriodicallyResponse
+ )
+
+def PingDoPingPeriodicallyRequestToProto(
+ request: Ping.DoPingPeriodicallyRequest
+) -> reboot.ping.ping_api_pb2.PingDoPingPeriodicallyRequest:
+ assert isinstance(request, IMPORT_reboot_api.Model)
+ return IMPORT_reboot_api.pydantic_to_proto(
+ request,
+ Ping.DoPingPeriodicallyRequest,
+ reboot.ping.ping_api_pb2.PingDoPingPeriodicallyRequest,
+ )
+
+def PingDoPingPeriodicallyRequestFromProto(
+ request: reboot.ping.ping_api_pb2.PingDoPingPeriodicallyRequest
+) -> Ping.DoPingPeriodicallyRequest:
+ assert isinstance(request, IMPORT_google_protobuf_message.Message)
+ return IMPORT_reboot_api.proto_to_pydantic(
+ request,
+ Ping.DoPingPeriodicallyRequest
+ )
+
+def PingDoPingPeriodicallyRequestFromInputFields(
+ num_pings: int | Unset,
+ period_seconds: float | Unset,
+):
+ assert Ping.DoPingPeriodicallyRequest is not None
+
+
+ __args__: dict[str, IMPORT_typing.Any] = {}
+
+ if not isinstance(num_pings, Unset):
+ __args__['num_pings'] = num_pings
+ if not isinstance(period_seconds, Unset):
+ __args__['period_seconds'] = period_seconds
+
+ return Ping.DoPingPeriodicallyRequest(
+ **__args__,
+ )
+
+def PingDescribeResponseToProto(
+ response: Ping.DescribeResponse
+) -> reboot.ping.ping_api_pb2.PingDescribeResponse:
+ assert isinstance(response, IMPORT_reboot_api.Model)
+ return IMPORT_reboot_api.pydantic_to_proto(
+ response,
+ Ping.DescribeResponse,
+ reboot.ping.ping_api_pb2.PingDescribeResponse
+ )
+
+def PingDescribeResponseFromProto(
+ response: reboot.ping.ping_api_pb2.PingDescribeResponse
+) -> Ping.DescribeResponse:
+ assert isinstance(response, IMPORT_google_protobuf_message.Message)
+ return IMPORT_reboot_api.proto_to_pydantic(
+ response,
+ Ping.DescribeResponse
+ )
+
+def PingDescribeRequestToProto(
+) -> google.protobuf.empty_pb2.Empty:
+ assert issubclass(google.protobuf.empty_pb2.Empty, IMPORT_google_protobuf_empty_pb2_Empty)
+ return google.protobuf.empty_pb2.Empty()
+
+
+def PingNumPingsResponseToProto(
+ response: Ping.NumPingsResponse
+) -> reboot.ping.ping_api_pb2.PingNumPingsResponse:
+ assert isinstance(response, IMPORT_reboot_api.Model)
+ return IMPORT_reboot_api.pydantic_to_proto(
+ response,
+ Ping.NumPingsResponse,
+ reboot.ping.ping_api_pb2.PingNumPingsResponse
+ )
+
+def PingNumPingsResponseFromProto(
+ response: reboot.ping.ping_api_pb2.PingNumPingsResponse
+) -> Ping.NumPingsResponse:
+ assert isinstance(response, IMPORT_google_protobuf_message.Message)
+ return IMPORT_reboot_api.proto_to_pydantic(
+ response,
+ Ping.NumPingsResponse
+ )
+
+def PingNumPingsRequestToProto(
+) -> google.protobuf.empty_pb2.Empty:
+ assert issubclass(google.protobuf.empty_pb2.Empty, IMPORT_google_protobuf_empty_pb2_Empty)
+ return google.protobuf.empty_pb2.Empty()
+
+
+
+def PongToProto(state: Pong.State, protobuf_state: reboot.ping.ping_api_pb2.Pong):
+ assert isinstance(state, IMPORT_reboot_api.Model)
+ proto = IMPORT_reboot_api.pydantic_to_proto(
+ state,
+ Pong.State,
+ reboot.ping.ping_api_pb2.Pong,
+ )
+ protobuf_state.CopyFrom(proto)
+
+def PongFromProto(
+ state: reboot.ping.ping_api_pb2.Pong,
+ is_initial_state: bool = False,
+) -> Pong.State:
+ assert isinstance(state, IMPORT_google_protobuf_message.Message)
+ if is_initial_state:
+ # Currently we have a special case when we are creating a state, so
+ # 'is_initial_state=True' is passed. In that case we skip any validation
+ # and create the Pydantic model using 'model_construct', which will
+ # apply all 'default' and 'default_factory' values defined in the
+ # Pydantic model, since we know the Protobuf 'state' will be always
+ # "empty" and later when the constructor is done, we will do
+ # the proper validation and convert that Pydantic model back to
+ # Protobuf.
+ return Pong.State.model_construct()
+
+ return IMPORT_reboot_api.proto_to_pydantic(
+ state,
+ Pong.State,
+ )
+def PongDoPongResponseToProto(
+ response: Pong.DoPongResponse
+) -> reboot.ping.ping_api_pb2.PongDoPongResponse:
+ assert isinstance(response, IMPORT_reboot_api.Model)
+ return IMPORT_reboot_api.pydantic_to_proto(
+ response,
+ Pong.DoPongResponse,
+ reboot.ping.ping_api_pb2.PongDoPongResponse
+ )
+
+def PongDoPongResponseFromProto(
+ response: reboot.ping.ping_api_pb2.PongDoPongResponse
+) -> Pong.DoPongResponse:
+ assert isinstance(response, IMPORT_google_protobuf_message.Message)
+ return IMPORT_reboot_api.proto_to_pydantic(
+ response,
+ Pong.DoPongResponse
+ )
+
+def PongDoPongRequestToProto(
+) -> google.protobuf.empty_pb2.Empty:
+ assert issubclass(google.protobuf.empty_pb2.Empty, IMPORT_google_protobuf_empty_pb2_Empty)
+ return google.protobuf.empty_pb2.Empty()
+
+
+def PongNumPongsResponseToProto(
+ response: Pong.NumPongsResponse
+) -> reboot.ping.ping_api_pb2.PongNumPongsResponse:
+ assert isinstance(response, IMPORT_reboot_api.Model)
+ return IMPORT_reboot_api.pydantic_to_proto(
+ response,
+ Pong.NumPongsResponse,
+ reboot.ping.ping_api_pb2.PongNumPongsResponse
+ )
+
+def PongNumPongsResponseFromProto(
+ response: reboot.ping.ping_api_pb2.PongNumPongsResponse
+) -> Pong.NumPongsResponse:
+ assert isinstance(response, IMPORT_google_protobuf_message.Message)
+ return IMPORT_reboot_api.proto_to_pydantic(
+ response,
+ Pong.NumPongsResponse
+ )
+
+def PongNumPongsRequestToProto(
+) -> google.protobuf.empty_pb2.Empty:
+ assert issubclass(google.protobuf.empty_pb2.Empty, IMPORT_google_protobuf_empty_pb2_Empty)
+ return google.protobuf.empty_pb2.Empty()
+
+
+
+def UserToProto(state: User.State, protobuf_state: reboot.ping.ping_api_pb2.User):
+ assert isinstance(state, IMPORT_reboot_api.Model)
+ proto = IMPORT_reboot_api.pydantic_to_proto(
+ state,
+ User.State,
+ reboot.ping.ping_api_pb2.User,
+ )
+ protobuf_state.CopyFrom(proto)
+
+def UserFromProto(
+ state: reboot.ping.ping_api_pb2.User,
+ is_initial_state: bool = False,
+) -> User.State:
+ assert isinstance(state, IMPORT_google_protobuf_message.Message)
+ if is_initial_state:
+ # Currently we have a special case when we are creating a state, so
+ # 'is_initial_state=True' is passed. In that case we skip any validation
+ # and create the Pydantic model using 'model_construct', which will
+ # apply all 'default' and 'default_factory' values defined in the
+ # Pydantic model, since we know the Protobuf 'state' will be always
+ # "empty" and later when the constructor is done, we will do
+ # the proper validation and convert that Pydantic model back to
+ # Protobuf.
+ return User.State.model_construct()
+
+ return IMPORT_reboot_api.proto_to_pydantic(
+ state,
+ User.State,
+ )
+def UserCreateCounterResponseToProto(
+ response: User.CreateCounterResponse
+) -> reboot.ping.ping_api_pb2.UserCreateCounterResponse:
+ assert isinstance(response, IMPORT_reboot_api.Model)
+ return IMPORT_reboot_api.pydantic_to_proto(
+ response,
+ User.CreateCounterResponse,
+ reboot.ping.ping_api_pb2.UserCreateCounterResponse
+ )
+
+def UserCreateCounterResponseFromProto(
+ response: reboot.ping.ping_api_pb2.UserCreateCounterResponse
+) -> User.CreateCounterResponse:
+ assert isinstance(response, IMPORT_google_protobuf_message.Message)
+ return IMPORT_reboot_api.proto_to_pydantic(
+ response,
+ User.CreateCounterResponse
+ )
+
+def UserCreateCounterRequestToProto(
+ request: User.CreateCounterRequest
+) -> reboot.ping.ping_api_pb2.UserCreateCounterRequest:
+ assert isinstance(request, IMPORT_reboot_api.Model)
+ return IMPORT_reboot_api.pydantic_to_proto(
+ request,
+ User.CreateCounterRequest,
+ reboot.ping.ping_api_pb2.UserCreateCounterRequest,
+ )
+
+def UserCreateCounterRequestFromProto(
+ request: reboot.ping.ping_api_pb2.UserCreateCounterRequest
+) -> User.CreateCounterRequest:
+ assert isinstance(request, IMPORT_google_protobuf_message.Message)
+ return IMPORT_reboot_api.proto_to_pydantic(
+ request,
+ User.CreateCounterRequest
+ )
+
+def UserCreateCounterRequestFromInputFields(
+ description: str | Unset,
+):
+ assert User.CreateCounterRequest is not None
+
+
+ __args__: dict[str, IMPORT_typing.Any] = {}
+
+ if not isinstance(description, Unset):
+ __args__['description'] = description
+
+ return User.CreateCounterRequest(
+ **__args__,
+ )
+
+def UserListCountersResponseToProto(
+ response: User.ListCountersResponse
+) -> reboot.ping.ping_api_pb2.UserListCountersResponse:
+ assert isinstance(response, IMPORT_reboot_api.Model)
+ return IMPORT_reboot_api.pydantic_to_proto(
+ response,
+ User.ListCountersResponse,
+ reboot.ping.ping_api_pb2.UserListCountersResponse
+ )
+
+def UserListCountersResponseFromProto(
+ response: reboot.ping.ping_api_pb2.UserListCountersResponse
+) -> User.ListCountersResponse:
+ assert isinstance(response, IMPORT_google_protobuf_message.Message)
+ return IMPORT_reboot_api.proto_to_pydantic(
+ response,
+ User.ListCountersResponse
+ )
+
+def UserListCountersRequestToProto(
+) -> google.protobuf.empty_pb2.Empty:
+ assert issubclass(google.protobuf.empty_pb2.Empty, IMPORT_google_protobuf_empty_pb2_Empty)
+ return google.protobuf.empty_pb2.Empty()
+
+
+def UserWhoamiResponseToProto(
+ response: User.WhoamiResponse
+) -> reboot.ping.ping_api_pb2.UserWhoamiResponse:
+ assert isinstance(response, IMPORT_reboot_api.Model)
+ return IMPORT_reboot_api.pydantic_to_proto(
+ response,
+ User.WhoamiResponse,
+ reboot.ping.ping_api_pb2.UserWhoamiResponse
+ )
+
+def UserWhoamiResponseFromProto(
+ response: reboot.ping.ping_api_pb2.UserWhoamiResponse
+) -> User.WhoamiResponse:
+ assert isinstance(response, IMPORT_google_protobuf_message.Message)
+ return IMPORT_reboot_api.proto_to_pydantic(
+ response,
+ User.WhoamiResponse
+ )
+
+def UserWhoamiRequestToProto(
+) -> google.protobuf.empty_pb2.Empty:
+ assert issubclass(google.protobuf.empty_pb2.Empty, IMPORT_google_protobuf_empty_pb2_Empty)
+ return google.protobuf.empty_pb2.Empty()
+
+
+def UserCreateResponseToProto(
+ response: None
+) -> google.protobuf.empty_pb2.Empty:
+ assert response is None
+ return google.protobuf.empty_pb2.Empty()
+
+def UserCreateResponseFromProto(
+ response: google.protobuf.empty_pb2.Empty
+) -> None:
+ assert isinstance(response, IMPORT_google_protobuf_empty_pb2_Empty)
+ return None
+
+def UserCreateRequestToProto(
+) -> google.protobuf.empty_pb2.Empty:
+ assert issubclass(google.protobuf.empty_pb2.Empty, IMPORT_google_protobuf_empty_pb2_Empty)
+ return google.protobuf.empty_pb2.Empty()
+
+
+
+def CounterToProto(state: Counter.State, protobuf_state: reboot.ping.ping_api_pb2.Counter):
+ assert isinstance(state, IMPORT_reboot_api.Model)
+ proto = IMPORT_reboot_api.pydantic_to_proto(
+ state,
+ Counter.State,
+ reboot.ping.ping_api_pb2.Counter,
+ )
+ protobuf_state.CopyFrom(proto)
+
+def CounterFromProto(
+ state: reboot.ping.ping_api_pb2.Counter,
+ is_initial_state: bool = False,
+) -> Counter.State:
+ assert isinstance(state, IMPORT_google_protobuf_message.Message)
+ if is_initial_state:
+ # Currently we have a special case when we are creating a state, so
+ # 'is_initial_state=True' is passed. In that case we skip any validation
+ # and create the Pydantic model using 'model_construct', which will
+ # apply all 'default' and 'default_factory' values defined in the
+ # Pydantic model, since we know the Protobuf 'state' will be always
+ # "empty" and later when the constructor is done, we will do
+ # the proper validation and convert that Pydantic model back to
+ # Protobuf.
+ return Counter.State.model_construct()
+
+ return IMPORT_reboot_api.proto_to_pydantic(
+ state,
+ Counter.State,
+ )
+def CounterCreateResponseToProto(
+ response: None
+) -> google.protobuf.empty_pb2.Empty:
+ assert response is None
+ return google.protobuf.empty_pb2.Empty()
+
+def CounterCreateResponseFromProto(
+ response: google.protobuf.empty_pb2.Empty
+) -> None:
+ assert isinstance(response, IMPORT_google_protobuf_empty_pb2_Empty)
+ return None
+
+def CounterCreateRequestToProto(
+ request: Counter.CreateRequest
+) -> reboot.ping.ping_api_pb2.CounterCreateRequest:
+ assert isinstance(request, IMPORT_reboot_api.Model)
+ return IMPORT_reboot_api.pydantic_to_proto(
+ request,
+ Counter.CreateRequest,
+ reboot.ping.ping_api_pb2.CounterCreateRequest,
+ )
+
+def CounterCreateRequestFromProto(
+ request: reboot.ping.ping_api_pb2.CounterCreateRequest
+) -> Counter.CreateRequest:
+ assert isinstance(request, IMPORT_google_protobuf_message.Message)
+ return IMPORT_reboot_api.proto_to_pydantic(
+ request,
+ Counter.CreateRequest
+ )
+
+def CounterCreateRequestFromInputFields(
+ description: str | Unset,
+):
+ assert Counter.CreateRequest is not None
+
+
+ __args__: dict[str, IMPORT_typing.Any] = {}
+
+ if not isinstance(description, Unset):
+ __args__['description'] = description
+
+ return Counter.CreateRequest(
+ **__args__,
+ )
+
+def CounterIncrementResponseToProto(
+ response: Counter.IncrementResponse
+) -> reboot.ping.ping_api_pb2.CounterIncrementResponse:
+ assert isinstance(response, IMPORT_reboot_api.Model)
+ return IMPORT_reboot_api.pydantic_to_proto(
+ response,
+ Counter.IncrementResponse,
+ reboot.ping.ping_api_pb2.CounterIncrementResponse
+ )
+
+def CounterIncrementResponseFromProto(
+ response: reboot.ping.ping_api_pb2.CounterIncrementResponse
+) -> Counter.IncrementResponse:
+ assert isinstance(response, IMPORT_google_protobuf_message.Message)
+ return IMPORT_reboot_api.proto_to_pydantic(
+ response,
+ Counter.IncrementResponse
+ )
+
+def CounterIncrementRequestToProto(
+) -> google.protobuf.empty_pb2.Empty:
+ assert issubclass(google.protobuf.empty_pb2.Empty, IMPORT_google_protobuf_empty_pb2_Empty)
+ return google.protobuf.empty_pb2.Empty()
+
+
+def CounterValueResponseToProto(
+ response: Counter.ValueResponse
+) -> reboot.ping.ping_api_pb2.CounterValueResponse:
+ assert isinstance(response, IMPORT_reboot_api.Model)
+ return IMPORT_reboot_api.pydantic_to_proto(
+ response,
+ Counter.ValueResponse,
+ reboot.ping.ping_api_pb2.CounterValueResponse
+ )
+
+def CounterValueResponseFromProto(
+ response: reboot.ping.ping_api_pb2.CounterValueResponse
+) -> Counter.ValueResponse:
+ assert isinstance(response, IMPORT_google_protobuf_message.Message)
+ return IMPORT_reboot_api.proto_to_pydantic(
+ response,
+ Counter.ValueResponse
+ )
+
+def CounterValueRequestToProto(
+) -> google.protobuf.empty_pb2.Empty:
+ assert issubclass(google.protobuf.empty_pb2.Empty, IMPORT_google_protobuf_empty_pb2_Empty)
+ return google.protobuf.empty_pb2.Empty()
+
+
+def CounterDescriptionResponseToProto(
+ response: Counter.DescriptionResponse
+) -> reboot.ping.ping_api_pb2.CounterDescriptionResponse:
+ assert isinstance(response, IMPORT_reboot_api.Model)
+ return IMPORT_reboot_api.pydantic_to_proto(
+ response,
+ Counter.DescriptionResponse,
+ reboot.ping.ping_api_pb2.CounterDescriptionResponse
+ )
+
+def CounterDescriptionResponseFromProto(
+ response: reboot.ping.ping_api_pb2.CounterDescriptionResponse
+) -> Counter.DescriptionResponse:
+ assert isinstance(response, IMPORT_google_protobuf_message.Message)
+ return IMPORT_reboot_api.proto_to_pydantic(
+ response,
+ Counter.DescriptionResponse
+ )
+
+def CounterDescriptionRequestToProto(
+) -> google.protobuf.empty_pb2.Empty:
+ assert issubclass(google.protobuf.empty_pb2.Empty, IMPORT_google_protobuf_empty_pb2_Empty)
+ return google.protobuf.empty_pb2.Empty()
+
+
+
+############################ Legacy gRPC Servicers ############################
+# This section is relevant (only) for servicers that implement a legacy gRPC
+# service in a Reboot context. It is irrelevant to clients.
+
+def MakeLegacyGrpcServiceable(
+ # A legacy gRPC servicer type can't be more specific than `type`,
+ # because legacy gRPC servicers (as generated by the gRPC `protoc`
+ # plugin) do not share any common base class other than `object`.
+ servicer_type: type
+) -> IMPORT_reboot_aio_servicers.Serviceable:
+ raise ValueError(f"Unknown legacy gRPC servicer type '{servicer_type}'")
+
+
+
+############################ Reboot Servicer Middlewares ############################
+# This section is relevant (only) for servicers implementing a Reboot servicer. It
+# is irrelevant to clients, except for the fact that some clients are _also_ such
+# servicers.
+
+# For internal calls, we can use a magic token to bypass token verification and
+# authorization checks. The token provides no auth information (e.g.,
+# `context.auth is None`).
+__internal_magic_token__: str = f'internal-{IMPORT_uuid.uuid4()}'
+
+class PingServicerMiddleware(IMPORT_reboot_aio_internals_middleware.Middleware):
+
+ def __init__(
+ self,
+ *,
+ servicer: PingBaseServicer,
+ application_id: IMPORT_reboot_aio_types.ApplicationId,
+ server_id: IMPORT_reboot_aio_types.ServerId,
+ state_manager: IMPORT_reboot_aio_state_managers.StateManager,
+ placement_client: IMPORT_reboot_aio_placement.PlacementClient,
+ channel_manager: IMPORT_reboot_aio_internals_channel_manager._ChannelManager,
+ tasks_cache: IMPORT_reboot_aio_internals_tasks_cache.TasksCache,
+ token_verifier: IMPORT_typing.Optional[IMPORT_reboot_aio_auth_token_verifiers.TokenVerifier],
+ effect_validation: IMPORT_reboot_aio_contexts.EffectValidation,
+ ready: IMPORT_asyncio.Event,
+ ):
+ super().__init__(
+ application_id=application_id,
+ server_id=server_id,
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping'),
+ service_names = [
+ IMPORT_reboot_aio_types.ServiceName("reboot.ping.PingMethods"),
+ ],
+ placement_client=placement_client,
+ channel_manager=channel_manager,
+ effect_validation=effect_validation,
+ get_database_timestamp_ms=lambda: state_manager.latest_timestamp_ms,
+ )
+
+ self._servicer = servicer
+ self._state_manager = state_manager
+ self.tasks_dispatcher = IMPORT_reboot_aio_internals_tasks_dispatcher.TasksDispatcher(
+ application_id=application_id,
+ dispatch=self.dispatch,
+ tasks_cache=tasks_cache,
+ ready=ready,
+ complete_task=self._state_manager.complete_task,
+ )
+
+ # Store the type of each method's request so that stored requests can be
+ # deserialized into the correct type.
+ self.request_type_by_method_name: dict[str, type[IMPORT_google_protobuf_message.Message]] = {
+ 'DoPing': google.protobuf.empty_pb2.Empty,
+ 'DoPingPeriodically': reboot.ping.ping_api_pb2.PingDoPingPeriodicallyRequest,
+ 'Describe': google.protobuf.empty_pb2.Empty,
+ 'NumPings': google.protobuf.empty_pb2.Empty,
+ }
+
+ # Get authorizer, if any, converting from a rule if necessary.
+ def convert_authorizer_rule_if_necessary(
+ authorizer_or_rule: IMPORT_typing.Optional[
+ IMPORT_reboot_aio_auth_authorizers.Authorizer | IMPORT_reboot_aio_auth_authorizers.AuthorizerRule
+ ]
+ ) -> IMPORT_reboot_aio_auth_authorizers.Authorizer:
+
+ # If no authorizer or rule is provided, return the default
+ # authorizer which allows if app internal or allows if in
+ # dev mode (and logs some warnings to help the user
+ # realize where they are missing authorization).
+ if authorizer_or_rule is None:
+ return IMPORT_reboot_aio_auth_authorizers.DefaultAuthorizer(
+ 'Ping',
+ is_user_type=False,
+ )
+
+ if isinstance(authorizer_or_rule, IMPORT_reboot_aio_auth_authorizers.AuthorizerRule):
+ return PingAuthorizer(
+ _default=authorizer_or_rule
+ )
+
+ return authorizer_or_rule
+
+ self._authorizer = convert_authorizer_rule_if_necessary(
+ servicer.authorizer()
+ )
+
+ # Create token verifier.
+ self._token_verifier: IMPORT_typing.Optional[IMPORT_reboot_aio_auth_token_verifiers.TokenVerifier] = (
+ servicer.token_verifier() or token_verifier
+ )
+
+ # Since users specify errors as proto messages they can't raise them
+ # directly - to do so they have to use the `Aborted` wrapper, which will
+ # hold the original proto message. On errors we'll need to check whether
+ # such wrappers hold a proto message for a specified error, so we can
+ # avoid retrying tasks that complete with a specified error.
+ self._specified_errors_by_service_method_name: dict[str, list[str]] = {
+ }
+
+
+ def add_to_server(self, server: IMPORT_grpc.aio.Server) -> None:
+ reboot.ping.ping_api_pb2_grpc.add_PingMethodsServicer_to_server(
+ self, server
+ )
+
+ async def inspect(self, state_ref: IMPORT_reboot_aio_types.StateRef) -> IMPORT_typing.AsyncIterator[IMPORT_google_protobuf_message.Message]:
+ """Implementation of `Middleware.inspect()`."""
+ context = self.create_context(
+ headers=IMPORT_reboot_aio_headers.Headers(
+ application_id=self.application_id,
+ state_ref=state_ref,
+ ),
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping'),
+ method="inspect",
+ context_type=IMPORT_reboot_aio_contexts.ReaderContext,
+ )
+
+ async with self._state_manager.streaming_reader_idempotency_key(
+ context,
+ self._servicer.__state_type__,
+ authorize=None,
+ ) as states:
+ async for (state, idempotency_key) in states:
+ yield state
+
+ async def react_query(
+ self,
+ headers: IMPORT_reboot_aio_headers.Headers,
+ method: str,
+ request_bytes: bytes,
+ ) -> IMPORT_typing.AsyncIterator[tuple[IMPORT_typing.Optional[IMPORT_google_protobuf_message.Message], list[IMPORT_uuid.UUID]]]:
+ """Returns the response of calling 'method' given a message
+ deserialized from the provided 'request_bytes' for each state
+ update that creates a different response.
+
+ # The caller (react.py) should have already ensured that this server
+ # is authoritative for this traffic.
+ assert self.placement_client.server_for_actor(
+ headers.application_id,
+ headers.state_ref,
+ ) == self._server_id
+
+ NOTE: only unary reader methods are supported."""
+ # Need to define these up here since we can only do that once.
+ last_response: IMPORT_typing.Optional[IMPORT_google_protobuf_message.Message] = None
+ aggregated_idempotency_keys: list[IMPORT_uuid.UUID] = []
+ if method == 'DoPing':
+ # Invariant here is that users should not have called this
+ # directly but only through code generated React
+ # components which should not have been generated except
+ # for valid method candidates.
+ logger.warning(
+ "Got a React query request with an invalid method name: "
+ f"Method '{method}' is invalid for servicer Ping."
+ "\n"
+ "Do you have a browser tab open for an older version "
+ "of this application, or for a different application all together?"
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.InvalidMethod(),
+ message=
+ f"Method '{method}' is invalid"
+ )
+ yield # Necessary for type checking.
+ elif method == 'DoPingPeriodically':
+ # Invariant here is that users should not have called this
+ # directly but only through code generated React
+ # components which should not have been generated except
+ # for valid method candidates.
+ logger.warning(
+ "Got a React query request with an invalid method name: "
+ f"Method '{method}' is invalid for servicer Ping."
+ "\n"
+ "Do you have a browser tab open for an older version "
+ "of this application, or for a different application all together?"
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.InvalidMethod(),
+ message=
+ f"Method '{method}' is invalid"
+ )
+ yield # Necessary for type checking.
+ elif method == 'Describe':
+
+ context = self.create_context(
+ headers=headers,
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping'),
+ method='Describe',
+ context_type=IMPORT_reboot_aio_contexts.ReaderContext,
+ )
+
+ with IMPORT_reboot_aio_tracing.span(
+ state_name=f"{context.state_type_name}('{context.state_id}')",
+ span_name="Describe() (reactively)",
+ # The naming above matches Python, but not TypeScript.
+ python_specific=True,
+ level=IMPORT_reboot_aio_tracing.TraceLevel.CUSTOMER,
+ ):
+ context.auth = await self._maybe_verify_token(
+ headers=headers, method='Describe'
+ )
+
+ request = google.protobuf.empty_pb2.Empty()
+ request.ParseFromString(request_bytes)
+
+ async with self._state_manager.reactively(
+ context,
+ self._servicer.__state_type__,
+ authorize=self._maybe_authorize(
+ method_name='reboot.ping.PingMethods.Describe',
+ headers=headers,
+ auth=context.auth,
+ request=request,
+ ),
+ ) as states:
+ async for (state, idempotency_keys) in states:
+
+ aggregated_idempotency_keys.extend(idempotency_keys)
+
+ # Note: This does not do any defensive copying currently:
+ # see https://github.com/reboot-dev/respect/issues/2636.
+ @IMPORT_reboot_aio_internals_middleware.maybe_run_function_twice_to_validate_effects
+ async def run__Describe(validating_effects: bool) -> IMPORT_google_protobuf_message.Message:
+ return await self.__Describe(
+ context,
+ state,
+ request,
+ validating_effects=validating_effects,
+ )
+
+ response = await run__Describe()
+
+ if last_response != response:
+ yield (response, aggregated_idempotency_keys)
+ last_response = response
+ else:
+ yield (None, aggregated_idempotency_keys)
+
+ aggregated_idempotency_keys.clear()
+ elif method == 'NumPings':
+
+ context = self.create_context(
+ headers=headers,
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping'),
+ method='NumPings',
+ context_type=IMPORT_reboot_aio_contexts.ReaderContext,
+ )
+
+ with IMPORT_reboot_aio_tracing.span(
+ state_name=f"{context.state_type_name}('{context.state_id}')",
+ span_name="NumPings() (reactively)",
+ # The naming above matches Python, but not TypeScript.
+ python_specific=True,
+ level=IMPORT_reboot_aio_tracing.TraceLevel.CUSTOMER,
+ ):
+ context.auth = await self._maybe_verify_token(
+ headers=headers, method='NumPings'
+ )
+
+ request = google.protobuf.empty_pb2.Empty()
+ request.ParseFromString(request_bytes)
+
+ async with self._state_manager.reactively(
+ context,
+ self._servicer.__state_type__,
+ authorize=self._maybe_authorize(
+ method_name='reboot.ping.PingMethods.NumPings',
+ headers=headers,
+ auth=context.auth,
+ request=request,
+ ),
+ ) as states:
+ async for (state, idempotency_keys) in states:
+
+ aggregated_idempotency_keys.extend(idempotency_keys)
+
+ # Note: This does not do any defensive copying currently:
+ # see https://github.com/reboot-dev/respect/issues/2636.
+ @IMPORT_reboot_aio_internals_middleware.maybe_run_function_twice_to_validate_effects
+ async def run__NumPings(validating_effects: bool) -> IMPORT_google_protobuf_message.Message:
+ return await self.__NumPings(
+ context,
+ state,
+ request,
+ validating_effects=validating_effects,
+ )
+
+ response = await run__NumPings()
+
+ if last_response != response:
+ yield (response, aggregated_idempotency_keys)
+ last_response = response
+ else:
+ yield (None, aggregated_idempotency_keys)
+
+ aggregated_idempotency_keys.clear()
+ else:
+ logger.warning(
+ "Got a React query request with an invalid method name: "
+ f"Method '{method}' is invalid for servicer Ping."
+ "\n"
+ "Do you have a browser tab open for an older version "
+ "of this application, or for a different application all together?"
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.InvalidMethod(),
+ message=
+ f"Method '{method}' not found"
+ )
+ yield # Unreachable but necessary for mypy.
+
+ async def react_mutate(
+ self,
+ headers: IMPORT_reboot_aio_headers.Headers,
+ method: str,
+ request_bytes: bytes,
+ ) -> IMPORT_google_protobuf_message.Message:
+ """Returns the response of calling 'method' given a message
+ deserialized from the provided 'request_bytes'."""
+ if method == 'DoPing':
+ request = google.protobuf.empty_pb2.Empty()
+ request.ParseFromString(request_bytes)
+
+ # NOTE: we automatically retry mutations that come through
+ # React when we get a `IMPORT_grpc.StatusCode.UNAVAILABLE` to
+ # match the retry logic we do in the React code generated
+ # to handle lack/loss of connectivity.
+ #
+ # TODO(benh): revisit this decision if we ever see reason
+ # to call `react_mutate()` from any place other than where
+ # we're executing React (e.g., browser, next.js server
+ # component, etc).
+ call_backoff = IMPORT_reboot_aio_backoff.Backoff()
+ while True:
+ # We make a full-fledged gRPC call, so that if this traffic
+ # was misrouted (i.e. this server is not authoritative
+ # for the state), it will now go to the right place. The
+ # receiving middleware will handle things like effect
+ # validation and so forth.
+ assert headers.application_id is not None # Guaranteed by `Headers`.
+ stub = reboot.ping.ping_api_pb2_grpc.PingMethodsStub(
+ self.channel_manager.get_channel_to(
+ self.placement_client.address_for_actor(
+ headers.application_id,
+ headers.state_ref,
+ )
+ )
+ )
+ call = stub.DoPing(
+ request=request,
+ metadata=headers.to_grpc_metadata(),
+ )
+ try:
+ return await call
+ except IMPORT_grpc.aio.AioRpcError as error:
+ if error.code() == IMPORT_grpc.StatusCode.UNAVAILABLE:
+ await call_backoff()
+ continue
+
+ # Reconstitute the error that the server threw, if it was a declared error.
+ status = await IMPORT_rpc_status_async.from_call(call)
+ if status is not None:
+ raise Ping.DoPingAborted.from_status(
+ status
+ ) from None
+ raise Ping.DoPingAborted.from_grpc_aio_rpc_error(
+ error
+ ) from None
+
+ elif method == 'DoPingPeriodically':
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.InvalidMethod(),
+ message=
+ f"Method 'DoPingPeriodically' can not be called via React (for now)"
+ )
+ elif method == 'Describe':
+ # Invariant here is that users should not have called this
+ # directly but only through code generated React
+ # components which should not have been generated except
+ # for valid method candidates.
+ logger.warning(
+ "Got a react mutate request with an invalid method name: "
+ "Method 'Describe' is invalid for servicer Ping."
+ "\n"
+ "Do you have an old browser tab still open for an older version "
+ "of this application, or a different application all together?"
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.InvalidMethod(),
+ message=f"Method '{method}' is invalid"
+ )
+ elif method == 'NumPings':
+ # Invariant here is that users should not have called this
+ # directly but only through code generated React
+ # components which should not have been generated except
+ # for valid method candidates.
+ logger.warning(
+ "Got a react mutate request with an invalid method name: "
+ "Method 'NumPings' is invalid for servicer Ping."
+ "\n"
+ "Do you have an old browser tab still open for an older version "
+ "of this application, or a different application all together?"
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.InvalidMethod(),
+ message=f"Method '{method}' is invalid"
+ )
+ else:
+ logger.warning(
+ "Got a react mutate request with an invalid method name: "
+ f"Method '{method}' is invalid for servicer Ping."
+ "\n"
+ "Do you have an old browser tab still open for an older version "
+ "of this application, or a different application all together?"
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.InvalidMethod(),
+ message=
+ f"Method '{method}' not found"
+ )
+
+ async def dispatch(
+ self,
+ task: IMPORT_reboot_aio_tasks.TaskEffect,
+ *,
+ only_validate: bool = False,
+ on_loop_iteration: IMPORT_reboot_aio_internals_tasks_dispatcher.OnLoopIterationCallable = (lambda iteration, next_iteration_schedule: None),
+ ) -> IMPORT_reboot_aio_internals_tasks_dispatcher.TaskResponseOrStatus:
+ """Dispatches the tasks to execute unless 'only_validate' is set to
+ true, in which case just ensures that the task actually exists.
+ Note that this function will be called *by* tasks_dispatcher; it will
+ not itself call into tasks_dispatcher."""
+
+ if 'DoPing' == task.method_name:
+ if only_validate:
+ # TODO(benh): validate 'task.request' is correct type.
+ return (reboot.ping.ping_api_pb2.PingDoPingResponse(), None)
+
+ # Use an inline method to create a new scope, so that we can use
+ # variable names like `context` and `effects` in multiple branches
+ # in this code (notably when there are multiple task types) without
+ # hitting a mypy error that the variable's type is not consistent.
+ async def run_DoPing(
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ *,
+ validating_effects: bool = False,
+ ):
+ async with self._state_manager.task_workflow(
+ context,
+ task,
+ on_loop_iteration=on_loop_iteration,
+ validating_effects=validating_effects,
+ ) as complete:
+ try:
+ response = await (PingWorkflowStub(
+ context=context,
+ state_ref=context._state_ref,
+ ).DoPing(
+ bearer_token=__internal_magic_token__,
+ idempotency=IMPORT_reboot_aio_idempotency.Idempotency(
+ alias=f'Task {IMPORT_uuid.UUID(bytes=task.task_id.task_uuid)}',
+ ),
+ ))
+ await complete(task, (response, None))
+ return (response, None)
+ except IMPORT_asyncio.CancelledError:
+ # Do not retry a task if it was cancelled by a caller.
+ if self.tasks_dispatcher.is_task_cancelled(task.task_id.task_uuid):
+ result = (
+ None,
+ IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Cancelled(),
+ ).to_status(),
+ )
+ await complete(task, result)
+ return result
+ else:
+ raise
+ except IMPORT_reboot.aio.aborted.Aborted as aborted:
+ error_type = f'{aborted._protobuf_error.__class__.__module__}.{aborted._protobuf_error.__class__.__qualname__}'
+ # Do not retry a task if the error was specified in the
+ # proto file.
+ if error_type in self._specified_errors_by_service_method_name.get('reboot.ping.PingMethods.DoPing', []):
+ result = (None, aborted.to_status())
+ await complete(task, result)
+ return result
+ raise
+
+
+ return await run_DoPing(
+ self.create_context(
+ headers=IMPORT_reboot_aio_headers.Headers(
+ application_id=self.application_id,
+ state_ref=IMPORT_reboot_aio_types.StateRef(task.task_id.state_ref),
+ ),
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping'),
+ method='DoPing',
+ context_type=IMPORT_reboot_aio_contexts.WorkflowContext,
+ task=task,
+ # Propagate state manager and state type so that
+ # `until()` calls (via `WorkflowContext.retry_reactively_until()`)
+ # can enter `reactively()` scoped to each `until()`
+ # invocation.
+ reactively_state_manager=self._state_manager,
+ reactively_state_type=(
+ self._servicer.__state_type__
+ ),
+ )
+ )
+ elif 'DoPingPeriodically' == task.method_name:
+ if only_validate:
+ # TODO(benh): validate 'task.request' is correct type.
+ return (reboot.ping.ping_api_pb2.PingDoPingPeriodicallyResponse(), None)
+
+ # Use an inline method to create a new scope, so that we can use
+ # variable names like `context` and `effects` in multiple branches
+ # in this code (notably when there are multiple task types) without
+ # hitting a mypy error that the variable's type is not consistent.
+ async def run_DoPingPeriodically(
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ *,
+ validating_effects: bool = False,
+ ):
+ async with self._state_manager.task_workflow(
+ context,
+ task,
+ on_loop_iteration=on_loop_iteration,
+ validating_effects=validating_effects,
+ ) as complete:
+ try:
+ response = await self.__DoPingPeriodically(
+ context,
+ IMPORT_typing.cast(reboot.ping.ping_api_pb2.PingDoPingPeriodicallyRequest, task.request),
+ validating_effects=validating_effects,
+ )
+ await complete(task, (response, None))
+ return (response, None)
+ except IMPORT_asyncio.CancelledError:
+ # Do not retry a task if it was cancelled by a caller.
+ if self.tasks_dispatcher.is_task_cancelled(task.task_id.task_uuid):
+ result = (
+ None,
+ IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Cancelled(),
+ ).to_status(),
+ )
+ await complete(task, result)
+ return result
+ else:
+ raise
+ except IMPORT_reboot.aio.aborted.Aborted as aborted:
+ error_type = f'{aborted._protobuf_error.__class__.__module__}.{aborted._protobuf_error.__class__.__qualname__}'
+ # Do not retry a task if the error was specified in the
+ # proto file.
+ if error_type in self._specified_errors_by_service_method_name.get('reboot.ping.PingMethods.DoPingPeriodically', []):
+ result = (None, aborted.to_status())
+ await complete(task, result)
+ return result
+ raise
+
+ @IMPORT_reboot_aio_internals_middleware.maybe_run_function_twice_to_validate_effects
+ async def run_DoPingPeriodically_workflow(
+ validating_effects: bool,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ ):
+ try:
+ # When we're validating effects we
+ # periodically timeout so that we can log
+ # that a workflow might be hung, i.e., the
+ # user has a bug.
+ task = IMPORT_asyncio.create_task(
+ run_DoPingPeriodically(
+ context,
+ validating_effects=validating_effects,
+ )
+ )
+ timeout = None if not validating_effects else 5 # seconds
+ while True:
+ done, pending = await IMPORT_asyncio.wait(
+ [task],
+ timeout=timeout,
+ )
+ # Check if we've timed out, which
+ # should only occur if we're
+ # validating effects.
+ if len(done) == 0:
+ assert validating_effects and timeout is not None
+ logger.warning(
+ f'Still waiting for method Ping.DoPingPeriodically '
+ 'to complete after re-running to validate effects.'
+ )
+ timeout += 5 # seconds
+ continue
+ return task.result()
+ finally:
+ if not task.done():
+ task.cancel()
+ # Need to actually await the task so if
+ # there is an exception we don't get a
+ # warning logged that the exception was
+ # never retrieved, but we don't care about
+ # the exception because we're done with
+ # the task.
+ try:
+ await task
+ except:
+ pass
+
+ return await run_DoPingPeriodically_workflow(
+ self.create_context(
+ headers=IMPORT_reboot_aio_headers.Headers(
+ application_id=self.application_id,
+ state_ref=IMPORT_reboot_aio_types.StateRef(task.task_id.state_ref),
+ workflow_id=IMPORT_uuid.UUID(bytes=task.task_id.task_uuid),
+ ),
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping'),
+ method='DoPingPeriodically',
+ context_type=IMPORT_reboot_aio_contexts.WorkflowContext,
+ task=task,
+ # Propagate state manager and state type so that
+ # `until()` calls (via `WorkflowContext.retry_reactively_until()`)
+ # can enter `reactively()` scoped to each `until()`
+ # invocation.
+ reactively_state_manager=self._state_manager,
+ reactively_state_type=(
+ self._servicer.__state_type__
+ ),
+ )
+ )
+ elif 'Describe' == task.method_name:
+ if only_validate:
+ # TODO(benh): validate 'task.request' is correct type.
+ return (reboot.ping.ping_api_pb2.PingDescribeResponse(), None)
+
+ # Use an inline method to create a new scope, so that we can use
+ # variable names like `context` and `effects` in multiple branches
+ # in this code (notably when there are multiple task types) without
+ # hitting a mypy error that the variable's type is not consistent.
+ async def run_Describe(
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ *,
+ validating_effects: bool = False,
+ ):
+ async with self._state_manager.task_workflow(
+ context,
+ task,
+ on_loop_iteration=on_loop_iteration,
+ validating_effects=validating_effects,
+ ) as complete:
+ try:
+ response = await (PingWorkflowStub(
+ context=context,
+ state_ref=context._state_ref,
+ ).Describe(
+ bearer_token=__internal_magic_token__,
+ ))
+ await complete(task, (response, None))
+ return (response, None)
+ except IMPORT_asyncio.CancelledError:
+ # Do not retry a task if it was cancelled by a caller.
+ if self.tasks_dispatcher.is_task_cancelled(task.task_id.task_uuid):
+ result = (
+ None,
+ IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Cancelled(),
+ ).to_status(),
+ )
+ await complete(task, result)
+ return result
+ else:
+ raise
+ except IMPORT_reboot.aio.aborted.Aborted as aborted:
+ error_type = f'{aborted._protobuf_error.__class__.__module__}.{aborted._protobuf_error.__class__.__qualname__}'
+ # Do not retry a task if the error was specified in the
+ # proto file.
+ if error_type in self._specified_errors_by_service_method_name.get('reboot.ping.PingMethods.Describe', []):
+ result = (None, aborted.to_status())
+ await complete(task, result)
+ return result
+ raise
+
+
+ return await run_Describe(
+ self.create_context(
+ headers=IMPORT_reboot_aio_headers.Headers(
+ application_id=self.application_id,
+ state_ref=IMPORT_reboot_aio_types.StateRef(task.task_id.state_ref),
+ ),
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping'),
+ method='Describe',
+ context_type=IMPORT_reboot_aio_contexts.WorkflowContext,
+ task=task,
+ # Propagate state manager and state type so that
+ # `until()` calls (via `WorkflowContext.retry_reactively_until()`)
+ # can enter `reactively()` scoped to each `until()`
+ # invocation.
+ reactively_state_manager=self._state_manager,
+ reactively_state_type=(
+ self._servicer.__state_type__
+ ),
+ )
+ )
+ elif 'NumPings' == task.method_name:
+ if only_validate:
+ # TODO(benh): validate 'task.request' is correct type.
+ return (reboot.ping.ping_api_pb2.PingNumPingsResponse(), None)
+
+ # Use an inline method to create a new scope, so that we can use
+ # variable names like `context` and `effects` in multiple branches
+ # in this code (notably when there are multiple task types) without
+ # hitting a mypy error that the variable's type is not consistent.
+ async def run_NumPings(
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ *,
+ validating_effects: bool = False,
+ ):
+ async with self._state_manager.task_workflow(
+ context,
+ task,
+ on_loop_iteration=on_loop_iteration,
+ validating_effects=validating_effects,
+ ) as complete:
+ try:
+ response = await (PingWorkflowStub(
+ context=context,
+ state_ref=context._state_ref,
+ ).NumPings(
+ bearer_token=__internal_magic_token__,
+ ))
+ await complete(task, (response, None))
+ return (response, None)
+ except IMPORT_asyncio.CancelledError:
+ # Do not retry a task if it was cancelled by a caller.
+ if self.tasks_dispatcher.is_task_cancelled(task.task_id.task_uuid):
+ result = (
+ None,
+ IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Cancelled(),
+ ).to_status(),
+ )
+ await complete(task, result)
+ return result
+ else:
+ raise
+ except IMPORT_reboot.aio.aborted.Aborted as aborted:
+ error_type = f'{aborted._protobuf_error.__class__.__module__}.{aborted._protobuf_error.__class__.__qualname__}'
+ # Do not retry a task if the error was specified in the
+ # proto file.
+ if error_type in self._specified_errors_by_service_method_name.get('reboot.ping.PingMethods.NumPings', []):
+ result = (None, aborted.to_status())
+ await complete(task, result)
+ return result
+ raise
+
+
+ return await run_NumPings(
+ self.create_context(
+ headers=IMPORT_reboot_aio_headers.Headers(
+ application_id=self.application_id,
+ state_ref=IMPORT_reboot_aio_types.StateRef(task.task_id.state_ref),
+ ),
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping'),
+ method='NumPings',
+ context_type=IMPORT_reboot_aio_contexts.WorkflowContext,
+ task=task,
+ # Propagate state manager and state type so that
+ # `until()` calls (via `WorkflowContext.retry_reactively_until()`)
+ # can enter `reactively()` scoped to each `until()`
+ # invocation.
+ reactively_state_manager=self._state_manager,
+ reactively_state_type=(
+ self._servicer.__state_type__
+ ),
+ )
+ )
+
+ # There are no tasks for this service.
+ start_or_validate = "start" if not only_validate else "validate"
+ raise RuntimeError(
+ f"Attempted to {start_or_validate} task '{task.method_name}' "
+ f"on 'Ping' which does not exist"
+ )
+
+ # Ping specific methods:
+ async def __DoPing(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext,
+ state: reboot.ping.ping_api_pb2.Ping,
+ request: google.protobuf.empty_pb2.Empty,
+ *,
+ validating_effects: bool,
+ ) -> reboot.ping.ping_api_pb2.PingDoPingResponse:
+ try:
+ typed_state: Ping.State = PingFromProto(state, is_initial_state=(context.state_id in states_being_constructed))
+
+ # TODO: assert that there are no ongoing transactions for this state.
+ #
+ # The `typed_state` should be already validated above, so we can
+ # just store it here.
+ ongoing_transaction_states[ongoing_transaction_state_key(context)] = typed_state
+ response = (
+ await self._servicer._DoPing(
+ context=context,
+ state=typed_state,
+ request=request
+ )
+ )
+
+
+ PingToProto(typed_state, state)
+
+ IMPORT_reboot_aio_types.assert_type(
+ response,
+ [reboot.ping.ping_api_pb2.PingDoPingResponse],
+ )
+ self.maybe_raise_effect_validation_retry(
+ logger=logger,
+ idempotency_manager=context,
+ method_name='Ping.DoPing',
+ validating_effects=validating_effects,
+ context=context,
+ )
+ return response
+ except IMPORT_reboot_aio_contexts.EffectValidationRetry:
+ # Doing effect validation, just let this propagate.
+ raise
+ except IMPORT_reboot.aio.aborted.Aborted as aborted:
+ # If the caller aborted due to a retryable error, just
+ # propagate the aborted instead of propagating `Unknown`
+ # so that a client can transparently retry.
+ if IMPORT_reboot.aio.aborted.is_retryable(aborted):
+ raise aborted
+ # Log any _unhandled_ abort stack traces to make it
+ # easier for debugging.
+ #
+ # NOTE: we don't log if we're a task as it will be logged
+ # in `public/reboot/aio/internals/tasks_dispatcher.py` instead.
+ aborted_type: IMPORT_typing.Optional[type] = None
+ aborted_type = Ping.DoPingAborted
+ if isinstance(aborted, IMPORT_reboot.aio.aborted.SystemAborted):
+ # Not logging when within `node` as we already log there.
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ f"Unhandled (in 'reboot.ping.Ping.DoPing') {aborted}; propagating as 'Unknown'\n" +
+ ''.join(IMPORT_traceback.format_exception(aborted))
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ # TODO(benh): consider whether or not we want to
+ # include the 'package.service.method' which may
+ # get concatenated together forming a kind of
+ # "stack trace"; while it's super helpful for
+ # debugging, it does expose implementation
+ # information.
+ message=f"unhandled (in 'reboot.ping.Ping.DoPing') {aborted}"
+ )
+ else:
+ if (
+ aborted_type is not None and
+ not isinstance(aborted, aborted_type) and
+ aborted_type.is_declared_error(aborted.error)
+ ):
+ # We propagate declared errors that might have
+ # come from another call, i.e., we might have an
+ # `Aborted` but not for this method but the
+ # `Aborted` that we have has an error that this
+ # method declared. This allows a developer to
+ # simply add the declared error to their `.proto`
+ # file rather than having to catch and re-raise
+ # the error with their own aborted type.
+ if context.task is None:
+ logger.warning(
+ f"Propagating unhandled but declared error (in 'reboot.ping.Ping.DoPing') {aborted}"
+ )
+ elif (
+ aborted_type is None or
+ not isinstance(aborted, aborted_type)
+ ):
+ # Not logging when within `node` as we already log there.
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ f"Unhandled (in 'reboot.ping.Ping.DoPing') {aborted}; propagating as 'Unknown'\n" +
+ ''.join(IMPORT_traceback.format_exception(aborted))
+ )
+ # If this wasn't a declared error than we
+ # propagate it as `Unknown`.
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ # TODO(benh): consider whether or not we want to
+ # include the 'package.service.method' which may
+ # get concatenated together forming a kind of
+ # "stack trace"; while it's super helpful for
+ # debugging, it does expose implementation
+ # information.
+ message=f"unhandled (in 'reboot.ping.Ping.DoPing') {aborted}"
+ )
+
+ raise
+ except IMPORT_asyncio.CancelledError:
+ # It's pretty normal for an RPC to be cancelled; it's not useful to
+ # print a stack trace.
+ raise
+ except IMPORT_google_protobuf_message.DecodeError as decode_error:
+ # We usually see this error when we are trying to construct a proto
+ # message which is too deeply nested: protobuf has a limit of 100
+ # nested messages. See the limits here:
+ # https://protobuf.dev/programming-guides/proto-limits/
+
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ "Unhandled (in 'reboot.ping.Ping.DoPing') "
+ f"{type(decode_error).__name__}{': ' + str(decode_error) if len(str(decode_error)) > 0 else ''}; "
+ "This is usually caused by a deeply nested protobuf message, which is not supported by protobuf.\n"
+ "See the limits here: https://protobuf.dev/programming-guides/proto-limits/" +
+ ''.join(IMPORT_traceback.format_exception(decode_error))
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ message=f"unhandled (in 'reboot.ping.Ping.DoPing') {decode_error}; "
+ "This is usually caused by a deeply nested protobuf message, which is not supported by protobuf.\n"
+ "See the limits here: https://protobuf.dev/programming-guides/proto-limits/"
+ )
+ except BaseException as exception:
+ # Not logging when within `node` as we already log there.
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ "Unhandled (in 'reboot.ping.Ping.DoPing') "
+ f"{type(exception).__name__}{': ' + str(exception) if len(str(exception)) > 0 else ''}; "
+ "propagating as 'Unknown'\n" +
+ ''.join(IMPORT_traceback.format_exception(exception))
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ # TODO(benh): consider whether or not we want to
+ # include the 'package.service.method' which may
+ # get concatenated together forming a kind of
+ # "stack trace"; while it's super helpful for
+ # debugging, it does expose implementation
+ # information.
+ message=f"unhandled (in 'reboot.ping.Ping.DoPing') {type(exception).__name__}: {exception}"
+ )
+ finally:
+ if ongoing_transaction_state_key(context) in ongoing_transaction_states:
+ del ongoing_transaction_states[ongoing_transaction_state_key(context)]
+
+ @IMPORT_reboot_aio_tracing.function_span(
+ # We expect an `EffectValidationRetry` exception; that's not an error.
+ set_status_on_exception=False
+ )
+ async def _DoPing(
+ self,
+ request: google.protobuf.empty_pb2.Empty,
+ context: IMPORT_reboot_aio_contexts.TransactionContext,
+ *,
+ validating_effects: bool,
+ grpc_context: IMPORT_typing.Optional[IMPORT_grpc.aio.ServicerContext] = None,
+ ) -> reboot.ping.ping_api_pb2.PingDoPingResponse:
+ # Try to verify the token if a token verifier exists.
+ context.auth = await self._maybe_verify_token(
+ headers=context._headers, method='DoPing'
+ )
+
+ # Try and "preload" to opportunistically load both state and
+ # idempotent mutations from the database in a single
+ # round-trip. This is fire-and-forget; subsequent calls to
+ # `_load()` and `check_for_idempotent_mutation()` should find
+ # the preloaded data and skip their own calls to the database.
+ #
+ # TODO: a tradeoff we're making here is that we load
+ # idempotent mutations even if this is just a reader and we
+ # don't need them. Consider only preloading for writers and
+ # transactions if loading idempotent mutations is
+ # unnecessarily slow (and we haven't optimized that via a
+ # database stored bloom filter).
+ #
+ # We do this _after_ verifying the token so it is not a
+ # denial-of-service attack vector, i.e., only callers with
+ # valid tokens may trigger preloads on the database.
+ self._state_manager.preload(
+ context.state_type_name,
+ context._state_ref,
+ )
+
+ # Check if we already have performed this mutation!
+ #
+ # We do this _before_ calling 'transactionally()' because
+ # if this call is for a transaction method _and_ we've
+ # already performed the transaction then we don't want to
+ # become a transaction participant (again) we just want to
+ # return the transaction's response.
+ idempotent_mutation = await self._state_manager.check_for_idempotent_mutation(
+ context
+ )
+
+ if idempotent_mutation is not None:
+ response = reboot.ping.ping_api_pb2.PingDoPingResponse()
+ response.ParseFromString(idempotent_mutation.response)
+ return response
+
+ async with self._state_manager.transactionally(
+ context,
+ self.tasks_dispatcher,
+ aborted_type=Ping.DoPingAborted,
+ ) as transaction:
+ assert transaction is not None
+ async with self._state_manager.transaction(
+ context,
+ self._servicer.__state_type__,
+ transaction,
+ authorize=self._maybe_authorize(
+ method_name='reboot.ping.PingMethods.DoPing',
+ headers=context._headers,
+ auth=context.auth,
+ request=request,
+ ),
+ from_constructor=False,
+ requires_constructor=False
+ ) as (state, complete):
+
+ response = await self.__DoPing(
+ context,
+ state,
+ request,
+ validating_effects=validating_effects,
+ )
+
+ await complete(
+ IMPORT_reboot_aio_state_managers.Effects(
+ state=state,
+ response=response,
+ )
+ )
+ return response
+
+ async def _schedule_DoPing(
+ self,
+ *,
+ request: google.protobuf.empty_pb2.Empty,
+ headers: IMPORT_reboot_aio_headers.Headers,
+ grpc_context: IMPORT_grpc.aio.ServicerContext,
+ ) -> tuple[IMPORT_reboot_aio_contexts.WriterContext, reboot.ping.ping_api_pb2.PingDoPingResponse]:
+ context: IMPORT_reboot_aio_contexts.WriterContext = self.create_context(
+ headers=headers,
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping'),
+ method='DoPing',
+ context_type=IMPORT_reboot_aio_contexts.WriterContext,
+ )
+ response = reboot.ping.ping_api_pb2.PingDoPingResponse()
+
+ # Try to verify the token if a token verifier exists.
+ context.auth = await self._maybe_verify_token(
+ headers=headers, method='DoPing'
+ )
+
+ # Try and "preload" to opportunistically load both state and
+ # idempotent mutations from the database in a single
+ # round-trip. This is fire-and-forget; subsequent calls to
+ # `_load()` and `check_for_idempotent_mutation()` should find
+ # the preloaded data and skip their own calls to the database.
+ #
+ # TODO: a tradeoff we're making here is that we load
+ # idempotent mutations even if this is just a reader and we
+ # don't need them. Consider only preloading for writers and
+ # transactions if loading idempotent mutations is
+ # unnecessarily slow (and we haven't optimized that via a
+ # database stored bloom filter).
+ #
+ # We do this _after_ verifying the token so it is not a
+ # denial-of-service attack vector, i.e., only callers with
+ # valid tokens may trigger preloads on the database.
+ self._state_manager.preload(
+ context.state_type_name,
+ context._state_ref,
+ )
+
+ # Check if we already have performed this schedule! Note that
+ # we need to do this for all kinds of methods because this is
+ # effectively a mutation (actually a `writer`, see below).
+ #
+ # We do this _before_ calling 'transactionally()' because
+ # if this call is for a transaction method _and_ we've
+ # already performed the transaction then we don't want to
+ # become a transaction participant (again) we just want to
+ # return the transaction's response.
+ idempotent_mutation = await self._state_manager.check_for_idempotent_mutation(
+ context
+ )
+
+ if idempotent_mutation is not None:
+ response.ParseFromString(idempotent_mutation.response)
+
+ # We should have only scheduled a single task!
+ assert len(idempotent_mutation.task_ids) == 1
+ assert grpc_context is not None
+ grpc_context.set_trailing_metadata(
+ grpc_context.trailing_metadata() +
+ (
+ (
+ IMPORT_reboot_aio_headers.TASK_ID_UUID,
+ str(IMPORT_uuid.UUID(bytes=idempotent_mutation.task_ids[0].task_uuid))
+ ),
+ )
+ )
+
+ return context, response
+
+ async with self._state_manager.transactionally(
+ context,
+ self.tasks_dispatcher,
+ aborted_type=Ping.DoPingAborted,
+ ) as transaction:
+
+ async with self._state_manager.writer(
+ context,
+ self._servicer.__state_type__,
+ self.tasks_dispatcher,
+ transaction=transaction,
+ authorize=self._maybe_authorize(
+ method_name='reboot.ping.PingMethods.DoPing',
+ headers=context._headers,
+ auth=context.auth,
+ request=request,
+ ),
+ from_constructor=False,
+ requires_constructor=False
+ ) as (state, writer):
+
+ task = await PingServicerTasks(
+ context=context,
+ state_ref=context._state_ref,
+ ).DoPing(
+ schedule=context._headers.task_schedule,
+ )
+
+ effects = IMPORT_reboot_aio_state_managers.Effects(
+ response=response,
+ state=state,
+ tasks=[task],
+ )
+
+ assert effects.tasks is not None
+
+ await writer.complete(effects)
+
+ assert grpc_context is not None
+
+ grpc_context.set_trailing_metadata(
+ grpc_context.trailing_metadata() +
+ (
+ (
+ IMPORT_reboot_aio_headers.TASK_ID_UUID,
+ str(IMPORT_uuid.UUID(bytes=task.task_id.task_uuid))
+ ),
+ )
+ )
+
+ return context, response
+
+ return context, response
+
+
+ # Entrypoint for non-reactive network calls (i.e. typical gRPC calls).
+ async def DoPing(
+ self,
+ request: google.protobuf.empty_pb2.Empty,
+ grpc_context: IMPORT_grpc.aio.ServicerContext,
+ ) -> reboot.ping.ping_api_pb2.PingDoPingResponse:
+ headers = IMPORT_reboot_aio_headers.Headers.from_grpc_context(grpc_context)
+ assert headers.application_id is not None # Guaranteed by `Headers`.
+
+ # Confirm whether this is the right server to be serving this
+ # request.
+ try:
+ authoritative_server = self.placement_client.server_for_actor(
+ headers.application_id,
+ headers.state_ref,
+ )
+ except IMPORT_reboot_aio_placement.UnknownApplicationError:
+ # It's possible that the user did indeed type an application ID
+ # that doesn't exist, but it's also quite possible that this
+ # request reached us before the placement planner had gossipped
+ # out the information about which applications exist (we see
+ # this e.g. after `rbt dev`'s chaos monkey restarts). For that
+ # reason, abort with a retryable error.
+ await grpc_context.abort(
+ IMPORT_grpc.StatusCode.UNAVAILABLE,
+ f"Application '{headers.application_id}' not found. If you "
+ "are confident the application exists, this may be because "
+ "the system is still starting.",
+ )
+ raise # Unreachable but necessary for mypy.
+ if authoritative_server != self.server_id:
+ # This is NOT the correct server. Fail.
+ await grpc_context.abort(
+ IMPORT_grpc.StatusCode.UNAVAILABLE,
+ f"Server '{self.server_id}' is not authoritative for this "
+ f"request; server '{authoritative_server}' is.",
+ )
+ raise # Unreachable but necessary for mypy.
+
+ @IMPORT_reboot_aio_internals_middleware.maybe_run_function_twice_to_validate_effects
+ async def _run(
+ validating_effects: bool,
+ ) -> reboot.ping.ping_api_pb2.PingDoPingResponse:
+ context: IMPORT_typing.Optional[IMPORT_reboot_aio_contexts.Context] = None
+ try:
+ if headers.task_schedule is not None:
+ context, response = await self._schedule_DoPing(
+ headers=headers,
+ request=request,
+ grpc_context=grpc_context,
+ )
+ return response
+
+ context = self.create_context(
+ headers=headers,
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping'),
+ method='DoPing',
+ context_type=IMPORT_reboot_aio_contexts.TransactionContext,
+ )
+ assert context is not None
+
+ return await self._DoPing(
+ request,
+ context,
+ validating_effects=validating_effects,
+ grpc_context=grpc_context,
+ )
+ except IMPORT_reboot_aio_contexts.EffectValidationRetry:
+ # Doing effect validation, just let this propagate.
+ raise
+ except IMPORT_reboot.aio.aborted.Aborted as aborted:
+ status = IMPORT_rpc_status_sync.to_status(aborted.to_status())
+ # Need to add transaction participants here because
+ # calling `grpc_context.abort_with_status()` will
+ # ignore any other trailing metadata. Only propagate
+ # transaction participants metadata if the caller cares.
+ # Callers that care are those that are themselves transactions.
+ # It's important to not just send this information to everyone;
+ # some clients can't tolerate trailers, see:
+ # https://github.com/reboot-dev/mono/issues/5081
+ if context is not None and headers.transaction_ids is not None:
+ assert context.transaction_id is not None
+ status = status._replace(
+ trailing_metadata=status.trailing_metadata + context.participants.to_grpc_metadata(
+ read_only_aware=headers.coordinator_read_only_aware,
+ )
+ )
+ await grpc_context.abort_with_status(status)
+ raise # Unreachable but necessary for mypy.
+ except IMPORT_asyncio.CancelledError:
+ # It's pretty normal for an RPC to be cancelled; it's not useful to
+ # print a stack trace.
+ raise
+ except BaseException as exception:
+ # Print the exception stack trace for easier debugging. Note
+ # that we don't include the stack trace in an error message
+ # for the same reason that gRPC doesn't do so by default,
+ # see https://github.com/grpc/grpc/issues/14897, but since this
+ # should only get logged on the server side it is safe.
+ logger.warning(
+ 'Unhandled exception\n' +
+ ''.join(IMPORT_traceback.format_exc() if IMPORT_reboot_nodejs_python.should_print_stacktrace() else [f"{type(exception).__name__}: {exception}"])
+ )
+
+ # Re-raise the exception for gRPC to handle!
+ #
+ # TODO: gRPC will print a stack trace from this
+ # exception which we don't want if we're executing via
+ # Node.js.
+ raise
+ finally:
+ # Propagate transaction participants, if the caller cares.
+ # Callers that care are those that are themselves transactions.
+ # It's important to not just send this information to everyone;
+ # some clients can't tolerate trailers, see:
+ # https://github.com/reboot-dev/mono/issues/5081
+ if context is not None and headers.transaction_ids is not None:
+ assert context.transaction_id is not None
+ grpc_context.set_trailing_metadata(
+ grpc_context.trailing_metadata() +
+ context.participants.to_grpc_metadata(
+ read_only_aware=headers.coordinator_read_only_aware,
+ )
+ )
+
+ with IMPORT_reboot_aio_tracing.context_from_headers(headers):
+ return await _run()
+
+ async def __DoPingPeriodically(
+ self,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ request: reboot.ping.ping_api_pb2.PingDoPingPeriodicallyRequest,
+ *,
+ validating_effects: bool,
+ ) -> reboot.ping.ping_api_pb2.PingDoPingPeriodicallyResponse:
+ try:
+ # We should have an asyncio task and thus context per request,
+ # let's confirm this assumption by making sure that
+ # `__servicer__ is None`.
+ assert self._servicer.__servicer__.get() is None
+ self._servicer.__servicer__.set(self._servicer)
+ response = (
+ await self._servicer._DoPingPeriodically(
+ context=context,
+ request=request
+ )
+ )
+
+
+
+ IMPORT_reboot_aio_types.assert_type(
+ response,
+ [reboot.ping.ping_api_pb2.PingDoPingPeriodicallyResponse],
+ )
+ self.maybe_raise_effect_validation_retry(
+ logger=logger,
+ idempotency_manager=context,
+ method_name='Ping.DoPingPeriodically',
+ validating_effects=validating_effects,
+ context=context,
+ )
+ return response
+ except IMPORT_reboot_aio_contexts.EffectValidationRetry:
+ # Doing effect validation, just let this propagate.
+ raise
+ except IMPORT_reboot.aio.aborted.Aborted as aborted:
+ # If the caller aborted due to a retryable error, just
+ # propagate the aborted instead of propagating `Unknown`
+ # so that a client can transparently retry.
+ if IMPORT_reboot.aio.aborted.is_retryable(aborted):
+ raise aborted
+ # Log any _unhandled_ abort stack traces to make it
+ # easier for debugging.
+ #
+ # NOTE: we don't log if we're a task as it will be logged
+ # in `public/reboot/aio/internals/tasks_dispatcher.py` instead.
+ aborted_type: IMPORT_typing.Optional[type] = None
+ aborted_type = Ping.DoPingPeriodicallyAborted
+ if isinstance(aborted, IMPORT_reboot.aio.aborted.SystemAborted):
+ # Not logging when within `node` as we already log there.
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ f"Unhandled (in 'reboot.ping.Ping.DoPingPeriodically') {aborted}; propagating as 'Unknown'\n" +
+ ''.join(IMPORT_traceback.format_exception(aborted))
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ # TODO(benh): consider whether or not we want to
+ # include the 'package.service.method' which may
+ # get concatenated together forming a kind of
+ # "stack trace"; while it's super helpful for
+ # debugging, it does expose implementation
+ # information.
+ message=f"unhandled (in 'reboot.ping.Ping.DoPingPeriodically') {aborted}"
+ )
+ else:
+ if (
+ aborted_type is not None and
+ not isinstance(aborted, aborted_type) and
+ aborted_type.is_declared_error(aborted.error)
+ ):
+ # We propagate declared errors that might have
+ # come from another call, i.e., we might have an
+ # `Aborted` but not for this method but the
+ # `Aborted` that we have has an error that this
+ # method declared. This allows a developer to
+ # simply add the declared error to their `.proto`
+ # file rather than having to catch and re-raise
+ # the error with their own aborted type.
+ if context.task is None:
+ logger.warning(
+ f"Propagating unhandled but declared error (in 'reboot.ping.Ping.DoPingPeriodically') {aborted}"
+ )
+ elif (
+ aborted_type is None or
+ not isinstance(aborted, aborted_type)
+ ):
+ # Not logging when within `node` as we already log there.
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ f"Unhandled (in 'reboot.ping.Ping.DoPingPeriodically') {aborted}; propagating as 'Unknown'\n" +
+ ''.join(IMPORT_traceback.format_exception(aborted))
+ )
+ # If this wasn't a declared error than we
+ # propagate it as `Unknown`.
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ # TODO(benh): consider whether or not we want to
+ # include the 'package.service.method' which may
+ # get concatenated together forming a kind of
+ # "stack trace"; while it's super helpful for
+ # debugging, it does expose implementation
+ # information.
+ message=f"unhandled (in 'reboot.ping.Ping.DoPingPeriodically') {aborted}"
+ )
+
+ raise
+ except IMPORT_asyncio.CancelledError:
+ # It's pretty normal for an RPC to be cancelled; it's not useful to
+ # print a stack trace.
+ raise
+ except IMPORT_google_protobuf_message.DecodeError as decode_error:
+ # We usually see this error when we are trying to construct a proto
+ # message which is too deeply nested: protobuf has a limit of 100
+ # nested messages. See the limits here:
+ # https://protobuf.dev/programming-guides/proto-limits/
+
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ "Unhandled (in 'reboot.ping.Ping.DoPingPeriodically') "
+ f"{type(decode_error).__name__}{': ' + str(decode_error) if len(str(decode_error)) > 0 else ''}; "
+ "This is usually caused by a deeply nested protobuf message, which is not supported by protobuf.\n"
+ "See the limits here: https://protobuf.dev/programming-guides/proto-limits/" +
+ ''.join(IMPORT_traceback.format_exception(decode_error))
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ message=f"unhandled (in 'reboot.ping.Ping.DoPingPeriodically') {decode_error}; "
+ "This is usually caused by a deeply nested protobuf message, which is not supported by protobuf.\n"
+ "See the limits here: https://protobuf.dev/programming-guides/proto-limits/"
+ )
+ except BaseException as exception:
+ # Not logging when within `node` as we already log there.
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ "Unhandled (in 'reboot.ping.Ping.DoPingPeriodically') "
+ f"{type(exception).__name__}{': ' + str(exception) if len(str(exception)) > 0 else ''}; "
+ "propagating as 'Unknown'\n" +
+ ''.join(IMPORT_traceback.format_exception(exception))
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ # TODO(benh): consider whether or not we want to
+ # include the 'package.service.method' which may
+ # get concatenated together forming a kind of
+ # "stack trace"; while it's super helpful for
+ # debugging, it does expose implementation
+ # information.
+ message=f"unhandled (in 'reboot.ping.Ping.DoPingPeriodically') {type(exception).__name__}: {exception}"
+ )
+ finally:
+ self._servicer.__servicer__.set(None)
+
+
+ async def _schedule_DoPingPeriodically(
+ self,
+ *,
+ request: reboot.ping.ping_api_pb2.PingDoPingPeriodicallyRequest,
+ headers: IMPORT_reboot_aio_headers.Headers,
+ grpc_context: IMPORT_grpc.aio.ServicerContext,
+ ) -> tuple[IMPORT_reboot_aio_contexts.WriterContext, reboot.ping.ping_api_pb2.PingDoPingPeriodicallyResponse]:
+ context: IMPORT_reboot_aio_contexts.WriterContext = self.create_context(
+ headers=headers,
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping'),
+ method='DoPingPeriodically',
+ context_type=IMPORT_reboot_aio_contexts.WriterContext,
+ )
+ response = reboot.ping.ping_api_pb2.PingDoPingPeriodicallyResponse()
+
+ # Try to verify the token if a token verifier exists.
+ context.auth = await self._maybe_verify_token(
+ headers=headers, method='DoPingPeriodically'
+ )
+
+ # Try and "preload" to opportunistically load both state and
+ # idempotent mutations from the database in a single
+ # round-trip. This is fire-and-forget; subsequent calls to
+ # `_load()` and `check_for_idempotent_mutation()` should find
+ # the preloaded data and skip their own calls to the database.
+ #
+ # TODO: a tradeoff we're making here is that we load
+ # idempotent mutations even if this is just a reader and we
+ # don't need them. Consider only preloading for writers and
+ # transactions if loading idempotent mutations is
+ # unnecessarily slow (and we haven't optimized that via a
+ # database stored bloom filter).
+ #
+ # We do this _after_ verifying the token so it is not a
+ # denial-of-service attack vector, i.e., only callers with
+ # valid tokens may trigger preloads on the database.
+ self._state_manager.preload(
+ context.state_type_name,
+ context._state_ref,
+ )
+
+ # Check if we already have performed this schedule! Note that
+ # we need to do this for all kinds of methods because this is
+ # effectively a mutation (actually a `writer`, see below).
+ #
+ # We do this _before_ calling 'transactionally()' because
+ # if this call is for a transaction method _and_ we've
+ # already performed the transaction then we don't want to
+ # become a transaction participant (again) we just want to
+ # return the transaction's response.
+ idempotent_mutation = await self._state_manager.check_for_idempotent_mutation(
+ context
+ )
+
+ if idempotent_mutation is not None:
+ response.ParseFromString(idempotent_mutation.response)
+
+ # We should have only scheduled a single task!
+ assert len(idempotent_mutation.task_ids) == 1
+ assert grpc_context is not None
+ grpc_context.set_trailing_metadata(
+ grpc_context.trailing_metadata() +
+ (
+ (
+ IMPORT_reboot_aio_headers.TASK_ID_UUID,
+ str(IMPORT_uuid.UUID(bytes=idempotent_mutation.task_ids[0].task_uuid))
+ ),
+ )
+ )
+
+ return context, response
+
+ async with self._state_manager.transactionally(
+ context,
+ self.tasks_dispatcher,
+ aborted_type=Ping.DoPingPeriodicallyAborted,
+ ) as transaction:
+
+ async with self._state_manager.writer(
+ context,
+ self._servicer.__state_type__,
+ self.tasks_dispatcher,
+ transaction=transaction,
+ authorize=self._maybe_authorize(
+ method_name='reboot.ping.PingMethods.DoPingPeriodically',
+ headers=context._headers,
+ auth=context.auth,
+ request=request,
+ ),
+ from_constructor=False,
+ requires_constructor=False
+ ) as (state, writer):
+
+ task = await PingServicerTasks(
+ context=context,
+ state_ref=context._state_ref,
+ ).DoPingPeriodically(
+ PingDoPingPeriodicallyRequestFromProto(request),
+ schedule=context._headers.task_schedule,
+ )
+
+ effects = IMPORT_reboot_aio_state_managers.Effects(
+ response=response,
+ state=state,
+ tasks=[task],
+ )
+
+ assert effects.tasks is not None
+
+ await writer.complete(effects)
+
+ assert grpc_context is not None
+
+ grpc_context.set_trailing_metadata(
+ grpc_context.trailing_metadata() +
+ (
+ (
+ IMPORT_reboot_aio_headers.TASK_ID_UUID,
+ str(IMPORT_uuid.UUID(bytes=task.task_id.task_uuid))
+ ),
+ )
+ )
+
+ return context, response
+
+ return context, response
+
+
+ # Entrypoint for non-reactive network calls (i.e. typical gRPC calls).
+ async def DoPingPeriodically(
+ self,
+ request: reboot.ping.ping_api_pb2.PingDoPingPeriodicallyRequest,
+ grpc_context: IMPORT_grpc.aio.ServicerContext,
+ ) -> reboot.ping.ping_api_pb2.PingDoPingPeriodicallyResponse:
+ headers = IMPORT_reboot_aio_headers.Headers.from_grpc_context(grpc_context)
+ assert headers.application_id is not None # Guaranteed by `Headers`.
+
+ # Confirm whether this is the right server to be serving this
+ # request.
+ try:
+ authoritative_server = self.placement_client.server_for_actor(
+ headers.application_id,
+ headers.state_ref,
+ )
+ except IMPORT_reboot_aio_placement.UnknownApplicationError:
+ # It's possible that the user did indeed type an application ID
+ # that doesn't exist, but it's also quite possible that this
+ # request reached us before the placement planner had gossipped
+ # out the information about which applications exist (we see
+ # this e.g. after `rbt dev`'s chaos monkey restarts). For that
+ # reason, abort with a retryable error.
+ await grpc_context.abort(
+ IMPORT_grpc.StatusCode.UNAVAILABLE,
+ f"Application '{headers.application_id}' not found. If you "
+ "are confident the application exists, this may be because "
+ "the system is still starting.",
+ )
+ raise # Unreachable but necessary for mypy.
+ if authoritative_server != self.server_id:
+ # This is NOT the correct server. Fail.
+ await grpc_context.abort(
+ IMPORT_grpc.StatusCode.UNAVAILABLE,
+ f"Server '{self.server_id}' is not authoritative for this "
+ f"request; server '{authoritative_server}' is.",
+ )
+ raise # Unreachable but necessary for mypy.
+
+ @IMPORT_reboot_aio_internals_middleware.maybe_run_function_twice_to_validate_effects
+ async def _run(
+ validating_effects: bool,
+ ) -> reboot.ping.ping_api_pb2.PingDoPingPeriodicallyResponse:
+ context: IMPORT_typing.Optional[IMPORT_reboot_aio_contexts.Context] = None
+ try:
+ # reboot.aio.contexts.WorkflowContext must be scheduled!
+ assert headers.task_schedule is not None
+ context, response = await self._schedule_DoPingPeriodically(
+ headers=headers,
+ request=request,
+ grpc_context=grpc_context,
+ )
+ return response
+
+ except IMPORT_reboot_aio_contexts.EffectValidationRetry:
+ # Doing effect validation, just let this propagate.
+ raise
+ except IMPORT_reboot.aio.aborted.Aborted as aborted:
+ status = IMPORT_rpc_status_sync.to_status(aborted.to_status())
+ # Need to add transaction participants here because
+ # calling `grpc_context.abort_with_status()` will
+ # ignore any other trailing metadata. Only propagate
+ # transaction participants metadata if the caller cares.
+ # Callers that care are those that are themselves transactions.
+ # It's important to not just send this information to everyone;
+ # some clients can't tolerate trailers, see:
+ # https://github.com/reboot-dev/mono/issues/5081
+ if context is not None and headers.transaction_ids is not None:
+ assert context.transaction_id is not None
+ status = status._replace(
+ trailing_metadata=status.trailing_metadata + context.participants.to_grpc_metadata(
+ read_only_aware=headers.coordinator_read_only_aware,
+ )
+ )
+ await grpc_context.abort_with_status(status)
+ raise # Unreachable but necessary for mypy.
+ except IMPORT_asyncio.CancelledError:
+ # It's pretty normal for an RPC to be cancelled; it's not useful to
+ # print a stack trace.
+ raise
+ except BaseException as exception:
+ # Print the exception stack trace for easier debugging. Note
+ # that we don't include the stack trace in an error message
+ # for the same reason that gRPC doesn't do so by default,
+ # see https://github.com/grpc/grpc/issues/14897, but since this
+ # should only get logged on the server side it is safe.
+ logger.warning(
+ 'Unhandled exception\n' +
+ ''.join(IMPORT_traceback.format_exc() if IMPORT_reboot_nodejs_python.should_print_stacktrace() else [f"{type(exception).__name__}: {exception}"])
+ )
+
+ # Re-raise the exception for gRPC to handle!
+ #
+ # TODO: gRPC will print a stack trace from this
+ # exception which we don't want if we're executing via
+ # Node.js.
+ raise
+ finally:
+ # Propagate transaction participants, if the caller cares.
+ # Callers that care are those that are themselves transactions.
+ # It's important to not just send this information to everyone;
+ # some clients can't tolerate trailers, see:
+ # https://github.com/reboot-dev/mono/issues/5081
+ if context is not None and headers.transaction_ids is not None:
+ assert context.transaction_id is not None
+ grpc_context.set_trailing_metadata(
+ grpc_context.trailing_metadata() +
+ context.participants.to_grpc_metadata(
+ read_only_aware=headers.coordinator_read_only_aware,
+ )
+ )
+
+ with IMPORT_reboot_aio_tracing.context_from_headers(headers):
+ return await _run()
+
+ async def __Describe(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: reboot.ping.ping_api_pb2.Ping,
+ request: google.protobuf.empty_pb2.Empty,
+ *,
+ validating_effects: bool,
+ ) -> reboot.ping.ping_api_pb2.PingDescribeResponse:
+ try:
+ typed_state: Ping.State = PingFromProto(state, is_initial_state=(context.state_id in states_being_constructed))
+
+ if ongoing_transaction_state_key(context) in ongoing_transaction_states:
+ typed_state = ongoing_transaction_states[ongoing_transaction_state_key(context)].model_copy(deep=True)
+ response = (
+ await self._servicer._Describe(
+ context=context,
+ state=typed_state,
+ request=request
+ )
+ )
+
+
+ PingToProto(typed_state, state)
+
+ IMPORT_reboot_aio_types.assert_type(
+ response,
+ [reboot.ping.ping_api_pb2.PingDescribeResponse],
+ )
+ self.maybe_raise_effect_validation_retry(
+ logger=logger,
+ idempotency_manager=context,
+ method_name='Ping.Describe',
+ validating_effects=validating_effects,
+ context=context,
+ )
+ return response
+ except IMPORT_reboot_aio_contexts.EffectValidationRetry:
+ # Doing effect validation, just let this propagate.
+ raise
+ except IMPORT_reboot.aio.aborted.Aborted as aborted:
+ # If the caller aborted due to a retryable error, just
+ # propagate the aborted instead of propagating `Unknown`
+ # so that a client can transparently retry.
+ if IMPORT_reboot.aio.aborted.is_retryable(aborted):
+ raise aborted
+ # Log any _unhandled_ abort stack traces to make it
+ # easier for debugging.
+ #
+ # NOTE: we don't log if we're a task as it will be logged
+ # in `public/reboot/aio/internals/tasks_dispatcher.py` instead.
+ aborted_type: IMPORT_typing.Optional[type] = None
+ aborted_type = Ping.DescribeAborted
+ if isinstance(aborted, IMPORT_reboot.aio.aborted.SystemAborted):
+ # Not logging when within `node` as we already log there.
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ f"Unhandled (in 'reboot.ping.Ping.Describe') {aborted}; propagating as 'Unknown'\n" +
+ ''.join(IMPORT_traceback.format_exception(aborted))
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ # TODO(benh): consider whether or not we want to
+ # include the 'package.service.method' which may
+ # get concatenated together forming a kind of
+ # "stack trace"; while it's super helpful for
+ # debugging, it does expose implementation
+ # information.
+ message=f"unhandled (in 'reboot.ping.Ping.Describe') {aborted}"
+ )
+ else:
+ if (
+ aborted_type is not None and
+ not isinstance(aborted, aborted_type) and
+ aborted_type.is_declared_error(aborted.error)
+ ):
+ # We propagate declared errors that might have
+ # come from another call, i.e., we might have an
+ # `Aborted` but not for this method but the
+ # `Aborted` that we have has an error that this
+ # method declared. This allows a developer to
+ # simply add the declared error to their `.proto`
+ # file rather than having to catch and re-raise
+ # the error with their own aborted type.
+ if context.task is None:
+ logger.warning(
+ f"Propagating unhandled but declared error (in 'reboot.ping.Ping.Describe') {aborted}"
+ )
+ elif (
+ aborted_type is None or
+ not isinstance(aborted, aborted_type)
+ ):
+ # Not logging when within `node` as we already log there.
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ f"Unhandled (in 'reboot.ping.Ping.Describe') {aborted}; propagating as 'Unknown'\n" +
+ ''.join(IMPORT_traceback.format_exception(aborted))
+ )
+ # If this wasn't a declared error than we
+ # propagate it as `Unknown`.
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ # TODO(benh): consider whether or not we want to
+ # include the 'package.service.method' which may
+ # get concatenated together forming a kind of
+ # "stack trace"; while it's super helpful for
+ # debugging, it does expose implementation
+ # information.
+ message=f"unhandled (in 'reboot.ping.Ping.Describe') {aborted}"
+ )
+
+ raise
+ except IMPORT_asyncio.CancelledError:
+ # It's pretty normal for an RPC to be cancelled; it's not useful to
+ # print a stack trace.
+ raise
+ except IMPORT_google_protobuf_message.DecodeError as decode_error:
+ # We usually see this error when we are trying to construct a proto
+ # message which is too deeply nested: protobuf has a limit of 100
+ # nested messages. See the limits here:
+ # https://protobuf.dev/programming-guides/proto-limits/
+
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ "Unhandled (in 'reboot.ping.Ping.Describe') "
+ f"{type(decode_error).__name__}{': ' + str(decode_error) if len(str(decode_error)) > 0 else ''}; "
+ "This is usually caused by a deeply nested protobuf message, which is not supported by protobuf.\n"
+ "See the limits here: https://protobuf.dev/programming-guides/proto-limits/" +
+ ''.join(IMPORT_traceback.format_exception(decode_error))
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ message=f"unhandled (in 'reboot.ping.Ping.Describe') {decode_error}; "
+ "This is usually caused by a deeply nested protobuf message, which is not supported by protobuf.\n"
+ "See the limits here: https://protobuf.dev/programming-guides/proto-limits/"
+ )
+ except BaseException as exception:
+ # Not logging when within `node` as we already log there.
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ "Unhandled (in 'reboot.ping.Ping.Describe') "
+ f"{type(exception).__name__}{': ' + str(exception) if len(str(exception)) > 0 else ''}; "
+ "propagating as 'Unknown'\n" +
+ ''.join(IMPORT_traceback.format_exception(exception))
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ # TODO(benh): consider whether or not we want to
+ # include the 'package.service.method' which may
+ # get concatenated together forming a kind of
+ # "stack trace"; while it's super helpful for
+ # debugging, it does expose implementation
+ # information.
+ message=f"unhandled (in 'reboot.ping.Ping.Describe') {type(exception).__name__}: {exception}"
+ )
+ finally:
+ pass
+
+ @IMPORT_reboot_aio_tracing.function_span(
+ # We expect an `EffectValidationRetry` exception; that's not an error.
+ set_status_on_exception=False
+ )
+ async def _Describe(
+ self,
+ request: google.protobuf.empty_pb2.Empty,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ *,
+ validating_effects: bool,
+ grpc_context: IMPORT_typing.Optional[IMPORT_grpc.aio.ServicerContext] = None,
+ ) -> reboot.ping.ping_api_pb2.PingDescribeResponse:
+ # Try to verify the token if a token verifier exists.
+ context.auth = await self._maybe_verify_token(
+ headers=context._headers, method='Describe'
+ )
+
+ # Try and "preload" to opportunistically load both state and
+ # idempotent mutations from the database in a single
+ # round-trip. This is fire-and-forget; subsequent calls to
+ # `_load()` and `check_for_idempotent_mutation()` should find
+ # the preloaded data and skip their own calls to the database.
+ #
+ # TODO: a tradeoff we're making here is that we load
+ # idempotent mutations even if this is just a reader and we
+ # don't need them. Consider only preloading for writers and
+ # transactions if loading idempotent mutations is
+ # unnecessarily slow (and we haven't optimized that via a
+ # database stored bloom filter).
+ #
+ # We do this _after_ verifying the token so it is not a
+ # denial-of-service attack vector, i.e., only callers with
+ # valid tokens may trigger preloads on the database.
+ self._state_manager.preload(
+ context.state_type_name,
+ context._state_ref,
+ )
+
+
+ async with self._state_manager.transactionally(
+ context,
+ self.tasks_dispatcher,
+ aborted_type=Ping.DescribeAborted,
+ ) as transaction:
+ authorizer = self._maybe_authorize(
+ method_name='reboot.ping.PingMethods.Describe',
+ headers=context._headers,
+ auth=context.auth,
+ request=request,
+ )
+ async with self._state_manager.reader(
+ context,
+ self._servicer.__state_type__,
+ authorize=authorizer,
+ ) as state:
+ response = await self.__Describe(
+ context,
+ state,
+ request,
+ validating_effects=validating_effects,
+ )
+ return response
+
+ async def _schedule_Describe(
+ self,
+ *,
+ request: google.protobuf.empty_pb2.Empty,
+ headers: IMPORT_reboot_aio_headers.Headers,
+ grpc_context: IMPORT_grpc.aio.ServicerContext,
+ ) -> tuple[IMPORT_reboot_aio_contexts.WriterContext, reboot.ping.ping_api_pb2.PingDescribeResponse]:
+ context: IMPORT_reboot_aio_contexts.WriterContext = self.create_context(
+ headers=headers,
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping'),
+ method='Describe',
+ context_type=IMPORT_reboot_aio_contexts.WriterContext,
+ )
+ response = reboot.ping.ping_api_pb2.PingDescribeResponse()
+
+ # Try to verify the token if a token verifier exists.
+ context.auth = await self._maybe_verify_token(
+ headers=headers, method='Describe'
+ )
+
+ # Try and "preload" to opportunistically load both state and
+ # idempotent mutations from the database in a single
+ # round-trip. This is fire-and-forget; subsequent calls to
+ # `_load()` and `check_for_idempotent_mutation()` should find
+ # the preloaded data and skip their own calls to the database.
+ #
+ # TODO: a tradeoff we're making here is that we load
+ # idempotent mutations even if this is just a reader and we
+ # don't need them. Consider only preloading for writers and
+ # transactions if loading idempotent mutations is
+ # unnecessarily slow (and we haven't optimized that via a
+ # database stored bloom filter).
+ #
+ # We do this _after_ verifying the token so it is not a
+ # denial-of-service attack vector, i.e., only callers with
+ # valid tokens may trigger preloads on the database.
+ self._state_manager.preload(
+ context.state_type_name,
+ context._state_ref,
+ )
+
+ # Check if we already have performed this schedule! Note that
+ # we need to do this for all kinds of methods because this is
+ # effectively a mutation (actually a `writer`, see below).
+ #
+ # We do this _before_ calling 'transactionally()' because
+ # if this call is for a transaction method _and_ we've
+ # already performed the transaction then we don't want to
+ # become a transaction participant (again) we just want to
+ # return the transaction's response.
+ idempotent_mutation = await self._state_manager.check_for_idempotent_mutation(
+ context
+ )
+
+ if idempotent_mutation is not None:
+ response.ParseFromString(idempotent_mutation.response)
+
+ # We should have only scheduled a single task!
+ assert len(idempotent_mutation.task_ids) == 1
+ assert grpc_context is not None
+ grpc_context.set_trailing_metadata(
+ grpc_context.trailing_metadata() +
+ (
+ (
+ IMPORT_reboot_aio_headers.TASK_ID_UUID,
+ str(IMPORT_uuid.UUID(bytes=idempotent_mutation.task_ids[0].task_uuid))
+ ),
+ )
+ )
+
+ return context, response
+
+ async with self._state_manager.transactionally(
+ context,
+ self.tasks_dispatcher,
+ aborted_type=Ping.DescribeAborted,
+ ) as transaction:
+
+ async with self._state_manager.writer(
+ context,
+ self._servicer.__state_type__,
+ self.tasks_dispatcher,
+ transaction=transaction,
+ authorize=self._maybe_authorize(
+ method_name='reboot.ping.PingMethods.Describe',
+ headers=context._headers,
+ auth=context.auth,
+ request=request,
+ ),
+ from_constructor=False,
+ requires_constructor=False
+ ) as (state, writer):
+
+ task = await PingServicerTasks(
+ context=context,
+ state_ref=context._state_ref,
+ ).Describe(
+ schedule=context._headers.task_schedule,
+ )
+
+ effects = IMPORT_reboot_aio_state_managers.Effects(
+ response=response,
+ state=state,
+ tasks=[task],
+ )
+
+ assert effects.tasks is not None
+
+ await writer.complete(effects)
+
+ assert grpc_context is not None
+
+ grpc_context.set_trailing_metadata(
+ grpc_context.trailing_metadata() +
+ (
+ (
+ IMPORT_reboot_aio_headers.TASK_ID_UUID,
+ str(IMPORT_uuid.UUID(bytes=task.task_id.task_uuid))
+ ),
+ )
+ )
+
+ return context, response
+
+ return context, response
+
+
+ # Entrypoint for non-reactive network calls (i.e. typical gRPC calls).
+ async def Describe(
+ self,
+ request: google.protobuf.empty_pb2.Empty,
+ grpc_context: IMPORT_grpc.aio.ServicerContext,
+ ) -> reboot.ping.ping_api_pb2.PingDescribeResponse:
+ headers = IMPORT_reboot_aio_headers.Headers.from_grpc_context(grpc_context)
+ assert headers.application_id is not None # Guaranteed by `Headers`.
+
+ # Confirm whether this is the right server to be serving this
+ # request.
+ try:
+ authoritative_server = self.placement_client.server_for_actor(
+ headers.application_id,
+ headers.state_ref,
+ )
+ except IMPORT_reboot_aio_placement.UnknownApplicationError:
+ # It's possible that the user did indeed type an application ID
+ # that doesn't exist, but it's also quite possible that this
+ # request reached us before the placement planner had gossipped
+ # out the information about which applications exist (we see
+ # this e.g. after `rbt dev`'s chaos monkey restarts). For that
+ # reason, abort with a retryable error.
+ await grpc_context.abort(
+ IMPORT_grpc.StatusCode.UNAVAILABLE,
+ f"Application '{headers.application_id}' not found. If you "
+ "are confident the application exists, this may be because "
+ "the system is still starting.",
+ )
+ raise # Unreachable but necessary for mypy.
+ if authoritative_server != self.server_id:
+ # This is NOT the correct server. Fail.
+ await grpc_context.abort(
+ IMPORT_grpc.StatusCode.UNAVAILABLE,
+ f"Server '{self.server_id}' is not authoritative for this "
+ f"request; server '{authoritative_server}' is.",
+ )
+ raise # Unreachable but necessary for mypy.
+
+ @IMPORT_reboot_aio_internals_middleware.maybe_run_function_twice_to_validate_effects
+ async def _run(
+ validating_effects: bool,
+ ) -> reboot.ping.ping_api_pb2.PingDescribeResponse:
+ context: IMPORT_typing.Optional[IMPORT_reboot_aio_contexts.Context] = None
+ try:
+ if headers.task_schedule is not None:
+ context, response = await self._schedule_Describe(
+ headers=headers,
+ request=request,
+ grpc_context=grpc_context,
+ )
+ return response
+
+ context = self.create_context(
+ headers=headers,
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping'),
+ method='Describe',
+ context_type=IMPORT_reboot_aio_contexts.ReaderContext,
+ )
+ assert context is not None
+
+ return await self._Describe(
+ request,
+ context,
+ validating_effects=validating_effects,
+ grpc_context=grpc_context,
+ )
+ except IMPORT_reboot_aio_contexts.EffectValidationRetry:
+ # Doing effect validation, just let this propagate.
+ raise
+ except IMPORT_reboot.aio.aborted.Aborted as aborted:
+ status = IMPORT_rpc_status_sync.to_status(aborted.to_status())
+ # Need to add transaction participants here because
+ # calling `grpc_context.abort_with_status()` will
+ # ignore any other trailing metadata. Only propagate
+ # transaction participants metadata if the caller cares.
+ # Callers that care are those that are themselves transactions.
+ # It's important to not just send this information to everyone;
+ # some clients can't tolerate trailers, see:
+ # https://github.com/reboot-dev/mono/issues/5081
+ if context is not None and headers.transaction_ids is not None:
+ assert context.transaction_id is not None
+ status = status._replace(
+ trailing_metadata=status.trailing_metadata + context.participants.to_grpc_metadata(
+ read_only_aware=headers.coordinator_read_only_aware,
+ )
+ )
+ await grpc_context.abort_with_status(status)
+ raise # Unreachable but necessary for mypy.
+ except IMPORT_asyncio.CancelledError:
+ # It's pretty normal for an RPC to be cancelled; it's not useful to
+ # print a stack trace.
+ raise
+ except BaseException as exception:
+ # Print the exception stack trace for easier debugging. Note
+ # that we don't include the stack trace in an error message
+ # for the same reason that gRPC doesn't do so by default,
+ # see https://github.com/grpc/grpc/issues/14897, but since this
+ # should only get logged on the server side it is safe.
+ logger.warning(
+ 'Unhandled exception\n' +
+ ''.join(IMPORT_traceback.format_exc() if IMPORT_reboot_nodejs_python.should_print_stacktrace() else [f"{type(exception).__name__}: {exception}"])
+ )
+
+ # Re-raise the exception for gRPC to handle!
+ #
+ # TODO: gRPC will print a stack trace from this
+ # exception which we don't want if we're executing via
+ # Node.js.
+ raise
+ finally:
+ # Propagate transaction participants, if the caller cares.
+ # Callers that care are those that are themselves transactions.
+ # It's important to not just send this information to everyone;
+ # some clients can't tolerate trailers, see:
+ # https://github.com/reboot-dev/mono/issues/5081
+ if context is not None and headers.transaction_ids is not None:
+ assert context.transaction_id is not None
+ grpc_context.set_trailing_metadata(
+ grpc_context.trailing_metadata() +
+ context.participants.to_grpc_metadata(
+ read_only_aware=headers.coordinator_read_only_aware,
+ )
+ )
+
+ with IMPORT_reboot_aio_tracing.context_from_headers(headers):
+ return await _run()
+
+ async def __NumPings(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: reboot.ping.ping_api_pb2.Ping,
+ request: google.protobuf.empty_pb2.Empty,
+ *,
+ validating_effects: bool,
+ ) -> reboot.ping.ping_api_pb2.PingNumPingsResponse:
+ try:
+ typed_state: Ping.State = PingFromProto(state, is_initial_state=(context.state_id in states_being_constructed))
+
+ if ongoing_transaction_state_key(context) in ongoing_transaction_states:
+ typed_state = ongoing_transaction_states[ongoing_transaction_state_key(context)].model_copy(deep=True)
+ response = (
+ await self._servicer._NumPings(
+ context=context,
+ state=typed_state,
+ request=request
+ )
+ )
+
+
+ PingToProto(typed_state, state)
+
+ IMPORT_reboot_aio_types.assert_type(
+ response,
+ [reboot.ping.ping_api_pb2.PingNumPingsResponse],
+ )
+ self.maybe_raise_effect_validation_retry(
+ logger=logger,
+ idempotency_manager=context,
+ method_name='Ping.NumPings',
+ validating_effects=validating_effects,
+ context=context,
+ )
+ return response
+ except IMPORT_reboot_aio_contexts.EffectValidationRetry:
+ # Doing effect validation, just let this propagate.
+ raise
+ except IMPORT_reboot.aio.aborted.Aborted as aborted:
+ # If the caller aborted due to a retryable error, just
+ # propagate the aborted instead of propagating `Unknown`
+ # so that a client can transparently retry.
+ if IMPORT_reboot.aio.aborted.is_retryable(aborted):
+ raise aborted
+ # Log any _unhandled_ abort stack traces to make it
+ # easier for debugging.
+ #
+ # NOTE: we don't log if we're a task as it will be logged
+ # in `public/reboot/aio/internals/tasks_dispatcher.py` instead.
+ aborted_type: IMPORT_typing.Optional[type] = None
+ aborted_type = Ping.NumPingsAborted
+ if isinstance(aborted, IMPORT_reboot.aio.aborted.SystemAborted):
+ # Not logging when within `node` as we already log there.
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ f"Unhandled (in 'reboot.ping.Ping.NumPings') {aborted}; propagating as 'Unknown'\n" +
+ ''.join(IMPORT_traceback.format_exception(aborted))
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ # TODO(benh): consider whether or not we want to
+ # include the 'package.service.method' which may
+ # get concatenated together forming a kind of
+ # "stack trace"; while it's super helpful for
+ # debugging, it does expose implementation
+ # information.
+ message=f"unhandled (in 'reboot.ping.Ping.NumPings') {aborted}"
+ )
+ else:
+ if (
+ aborted_type is not None and
+ not isinstance(aborted, aborted_type) and
+ aborted_type.is_declared_error(aborted.error)
+ ):
+ # We propagate declared errors that might have
+ # come from another call, i.e., we might have an
+ # `Aborted` but not for this method but the
+ # `Aborted` that we have has an error that this
+ # method declared. This allows a developer to
+ # simply add the declared error to their `.proto`
+ # file rather than having to catch and re-raise
+ # the error with their own aborted type.
+ if context.task is None:
+ logger.warning(
+ f"Propagating unhandled but declared error (in 'reboot.ping.Ping.NumPings') {aborted}"
+ )
+ elif (
+ aborted_type is None or
+ not isinstance(aborted, aborted_type)
+ ):
+ # Not logging when within `node` as we already log there.
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ f"Unhandled (in 'reboot.ping.Ping.NumPings') {aborted}; propagating as 'Unknown'\n" +
+ ''.join(IMPORT_traceback.format_exception(aborted))
+ )
+ # If this wasn't a declared error than we
+ # propagate it as `Unknown`.
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ # TODO(benh): consider whether or not we want to
+ # include the 'package.service.method' which may
+ # get concatenated together forming a kind of
+ # "stack trace"; while it's super helpful for
+ # debugging, it does expose implementation
+ # information.
+ message=f"unhandled (in 'reboot.ping.Ping.NumPings') {aborted}"
+ )
+
+ raise
+ except IMPORT_asyncio.CancelledError:
+ # It's pretty normal for an RPC to be cancelled; it's not useful to
+ # print a stack trace.
+ raise
+ except IMPORT_google_protobuf_message.DecodeError as decode_error:
+ # We usually see this error when we are trying to construct a proto
+ # message which is too deeply nested: protobuf has a limit of 100
+ # nested messages. See the limits here:
+ # https://protobuf.dev/programming-guides/proto-limits/
+
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ "Unhandled (in 'reboot.ping.Ping.NumPings') "
+ f"{type(decode_error).__name__}{': ' + str(decode_error) if len(str(decode_error)) > 0 else ''}; "
+ "This is usually caused by a deeply nested protobuf message, which is not supported by protobuf.\n"
+ "See the limits here: https://protobuf.dev/programming-guides/proto-limits/" +
+ ''.join(IMPORT_traceback.format_exception(decode_error))
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ message=f"unhandled (in 'reboot.ping.Ping.NumPings') {decode_error}; "
+ "This is usually caused by a deeply nested protobuf message, which is not supported by protobuf.\n"
+ "See the limits here: https://protobuf.dev/programming-guides/proto-limits/"
+ )
+ except BaseException as exception:
+ # Not logging when within `node` as we already log there.
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ "Unhandled (in 'reboot.ping.Ping.NumPings') "
+ f"{type(exception).__name__}{': ' + str(exception) if len(str(exception)) > 0 else ''}; "
+ "propagating as 'Unknown'\n" +
+ ''.join(IMPORT_traceback.format_exception(exception))
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ # TODO(benh): consider whether or not we want to
+ # include the 'package.service.method' which may
+ # get concatenated together forming a kind of
+ # "stack trace"; while it's super helpful for
+ # debugging, it does expose implementation
+ # information.
+ message=f"unhandled (in 'reboot.ping.Ping.NumPings') {type(exception).__name__}: {exception}"
+ )
+ finally:
+ pass
+
+ @IMPORT_reboot_aio_tracing.function_span(
+ # We expect an `EffectValidationRetry` exception; that's not an error.
+ set_status_on_exception=False
+ )
+ async def _NumPings(
+ self,
+ request: google.protobuf.empty_pb2.Empty,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ *,
+ validating_effects: bool,
+ grpc_context: IMPORT_typing.Optional[IMPORT_grpc.aio.ServicerContext] = None,
+ ) -> reboot.ping.ping_api_pb2.PingNumPingsResponse:
+ # Try to verify the token if a token verifier exists.
+ context.auth = await self._maybe_verify_token(
+ headers=context._headers, method='NumPings'
+ )
+
+ # Try and "preload" to opportunistically load both state and
+ # idempotent mutations from the database in a single
+ # round-trip. This is fire-and-forget; subsequent calls to
+ # `_load()` and `check_for_idempotent_mutation()` should find
+ # the preloaded data and skip their own calls to the database.
+ #
+ # TODO: a tradeoff we're making here is that we load
+ # idempotent mutations even if this is just a reader and we
+ # don't need them. Consider only preloading for writers and
+ # transactions if loading idempotent mutations is
+ # unnecessarily slow (and we haven't optimized that via a
+ # database stored bloom filter).
+ #
+ # We do this _after_ verifying the token so it is not a
+ # denial-of-service attack vector, i.e., only callers with
+ # valid tokens may trigger preloads on the database.
+ self._state_manager.preload(
+ context.state_type_name,
+ context._state_ref,
+ )
+
+
+ async with self._state_manager.transactionally(
+ context,
+ self.tasks_dispatcher,
+ aborted_type=Ping.NumPingsAborted,
+ ) as transaction:
+ authorizer = self._maybe_authorize(
+ method_name='reboot.ping.PingMethods.NumPings',
+ headers=context._headers,
+ auth=context.auth,
+ request=request,
+ )
+ async with self._state_manager.reader(
+ context,
+ self._servicer.__state_type__,
+ authorize=authorizer,
+ ) as state:
+ response = await self.__NumPings(
+ context,
+ state,
+ request,
+ validating_effects=validating_effects,
+ )
+ return response
+
+ async def _schedule_NumPings(
+ self,
+ *,
+ request: google.protobuf.empty_pb2.Empty,
+ headers: IMPORT_reboot_aio_headers.Headers,
+ grpc_context: IMPORT_grpc.aio.ServicerContext,
+ ) -> tuple[IMPORT_reboot_aio_contexts.WriterContext, reboot.ping.ping_api_pb2.PingNumPingsResponse]:
+ context: IMPORT_reboot_aio_contexts.WriterContext = self.create_context(
+ headers=headers,
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping'),
+ method='NumPings',
+ context_type=IMPORT_reboot_aio_contexts.WriterContext,
+ )
+ response = reboot.ping.ping_api_pb2.PingNumPingsResponse()
+
+ # Try to verify the token if a token verifier exists.
+ context.auth = await self._maybe_verify_token(
+ headers=headers, method='NumPings'
+ )
+
+ # Try and "preload" to opportunistically load both state and
+ # idempotent mutations from the database in a single
+ # round-trip. This is fire-and-forget; subsequent calls to
+ # `_load()` and `check_for_idempotent_mutation()` should find
+ # the preloaded data and skip their own calls to the database.
+ #
+ # TODO: a tradeoff we're making here is that we load
+ # idempotent mutations even if this is just a reader and we
+ # don't need them. Consider only preloading for writers and
+ # transactions if loading idempotent mutations is
+ # unnecessarily slow (and we haven't optimized that via a
+ # database stored bloom filter).
+ #
+ # We do this _after_ verifying the token so it is not a
+ # denial-of-service attack vector, i.e., only callers with
+ # valid tokens may trigger preloads on the database.
+ self._state_manager.preload(
+ context.state_type_name,
+ context._state_ref,
+ )
+
+ # Check if we already have performed this schedule! Note that
+ # we need to do this for all kinds of methods because this is
+ # effectively a mutation (actually a `writer`, see below).
+ #
+ # We do this _before_ calling 'transactionally()' because
+ # if this call is for a transaction method _and_ we've
+ # already performed the transaction then we don't want to
+ # become a transaction participant (again) we just want to
+ # return the transaction's response.
+ idempotent_mutation = await self._state_manager.check_for_idempotent_mutation(
+ context
+ )
+
+ if idempotent_mutation is not None:
+ response.ParseFromString(idempotent_mutation.response)
+
+ # We should have only scheduled a single task!
+ assert len(idempotent_mutation.task_ids) == 1
+ assert grpc_context is not None
+ grpc_context.set_trailing_metadata(
+ grpc_context.trailing_metadata() +
+ (
+ (
+ IMPORT_reboot_aio_headers.TASK_ID_UUID,
+ str(IMPORT_uuid.UUID(bytes=idempotent_mutation.task_ids[0].task_uuid))
+ ),
+ )
+ )
+
+ return context, response
+
+ async with self._state_manager.transactionally(
+ context,
+ self.tasks_dispatcher,
+ aborted_type=Ping.NumPingsAborted,
+ ) as transaction:
+
+ async with self._state_manager.writer(
+ context,
+ self._servicer.__state_type__,
+ self.tasks_dispatcher,
+ transaction=transaction,
+ authorize=self._maybe_authorize(
+ method_name='reboot.ping.PingMethods.NumPings',
+ headers=context._headers,
+ auth=context.auth,
+ request=request,
+ ),
+ from_constructor=False,
+ requires_constructor=False
+ ) as (state, writer):
+
+ task = await PingServicerTasks(
+ context=context,
+ state_ref=context._state_ref,
+ ).NumPings(
+ schedule=context._headers.task_schedule,
+ )
+
+ effects = IMPORT_reboot_aio_state_managers.Effects(
+ response=response,
+ state=state,
+ tasks=[task],
+ )
+
+ assert effects.tasks is not None
+
+ await writer.complete(effects)
+
+ assert grpc_context is not None
+
+ grpc_context.set_trailing_metadata(
+ grpc_context.trailing_metadata() +
+ (
+ (
+ IMPORT_reboot_aio_headers.TASK_ID_UUID,
+ str(IMPORT_uuid.UUID(bytes=task.task_id.task_uuid))
+ ),
+ )
+ )
+
+ return context, response
+
+ return context, response
+
+
+ # Entrypoint for non-reactive network calls (i.e. typical gRPC calls).
+ async def NumPings(
+ self,
+ request: google.protobuf.empty_pb2.Empty,
+ grpc_context: IMPORT_grpc.aio.ServicerContext,
+ ) -> reboot.ping.ping_api_pb2.PingNumPingsResponse:
+ headers = IMPORT_reboot_aio_headers.Headers.from_grpc_context(grpc_context)
+ assert headers.application_id is not None # Guaranteed by `Headers`.
+
+ # Confirm whether this is the right server to be serving this
+ # request.
+ try:
+ authoritative_server = self.placement_client.server_for_actor(
+ headers.application_id,
+ headers.state_ref,
+ )
+ except IMPORT_reboot_aio_placement.UnknownApplicationError:
+ # It's possible that the user did indeed type an application ID
+ # that doesn't exist, but it's also quite possible that this
+ # request reached us before the placement planner had gossipped
+ # out the information about which applications exist (we see
+ # this e.g. after `rbt dev`'s chaos monkey restarts). For that
+ # reason, abort with a retryable error.
+ await grpc_context.abort(
+ IMPORT_grpc.StatusCode.UNAVAILABLE,
+ f"Application '{headers.application_id}' not found. If you "
+ "are confident the application exists, this may be because "
+ "the system is still starting.",
+ )
+ raise # Unreachable but necessary for mypy.
+ if authoritative_server != self.server_id:
+ # This is NOT the correct server. Fail.
+ await grpc_context.abort(
+ IMPORT_grpc.StatusCode.UNAVAILABLE,
+ f"Server '{self.server_id}' is not authoritative for this "
+ f"request; server '{authoritative_server}' is.",
+ )
+ raise # Unreachable but necessary for mypy.
+
+ @IMPORT_reboot_aio_internals_middleware.maybe_run_function_twice_to_validate_effects
+ async def _run(
+ validating_effects: bool,
+ ) -> reboot.ping.ping_api_pb2.PingNumPingsResponse:
+ context: IMPORT_typing.Optional[IMPORT_reboot_aio_contexts.Context] = None
+ try:
+ if headers.task_schedule is not None:
+ context, response = await self._schedule_NumPings(
+ headers=headers,
+ request=request,
+ grpc_context=grpc_context,
+ )
+ return response
+
+ context = self.create_context(
+ headers=headers,
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping'),
+ method='NumPings',
+ context_type=IMPORT_reboot_aio_contexts.ReaderContext,
+ )
+ assert context is not None
+
+ return await self._NumPings(
+ request,
+ context,
+ validating_effects=validating_effects,
+ grpc_context=grpc_context,
+ )
+ except IMPORT_reboot_aio_contexts.EffectValidationRetry:
+ # Doing effect validation, just let this propagate.
+ raise
+ except IMPORT_reboot.aio.aborted.Aborted as aborted:
+ status = IMPORT_rpc_status_sync.to_status(aborted.to_status())
+ # Need to add transaction participants here because
+ # calling `grpc_context.abort_with_status()` will
+ # ignore any other trailing metadata. Only propagate
+ # transaction participants metadata if the caller cares.
+ # Callers that care are those that are themselves transactions.
+ # It's important to not just send this information to everyone;
+ # some clients can't tolerate trailers, see:
+ # https://github.com/reboot-dev/mono/issues/5081
+ if context is not None and headers.transaction_ids is not None:
+ assert context.transaction_id is not None
+ status = status._replace(
+ trailing_metadata=status.trailing_metadata + context.participants.to_grpc_metadata(
+ read_only_aware=headers.coordinator_read_only_aware,
+ )
+ )
+ await grpc_context.abort_with_status(status)
+ raise # Unreachable but necessary for mypy.
+ except IMPORT_asyncio.CancelledError:
+ # It's pretty normal for an RPC to be cancelled; it's not useful to
+ # print a stack trace.
+ raise
+ except BaseException as exception:
+ # Print the exception stack trace for easier debugging. Note
+ # that we don't include the stack trace in an error message
+ # for the same reason that gRPC doesn't do so by default,
+ # see https://github.com/grpc/grpc/issues/14897, but since this
+ # should only get logged on the server side it is safe.
+ logger.warning(
+ 'Unhandled exception\n' +
+ ''.join(IMPORT_traceback.format_exc() if IMPORT_reboot_nodejs_python.should_print_stacktrace() else [f"{type(exception).__name__}: {exception}"])
+ )
+
+ # Re-raise the exception for gRPC to handle!
+ #
+ # TODO: gRPC will print a stack trace from this
+ # exception which we don't want if we're executing via
+ # Node.js.
+ raise
+ finally:
+ # Propagate transaction participants, if the caller cares.
+ # Callers that care are those that are themselves transactions.
+ # It's important to not just send this information to everyone;
+ # some clients can't tolerate trailers, see:
+ # https://github.com/reboot-dev/mono/issues/5081
+ if context is not None and headers.transaction_ids is not None:
+ assert context.transaction_id is not None
+ grpc_context.set_trailing_metadata(
+ grpc_context.trailing_metadata() +
+ context.participants.to_grpc_metadata(
+ read_only_aware=headers.coordinator_read_only_aware,
+ )
+ )
+
+ with IMPORT_reboot_aio_tracing.context_from_headers(headers):
+ return await _run()
+
+ def _maybe_authorize(
+ self,
+ *,
+ method_name: str,
+ headers: IMPORT_reboot_aio_headers.Headers,
+ auth: IMPORT_typing.Optional[IMPORT_reboot_aio_auth.Auth],
+ request: IMPORT_typing.Optional[PingRequestTypes] = None,
+ ) -> IMPORT_typing.Optional[IMPORT_typing.Callable[[IMPORT_typing.Optional[PingStateType]], IMPORT_typing.Awaitable[None]]]:
+ """Returns a function to check authorization for the given method.
+
+ Raises `PermissionDenied` in case Authorizer is present but the request
+ is not authorized.
+ """
+ # To authorize internal calls, we use an internal magic token.
+ if headers.bearer_token == __internal_magic_token__:
+ return None
+
+ assert self._authorizer is not None
+
+ async def authorize(state: IMPORT_typing.Optional[PingStateType]) -> None:
+ # Create context for the authorizer. This is a `ReaderContext`
+ # independently of the calling context.
+ with self.use_context(
+ headers=(
+ # Get headers suitable for doing authorization.
+ headers.copy_for_token_verification_and_authorization()
+ ),
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping'),
+ method=method_name,
+ context_type=IMPORT_reboot_aio_contexts.ReaderContext,
+ ) as context:
+ context.auth = auth
+
+ # Get the authorizer decision.
+ authorization_decision = await self._authorizer.authorize(
+ method_name=method_name,
+ context=context,
+ state=state,
+ request=request,
+ )
+
+ # Enforce correct authorizer decision type.
+ try:
+ IMPORT_reboot_aio_types.assert_type(
+ authorization_decision,
+ [
+ IMPORT_rbt_v1alpha1.errors_pb2.Ok,
+ IMPORT_rbt_v1alpha1.errors_pb2.Unauthenticated,
+ IMPORT_rbt_v1alpha1.errors_pb2.PermissionDenied,
+ ]
+ )
+ except TypeError as e:
+ # Retyping.cast the exception to provide more context.
+ authorizer_type = f"{type(self._authorizer).__module__}.{type(self._authorizer).__name__}"
+ raise TypeError(
+ f"Authorizer '{authorizer_type}' "
+ f"returned unexpected type '{type(authorization_decision).__name__}' "
+ f"for method '{method_name}' on "
+ f"`reboot.ping.Ping('{headers.state_ref.id}')`"
+ ) from e
+
+ # If the decision is not `True`, raise a `SystemAborted` with either a
+ # `PermissionDenied` error (in case of `False`) or an `Unauthenticated`
+ # error.
+ if not isinstance(authorization_decision, IMPORT_rbt_v1alpha1.errors_pb2.Ok):
+ if isinstance(authorization_decision, IMPORT_rbt_v1alpha1.errors_pb2.Unauthenticated):
+ logger.warning(
+ f"Unauthenticated call to '{method_name}' on "
+ f"`reboot.ping.Ping('{headers.state_ref.id}')`"
+ )
+
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ authorization_decision,
+ message=
+ f"You are not authorized to call '{method_name}' on "
+ f"`reboot.ping.Ping('{headers.state_ref.id}')`"
+ )
+
+ return authorize
+
+ async def _maybe_verify_token(
+ self,
+ *,
+ headers: IMPORT_reboot_aio_headers.Headers,
+ method: str,
+ ) -> IMPORT_typing.Optional[IMPORT_reboot_aio_auth.Auth]:
+ """Verify the bearer token and if a token verifier is present.
+
+ Returns the (optional) `reboot.aio.auth.Auth` object
+ produced by the token verifier if the token can be verified.
+ """
+ if self._token_verifier is not None:
+ if headers.bearer_token == __internal_magic_token__:
+ return None
+
+ with self.use_context(
+ headers=(
+ # Get headers suitable for doing token verification.
+ headers.copy_for_token_verification_and_authorization()
+ ),
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping'),
+ method=method,
+ context_type=IMPORT_reboot_aio_contexts.ReaderContext,
+ ) as context:
+ result = await self._token_verifier.verify_token(
+ context=context,
+ token=headers.bearer_token,
+ )
+ if isinstance(result, IMPORT_rbt_v1alpha1.errors_pb2.Unauthenticated):
+ raise IMPORT_reboot_aio_aborted.SystemAborted(
+ result,
+ message=result.message or None,
+ )
+ return result
+
+ return None
+
+class PongServicerMiddleware(IMPORT_reboot_aio_internals_middleware.Middleware):
+
+ def __init__(
+ self,
+ *,
+ servicer: PongBaseServicer,
+ application_id: IMPORT_reboot_aio_types.ApplicationId,
+ server_id: IMPORT_reboot_aio_types.ServerId,
+ state_manager: IMPORT_reboot_aio_state_managers.StateManager,
+ placement_client: IMPORT_reboot_aio_placement.PlacementClient,
+ channel_manager: IMPORT_reboot_aio_internals_channel_manager._ChannelManager,
+ tasks_cache: IMPORT_reboot_aio_internals_tasks_cache.TasksCache,
+ token_verifier: IMPORT_typing.Optional[IMPORT_reboot_aio_auth_token_verifiers.TokenVerifier],
+ effect_validation: IMPORT_reboot_aio_contexts.EffectValidation,
+ ready: IMPORT_asyncio.Event,
+ ):
+ super().__init__(
+ application_id=application_id,
+ server_id=server_id,
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Pong'),
+ service_names = [
+ IMPORT_reboot_aio_types.ServiceName("reboot.ping.PongMethods"),
+ ],
+ placement_client=placement_client,
+ channel_manager=channel_manager,
+ effect_validation=effect_validation,
+ get_database_timestamp_ms=lambda: state_manager.latest_timestamp_ms,
+ )
+
+ self._servicer = servicer
+ self._state_manager = state_manager
+ self.tasks_dispatcher = IMPORT_reboot_aio_internals_tasks_dispatcher.TasksDispatcher(
+ application_id=application_id,
+ dispatch=self.dispatch,
+ tasks_cache=tasks_cache,
+ ready=ready,
+ complete_task=self._state_manager.complete_task,
+ )
+
+ # Store the type of each method's request so that stored requests can be
+ # deserialized into the correct type.
+ self.request_type_by_method_name: dict[str, type[IMPORT_google_protobuf_message.Message]] = {
+ 'DoPong': google.protobuf.empty_pb2.Empty,
+ 'NumPongs': google.protobuf.empty_pb2.Empty,
+ }
+
+ # Get authorizer, if any, converting from a rule if necessary.
+ def convert_authorizer_rule_if_necessary(
+ authorizer_or_rule: IMPORT_typing.Optional[
+ IMPORT_reboot_aio_auth_authorizers.Authorizer | IMPORT_reboot_aio_auth_authorizers.AuthorizerRule
+ ]
+ ) -> IMPORT_reboot_aio_auth_authorizers.Authorizer:
+
+ # If no authorizer or rule is provided, return the default
+ # authorizer which allows if app internal or allows if in
+ # dev mode (and logs some warnings to help the user
+ # realize where they are missing authorization).
+ if authorizer_or_rule is None:
+ return IMPORT_reboot_aio_auth_authorizers.DefaultAuthorizer(
+ 'Pong',
+ is_user_type=False,
+ )
+
+ if isinstance(authorizer_or_rule, IMPORT_reboot_aio_auth_authorizers.AuthorizerRule):
+ return PongAuthorizer(
+ _default=authorizer_or_rule
+ )
+
+ return authorizer_or_rule
+
+ self._authorizer = convert_authorizer_rule_if_necessary(
+ servicer.authorizer()
+ )
+
+ # Create token verifier.
+ self._token_verifier: IMPORT_typing.Optional[IMPORT_reboot_aio_auth_token_verifiers.TokenVerifier] = (
+ servicer.token_verifier() or token_verifier
+ )
+
+ # Since users specify errors as proto messages they can't raise them
+ # directly - to do so they have to use the `Aborted` wrapper, which will
+ # hold the original proto message. On errors we'll need to check whether
+ # such wrappers hold a proto message for a specified error, so we can
+ # avoid retrying tasks that complete with a specified error.
+ self._specified_errors_by_service_method_name: dict[str, list[str]] = {
+ }
+
+
+ def add_to_server(self, server: IMPORT_grpc.aio.Server) -> None:
+ reboot.ping.ping_api_pb2_grpc.add_PongMethodsServicer_to_server(
+ self, server
+ )
+
+ async def inspect(self, state_ref: IMPORT_reboot_aio_types.StateRef) -> IMPORT_typing.AsyncIterator[IMPORT_google_protobuf_message.Message]:
+ """Implementation of `Middleware.inspect()`."""
+ context = self.create_context(
+ headers=IMPORT_reboot_aio_headers.Headers(
+ application_id=self.application_id,
+ state_ref=state_ref,
+ ),
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Pong'),
+ method="inspect",
+ context_type=IMPORT_reboot_aio_contexts.ReaderContext,
+ )
+
+ async with self._state_manager.streaming_reader_idempotency_key(
+ context,
+ self._servicer.__state_type__,
+ authorize=None,
+ ) as states:
+ async for (state, idempotency_key) in states:
+ yield state
+
+ async def react_query(
+ self,
+ headers: IMPORT_reboot_aio_headers.Headers,
+ method: str,
+ request_bytes: bytes,
+ ) -> IMPORT_typing.AsyncIterator[tuple[IMPORT_typing.Optional[IMPORT_google_protobuf_message.Message], list[IMPORT_uuid.UUID]]]:
+ """Returns the response of calling 'method' given a message
+ deserialized from the provided 'request_bytes' for each state
+ update that creates a different response.
+
+ # The caller (react.py) should have already ensured that this server
+ # is authoritative for this traffic.
+ assert self.placement_client.server_for_actor(
+ headers.application_id,
+ headers.state_ref,
+ ) == self._server_id
+
+ NOTE: only unary reader methods are supported."""
+ # Need to define these up here since we can only do that once.
+ last_response: IMPORT_typing.Optional[IMPORT_google_protobuf_message.Message] = None
+ aggregated_idempotency_keys: list[IMPORT_uuid.UUID] = []
+ if method == 'DoPong':
+ # Invariant here is that users should not have called this
+ # directly but only through code generated React
+ # components which should not have been generated except
+ # for valid method candidates.
+ logger.warning(
+ "Got a React query request with an invalid method name: "
+ f"Method '{method}' is invalid for servicer Pong."
+ "\n"
+ "Do you have a browser tab open for an older version "
+ "of this application, or for a different application all together?"
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.InvalidMethod(),
+ message=
+ f"Method '{method}' is invalid"
+ )
+ yield # Necessary for type checking.
+ elif method == 'NumPongs':
+
+ context = self.create_context(
+ headers=headers,
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Pong'),
+ method='NumPongs',
+ context_type=IMPORT_reboot_aio_contexts.ReaderContext,
+ )
+
+ with IMPORT_reboot_aio_tracing.span(
+ state_name=f"{context.state_type_name}('{context.state_id}')",
+ span_name="NumPongs() (reactively)",
+ # The naming above matches Python, but not TypeScript.
+ python_specific=True,
+ level=IMPORT_reboot_aio_tracing.TraceLevel.CUSTOMER,
+ ):
+ context.auth = await self._maybe_verify_token(
+ headers=headers, method='NumPongs'
+ )
+
+ request = google.protobuf.empty_pb2.Empty()
+ request.ParseFromString(request_bytes)
+
+ async with self._state_manager.reactively(
+ context,
+ self._servicer.__state_type__,
+ authorize=self._maybe_authorize(
+ method_name='reboot.ping.PongMethods.NumPongs',
+ headers=headers,
+ auth=context.auth,
+ request=request,
+ ),
+ ) as states:
+ async for (state, idempotency_keys) in states:
+
+ aggregated_idempotency_keys.extend(idempotency_keys)
+
+ # Note: This does not do any defensive copying currently:
+ # see https://github.com/reboot-dev/respect/issues/2636.
+ @IMPORT_reboot_aio_internals_middleware.maybe_run_function_twice_to_validate_effects
+ async def run__NumPongs(validating_effects: bool) -> IMPORT_google_protobuf_message.Message:
+ return await self.__NumPongs(
+ context,
+ state,
+ request,
+ validating_effects=validating_effects,
+ )
+
+ response = await run__NumPongs()
+
+ if last_response != response:
+ yield (response, aggregated_idempotency_keys)
+ last_response = response
+ else:
+ yield (None, aggregated_idempotency_keys)
+
+ aggregated_idempotency_keys.clear()
+ else:
+ logger.warning(
+ "Got a React query request with an invalid method name: "
+ f"Method '{method}' is invalid for servicer Pong."
+ "\n"
+ "Do you have a browser tab open for an older version "
+ "of this application, or for a different application all together?"
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.InvalidMethod(),
+ message=
+ f"Method '{method}' not found"
+ )
+ yield # Unreachable but necessary for mypy.
+
+ async def react_mutate(
+ self,
+ headers: IMPORT_reboot_aio_headers.Headers,
+ method: str,
+ request_bytes: bytes,
+ ) -> IMPORT_google_protobuf_message.Message:
+ """Returns the response of calling 'method' given a message
+ deserialized from the provided 'request_bytes'."""
+ if method == 'DoPong':
+ request = google.protobuf.empty_pb2.Empty()
+ request.ParseFromString(request_bytes)
+
+ # NOTE: we automatically retry mutations that come through
+ # React when we get a `IMPORT_grpc.StatusCode.UNAVAILABLE` to
+ # match the retry logic we do in the React code generated
+ # to handle lack/loss of connectivity.
+ #
+ # TODO(benh): revisit this decision if we ever see reason
+ # to call `react_mutate()` from any place other than where
+ # we're executing React (e.g., browser, next.js server
+ # component, etc).
+ call_backoff = IMPORT_reboot_aio_backoff.Backoff()
+ while True:
+ # We make a full-fledged gRPC call, so that if this traffic
+ # was misrouted (i.e. this server is not authoritative
+ # for the state), it will now go to the right place. The
+ # receiving middleware will handle things like effect
+ # validation and so forth.
+ assert headers.application_id is not None # Guaranteed by `Headers`.
+ stub = reboot.ping.ping_api_pb2_grpc.PongMethodsStub(
+ self.channel_manager.get_channel_to(
+ self.placement_client.address_for_actor(
+ headers.application_id,
+ headers.state_ref,
+ )
+ )
+ )
+ call = stub.DoPong(
+ request=request,
+ metadata=headers.to_grpc_metadata(),
+ )
+ try:
+ return await call
+ except IMPORT_grpc.aio.AioRpcError as error:
+ if error.code() == IMPORT_grpc.StatusCode.UNAVAILABLE:
+ await call_backoff()
+ continue
+
+ # Reconstitute the error that the server threw, if it was a declared error.
+ status = await IMPORT_rpc_status_async.from_call(call)
+ if status is not None:
+ raise Pong.DoPongAborted.from_status(
+ status
+ ) from None
+ raise Pong.DoPongAborted.from_grpc_aio_rpc_error(
+ error
+ ) from None
+
+ elif method == 'NumPongs':
+ # Invariant here is that users should not have called this
+ # directly but only through code generated React
+ # components which should not have been generated except
+ # for valid method candidates.
+ logger.warning(
+ "Got a react mutate request with an invalid method name: "
+ "Method 'NumPongs' is invalid for servicer Pong."
+ "\n"
+ "Do you have an old browser tab still open for an older version "
+ "of this application, or a different application all together?"
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.InvalidMethod(),
+ message=f"Method '{method}' is invalid"
+ )
+ else:
+ logger.warning(
+ "Got a react mutate request with an invalid method name: "
+ f"Method '{method}' is invalid for servicer Pong."
+ "\n"
+ "Do you have an old browser tab still open for an older version "
+ "of this application, or a different application all together?"
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.InvalidMethod(),
+ message=
+ f"Method '{method}' not found"
+ )
+
+ async def dispatch(
+ self,
+ task: IMPORT_reboot_aio_tasks.TaskEffect,
+ *,
+ only_validate: bool = False,
+ on_loop_iteration: IMPORT_reboot_aio_internals_tasks_dispatcher.OnLoopIterationCallable = (lambda iteration, next_iteration_schedule: None),
+ ) -> IMPORT_reboot_aio_internals_tasks_dispatcher.TaskResponseOrStatus:
+ """Dispatches the tasks to execute unless 'only_validate' is set to
+ true, in which case just ensures that the task actually exists.
+ Note that this function will be called *by* tasks_dispatcher; it will
+ not itself call into tasks_dispatcher."""
+
+ if 'DoPong' == task.method_name:
+ if only_validate:
+ # TODO(benh): validate 'task.request' is correct type.
+ return (reboot.ping.ping_api_pb2.PongDoPongResponse(), None)
+
+ # Use an inline method to create a new scope, so that we can use
+ # variable names like `context` and `effects` in multiple branches
+ # in this code (notably when there are multiple task types) without
+ # hitting a mypy error that the variable's type is not consistent.
+ async def run_DoPong(
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ *,
+ validating_effects: bool = False,
+ ):
+ async with self._state_manager.task_workflow(
+ context,
+ task,
+ on_loop_iteration=on_loop_iteration,
+ validating_effects=validating_effects,
+ ) as complete:
+ try:
+ response = await (PongWorkflowStub(
+ context=context,
+ state_ref=context._state_ref,
+ ).DoPong(
+ bearer_token=__internal_magic_token__,
+ idempotency=IMPORT_reboot_aio_idempotency.Idempotency(
+ alias=f'Task {IMPORT_uuid.UUID(bytes=task.task_id.task_uuid)}',
+ ),
+ ))
+ await complete(task, (response, None))
+ return (response, None)
+ except IMPORT_asyncio.CancelledError:
+ # Do not retry a task if it was cancelled by a caller.
+ if self.tasks_dispatcher.is_task_cancelled(task.task_id.task_uuid):
+ result = (
+ None,
+ IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Cancelled(),
+ ).to_status(),
+ )
+ await complete(task, result)
+ return result
+ else:
+ raise
+ except IMPORT_reboot.aio.aborted.Aborted as aborted:
+ error_type = f'{aborted._protobuf_error.__class__.__module__}.{aborted._protobuf_error.__class__.__qualname__}'
+ # Do not retry a task if the error was specified in the
+ # proto file.
+ if error_type in self._specified_errors_by_service_method_name.get('reboot.ping.PongMethods.DoPong', []):
+ result = (None, aborted.to_status())
+ await complete(task, result)
+ return result
+ raise
+
+
+ return await run_DoPong(
+ self.create_context(
+ headers=IMPORT_reboot_aio_headers.Headers(
+ application_id=self.application_id,
+ state_ref=IMPORT_reboot_aio_types.StateRef(task.task_id.state_ref),
+ ),
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Pong'),
+ method='DoPong',
+ context_type=IMPORT_reboot_aio_contexts.WorkflowContext,
+ task=task,
+ # Propagate state manager and state type so that
+ # `until()` calls (via `WorkflowContext.retry_reactively_until()`)
+ # can enter `reactively()` scoped to each `until()`
+ # invocation.
+ reactively_state_manager=self._state_manager,
+ reactively_state_type=(
+ self._servicer.__state_type__
+ ),
+ )
+ )
+ elif 'NumPongs' == task.method_name:
+ if only_validate:
+ # TODO(benh): validate 'task.request' is correct type.
+ return (reboot.ping.ping_api_pb2.PongNumPongsResponse(), None)
+
+ # Use an inline method to create a new scope, so that we can use
+ # variable names like `context` and `effects` in multiple branches
+ # in this code (notably when there are multiple task types) without
+ # hitting a mypy error that the variable's type is not consistent.
+ async def run_NumPongs(
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ *,
+ validating_effects: bool = False,
+ ):
+ async with self._state_manager.task_workflow(
+ context,
+ task,
+ on_loop_iteration=on_loop_iteration,
+ validating_effects=validating_effects,
+ ) as complete:
+ try:
+ response = await (PongWorkflowStub(
+ context=context,
+ state_ref=context._state_ref,
+ ).NumPongs(
+ bearer_token=__internal_magic_token__,
+ ))
+ await complete(task, (response, None))
+ return (response, None)
+ except IMPORT_asyncio.CancelledError:
+ # Do not retry a task if it was cancelled by a caller.
+ if self.tasks_dispatcher.is_task_cancelled(task.task_id.task_uuid):
+ result = (
+ None,
+ IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Cancelled(),
+ ).to_status(),
+ )
+ await complete(task, result)
+ return result
+ else:
+ raise
+ except IMPORT_reboot.aio.aborted.Aborted as aborted:
+ error_type = f'{aborted._protobuf_error.__class__.__module__}.{aborted._protobuf_error.__class__.__qualname__}'
+ # Do not retry a task if the error was specified in the
+ # proto file.
+ if error_type in self._specified_errors_by_service_method_name.get('reboot.ping.PongMethods.NumPongs', []):
+ result = (None, aborted.to_status())
+ await complete(task, result)
+ return result
+ raise
+
+
+ return await run_NumPongs(
+ self.create_context(
+ headers=IMPORT_reboot_aio_headers.Headers(
+ application_id=self.application_id,
+ state_ref=IMPORT_reboot_aio_types.StateRef(task.task_id.state_ref),
+ ),
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Pong'),
+ method='NumPongs',
+ context_type=IMPORT_reboot_aio_contexts.WorkflowContext,
+ task=task,
+ # Propagate state manager and state type so that
+ # `until()` calls (via `WorkflowContext.retry_reactively_until()`)
+ # can enter `reactively()` scoped to each `until()`
+ # invocation.
+ reactively_state_manager=self._state_manager,
+ reactively_state_type=(
+ self._servicer.__state_type__
+ ),
+ )
+ )
+
+ # There are no tasks for this service.
+ start_or_validate = "start" if not only_validate else "validate"
+ raise RuntimeError(
+ f"Attempted to {start_or_validate} task '{task.method_name}' "
+ f"on 'Pong' which does not exist"
+ )
+
+ # Pong specific methods:
+ async def __DoPong(
+ self,
+ context: IMPORT_reboot_aio_contexts.WriterContext,
+ state: reboot.ping.ping_api_pb2.Pong,
+ request: google.protobuf.empty_pb2.Empty,
+ *,
+ validating_effects: bool,
+ ) -> Pong.DoPongEffects:
+ try:
+ typed_state: Pong.State = PongFromProto(state, is_initial_state=(context.state_id in states_being_constructed))
+
+ if ongoing_transaction_state_key(context) in ongoing_transaction_states:
+ typed_state = ongoing_transaction_states[ongoing_transaction_state_key(context)].model_copy(deep=True)
+ response = (
+ await self._servicer._DoPong(
+ context=context,
+ state=typed_state,
+ request=request
+ )
+ )
+
+ # TODO: it's premature to overwrite the state now given that the
+ # writer might still "fail" and an error will get propagated back
+ # to the ongoing transaction which will still see the effects of
+ # this writer. What we should be doing instead is creating a
+ # callback API that we invoke only after a writer completes
+ # that lets us update the state _reference_ then.
+ if ongoing_transaction_state_key(context) in ongoing_transaction_states:
+ for field, value in typed_state.model_dump().items():
+ setattr(ongoing_transaction_states[ongoing_transaction_state_key(context)], field, value)
+
+ PongToProto(typed_state, state)
+
+ IMPORT_reboot_aio_types.assert_type(
+ response,
+ [reboot.ping.ping_api_pb2.PongDoPongResponse],
+ )
+ self.maybe_raise_effect_validation_retry(
+ logger=logger,
+ idempotency_manager=context,
+ method_name='Pong.DoPong',
+ validating_effects=validating_effects,
+ context=context,
+ )
+ return Pong.DoPongEffects(
+ state=state,
+ response=response,
+ tasks=context._tasks,
+ _colocated_upserts=context._colocated_upserts,
+ )
+ except IMPORT_reboot_aio_contexts.EffectValidationRetry:
+ # Doing effect validation, just let this propagate.
+ raise
+ except IMPORT_reboot.aio.aborted.Aborted as aborted:
+ # If the caller aborted due to a retryable error, just
+ # propagate the aborted instead of propagating `Unknown`
+ # so that a client can transparently retry.
+ if IMPORT_reboot.aio.aborted.is_retryable(aborted):
+ raise aborted
+ # Log any _unhandled_ abort stack traces to make it
+ # easier for debugging.
+ #
+ # NOTE: we don't log if we're a task as it will be logged
+ # in `public/reboot/aio/internals/tasks_dispatcher.py` instead.
+ aborted_type: IMPORT_typing.Optional[type] = None
+ aborted_type = Pong.DoPongAborted
+ if isinstance(aborted, IMPORT_reboot.aio.aborted.SystemAborted):
+ # Not logging when within `node` as we already log there.
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ f"Unhandled (in 'reboot.ping.Pong.DoPong') {aborted}; propagating as 'Unknown'\n" +
+ ''.join(IMPORT_traceback.format_exception(aborted))
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ # TODO(benh): consider whether or not we want to
+ # include the 'package.service.method' which may
+ # get concatenated together forming a kind of
+ # "stack trace"; while it's super helpful for
+ # debugging, it does expose implementation
+ # information.
+ message=f"unhandled (in 'reboot.ping.Pong.DoPong') {aborted}"
+ )
+ else:
+ if (
+ aborted_type is not None and
+ not isinstance(aborted, aborted_type) and
+ aborted_type.is_declared_error(aborted.error)
+ ):
+ # We propagate declared errors that might have
+ # come from another call, i.e., we might have an
+ # `Aborted` but not for this method but the
+ # `Aborted` that we have has an error that this
+ # method declared. This allows a developer to
+ # simply add the declared error to their `.proto`
+ # file rather than having to catch and re-raise
+ # the error with their own aborted type.
+ if context.task is None:
+ logger.warning(
+ f"Propagating unhandled but declared error (in 'reboot.ping.Pong.DoPong') {aborted}"
+ )
+ elif (
+ aborted_type is None or
+ not isinstance(aborted, aborted_type)
+ ):
+ # Not logging when within `node` as we already log there.
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ f"Unhandled (in 'reboot.ping.Pong.DoPong') {aborted}; propagating as 'Unknown'\n" +
+ ''.join(IMPORT_traceback.format_exception(aborted))
+ )
+ # If this wasn't a declared error than we
+ # propagate it as `Unknown`.
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ # TODO(benh): consider whether or not we want to
+ # include the 'package.service.method' which may
+ # get concatenated together forming a kind of
+ # "stack trace"; while it's super helpful for
+ # debugging, it does expose implementation
+ # information.
+ message=f"unhandled (in 'reboot.ping.Pong.DoPong') {aborted}"
+ )
+
+ raise
+ except IMPORT_asyncio.CancelledError:
+ # It's pretty normal for an RPC to be cancelled; it's not useful to
+ # print a stack trace.
+ raise
+ except IMPORT_google_protobuf_message.DecodeError as decode_error:
+ # We usually see this error when we are trying to construct a proto
+ # message which is too deeply nested: protobuf has a limit of 100
+ # nested messages. See the limits here:
+ # https://protobuf.dev/programming-guides/proto-limits/
+
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ "Unhandled (in 'reboot.ping.Pong.DoPong') "
+ f"{type(decode_error).__name__}{': ' + str(decode_error) if len(str(decode_error)) > 0 else ''}; "
+ "This is usually caused by a deeply nested protobuf message, which is not supported by protobuf.\n"
+ "See the limits here: https://protobuf.dev/programming-guides/proto-limits/" +
+ ''.join(IMPORT_traceback.format_exception(decode_error))
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ message=f"unhandled (in 'reboot.ping.Pong.DoPong') {decode_error}; "
+ "This is usually caused by a deeply nested protobuf message, which is not supported by protobuf.\n"
+ "See the limits here: https://protobuf.dev/programming-guides/proto-limits/"
+ )
+ except BaseException as exception:
+ # Not logging when within `node` as we already log there.
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ "Unhandled (in 'reboot.ping.Pong.DoPong') "
+ f"{type(exception).__name__}{': ' + str(exception) if len(str(exception)) > 0 else ''}; "
+ "propagating as 'Unknown'\n" +
+ ''.join(IMPORT_traceback.format_exception(exception))
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ # TODO(benh): consider whether or not we want to
+ # include the 'package.service.method' which may
+ # get concatenated together forming a kind of
+ # "stack trace"; while it's super helpful for
+ # debugging, it does expose implementation
+ # information.
+ message=f"unhandled (in 'reboot.ping.Pong.DoPong') {type(exception).__name__}: {exception}"
+ )
+ finally:
+ pass
+
+ @IMPORT_reboot_aio_tracing.function_span(
+ # We expect an `EffectValidationRetry` exception; that's not an error.
+ set_status_on_exception=False
+ )
+ async def _DoPong(
+ self,
+ request: google.protobuf.empty_pb2.Empty,
+ context: IMPORT_reboot_aio_contexts.WriterContext,
+ *,
+ validating_effects: bool,
+ grpc_context: IMPORT_typing.Optional[IMPORT_grpc.aio.ServicerContext] = None,
+ ) -> reboot.ping.ping_api_pb2.PongDoPongResponse:
+ # Try to verify the token if a token verifier exists.
+ context.auth = await self._maybe_verify_token(
+ headers=context._headers, method='DoPong'
+ )
+
+ # Try and "preload" to opportunistically load both state and
+ # idempotent mutations from the database in a single
+ # round-trip. This is fire-and-forget; subsequent calls to
+ # `_load()` and `check_for_idempotent_mutation()` should find
+ # the preloaded data and skip their own calls to the database.
+ #
+ # TODO: a tradeoff we're making here is that we load
+ # idempotent mutations even if this is just a reader and we
+ # don't need them. Consider only preloading for writers and
+ # transactions if loading idempotent mutations is
+ # unnecessarily slow (and we haven't optimized that via a
+ # database stored bloom filter).
+ #
+ # We do this _after_ verifying the token so it is not a
+ # denial-of-service attack vector, i.e., only callers with
+ # valid tokens may trigger preloads on the database.
+ self._state_manager.preload(
+ context.state_type_name,
+ context._state_ref,
+ )
+
+ # Check if we already have performed this mutation!
+ #
+ # We do this _before_ calling 'transactionally()' because
+ # if this call is for a transaction method _and_ we've
+ # already performed the transaction then we don't want to
+ # become a transaction participant (again) we just want to
+ # return the transaction's response.
+ idempotent_mutation = await self._state_manager.check_for_idempotent_mutation(
+ context
+ )
+
+ if idempotent_mutation is not None:
+ response = reboot.ping.ping_api_pb2.PongDoPongResponse()
+ response.ParseFromString(idempotent_mutation.response)
+ return response
+
+ async with self._state_manager.transactionally(
+ context,
+ self.tasks_dispatcher,
+ aborted_type=Pong.DoPongAborted,
+ ) as transaction:
+ async with self._state_manager.writer(
+ context,
+ self._servicer.__state_type__,
+ self.tasks_dispatcher,
+ authorize=self._maybe_authorize(
+ method_name='reboot.ping.PongMethods.DoPong',
+ headers=context._headers,
+ auth=context.auth,
+ request=request,
+ ),
+ transaction=transaction,
+ from_constructor=False,
+ requires_constructor=False,
+ ) as (state, writer):
+
+ effects = await self.__DoPong(
+ context,
+ state,
+ request,
+ validating_effects=validating_effects,
+ )
+
+ await writer.complete(effects)
+
+ # TODO: We need a single `Effects` superclass for all methods, so we
+ # would need to make it "partially" generic (with per-method subclasses
+ # filling out the rest of the generic parameters) in order to fix this.
+ return effects.response # type: ignore[return-value]
+
+ async def _schedule_DoPong(
+ self,
+ *,
+ request: google.protobuf.empty_pb2.Empty,
+ headers: IMPORT_reboot_aio_headers.Headers,
+ grpc_context: IMPORT_grpc.aio.ServicerContext,
+ ) -> tuple[IMPORT_reboot_aio_contexts.WriterContext, reboot.ping.ping_api_pb2.PongDoPongResponse]:
+ context: IMPORT_reboot_aio_contexts.WriterContext = self.create_context(
+ headers=headers,
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Pong'),
+ method='DoPong',
+ context_type=IMPORT_reboot_aio_contexts.WriterContext,
+ )
+ response = reboot.ping.ping_api_pb2.PongDoPongResponse()
+
+ # Try to verify the token if a token verifier exists.
+ context.auth = await self._maybe_verify_token(
+ headers=headers, method='DoPong'
+ )
+
+ # Try and "preload" to opportunistically load both state and
+ # idempotent mutations from the database in a single
+ # round-trip. This is fire-and-forget; subsequent calls to
+ # `_load()` and `check_for_idempotent_mutation()` should find
+ # the preloaded data and skip their own calls to the database.
+ #
+ # TODO: a tradeoff we're making here is that we load
+ # idempotent mutations even if this is just a reader and we
+ # don't need them. Consider only preloading for writers and
+ # transactions if loading idempotent mutations is
+ # unnecessarily slow (and we haven't optimized that via a
+ # database stored bloom filter).
+ #
+ # We do this _after_ verifying the token so it is not a
+ # denial-of-service attack vector, i.e., only callers with
+ # valid tokens may trigger preloads on the database.
+ self._state_manager.preload(
+ context.state_type_name,
+ context._state_ref,
+ )
+
+ # Check if we already have performed this schedule! Note that
+ # we need to do this for all kinds of methods because this is
+ # effectively a mutation (actually a `writer`, see below).
+ #
+ # We do this _before_ calling 'transactionally()' because
+ # if this call is for a transaction method _and_ we've
+ # already performed the transaction then we don't want to
+ # become a transaction participant (again) we just want to
+ # return the transaction's response.
+ idempotent_mutation = await self._state_manager.check_for_idempotent_mutation(
+ context
+ )
+
+ if idempotent_mutation is not None:
+ response.ParseFromString(idempotent_mutation.response)
+
+ # We should have only scheduled a single task!
+ assert len(idempotent_mutation.task_ids) == 1
+ assert grpc_context is not None
+ grpc_context.set_trailing_metadata(
+ grpc_context.trailing_metadata() +
+ (
+ (
+ IMPORT_reboot_aio_headers.TASK_ID_UUID,
+ str(IMPORT_uuid.UUID(bytes=idempotent_mutation.task_ids[0].task_uuid))
+ ),
+ )
+ )
+
+ return context, response
+
+ async with self._state_manager.transactionally(
+ context,
+ self.tasks_dispatcher,
+ aborted_type=Pong.DoPongAborted,
+ ) as transaction:
+
+ async with self._state_manager.writer(
+ context,
+ self._servicer.__state_type__,
+ self.tasks_dispatcher,
+ transaction=transaction,
+ authorize=self._maybe_authorize(
+ method_name='reboot.ping.PongMethods.DoPong',
+ headers=context._headers,
+ auth=context.auth,
+ request=request,
+ ),
+ from_constructor=False,
+ requires_constructor=False
+ ) as (state, writer):
+
+ task = await PongServicerTasks(
+ context=context,
+ state_ref=context._state_ref,
+ ).DoPong(
+ schedule=context._headers.task_schedule,
+ )
+
+ effects = IMPORT_reboot_aio_state_managers.Effects(
+ response=response,
+ state=state,
+ tasks=[task],
+ )
+
+ assert effects.tasks is not None
+
+ await writer.complete(effects)
+
+ assert grpc_context is not None
+
+ grpc_context.set_trailing_metadata(
+ grpc_context.trailing_metadata() +
+ (
+ (
+ IMPORT_reboot_aio_headers.TASK_ID_UUID,
+ str(IMPORT_uuid.UUID(bytes=task.task_id.task_uuid))
+ ),
+ )
+ )
+
+ return context, response
+
+ return context, response
+
+
+ # Entrypoint for non-reactive network calls (i.e. typical gRPC calls).
+ async def DoPong(
+ self,
+ request: google.protobuf.empty_pb2.Empty,
+ grpc_context: IMPORT_grpc.aio.ServicerContext,
+ ) -> reboot.ping.ping_api_pb2.PongDoPongResponse:
+ headers = IMPORT_reboot_aio_headers.Headers.from_grpc_context(grpc_context)
+ assert headers.application_id is not None # Guaranteed by `Headers`.
+
+ # Confirm whether this is the right server to be serving this
+ # request.
+ try:
+ authoritative_server = self.placement_client.server_for_actor(
+ headers.application_id,
+ headers.state_ref,
+ )
+ except IMPORT_reboot_aio_placement.UnknownApplicationError:
+ # It's possible that the user did indeed type an application ID
+ # that doesn't exist, but it's also quite possible that this
+ # request reached us before the placement planner had gossipped
+ # out the information about which applications exist (we see
+ # this e.g. after `rbt dev`'s chaos monkey restarts). For that
+ # reason, abort with a retryable error.
+ await grpc_context.abort(
+ IMPORT_grpc.StatusCode.UNAVAILABLE,
+ f"Application '{headers.application_id}' not found. If you "
+ "are confident the application exists, this may be because "
+ "the system is still starting.",
+ )
+ raise # Unreachable but necessary for mypy.
+ if authoritative_server != self.server_id:
+ # This is NOT the correct server. Fail.
+ await grpc_context.abort(
+ IMPORT_grpc.StatusCode.UNAVAILABLE,
+ f"Server '{self.server_id}' is not authoritative for this "
+ f"request; server '{authoritative_server}' is.",
+ )
+ raise # Unreachable but necessary for mypy.
+
+ @IMPORT_reboot_aio_internals_middleware.maybe_run_function_twice_to_validate_effects
+ async def _run(
+ validating_effects: bool,
+ ) -> reboot.ping.ping_api_pb2.PongDoPongResponse:
+ context: IMPORT_typing.Optional[IMPORT_reboot_aio_contexts.Context] = None
+ try:
+ if headers.task_schedule is not None:
+ context, response = await self._schedule_DoPong(
+ headers=headers,
+ request=request,
+ grpc_context=grpc_context,
+ )
+ return response
+
+ context = self.create_context(
+ headers=headers,
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Pong'),
+ method='DoPong',
+ context_type=IMPORT_reboot_aio_contexts.WriterContext,
+ )
+ assert context is not None
+
+ return await self._DoPong(
+ request,
+ context,
+ validating_effects=validating_effects,
+ grpc_context=grpc_context,
+ )
+ except IMPORT_reboot_aio_contexts.EffectValidationRetry:
+ # Doing effect validation, just let this propagate.
+ raise
+ except IMPORT_reboot.aio.aborted.Aborted as aborted:
+ status = IMPORT_rpc_status_sync.to_status(aborted.to_status())
+ # Need to add transaction participants here because
+ # calling `grpc_context.abort_with_status()` will
+ # ignore any other trailing metadata. Only propagate
+ # transaction participants metadata if the caller cares.
+ # Callers that care are those that are themselves transactions.
+ # It's important to not just send this information to everyone;
+ # some clients can't tolerate trailers, see:
+ # https://github.com/reboot-dev/mono/issues/5081
+ if context is not None and headers.transaction_ids is not None:
+ assert context.transaction_id is not None
+ status = status._replace(
+ trailing_metadata=status.trailing_metadata + context.participants.to_grpc_metadata(
+ read_only_aware=headers.coordinator_read_only_aware,
+ )
+ )
+ await grpc_context.abort_with_status(status)
+ raise # Unreachable but necessary for mypy.
+ except IMPORT_asyncio.CancelledError:
+ # It's pretty normal for an RPC to be cancelled; it's not useful to
+ # print a stack trace.
+ raise
+ except BaseException as exception:
+ # Print the exception stack trace for easier debugging. Note
+ # that we don't include the stack trace in an error message
+ # for the same reason that gRPC doesn't do so by default,
+ # see https://github.com/grpc/grpc/issues/14897, but since this
+ # should only get logged on the server side it is safe.
+ logger.warning(
+ 'Unhandled exception\n' +
+ ''.join(IMPORT_traceback.format_exc() if IMPORT_reboot_nodejs_python.should_print_stacktrace() else [f"{type(exception).__name__}: {exception}"])
+ )
+
+ # Re-raise the exception for gRPC to handle!
+ #
+ # TODO: gRPC will print a stack trace from this
+ # exception which we don't want if we're executing via
+ # Node.js.
+ raise
+ finally:
+ # Propagate transaction participants, if the caller cares.
+ # Callers that care are those that are themselves transactions.
+ # It's important to not just send this information to everyone;
+ # some clients can't tolerate trailers, see:
+ # https://github.com/reboot-dev/mono/issues/5081
+ if context is not None and headers.transaction_ids is not None:
+ assert context.transaction_id is not None
+ grpc_context.set_trailing_metadata(
+ grpc_context.trailing_metadata() +
+ context.participants.to_grpc_metadata(
+ read_only_aware=headers.coordinator_read_only_aware,
+ )
+ )
+
+ with IMPORT_reboot_aio_tracing.context_from_headers(headers):
+ return await _run()
+
+ async def __NumPongs(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: reboot.ping.ping_api_pb2.Pong,
+ request: google.protobuf.empty_pb2.Empty,
+ *,
+ validating_effects: bool,
+ ) -> reboot.ping.ping_api_pb2.PongNumPongsResponse:
+ try:
+ typed_state: Pong.State = PongFromProto(state, is_initial_state=(context.state_id in states_being_constructed))
+
+ if ongoing_transaction_state_key(context) in ongoing_transaction_states:
+ typed_state = ongoing_transaction_states[ongoing_transaction_state_key(context)].model_copy(deep=True)
+ response = (
+ await self._servicer._NumPongs(
+ context=context,
+ state=typed_state,
+ request=request
+ )
+ )
+
+
+ PongToProto(typed_state, state)
+
+ IMPORT_reboot_aio_types.assert_type(
+ response,
+ [reboot.ping.ping_api_pb2.PongNumPongsResponse],
+ )
+ self.maybe_raise_effect_validation_retry(
+ logger=logger,
+ idempotency_manager=context,
+ method_name='Pong.NumPongs',
+ validating_effects=validating_effects,
+ context=context,
+ )
+ return response
+ except IMPORT_reboot_aio_contexts.EffectValidationRetry:
+ # Doing effect validation, just let this propagate.
+ raise
+ except IMPORT_reboot.aio.aborted.Aborted as aborted:
+ # If the caller aborted due to a retryable error, just
+ # propagate the aborted instead of propagating `Unknown`
+ # so that a client can transparently retry.
+ if IMPORT_reboot.aio.aborted.is_retryable(aborted):
+ raise aborted
+ # Log any _unhandled_ abort stack traces to make it
+ # easier for debugging.
+ #
+ # NOTE: we don't log if we're a task as it will be logged
+ # in `public/reboot/aio/internals/tasks_dispatcher.py` instead.
+ aborted_type: IMPORT_typing.Optional[type] = None
+ aborted_type = Pong.NumPongsAborted
+ if isinstance(aborted, IMPORT_reboot.aio.aborted.SystemAborted):
+ # Not logging when within `node` as we already log there.
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ f"Unhandled (in 'reboot.ping.Pong.NumPongs') {aborted}; propagating as 'Unknown'\n" +
+ ''.join(IMPORT_traceback.format_exception(aborted))
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ # TODO(benh): consider whether or not we want to
+ # include the 'package.service.method' which may
+ # get concatenated together forming a kind of
+ # "stack trace"; while it's super helpful for
+ # debugging, it does expose implementation
+ # information.
+ message=f"unhandled (in 'reboot.ping.Pong.NumPongs') {aborted}"
+ )
+ else:
+ if (
+ aborted_type is not None and
+ not isinstance(aborted, aborted_type) and
+ aborted_type.is_declared_error(aborted.error)
+ ):
+ # We propagate declared errors that might have
+ # come from another call, i.e., we might have an
+ # `Aborted` but not for this method but the
+ # `Aborted` that we have has an error that this
+ # method declared. This allows a developer to
+ # simply add the declared error to their `.proto`
+ # file rather than having to catch and re-raise
+ # the error with their own aborted type.
+ if context.task is None:
+ logger.warning(
+ f"Propagating unhandled but declared error (in 'reboot.ping.Pong.NumPongs') {aborted}"
+ )
+ elif (
+ aborted_type is None or
+ not isinstance(aborted, aborted_type)
+ ):
+ # Not logging when within `node` as we already log there.
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ f"Unhandled (in 'reboot.ping.Pong.NumPongs') {aborted}; propagating as 'Unknown'\n" +
+ ''.join(IMPORT_traceback.format_exception(aborted))
+ )
+ # If this wasn't a declared error than we
+ # propagate it as `Unknown`.
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ # TODO(benh): consider whether or not we want to
+ # include the 'package.service.method' which may
+ # get concatenated together forming a kind of
+ # "stack trace"; while it's super helpful for
+ # debugging, it does expose implementation
+ # information.
+ message=f"unhandled (in 'reboot.ping.Pong.NumPongs') {aborted}"
+ )
+
+ raise
+ except IMPORT_asyncio.CancelledError:
+ # It's pretty normal for an RPC to be cancelled; it's not useful to
+ # print a stack trace.
+ raise
+ except IMPORT_google_protobuf_message.DecodeError as decode_error:
+ # We usually see this error when we are trying to construct a proto
+ # message which is too deeply nested: protobuf has a limit of 100
+ # nested messages. See the limits here:
+ # https://protobuf.dev/programming-guides/proto-limits/
+
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ "Unhandled (in 'reboot.ping.Pong.NumPongs') "
+ f"{type(decode_error).__name__}{': ' + str(decode_error) if len(str(decode_error)) > 0 else ''}; "
+ "This is usually caused by a deeply nested protobuf message, which is not supported by protobuf.\n"
+ "See the limits here: https://protobuf.dev/programming-guides/proto-limits/" +
+ ''.join(IMPORT_traceback.format_exception(decode_error))
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ message=f"unhandled (in 'reboot.ping.Pong.NumPongs') {decode_error}; "
+ "This is usually caused by a deeply nested protobuf message, which is not supported by protobuf.\n"
+ "See the limits here: https://protobuf.dev/programming-guides/proto-limits/"
+ )
+ except BaseException as exception:
+ # Not logging when within `node` as we already log there.
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ "Unhandled (in 'reboot.ping.Pong.NumPongs') "
+ f"{type(exception).__name__}{': ' + str(exception) if len(str(exception)) > 0 else ''}; "
+ "propagating as 'Unknown'\n" +
+ ''.join(IMPORT_traceback.format_exception(exception))
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ # TODO(benh): consider whether or not we want to
+ # include the 'package.service.method' which may
+ # get concatenated together forming a kind of
+ # "stack trace"; while it's super helpful for
+ # debugging, it does expose implementation
+ # information.
+ message=f"unhandled (in 'reboot.ping.Pong.NumPongs') {type(exception).__name__}: {exception}"
+ )
+ finally:
+ pass
+
+ @IMPORT_reboot_aio_tracing.function_span(
+ # We expect an `EffectValidationRetry` exception; that's not an error.
+ set_status_on_exception=False
+ )
+ async def _NumPongs(
+ self,
+ request: google.protobuf.empty_pb2.Empty,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ *,
+ validating_effects: bool,
+ grpc_context: IMPORT_typing.Optional[IMPORT_grpc.aio.ServicerContext] = None,
+ ) -> reboot.ping.ping_api_pb2.PongNumPongsResponse:
+ # Try to verify the token if a token verifier exists.
+ context.auth = await self._maybe_verify_token(
+ headers=context._headers, method='NumPongs'
+ )
+
+ # Try and "preload" to opportunistically load both state and
+ # idempotent mutations from the database in a single
+ # round-trip. This is fire-and-forget; subsequent calls to
+ # `_load()` and `check_for_idempotent_mutation()` should find
+ # the preloaded data and skip their own calls to the database.
+ #
+ # TODO: a tradeoff we're making here is that we load
+ # idempotent mutations even if this is just a reader and we
+ # don't need them. Consider only preloading for writers and
+ # transactions if loading idempotent mutations is
+ # unnecessarily slow (and we haven't optimized that via a
+ # database stored bloom filter).
+ #
+ # We do this _after_ verifying the token so it is not a
+ # denial-of-service attack vector, i.e., only callers with
+ # valid tokens may trigger preloads on the database.
+ self._state_manager.preload(
+ context.state_type_name,
+ context._state_ref,
+ )
+
+
+ async with self._state_manager.transactionally(
+ context,
+ self.tasks_dispatcher,
+ aborted_type=Pong.NumPongsAborted,
+ ) as transaction:
+ authorizer = self._maybe_authorize(
+ method_name='reboot.ping.PongMethods.NumPongs',
+ headers=context._headers,
+ auth=context.auth,
+ request=request,
+ )
+ async with self._state_manager.reader(
+ context,
+ self._servicer.__state_type__,
+ authorize=authorizer,
+ ) as state:
+ response = await self.__NumPongs(
+ context,
+ state,
+ request,
+ validating_effects=validating_effects,
+ )
+ return response
+
+ async def _schedule_NumPongs(
+ self,
+ *,
+ request: google.protobuf.empty_pb2.Empty,
+ headers: IMPORT_reboot_aio_headers.Headers,
+ grpc_context: IMPORT_grpc.aio.ServicerContext,
+ ) -> tuple[IMPORT_reboot_aio_contexts.WriterContext, reboot.ping.ping_api_pb2.PongNumPongsResponse]:
+ context: IMPORT_reboot_aio_contexts.WriterContext = self.create_context(
+ headers=headers,
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Pong'),
+ method='NumPongs',
+ context_type=IMPORT_reboot_aio_contexts.WriterContext,
+ )
+ response = reboot.ping.ping_api_pb2.PongNumPongsResponse()
+
+ # Try to verify the token if a token verifier exists.
+ context.auth = await self._maybe_verify_token(
+ headers=headers, method='NumPongs'
+ )
+
+ # Try and "preload" to opportunistically load both state and
+ # idempotent mutations from the database in a single
+ # round-trip. This is fire-and-forget; subsequent calls to
+ # `_load()` and `check_for_idempotent_mutation()` should find
+ # the preloaded data and skip their own calls to the database.
+ #
+ # TODO: a tradeoff we're making here is that we load
+ # idempotent mutations even if this is just a reader and we
+ # don't need them. Consider only preloading for writers and
+ # transactions if loading idempotent mutations is
+ # unnecessarily slow (and we haven't optimized that via a
+ # database stored bloom filter).
+ #
+ # We do this _after_ verifying the token so it is not a
+ # denial-of-service attack vector, i.e., only callers with
+ # valid tokens may trigger preloads on the database.
+ self._state_manager.preload(
+ context.state_type_name,
+ context._state_ref,
+ )
+
+ # Check if we already have performed this schedule! Note that
+ # we need to do this for all kinds of methods because this is
+ # effectively a mutation (actually a `writer`, see below).
+ #
+ # We do this _before_ calling 'transactionally()' because
+ # if this call is for a transaction method _and_ we've
+ # already performed the transaction then we don't want to
+ # become a transaction participant (again) we just want to
+ # return the transaction's response.
+ idempotent_mutation = await self._state_manager.check_for_idempotent_mutation(
+ context
+ )
+
+ if idempotent_mutation is not None:
+ response.ParseFromString(idempotent_mutation.response)
+
+ # We should have only scheduled a single task!
+ assert len(idempotent_mutation.task_ids) == 1
+ assert grpc_context is not None
+ grpc_context.set_trailing_metadata(
+ grpc_context.trailing_metadata() +
+ (
+ (
+ IMPORT_reboot_aio_headers.TASK_ID_UUID,
+ str(IMPORT_uuid.UUID(bytes=idempotent_mutation.task_ids[0].task_uuid))
+ ),
+ )
+ )
+
+ return context, response
+
+ async with self._state_manager.transactionally(
+ context,
+ self.tasks_dispatcher,
+ aborted_type=Pong.NumPongsAborted,
+ ) as transaction:
+
+ async with self._state_manager.writer(
+ context,
+ self._servicer.__state_type__,
+ self.tasks_dispatcher,
+ transaction=transaction,
+ authorize=self._maybe_authorize(
+ method_name='reboot.ping.PongMethods.NumPongs',
+ headers=context._headers,
+ auth=context.auth,
+ request=request,
+ ),
+ from_constructor=False,
+ requires_constructor=False
+ ) as (state, writer):
+
+ task = await PongServicerTasks(
+ context=context,
+ state_ref=context._state_ref,
+ ).NumPongs(
+ schedule=context._headers.task_schedule,
+ )
+
+ effects = IMPORT_reboot_aio_state_managers.Effects(
+ response=response,
+ state=state,
+ tasks=[task],
+ )
+
+ assert effects.tasks is not None
+
+ await writer.complete(effects)
+
+ assert grpc_context is not None
+
+ grpc_context.set_trailing_metadata(
+ grpc_context.trailing_metadata() +
+ (
+ (
+ IMPORT_reboot_aio_headers.TASK_ID_UUID,
+ str(IMPORT_uuid.UUID(bytes=task.task_id.task_uuid))
+ ),
+ )
+ )
+
+ return context, response
+
+ return context, response
+
+
+ # Entrypoint for non-reactive network calls (i.e. typical gRPC calls).
+ async def NumPongs(
+ self,
+ request: google.protobuf.empty_pb2.Empty,
+ grpc_context: IMPORT_grpc.aio.ServicerContext,
+ ) -> reboot.ping.ping_api_pb2.PongNumPongsResponse:
+ headers = IMPORT_reboot_aio_headers.Headers.from_grpc_context(grpc_context)
+ assert headers.application_id is not None # Guaranteed by `Headers`.
+
+ # Confirm whether this is the right server to be serving this
+ # request.
+ try:
+ authoritative_server = self.placement_client.server_for_actor(
+ headers.application_id,
+ headers.state_ref,
+ )
+ except IMPORT_reboot_aio_placement.UnknownApplicationError:
+ # It's possible that the user did indeed type an application ID
+ # that doesn't exist, but it's also quite possible that this
+ # request reached us before the placement planner had gossipped
+ # out the information about which applications exist (we see
+ # this e.g. after `rbt dev`'s chaos monkey restarts). For that
+ # reason, abort with a retryable error.
+ await grpc_context.abort(
+ IMPORT_grpc.StatusCode.UNAVAILABLE,
+ f"Application '{headers.application_id}' not found. If you "
+ "are confident the application exists, this may be because "
+ "the system is still starting.",
+ )
+ raise # Unreachable but necessary for mypy.
+ if authoritative_server != self.server_id:
+ # This is NOT the correct server. Fail.
+ await grpc_context.abort(
+ IMPORT_grpc.StatusCode.UNAVAILABLE,
+ f"Server '{self.server_id}' is not authoritative for this "
+ f"request; server '{authoritative_server}' is.",
+ )
+ raise # Unreachable but necessary for mypy.
+
+ @IMPORT_reboot_aio_internals_middleware.maybe_run_function_twice_to_validate_effects
+ async def _run(
+ validating_effects: bool,
+ ) -> reboot.ping.ping_api_pb2.PongNumPongsResponse:
+ context: IMPORT_typing.Optional[IMPORT_reboot_aio_contexts.Context] = None
+ try:
+ if headers.task_schedule is not None:
+ context, response = await self._schedule_NumPongs(
+ headers=headers,
+ request=request,
+ grpc_context=grpc_context,
+ )
+ return response
+
+ context = self.create_context(
+ headers=headers,
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Pong'),
+ method='NumPongs',
+ context_type=IMPORT_reboot_aio_contexts.ReaderContext,
+ )
+ assert context is not None
+
+ return await self._NumPongs(
+ request,
+ context,
+ validating_effects=validating_effects,
+ grpc_context=grpc_context,
+ )
+ except IMPORT_reboot_aio_contexts.EffectValidationRetry:
+ # Doing effect validation, just let this propagate.
+ raise
+ except IMPORT_reboot.aio.aborted.Aborted as aborted:
+ status = IMPORT_rpc_status_sync.to_status(aborted.to_status())
+ # Need to add transaction participants here because
+ # calling `grpc_context.abort_with_status()` will
+ # ignore any other trailing metadata. Only propagate
+ # transaction participants metadata if the caller cares.
+ # Callers that care are those that are themselves transactions.
+ # It's important to not just send this information to everyone;
+ # some clients can't tolerate trailers, see:
+ # https://github.com/reboot-dev/mono/issues/5081
+ if context is not None and headers.transaction_ids is not None:
+ assert context.transaction_id is not None
+ status = status._replace(
+ trailing_metadata=status.trailing_metadata + context.participants.to_grpc_metadata(
+ read_only_aware=headers.coordinator_read_only_aware,
+ )
+ )
+ await grpc_context.abort_with_status(status)
+ raise # Unreachable but necessary for mypy.
+ except IMPORT_asyncio.CancelledError:
+ # It's pretty normal for an RPC to be cancelled; it's not useful to
+ # print a stack trace.
+ raise
+ except BaseException as exception:
+ # Print the exception stack trace for easier debugging. Note
+ # that we don't include the stack trace in an error message
+ # for the same reason that gRPC doesn't do so by default,
+ # see https://github.com/grpc/grpc/issues/14897, but since this
+ # should only get logged on the server side it is safe.
+ logger.warning(
+ 'Unhandled exception\n' +
+ ''.join(IMPORT_traceback.format_exc() if IMPORT_reboot_nodejs_python.should_print_stacktrace() else [f"{type(exception).__name__}: {exception}"])
+ )
+
+ # Re-raise the exception for gRPC to handle!
+ #
+ # TODO: gRPC will print a stack trace from this
+ # exception which we don't want if we're executing via
+ # Node.js.
+ raise
+ finally:
+ # Propagate transaction participants, if the caller cares.
+ # Callers that care are those that are themselves transactions.
+ # It's important to not just send this information to everyone;
+ # some clients can't tolerate trailers, see:
+ # https://github.com/reboot-dev/mono/issues/5081
+ if context is not None and headers.transaction_ids is not None:
+ assert context.transaction_id is not None
+ grpc_context.set_trailing_metadata(
+ grpc_context.trailing_metadata() +
+ context.participants.to_grpc_metadata(
+ read_only_aware=headers.coordinator_read_only_aware,
+ )
+ )
+
+ with IMPORT_reboot_aio_tracing.context_from_headers(headers):
+ return await _run()
+
+ def _maybe_authorize(
+ self,
+ *,
+ method_name: str,
+ headers: IMPORT_reboot_aio_headers.Headers,
+ auth: IMPORT_typing.Optional[IMPORT_reboot_aio_auth.Auth],
+ request: IMPORT_typing.Optional[PongRequestTypes] = None,
+ ) -> IMPORT_typing.Optional[IMPORT_typing.Callable[[IMPORT_typing.Optional[PongStateType]], IMPORT_typing.Awaitable[None]]]:
+ """Returns a function to check authorization for the given method.
+
+ Raises `PermissionDenied` in case Authorizer is present but the request
+ is not authorized.
+ """
+ # To authorize internal calls, we use an internal magic token.
+ if headers.bearer_token == __internal_magic_token__:
+ return None
+
+ assert self._authorizer is not None
+
+ async def authorize(state: IMPORT_typing.Optional[PongStateType]) -> None:
+ # Create context for the authorizer. This is a `ReaderContext`
+ # independently of the calling context.
+ with self.use_context(
+ headers=(
+ # Get headers suitable for doing authorization.
+ headers.copy_for_token_verification_and_authorization()
+ ),
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Pong'),
+ method=method_name,
+ context_type=IMPORT_reboot_aio_contexts.ReaderContext,
+ ) as context:
+ context.auth = auth
+
+ # Get the authorizer decision.
+ authorization_decision = await self._authorizer.authorize(
+ method_name=method_name,
+ context=context,
+ state=state,
+ request=request,
+ )
+
+ # Enforce correct authorizer decision type.
+ try:
+ IMPORT_reboot_aio_types.assert_type(
+ authorization_decision,
+ [
+ IMPORT_rbt_v1alpha1.errors_pb2.Ok,
+ IMPORT_rbt_v1alpha1.errors_pb2.Unauthenticated,
+ IMPORT_rbt_v1alpha1.errors_pb2.PermissionDenied,
+ ]
+ )
+ except TypeError as e:
+ # Retyping.cast the exception to provide more context.
+ authorizer_type = f"{type(self._authorizer).__module__}.{type(self._authorizer).__name__}"
+ raise TypeError(
+ f"Authorizer '{authorizer_type}' "
+ f"returned unexpected type '{type(authorization_decision).__name__}' "
+ f"for method '{method_name}' on "
+ f"`reboot.ping.Pong('{headers.state_ref.id}')`"
+ ) from e
+
+ # If the decision is not `True`, raise a `SystemAborted` with either a
+ # `PermissionDenied` error (in case of `False`) or an `Unauthenticated`
+ # error.
+ if not isinstance(authorization_decision, IMPORT_rbt_v1alpha1.errors_pb2.Ok):
+ if isinstance(authorization_decision, IMPORT_rbt_v1alpha1.errors_pb2.Unauthenticated):
+ logger.warning(
+ f"Unauthenticated call to '{method_name}' on "
+ f"`reboot.ping.Pong('{headers.state_ref.id}')`"
+ )
+
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ authorization_decision,
+ message=
+ f"You are not authorized to call '{method_name}' on "
+ f"`reboot.ping.Pong('{headers.state_ref.id}')`"
+ )
+
+ return authorize
+
+ async def _maybe_verify_token(
+ self,
+ *,
+ headers: IMPORT_reboot_aio_headers.Headers,
+ method: str,
+ ) -> IMPORT_typing.Optional[IMPORT_reboot_aio_auth.Auth]:
+ """Verify the bearer token and if a token verifier is present.
+
+ Returns the (optional) `reboot.aio.auth.Auth` object
+ produced by the token verifier if the token can be verified.
+ """
+ if self._token_verifier is not None:
+ if headers.bearer_token == __internal_magic_token__:
+ return None
+
+ with self.use_context(
+ headers=(
+ # Get headers suitable for doing token verification.
+ headers.copy_for_token_verification_and_authorization()
+ ),
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Pong'),
+ method=method,
+ context_type=IMPORT_reboot_aio_contexts.ReaderContext,
+ ) as context:
+ result = await self._token_verifier.verify_token(
+ context=context,
+ token=headers.bearer_token,
+ )
+ if isinstance(result, IMPORT_rbt_v1alpha1.errors_pb2.Unauthenticated):
+ raise IMPORT_reboot_aio_aborted.SystemAborted(
+ result,
+ message=result.message or None,
+ )
+ return result
+
+ return None
+
+class UserServicerMiddleware(IMPORT_reboot_aio_internals_middleware.Middleware):
+
+ def __init__(
+ self,
+ *,
+ servicer: UserBaseServicer,
+ application_id: IMPORT_reboot_aio_types.ApplicationId,
+ server_id: IMPORT_reboot_aio_types.ServerId,
+ state_manager: IMPORT_reboot_aio_state_managers.StateManager,
+ placement_client: IMPORT_reboot_aio_placement.PlacementClient,
+ channel_manager: IMPORT_reboot_aio_internals_channel_manager._ChannelManager,
+ tasks_cache: IMPORT_reboot_aio_internals_tasks_cache.TasksCache,
+ token_verifier: IMPORT_typing.Optional[IMPORT_reboot_aio_auth_token_verifiers.TokenVerifier],
+ effect_validation: IMPORT_reboot_aio_contexts.EffectValidation,
+ ready: IMPORT_asyncio.Event,
+ ):
+ super().__init__(
+ application_id=application_id,
+ server_id=server_id,
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User'),
+ service_names = [
+ IMPORT_reboot_aio_types.ServiceName("reboot.ping.UserMethods"),
+ ],
+ placement_client=placement_client,
+ channel_manager=channel_manager,
+ effect_validation=effect_validation,
+ get_database_timestamp_ms=lambda: state_manager.latest_timestamp_ms,
+ )
+
+ self._servicer = servicer
+ self._state_manager = state_manager
+ self.tasks_dispatcher = IMPORT_reboot_aio_internals_tasks_dispatcher.TasksDispatcher(
+ application_id=application_id,
+ dispatch=self.dispatch,
+ tasks_cache=tasks_cache,
+ ready=ready,
+ complete_task=self._state_manager.complete_task,
+ )
+
+ # Store the type of each method's request so that stored requests can be
+ # deserialized into the correct type.
+ self.request_type_by_method_name: dict[str, type[IMPORT_google_protobuf_message.Message]] = {
+ 'CreateCounter': reboot.ping.ping_api_pb2.UserCreateCounterRequest,
+ 'ListCounters': google.protobuf.empty_pb2.Empty,
+ 'Whoami': google.protobuf.empty_pb2.Empty,
+ 'Create': google.protobuf.empty_pb2.Empty,
+ }
+
+ # Get authorizer, if any, converting from a rule if necessary.
+ def convert_authorizer_rule_if_necessary(
+ authorizer_or_rule: IMPORT_typing.Optional[
+ IMPORT_reboot_aio_auth_authorizers.Authorizer | IMPORT_reboot_aio_auth_authorizers.AuthorizerRule
+ ]
+ ) -> IMPORT_reboot_aio_auth_authorizers.Authorizer:
+
+ # If no authorizer or rule is provided, return the default
+ # authorizer which allows if app internal or allows if in
+ # dev mode (and logs some warnings to help the user
+ # realize where they are missing authorization).
+ if authorizer_or_rule is None:
+ return IMPORT_reboot_aio_auth_authorizers.DefaultAuthorizer(
+ 'User',
+ is_user_type=True,
+ )
+
+ if isinstance(authorizer_or_rule, IMPORT_reboot_aio_auth_authorizers.AuthorizerRule):
+ return UserAuthorizer(
+ _default=authorizer_or_rule
+ )
+
+ return authorizer_or_rule
+
+ self._authorizer = convert_authorizer_rule_if_necessary(
+ servicer.authorizer()
+ )
+
+ # Create token verifier.
+ self._token_verifier: IMPORT_typing.Optional[IMPORT_reboot_aio_auth_token_verifiers.TokenVerifier] = (
+ servicer.token_verifier() or token_verifier
+ )
+
+ # Since users specify errors as proto messages they can't raise them
+ # directly - to do so they have to use the `Aborted` wrapper, which will
+ # hold the original proto message. On errors we'll need to check whether
+ # such wrappers hold a proto message for a specified error, so we can
+ # avoid retrying tasks that complete with a specified error.
+ self._specified_errors_by_service_method_name: dict[str, list[str]] = {
+ }
+
+
+ def add_to_server(self, server: IMPORT_grpc.aio.Server) -> None:
+ reboot.ping.ping_api_pb2_grpc.add_UserMethodsServicer_to_server(
+ self, server
+ )
+
+ async def inspect(self, state_ref: IMPORT_reboot_aio_types.StateRef) -> IMPORT_typing.AsyncIterator[IMPORT_google_protobuf_message.Message]:
+ """Implementation of `Middleware.inspect()`."""
+ context = self.create_context(
+ headers=IMPORT_reboot_aio_headers.Headers(
+ application_id=self.application_id,
+ state_ref=state_ref,
+ ),
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User'),
+ method="inspect",
+ context_type=IMPORT_reboot_aio_contexts.ReaderContext,
+ )
+
+ async with self._state_manager.streaming_reader_idempotency_key(
+ context,
+ self._servicer.__state_type__,
+ authorize=None,
+ ) as states:
+ async for (state, idempotency_key) in states:
+ yield state
+
+ async def react_query(
+ self,
+ headers: IMPORT_reboot_aio_headers.Headers,
+ method: str,
+ request_bytes: bytes,
+ ) -> IMPORT_typing.AsyncIterator[tuple[IMPORT_typing.Optional[IMPORT_google_protobuf_message.Message], list[IMPORT_uuid.UUID]]]:
+ """Returns the response of calling 'method' given a message
+ deserialized from the provided 'request_bytes' for each state
+ update that creates a different response.
+
+ # The caller (react.py) should have already ensured that this server
+ # is authoritative for this traffic.
+ assert self.placement_client.server_for_actor(
+ headers.application_id,
+ headers.state_ref,
+ ) == self._server_id
+
+ NOTE: only unary reader methods are supported."""
+ # Need to define these up here since we can only do that once.
+ last_response: IMPORT_typing.Optional[IMPORT_google_protobuf_message.Message] = None
+ aggregated_idempotency_keys: list[IMPORT_uuid.UUID] = []
+ if method == 'CreateCounter':
+ # Invariant here is that users should not have called this
+ # directly but only through code generated React
+ # components which should not have been generated except
+ # for valid method candidates.
+ logger.warning(
+ "Got a React query request with an invalid method name: "
+ f"Method '{method}' is invalid for servicer User."
+ "\n"
+ "Do you have a browser tab open for an older version "
+ "of this application, or for a different application all together?"
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.InvalidMethod(),
+ message=
+ f"Method '{method}' is invalid"
+ )
+ yield # Necessary for type checking.
+ elif method == 'ListCounters':
+
+ context = self.create_context(
+ headers=headers,
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User'),
+ method='ListCounters',
+ context_type=IMPORT_reboot_aio_contexts.ReaderContext,
+ )
+
+ with IMPORT_reboot_aio_tracing.span(
+ state_name=f"{context.state_type_name}('{context.state_id}')",
+ span_name="ListCounters() (reactively)",
+ # The naming above matches Python, but not TypeScript.
+ python_specific=True,
+ level=IMPORT_reboot_aio_tracing.TraceLevel.CUSTOMER,
+ ):
+ context.auth = await self._maybe_verify_token(
+ headers=headers, method='ListCounters'
+ )
+
+ request = google.protobuf.empty_pb2.Empty()
+ request.ParseFromString(request_bytes)
+
+ async with self._state_manager.reactively(
+ context,
+ self._servicer.__state_type__,
+ authorize=self._maybe_authorize(
+ method_name='reboot.ping.UserMethods.ListCounters',
+ headers=headers,
+ auth=context.auth,
+ request=request,
+ ),
+ ) as states:
+ async for (state, idempotency_keys) in states:
+
+ aggregated_idempotency_keys.extend(idempotency_keys)
+
+ # Note: This does not do any defensive copying currently:
+ # see https://github.com/reboot-dev/respect/issues/2636.
+ @IMPORT_reboot_aio_internals_middleware.maybe_run_function_twice_to_validate_effects
+ async def run__ListCounters(validating_effects: bool) -> IMPORT_google_protobuf_message.Message:
+ return await self.__ListCounters(
+ context,
+ state,
+ request,
+ validating_effects=validating_effects,
+ )
+
+ response = await run__ListCounters()
+
+ if last_response != response:
+ yield (response, aggregated_idempotency_keys)
+ last_response = response
+ else:
+ yield (None, aggregated_idempotency_keys)
+
+ aggregated_idempotency_keys.clear()
+ elif method == 'Whoami':
+
+ context = self.create_context(
+ headers=headers,
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User'),
+ method='Whoami',
+ context_type=IMPORT_reboot_aio_contexts.ReaderContext,
+ )
+
+ with IMPORT_reboot_aio_tracing.span(
+ state_name=f"{context.state_type_name}('{context.state_id}')",
+ span_name="Whoami() (reactively)",
+ # The naming above matches Python, but not TypeScript.
+ python_specific=True,
+ level=IMPORT_reboot_aio_tracing.TraceLevel.CUSTOMER,
+ ):
+ context.auth = await self._maybe_verify_token(
+ headers=headers, method='Whoami'
+ )
+
+ request = google.protobuf.empty_pb2.Empty()
+ request.ParseFromString(request_bytes)
+
+ async with self._state_manager.reactively(
+ context,
+ self._servicer.__state_type__,
+ authorize=self._maybe_authorize(
+ method_name='reboot.ping.UserMethods.Whoami',
+ headers=headers,
+ auth=context.auth,
+ request=request,
+ ),
+ ) as states:
+ async for (state, idempotency_keys) in states:
+
+ aggregated_idempotency_keys.extend(idempotency_keys)
+
+ # Note: This does not do any defensive copying currently:
+ # see https://github.com/reboot-dev/respect/issues/2636.
+ @IMPORT_reboot_aio_internals_middleware.maybe_run_function_twice_to_validate_effects
+ async def run__Whoami(validating_effects: bool) -> IMPORT_google_protobuf_message.Message:
+ return await self.__Whoami(
+ context,
+ state,
+ request,
+ validating_effects=validating_effects,
+ )
+
+ response = await run__Whoami()
+
+ if last_response != response:
+ yield (response, aggregated_idempotency_keys)
+ last_response = response
+ else:
+ yield (None, aggregated_idempotency_keys)
+
+ aggregated_idempotency_keys.clear()
+ elif method == 'Create':
+ # Invariant here is that users should not have called this
+ # directly but only through code generated React
+ # components which should not have been generated except
+ # for valid method candidates.
+ logger.warning(
+ "Got a React query request with an invalid method name: "
+ f"Method '{method}' is invalid for servicer User."
+ "\n"
+ "Do you have a browser tab open for an older version "
+ "of this application, or for a different application all together?"
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.InvalidMethod(),
+ message=
+ f"Method '{method}' is invalid"
+ )
+ yield # Necessary for type checking.
+ else:
+ logger.warning(
+ "Got a React query request with an invalid method name: "
+ f"Method '{method}' is invalid for servicer User."
+ "\n"
+ "Do you have a browser tab open for an older version "
+ "of this application, or for a different application all together?"
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.InvalidMethod(),
+ message=
+ f"Method '{method}' not found"
+ )
+ yield # Unreachable but necessary for mypy.
+
+ async def react_mutate(
+ self,
+ headers: IMPORT_reboot_aio_headers.Headers,
+ method: str,
+ request_bytes: bytes,
+ ) -> IMPORT_google_protobuf_message.Message:
+ """Returns the response of calling 'method' given a message
+ deserialized from the provided 'request_bytes'."""
+ if method == 'CreateCounter':
+ request = reboot.ping.ping_api_pb2.UserCreateCounterRequest()
+ request.ParseFromString(request_bytes)
+
+ # NOTE: we automatically retry mutations that come through
+ # React when we get a `IMPORT_grpc.StatusCode.UNAVAILABLE` to
+ # match the retry logic we do in the React code generated
+ # to handle lack/loss of connectivity.
+ #
+ # TODO(benh): revisit this decision if we ever see reason
+ # to call `react_mutate()` from any place other than where
+ # we're executing React (e.g., browser, next.js server
+ # component, etc).
+ call_backoff = IMPORT_reboot_aio_backoff.Backoff()
+ while True:
+ # We make a full-fledged gRPC call, so that if this traffic
+ # was misrouted (i.e. this server is not authoritative
+ # for the state), it will now go to the right place. The
+ # receiving middleware will handle things like effect
+ # validation and so forth.
+ assert headers.application_id is not None # Guaranteed by `Headers`.
+ stub = reboot.ping.ping_api_pb2_grpc.UserMethodsStub(
+ self.channel_manager.get_channel_to(
+ self.placement_client.address_for_actor(
+ headers.application_id,
+ headers.state_ref,
+ )
+ )
+ )
+ call = stub.CreateCounter(
+ request=request,
+ metadata=headers.to_grpc_metadata(),
+ )
+ try:
+ return await call
+ except IMPORT_grpc.aio.AioRpcError as error:
+ if error.code() == IMPORT_grpc.StatusCode.UNAVAILABLE:
+ await call_backoff()
+ continue
+
+ # Reconstitute the error that the server threw, if it was a declared error.
+ status = await IMPORT_rpc_status_async.from_call(call)
+ if status is not None:
+ raise User.CreateCounterAborted.from_status(
+ status
+ ) from None
+ raise User.CreateCounterAborted.from_grpc_aio_rpc_error(
+ error
+ ) from None
+
+ elif method == 'ListCounters':
+ # Invariant here is that users should not have called this
+ # directly but only through code generated React
+ # components which should not have been generated except
+ # for valid method candidates.
+ logger.warning(
+ "Got a react mutate request with an invalid method name: "
+ "Method 'ListCounters' is invalid for servicer User."
+ "\n"
+ "Do you have an old browser tab still open for an older version "
+ "of this application, or a different application all together?"
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.InvalidMethod(),
+ message=f"Method '{method}' is invalid"
+ )
+ elif method == 'Whoami':
+ # Invariant here is that users should not have called this
+ # directly but only through code generated React
+ # components which should not have been generated except
+ # for valid method candidates.
+ logger.warning(
+ "Got a react mutate request with an invalid method name: "
+ "Method 'Whoami' is invalid for servicer User."
+ "\n"
+ "Do you have an old browser tab still open for an older version "
+ "of this application, or a different application all together?"
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.InvalidMethod(),
+ message=f"Method '{method}' is invalid"
+ )
+ elif method == 'Create':
+ request = google.protobuf.empty_pb2.Empty()
+ request.ParseFromString(request_bytes)
+
+ # NOTE: we automatically retry mutations that come through
+ # React when we get a `IMPORT_grpc.StatusCode.UNAVAILABLE` to
+ # match the retry logic we do in the React code generated
+ # to handle lack/loss of connectivity.
+ #
+ # TODO(benh): revisit this decision if we ever see reason
+ # to call `react_mutate()` from any place other than where
+ # we're executing React (e.g., browser, next.js server
+ # component, etc).
+ call_backoff = IMPORT_reboot_aio_backoff.Backoff()
+ while True:
+ # We make a full-fledged gRPC call, so that if this traffic
+ # was misrouted (i.e. this server is not authoritative
+ # for the state), it will now go to the right place. The
+ # receiving middleware will handle things like effect
+ # validation and so forth.
+ assert headers.application_id is not None # Guaranteed by `Headers`.
+ stub = reboot.ping.ping_api_pb2_grpc.UserMethodsStub(
+ self.channel_manager.get_channel_to(
+ self.placement_client.address_for_actor(
+ headers.application_id,
+ headers.state_ref,
+ )
+ )
+ )
+ call = stub.Create(
+ request=request,
+ metadata=headers.to_grpc_metadata(),
+ )
+ try:
+ return await call
+ except IMPORT_grpc.aio.AioRpcError as error:
+ if error.code() == IMPORT_grpc.StatusCode.UNAVAILABLE:
+ await call_backoff()
+ continue
+
+ # Reconstitute the error that the server threw, if it was a declared error.
+ status = await IMPORT_rpc_status_async.from_call(call)
+ if status is not None:
+ raise User.CreateAborted.from_status(
+ status
+ ) from None
+ raise User.CreateAborted.from_grpc_aio_rpc_error(
+ error
+ ) from None
+
+ else:
+ logger.warning(
+ "Got a react mutate request with an invalid method name: "
+ f"Method '{method}' is invalid for servicer User."
+ "\n"
+ "Do you have an old browser tab still open for an older version "
+ "of this application, or a different application all together?"
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.InvalidMethod(),
+ message=
+ f"Method '{method}' not found"
+ )
+
+ async def dispatch(
+ self,
+ task: IMPORT_reboot_aio_tasks.TaskEffect,
+ *,
+ only_validate: bool = False,
+ on_loop_iteration: IMPORT_reboot_aio_internals_tasks_dispatcher.OnLoopIterationCallable = (lambda iteration, next_iteration_schedule: None),
+ ) -> IMPORT_reboot_aio_internals_tasks_dispatcher.TaskResponseOrStatus:
+ """Dispatches the tasks to execute unless 'only_validate' is set to
+ true, in which case just ensures that the task actually exists.
+ Note that this function will be called *by* tasks_dispatcher; it will
+ not itself call into tasks_dispatcher."""
+
+ if 'CreateCounter' == task.method_name:
+ if only_validate:
+ # TODO(benh): validate 'task.request' is correct type.
+ return (reboot.ping.ping_api_pb2.UserCreateCounterResponse(), None)
+
+ # Use an inline method to create a new scope, so that we can use
+ # variable names like `context` and `effects` in multiple branches
+ # in this code (notably when there are multiple task types) without
+ # hitting a mypy error that the variable's type is not consistent.
+ async def run_CreateCounter(
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ *,
+ validating_effects: bool = False,
+ ):
+ async with self._state_manager.task_workflow(
+ context,
+ task,
+ on_loop_iteration=on_loop_iteration,
+ validating_effects=validating_effects,
+ ) as complete:
+ try:
+ response = await (UserWorkflowStub(
+ context=context,
+ state_ref=context._state_ref,
+ ).CreateCounter(
+ UserCreateCounterRequestFromProto(IMPORT_typing.cast(reboot.ping.ping_api_pb2.UserCreateCounterRequest, task.request)),
+ bearer_token=__internal_magic_token__,
+ idempotency=IMPORT_reboot_aio_idempotency.Idempotency(
+ alias=f'Task {IMPORT_uuid.UUID(bytes=task.task_id.task_uuid)}',
+ ),
+ ))
+ await complete(task, (response, None))
+ return (response, None)
+ except IMPORT_asyncio.CancelledError:
+ # Do not retry a task if it was cancelled by a caller.
+ if self.tasks_dispatcher.is_task_cancelled(task.task_id.task_uuid):
+ result = (
+ None,
+ IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Cancelled(),
+ ).to_status(),
+ )
+ await complete(task, result)
+ return result
+ else:
+ raise
+ except IMPORT_reboot.aio.aborted.Aborted as aborted:
+ error_type = f'{aborted._protobuf_error.__class__.__module__}.{aborted._protobuf_error.__class__.__qualname__}'
+ # Do not retry a task if the error was specified in the
+ # proto file.
+ if error_type in self._specified_errors_by_service_method_name.get('reboot.ping.UserMethods.CreateCounter', []):
+ result = (None, aborted.to_status())
+ await complete(task, result)
+ return result
+ raise
+
+
+ return await run_CreateCounter(
+ self.create_context(
+ headers=IMPORT_reboot_aio_headers.Headers(
+ application_id=self.application_id,
+ state_ref=IMPORT_reboot_aio_types.StateRef(task.task_id.state_ref),
+ ),
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User'),
+ method='CreateCounter',
+ context_type=IMPORT_reboot_aio_contexts.WorkflowContext,
+ task=task,
+ # Propagate state manager and state type so that
+ # `until()` calls (via `WorkflowContext.retry_reactively_until()`)
+ # can enter `reactively()` scoped to each `until()`
+ # invocation.
+ reactively_state_manager=self._state_manager,
+ reactively_state_type=(
+ self._servicer.__state_type__
+ ),
+ )
+ )
+ elif 'ListCounters' == task.method_name:
+ if only_validate:
+ # TODO(benh): validate 'task.request' is correct type.
+ return (reboot.ping.ping_api_pb2.UserListCountersResponse(), None)
+
+ # Use an inline method to create a new scope, so that we can use
+ # variable names like `context` and `effects` in multiple branches
+ # in this code (notably when there are multiple task types) without
+ # hitting a mypy error that the variable's type is not consistent.
+ async def run_ListCounters(
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ *,
+ validating_effects: bool = False,
+ ):
+ async with self._state_manager.task_workflow(
+ context,
+ task,
+ on_loop_iteration=on_loop_iteration,
+ validating_effects=validating_effects,
+ ) as complete:
+ try:
+ response = await (UserWorkflowStub(
+ context=context,
+ state_ref=context._state_ref,
+ ).ListCounters(
+ bearer_token=__internal_magic_token__,
+ ))
+ await complete(task, (response, None))
+ return (response, None)
+ except IMPORT_asyncio.CancelledError:
+ # Do not retry a task if it was cancelled by a caller.
+ if self.tasks_dispatcher.is_task_cancelled(task.task_id.task_uuid):
+ result = (
+ None,
+ IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Cancelled(),
+ ).to_status(),
+ )
+ await complete(task, result)
+ return result
+ else:
+ raise
+ except IMPORT_reboot.aio.aborted.Aborted as aborted:
+ error_type = f'{aborted._protobuf_error.__class__.__module__}.{aborted._protobuf_error.__class__.__qualname__}'
+ # Do not retry a task if the error was specified in the
+ # proto file.
+ if error_type in self._specified_errors_by_service_method_name.get('reboot.ping.UserMethods.ListCounters', []):
+ result = (None, aborted.to_status())
+ await complete(task, result)
+ return result
+ raise
+
+
+ return await run_ListCounters(
+ self.create_context(
+ headers=IMPORT_reboot_aio_headers.Headers(
+ application_id=self.application_id,
+ state_ref=IMPORT_reboot_aio_types.StateRef(task.task_id.state_ref),
+ ),
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User'),
+ method='ListCounters',
+ context_type=IMPORT_reboot_aio_contexts.WorkflowContext,
+ task=task,
+ # Propagate state manager and state type so that
+ # `until()` calls (via `WorkflowContext.retry_reactively_until()`)
+ # can enter `reactively()` scoped to each `until()`
+ # invocation.
+ reactively_state_manager=self._state_manager,
+ reactively_state_type=(
+ self._servicer.__state_type__
+ ),
+ )
+ )
+ elif 'Whoami' == task.method_name:
+ if only_validate:
+ # TODO(benh): validate 'task.request' is correct type.
+ return (reboot.ping.ping_api_pb2.UserWhoamiResponse(), None)
+
+ # Use an inline method to create a new scope, so that we can use
+ # variable names like `context` and `effects` in multiple branches
+ # in this code (notably when there are multiple task types) without
+ # hitting a mypy error that the variable's type is not consistent.
+ async def run_Whoami(
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ *,
+ validating_effects: bool = False,
+ ):
+ async with self._state_manager.task_workflow(
+ context,
+ task,
+ on_loop_iteration=on_loop_iteration,
+ validating_effects=validating_effects,
+ ) as complete:
+ try:
+ response = await (UserWorkflowStub(
+ context=context,
+ state_ref=context._state_ref,
+ ).Whoami(
+ bearer_token=__internal_magic_token__,
+ ))
+ await complete(task, (response, None))
+ return (response, None)
+ except IMPORT_asyncio.CancelledError:
+ # Do not retry a task if it was cancelled by a caller.
+ if self.tasks_dispatcher.is_task_cancelled(task.task_id.task_uuid):
+ result = (
+ None,
+ IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Cancelled(),
+ ).to_status(),
+ )
+ await complete(task, result)
+ return result
+ else:
+ raise
+ except IMPORT_reboot.aio.aborted.Aborted as aborted:
+ error_type = f'{aborted._protobuf_error.__class__.__module__}.{aborted._protobuf_error.__class__.__qualname__}'
+ # Do not retry a task if the error was specified in the
+ # proto file.
+ if error_type in self._specified_errors_by_service_method_name.get('reboot.ping.UserMethods.Whoami', []):
+ result = (None, aborted.to_status())
+ await complete(task, result)
+ return result
+ raise
+
+
+ return await run_Whoami(
+ self.create_context(
+ headers=IMPORT_reboot_aio_headers.Headers(
+ application_id=self.application_id,
+ state_ref=IMPORT_reboot_aio_types.StateRef(task.task_id.state_ref),
+ ),
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User'),
+ method='Whoami',
+ context_type=IMPORT_reboot_aio_contexts.WorkflowContext,
+ task=task,
+ # Propagate state manager and state type so that
+ # `until()` calls (via `WorkflowContext.retry_reactively_until()`)
+ # can enter `reactively()` scoped to each `until()`
+ # invocation.
+ reactively_state_manager=self._state_manager,
+ reactively_state_type=(
+ self._servicer.__state_type__
+ ),
+ )
+ )
+ elif 'Create' == task.method_name:
+ if only_validate:
+ # TODO(benh): validate 'task.request' is correct type.
+ return (google.protobuf.empty_pb2.Empty(), None)
+
+ # Use an inline method to create a new scope, so that we can use
+ # variable names like `context` and `effects` in multiple branches
+ # in this code (notably when there are multiple task types) without
+ # hitting a mypy error that the variable's type is not consistent.
+ async def run_Create(
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ *,
+ validating_effects: bool = False,
+ ):
+ async with self._state_manager.task_workflow(
+ context,
+ task,
+ on_loop_iteration=on_loop_iteration,
+ validating_effects=validating_effects,
+ ) as complete:
+ try:
+ response = await (UserWorkflowStub(
+ context=context,
+ state_ref=context._state_ref,
+ ).Create(
+ bearer_token=__internal_magic_token__,
+ idempotency=IMPORT_reboot_aio_idempotency.Idempotency(
+ alias=f'Task {IMPORT_uuid.UUID(bytes=task.task_id.task_uuid)}',
+ ),
+ ))
+ await complete(task, (response, None))
+ return (response, None)
+ except IMPORT_asyncio.CancelledError:
+ # Do not retry a task if it was cancelled by a caller.
+ if self.tasks_dispatcher.is_task_cancelled(task.task_id.task_uuid):
+ result = (
+ None,
+ IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Cancelled(),
+ ).to_status(),
+ )
+ await complete(task, result)
+ return result
+ else:
+ raise
+ except IMPORT_reboot.aio.aborted.Aborted as aborted:
+ error_type = f'{aborted._protobuf_error.__class__.__module__}.{aborted._protobuf_error.__class__.__qualname__}'
+ # Do not retry a task if the error was specified in the
+ # proto file.
+ if error_type in self._specified_errors_by_service_method_name.get('reboot.ping.UserMethods.Create', []):
+ result = (None, aborted.to_status())
+ await complete(task, result)
+ return result
+ raise
+
+
+ return await run_Create(
+ self.create_context(
+ headers=IMPORT_reboot_aio_headers.Headers(
+ application_id=self.application_id,
+ state_ref=IMPORT_reboot_aio_types.StateRef(task.task_id.state_ref),
+ ),
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User'),
+ method='Create',
+ context_type=IMPORT_reboot_aio_contexts.WorkflowContext,
+ task=task,
+ # Propagate state manager and state type so that
+ # `until()` calls (via `WorkflowContext.retry_reactively_until()`)
+ # can enter `reactively()` scoped to each `until()`
+ # invocation.
+ reactively_state_manager=self._state_manager,
+ reactively_state_type=(
+ self._servicer.__state_type__
+ ),
+ )
+ )
+
+ # There are no tasks for this service.
+ start_or_validate = "start" if not only_validate else "validate"
+ raise RuntimeError(
+ f"Attempted to {start_or_validate} task '{task.method_name}' "
+ f"on 'User' which does not exist"
+ )
+
+ # User specific methods:
+ async def __CreateCounter(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext,
+ state: reboot.ping.ping_api_pb2.User,
+ request: reboot.ping.ping_api_pb2.UserCreateCounterRequest,
+ *,
+ validating_effects: bool,
+ ) -> reboot.ping.ping_api_pb2.UserCreateCounterResponse:
+ try:
+ typed_state: User.State = UserFromProto(state, is_initial_state=(context.state_id in states_being_constructed))
+
+ # TODO: assert that there are no ongoing transactions for this state.
+ #
+ # The `typed_state` should be already validated above, so we can
+ # just store it here.
+ ongoing_transaction_states[ongoing_transaction_state_key(context)] = typed_state
+ response = (
+ await self._servicer._CreateCounter(
+ context=context,
+ state=typed_state,
+ request=request
+ )
+ )
+
+
+ UserToProto(typed_state, state)
+
+ IMPORT_reboot_aio_types.assert_type(
+ response,
+ [reboot.ping.ping_api_pb2.UserCreateCounterResponse],
+ )
+ self.maybe_raise_effect_validation_retry(
+ logger=logger,
+ idempotency_manager=context,
+ method_name='User.CreateCounter',
+ validating_effects=validating_effects,
+ context=context,
+ )
+ return response
+ except IMPORT_reboot_aio_contexts.EffectValidationRetry:
+ # Doing effect validation, just let this propagate.
+ raise
+ except IMPORT_reboot.aio.aborted.Aborted as aborted:
+ # If the caller aborted due to a retryable error, just
+ # propagate the aborted instead of propagating `Unknown`
+ # so that a client can transparently retry.
+ if IMPORT_reboot.aio.aborted.is_retryable(aborted):
+ raise aborted
+ # Log any _unhandled_ abort stack traces to make it
+ # easier for debugging.
+ #
+ # NOTE: we don't log if we're a task as it will be logged
+ # in `public/reboot/aio/internals/tasks_dispatcher.py` instead.
+ aborted_type: IMPORT_typing.Optional[type] = None
+ aborted_type = User.CreateCounterAborted
+ if isinstance(aborted, IMPORT_reboot.aio.aborted.SystemAborted):
+ # Not logging when within `node` as we already log there.
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ f"Unhandled (in 'reboot.ping.User.CreateCounter') {aborted}; propagating as 'Unknown'\n" +
+ ''.join(IMPORT_traceback.format_exception(aborted))
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ # TODO(benh): consider whether or not we want to
+ # include the 'package.service.method' which may
+ # get concatenated together forming a kind of
+ # "stack trace"; while it's super helpful for
+ # debugging, it does expose implementation
+ # information.
+ message=f"unhandled (in 'reboot.ping.User.CreateCounter') {aborted}"
+ )
+ else:
+ if (
+ aborted_type is not None and
+ not isinstance(aborted, aborted_type) and
+ aborted_type.is_declared_error(aborted.error)
+ ):
+ # We propagate declared errors that might have
+ # come from another call, i.e., we might have an
+ # `Aborted` but not for this method but the
+ # `Aborted` that we have has an error that this
+ # method declared. This allows a developer to
+ # simply add the declared error to their `.proto`
+ # file rather than having to catch and re-raise
+ # the error with their own aborted type.
+ if context.task is None:
+ logger.warning(
+ f"Propagating unhandled but declared error (in 'reboot.ping.User.CreateCounter') {aborted}"
+ )
+ elif (
+ aborted_type is None or
+ not isinstance(aborted, aborted_type)
+ ):
+ # Not logging when within `node` as we already log there.
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ f"Unhandled (in 'reboot.ping.User.CreateCounter') {aborted}; propagating as 'Unknown'\n" +
+ ''.join(IMPORT_traceback.format_exception(aborted))
+ )
+ # If this wasn't a declared error than we
+ # propagate it as `Unknown`.
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ # TODO(benh): consider whether or not we want to
+ # include the 'package.service.method' which may
+ # get concatenated together forming a kind of
+ # "stack trace"; while it's super helpful for
+ # debugging, it does expose implementation
+ # information.
+ message=f"unhandled (in 'reboot.ping.User.CreateCounter') {aborted}"
+ )
+
+ raise
+ except IMPORT_asyncio.CancelledError:
+ # It's pretty normal for an RPC to be cancelled; it's not useful to
+ # print a stack trace.
+ raise
+ except IMPORT_google_protobuf_message.DecodeError as decode_error:
+ # We usually see this error when we are trying to construct a proto
+ # message which is too deeply nested: protobuf has a limit of 100
+ # nested messages. See the limits here:
+ # https://protobuf.dev/programming-guides/proto-limits/
+
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ "Unhandled (in 'reboot.ping.User.CreateCounter') "
+ f"{type(decode_error).__name__}{': ' + str(decode_error) if len(str(decode_error)) > 0 else ''}; "
+ "This is usually caused by a deeply nested protobuf message, which is not supported by protobuf.\n"
+ "See the limits here: https://protobuf.dev/programming-guides/proto-limits/" +
+ ''.join(IMPORT_traceback.format_exception(decode_error))
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ message=f"unhandled (in 'reboot.ping.User.CreateCounter') {decode_error}; "
+ "This is usually caused by a deeply nested protobuf message, which is not supported by protobuf.\n"
+ "See the limits here: https://protobuf.dev/programming-guides/proto-limits/"
+ )
+ except BaseException as exception:
+ # Not logging when within `node` as we already log there.
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ "Unhandled (in 'reboot.ping.User.CreateCounter') "
+ f"{type(exception).__name__}{': ' + str(exception) if len(str(exception)) > 0 else ''}; "
+ "propagating as 'Unknown'\n" +
+ ''.join(IMPORT_traceback.format_exception(exception))
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ # TODO(benh): consider whether or not we want to
+ # include the 'package.service.method' which may
+ # get concatenated together forming a kind of
+ # "stack trace"; while it's super helpful for
+ # debugging, it does expose implementation
+ # information.
+ message=f"unhandled (in 'reboot.ping.User.CreateCounter') {type(exception).__name__}: {exception}"
+ )
+ finally:
+ if ongoing_transaction_state_key(context) in ongoing_transaction_states:
+ del ongoing_transaction_states[ongoing_transaction_state_key(context)]
+
+ @IMPORT_reboot_aio_tracing.function_span(
+ # We expect an `EffectValidationRetry` exception; that's not an error.
+ set_status_on_exception=False
+ )
+ async def _CreateCounter(
+ self,
+ request: reboot.ping.ping_api_pb2.UserCreateCounterRequest,
+ context: IMPORT_reboot_aio_contexts.TransactionContext,
+ *,
+ validating_effects: bool,
+ grpc_context: IMPORT_typing.Optional[IMPORT_grpc.aio.ServicerContext] = None,
+ ) -> reboot.ping.ping_api_pb2.UserCreateCounterResponse:
+ # Try to verify the token if a token verifier exists.
+ context.auth = await self._maybe_verify_token(
+ headers=context._headers, method='CreateCounter'
+ )
+
+ # Try and "preload" to opportunistically load both state and
+ # idempotent mutations from the database in a single
+ # round-trip. This is fire-and-forget; subsequent calls to
+ # `_load()` and `check_for_idempotent_mutation()` should find
+ # the preloaded data and skip their own calls to the database.
+ #
+ # TODO: a tradeoff we're making here is that we load
+ # idempotent mutations even if this is just a reader and we
+ # don't need them. Consider only preloading for writers and
+ # transactions if loading idempotent mutations is
+ # unnecessarily slow (and we haven't optimized that via a
+ # database stored bloom filter).
+ #
+ # We do this _after_ verifying the token so it is not a
+ # denial-of-service attack vector, i.e., only callers with
+ # valid tokens may trigger preloads on the database.
+ self._state_manager.preload(
+ context.state_type_name,
+ context._state_ref,
+ )
+
+ # Check if we already have performed this mutation!
+ #
+ # We do this _before_ calling 'transactionally()' because
+ # if this call is for a transaction method _and_ we've
+ # already performed the transaction then we don't want to
+ # become a transaction participant (again) we just want to
+ # return the transaction's response.
+ idempotent_mutation = await self._state_manager.check_for_idempotent_mutation(
+ context
+ )
+
+ if idempotent_mutation is not None:
+ response = reboot.ping.ping_api_pb2.UserCreateCounterResponse()
+ response.ParseFromString(idempotent_mutation.response)
+ return response
+
+ async with self._state_manager.transactionally(
+ context,
+ self.tasks_dispatcher,
+ aborted_type=User.CreateCounterAborted,
+ ) as transaction:
+ assert transaction is not None
+ async with self._state_manager.transaction(
+ context,
+ self._servicer.__state_type__,
+ transaction,
+ authorize=self._maybe_authorize(
+ method_name='reboot.ping.UserMethods.CreateCounter',
+ headers=context._headers,
+ auth=context.auth,
+ request=request,
+ ),
+ from_constructor=False,
+ requires_constructor=True
+ ) as (state, complete):
+
+ response = await self.__CreateCounter(
+ context,
+ state,
+ request,
+ validating_effects=validating_effects,
+ )
+
+ await complete(
+ IMPORT_reboot_aio_state_managers.Effects(
+ state=state,
+ response=response,
+ )
+ )
+ return response
+
+ async def _schedule_CreateCounter(
+ self,
+ *,
+ request: reboot.ping.ping_api_pb2.UserCreateCounterRequest,
+ headers: IMPORT_reboot_aio_headers.Headers,
+ grpc_context: IMPORT_grpc.aio.ServicerContext,
+ ) -> tuple[IMPORT_reboot_aio_contexts.WriterContext, reboot.ping.ping_api_pb2.UserCreateCounterResponse]:
+ context: IMPORT_reboot_aio_contexts.WriterContext = self.create_context(
+ headers=headers,
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User'),
+ method='CreateCounter',
+ context_type=IMPORT_reboot_aio_contexts.WriterContext,
+ )
+ response = reboot.ping.ping_api_pb2.UserCreateCounterResponse()
+
+ # Try to verify the token if a token verifier exists.
+ context.auth = await self._maybe_verify_token(
+ headers=headers, method='CreateCounter'
+ )
+
+ # Try and "preload" to opportunistically load both state and
+ # idempotent mutations from the database in a single
+ # round-trip. This is fire-and-forget; subsequent calls to
+ # `_load()` and `check_for_idempotent_mutation()` should find
+ # the preloaded data and skip their own calls to the database.
+ #
+ # TODO: a tradeoff we're making here is that we load
+ # idempotent mutations even if this is just a reader and we
+ # don't need them. Consider only preloading for writers and
+ # transactions if loading idempotent mutations is
+ # unnecessarily slow (and we haven't optimized that via a
+ # database stored bloom filter).
+ #
+ # We do this _after_ verifying the token so it is not a
+ # denial-of-service attack vector, i.e., only callers with
+ # valid tokens may trigger preloads on the database.
+ self._state_manager.preload(
+ context.state_type_name,
+ context._state_ref,
+ )
+
+ # Check if we already have performed this schedule! Note that
+ # we need to do this for all kinds of methods because this is
+ # effectively a mutation (actually a `writer`, see below).
+ #
+ # We do this _before_ calling 'transactionally()' because
+ # if this call is for a transaction method _and_ we've
+ # already performed the transaction then we don't want to
+ # become a transaction participant (again) we just want to
+ # return the transaction's response.
+ idempotent_mutation = await self._state_manager.check_for_idempotent_mutation(
+ context
+ )
+
+ if idempotent_mutation is not None:
+ response.ParseFromString(idempotent_mutation.response)
+
+ # We should have only scheduled a single task!
+ assert len(idempotent_mutation.task_ids) == 1
+ assert grpc_context is not None
+ grpc_context.set_trailing_metadata(
+ grpc_context.trailing_metadata() +
+ (
+ (
+ IMPORT_reboot_aio_headers.TASK_ID_UUID,
+ str(IMPORT_uuid.UUID(bytes=idempotent_mutation.task_ids[0].task_uuid))
+ ),
+ )
+ )
+
+ return context, response
+
+ async with self._state_manager.transactionally(
+ context,
+ self.tasks_dispatcher,
+ aborted_type=User.CreateCounterAborted,
+ ) as transaction:
+
+ async with self._state_manager.writer(
+ context,
+ self._servicer.__state_type__,
+ self.tasks_dispatcher,
+ transaction=transaction,
+ authorize=self._maybe_authorize(
+ method_name='reboot.ping.UserMethods.CreateCounter',
+ headers=context._headers,
+ auth=context.auth,
+ request=request,
+ ),
+ from_constructor=False,
+ requires_constructor=True
+ ) as (state, writer):
+
+ task = await UserServicerTasks(
+ context=context,
+ state_ref=context._state_ref,
+ ).CreateCounter(
+ UserCreateCounterRequestFromProto(request),
+ schedule=context._headers.task_schedule,
+ )
+
+ effects = IMPORT_reboot_aio_state_managers.Effects(
+ response=response,
+ state=state,
+ tasks=[task],
+ )
+
+ assert effects.tasks is not None
+
+ await writer.complete(effects)
+
+ assert grpc_context is not None
+
+ grpc_context.set_trailing_metadata(
+ grpc_context.trailing_metadata() +
+ (
+ (
+ IMPORT_reboot_aio_headers.TASK_ID_UUID,
+ str(IMPORT_uuid.UUID(bytes=task.task_id.task_uuid))
+ ),
+ )
+ )
+
+ return context, response
+
+ return context, response
+
+
+ # Entrypoint for non-reactive network calls (i.e. typical gRPC calls).
+ async def CreateCounter(
+ self,
+ request: reboot.ping.ping_api_pb2.UserCreateCounterRequest,
+ grpc_context: IMPORT_grpc.aio.ServicerContext,
+ ) -> reboot.ping.ping_api_pb2.UserCreateCounterResponse:
+ headers = IMPORT_reboot_aio_headers.Headers.from_grpc_context(grpc_context)
+ assert headers.application_id is not None # Guaranteed by `Headers`.
+
+ # Confirm whether this is the right server to be serving this
+ # request.
+ try:
+ authoritative_server = self.placement_client.server_for_actor(
+ headers.application_id,
+ headers.state_ref,
+ )
+ except IMPORT_reboot_aio_placement.UnknownApplicationError:
+ # It's possible that the user did indeed type an application ID
+ # that doesn't exist, but it's also quite possible that this
+ # request reached us before the placement planner had gossipped
+ # out the information about which applications exist (we see
+ # this e.g. after `rbt dev`'s chaos monkey restarts). For that
+ # reason, abort with a retryable error.
+ await grpc_context.abort(
+ IMPORT_grpc.StatusCode.UNAVAILABLE,
+ f"Application '{headers.application_id}' not found. If you "
+ "are confident the application exists, this may be because "
+ "the system is still starting.",
+ )
+ raise # Unreachable but necessary for mypy.
+ if authoritative_server != self.server_id:
+ # This is NOT the correct server. Fail.
+ await grpc_context.abort(
+ IMPORT_grpc.StatusCode.UNAVAILABLE,
+ f"Server '{self.server_id}' is not authoritative for this "
+ f"request; server '{authoritative_server}' is.",
+ )
+ raise # Unreachable but necessary for mypy.
+
+ @IMPORT_reboot_aio_internals_middleware.maybe_run_function_twice_to_validate_effects
+ async def _run(
+ validating_effects: bool,
+ ) -> reboot.ping.ping_api_pb2.UserCreateCounterResponse:
+ context: IMPORT_typing.Optional[IMPORT_reboot_aio_contexts.Context] = None
+ try:
+ if headers.task_schedule is not None:
+ context, response = await self._schedule_CreateCounter(
+ headers=headers,
+ request=request,
+ grpc_context=grpc_context,
+ )
+ return response
+
+ context = self.create_context(
+ headers=headers,
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User'),
+ method='CreateCounter',
+ context_type=IMPORT_reboot_aio_contexts.TransactionContext,
+ )
+ assert context is not None
+
+ return await self._CreateCounter(
+ request,
+ context,
+ validating_effects=validating_effects,
+ grpc_context=grpc_context,
+ )
+ except IMPORT_reboot_aio_contexts.EffectValidationRetry:
+ # Doing effect validation, just let this propagate.
+ raise
+ except IMPORT_reboot.aio.aborted.Aborted as aborted:
+ status = IMPORT_rpc_status_sync.to_status(aborted.to_status())
+ # Need to add transaction participants here because
+ # calling `grpc_context.abort_with_status()` will
+ # ignore any other trailing metadata. Only propagate
+ # transaction participants metadata if the caller cares.
+ # Callers that care are those that are themselves transactions.
+ # It's important to not just send this information to everyone;
+ # some clients can't tolerate trailers, see:
+ # https://github.com/reboot-dev/mono/issues/5081
+ if context is not None and headers.transaction_ids is not None:
+ assert context.transaction_id is not None
+ status = status._replace(
+ trailing_metadata=status.trailing_metadata + context.participants.to_grpc_metadata(
+ read_only_aware=headers.coordinator_read_only_aware,
+ )
+ )
+ await grpc_context.abort_with_status(status)
+ raise # Unreachable but necessary for mypy.
+ except IMPORT_asyncio.CancelledError:
+ # It's pretty normal for an RPC to be cancelled; it's not useful to
+ # print a stack trace.
+ raise
+ except BaseException as exception:
+ # Print the exception stack trace for easier debugging. Note
+ # that we don't include the stack trace in an error message
+ # for the same reason that gRPC doesn't do so by default,
+ # see https://github.com/grpc/grpc/issues/14897, but since this
+ # should only get logged on the server side it is safe.
+ logger.warning(
+ 'Unhandled exception\n' +
+ ''.join(IMPORT_traceback.format_exc() if IMPORT_reboot_nodejs_python.should_print_stacktrace() else [f"{type(exception).__name__}: {exception}"])
+ )
+
+ # Re-raise the exception for gRPC to handle!
+ #
+ # TODO: gRPC will print a stack trace from this
+ # exception which we don't want if we're executing via
+ # Node.js.
+ raise
+ finally:
+ # Propagate transaction participants, if the caller cares.
+ # Callers that care are those that are themselves transactions.
+ # It's important to not just send this information to everyone;
+ # some clients can't tolerate trailers, see:
+ # https://github.com/reboot-dev/mono/issues/5081
+ if context is not None and headers.transaction_ids is not None:
+ assert context.transaction_id is not None
+ grpc_context.set_trailing_metadata(
+ grpc_context.trailing_metadata() +
+ context.participants.to_grpc_metadata(
+ read_only_aware=headers.coordinator_read_only_aware,
+ )
+ )
+
+ with IMPORT_reboot_aio_tracing.context_from_headers(headers):
+ return await _run()
+
+ async def __ListCounters(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: reboot.ping.ping_api_pb2.User,
+ request: google.protobuf.empty_pb2.Empty,
+ *,
+ validating_effects: bool,
+ ) -> reboot.ping.ping_api_pb2.UserListCountersResponse:
+ try:
+ typed_state: User.State = UserFromProto(state, is_initial_state=(context.state_id in states_being_constructed))
+
+ if ongoing_transaction_state_key(context) in ongoing_transaction_states:
+ typed_state = ongoing_transaction_states[ongoing_transaction_state_key(context)].model_copy(deep=True)
+ response = (
+ await self._servicer._ListCounters(
+ context=context,
+ state=typed_state,
+ request=request
+ )
+ )
+
+
+ UserToProto(typed_state, state)
+
+ IMPORT_reboot_aio_types.assert_type(
+ response,
+ [reboot.ping.ping_api_pb2.UserListCountersResponse],
+ )
+ self.maybe_raise_effect_validation_retry(
+ logger=logger,
+ idempotency_manager=context,
+ method_name='User.ListCounters',
+ validating_effects=validating_effects,
+ context=context,
+ )
+ return response
+ except IMPORT_reboot_aio_contexts.EffectValidationRetry:
+ # Doing effect validation, just let this propagate.
+ raise
+ except IMPORT_reboot.aio.aborted.Aborted as aborted:
+ # If the caller aborted due to a retryable error, just
+ # propagate the aborted instead of propagating `Unknown`
+ # so that a client can transparently retry.
+ if IMPORT_reboot.aio.aborted.is_retryable(aborted):
+ raise aborted
+ # Log any _unhandled_ abort stack traces to make it
+ # easier for debugging.
+ #
+ # NOTE: we don't log if we're a task as it will be logged
+ # in `public/reboot/aio/internals/tasks_dispatcher.py` instead.
+ aborted_type: IMPORT_typing.Optional[type] = None
+ aborted_type = User.ListCountersAborted
+ if isinstance(aborted, IMPORT_reboot.aio.aborted.SystemAborted):
+ # Not logging when within `node` as we already log there.
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ f"Unhandled (in 'reboot.ping.User.ListCounters') {aborted}; propagating as 'Unknown'\n" +
+ ''.join(IMPORT_traceback.format_exception(aborted))
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ # TODO(benh): consider whether or not we want to
+ # include the 'package.service.method' which may
+ # get concatenated together forming a kind of
+ # "stack trace"; while it's super helpful for
+ # debugging, it does expose implementation
+ # information.
+ message=f"unhandled (in 'reboot.ping.User.ListCounters') {aborted}"
+ )
+ else:
+ if (
+ aborted_type is not None and
+ not isinstance(aborted, aborted_type) and
+ aborted_type.is_declared_error(aborted.error)
+ ):
+ # We propagate declared errors that might have
+ # come from another call, i.e., we might have an
+ # `Aborted` but not for this method but the
+ # `Aborted` that we have has an error that this
+ # method declared. This allows a developer to
+ # simply add the declared error to their `.proto`
+ # file rather than having to catch and re-raise
+ # the error with their own aborted type.
+ if context.task is None:
+ logger.warning(
+ f"Propagating unhandled but declared error (in 'reboot.ping.User.ListCounters') {aborted}"
+ )
+ elif (
+ aborted_type is None or
+ not isinstance(aborted, aborted_type)
+ ):
+ # Not logging when within `node` as we already log there.
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ f"Unhandled (in 'reboot.ping.User.ListCounters') {aborted}; propagating as 'Unknown'\n" +
+ ''.join(IMPORT_traceback.format_exception(aborted))
+ )
+ # If this wasn't a declared error than we
+ # propagate it as `Unknown`.
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ # TODO(benh): consider whether or not we want to
+ # include the 'package.service.method' which may
+ # get concatenated together forming a kind of
+ # "stack trace"; while it's super helpful for
+ # debugging, it does expose implementation
+ # information.
+ message=f"unhandled (in 'reboot.ping.User.ListCounters') {aborted}"
+ )
+
+ raise
+ except IMPORT_asyncio.CancelledError:
+ # It's pretty normal for an RPC to be cancelled; it's not useful to
+ # print a stack trace.
+ raise
+ except IMPORT_google_protobuf_message.DecodeError as decode_error:
+ # We usually see this error when we are trying to construct a proto
+ # message which is too deeply nested: protobuf has a limit of 100
+ # nested messages. See the limits here:
+ # https://protobuf.dev/programming-guides/proto-limits/
+
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ "Unhandled (in 'reboot.ping.User.ListCounters') "
+ f"{type(decode_error).__name__}{': ' + str(decode_error) if len(str(decode_error)) > 0 else ''}; "
+ "This is usually caused by a deeply nested protobuf message, which is not supported by protobuf.\n"
+ "See the limits here: https://protobuf.dev/programming-guides/proto-limits/" +
+ ''.join(IMPORT_traceback.format_exception(decode_error))
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ message=f"unhandled (in 'reboot.ping.User.ListCounters') {decode_error}; "
+ "This is usually caused by a deeply nested protobuf message, which is not supported by protobuf.\n"
+ "See the limits here: https://protobuf.dev/programming-guides/proto-limits/"
+ )
+ except BaseException as exception:
+ # Not logging when within `node` as we already log there.
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ "Unhandled (in 'reboot.ping.User.ListCounters') "
+ f"{type(exception).__name__}{': ' + str(exception) if len(str(exception)) > 0 else ''}; "
+ "propagating as 'Unknown'\n" +
+ ''.join(IMPORT_traceback.format_exception(exception))
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ # TODO(benh): consider whether or not we want to
+ # include the 'package.service.method' which may
+ # get concatenated together forming a kind of
+ # "stack trace"; while it's super helpful for
+ # debugging, it does expose implementation
+ # information.
+ message=f"unhandled (in 'reboot.ping.User.ListCounters') {type(exception).__name__}: {exception}"
+ )
+ finally:
+ pass
+
+ @IMPORT_reboot_aio_tracing.function_span(
+ # We expect an `EffectValidationRetry` exception; that's not an error.
+ set_status_on_exception=False
+ )
+ async def _ListCounters(
+ self,
+ request: google.protobuf.empty_pb2.Empty,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ *,
+ validating_effects: bool,
+ grpc_context: IMPORT_typing.Optional[IMPORT_grpc.aio.ServicerContext] = None,
+ ) -> reboot.ping.ping_api_pb2.UserListCountersResponse:
+ # Try to verify the token if a token verifier exists.
+ context.auth = await self._maybe_verify_token(
+ headers=context._headers, method='ListCounters'
+ )
+
+ # Try and "preload" to opportunistically load both state and
+ # idempotent mutations from the database in a single
+ # round-trip. This is fire-and-forget; subsequent calls to
+ # `_load()` and `check_for_idempotent_mutation()` should find
+ # the preloaded data and skip their own calls to the database.
+ #
+ # TODO: a tradeoff we're making here is that we load
+ # idempotent mutations even if this is just a reader and we
+ # don't need them. Consider only preloading for writers and
+ # transactions if loading idempotent mutations is
+ # unnecessarily slow (and we haven't optimized that via a
+ # database stored bloom filter).
+ #
+ # We do this _after_ verifying the token so it is not a
+ # denial-of-service attack vector, i.e., only callers with
+ # valid tokens may trigger preloads on the database.
+ self._state_manager.preload(
+ context.state_type_name,
+ context._state_ref,
+ )
+
+
+ async with self._state_manager.transactionally(
+ context,
+ self.tasks_dispatcher,
+ aborted_type=User.ListCountersAborted,
+ ) as transaction:
+ authorizer = self._maybe_authorize(
+ method_name='reboot.ping.UserMethods.ListCounters',
+ headers=context._headers,
+ auth=context.auth,
+ request=request,
+ )
+ async with self._state_manager.reader(
+ context,
+ self._servicer.__state_type__,
+ authorize=authorizer,
+ ) as state:
+ response = await self.__ListCounters(
+ context,
+ state,
+ request,
+ validating_effects=validating_effects,
+ )
+ return response
+
+ async def _schedule_ListCounters(
+ self,
+ *,
+ request: google.protobuf.empty_pb2.Empty,
+ headers: IMPORT_reboot_aio_headers.Headers,
+ grpc_context: IMPORT_grpc.aio.ServicerContext,
+ ) -> tuple[IMPORT_reboot_aio_contexts.WriterContext, reboot.ping.ping_api_pb2.UserListCountersResponse]:
+ context: IMPORT_reboot_aio_contexts.WriterContext = self.create_context(
+ headers=headers,
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User'),
+ method='ListCounters',
+ context_type=IMPORT_reboot_aio_contexts.WriterContext,
+ )
+ response = reboot.ping.ping_api_pb2.UserListCountersResponse()
+
+ # Try to verify the token if a token verifier exists.
+ context.auth = await self._maybe_verify_token(
+ headers=headers, method='ListCounters'
+ )
+
+ # Try and "preload" to opportunistically load both state and
+ # idempotent mutations from the database in a single
+ # round-trip. This is fire-and-forget; subsequent calls to
+ # `_load()` and `check_for_idempotent_mutation()` should find
+ # the preloaded data and skip their own calls to the database.
+ #
+ # TODO: a tradeoff we're making here is that we load
+ # idempotent mutations even if this is just a reader and we
+ # don't need them. Consider only preloading for writers and
+ # transactions if loading idempotent mutations is
+ # unnecessarily slow (and we haven't optimized that via a
+ # database stored bloom filter).
+ #
+ # We do this _after_ verifying the token so it is not a
+ # denial-of-service attack vector, i.e., only callers with
+ # valid tokens may trigger preloads on the database.
+ self._state_manager.preload(
+ context.state_type_name,
+ context._state_ref,
+ )
+
+ # Check if we already have performed this schedule! Note that
+ # we need to do this for all kinds of methods because this is
+ # effectively a mutation (actually a `writer`, see below).
+ #
+ # We do this _before_ calling 'transactionally()' because
+ # if this call is for a transaction method _and_ we've
+ # already performed the transaction then we don't want to
+ # become a transaction participant (again) we just want to
+ # return the transaction's response.
+ idempotent_mutation = await self._state_manager.check_for_idempotent_mutation(
+ context
+ )
+
+ if idempotent_mutation is not None:
+ response.ParseFromString(idempotent_mutation.response)
+
+ # We should have only scheduled a single task!
+ assert len(idempotent_mutation.task_ids) == 1
+ assert grpc_context is not None
+ grpc_context.set_trailing_metadata(
+ grpc_context.trailing_metadata() +
+ (
+ (
+ IMPORT_reboot_aio_headers.TASK_ID_UUID,
+ str(IMPORT_uuid.UUID(bytes=idempotent_mutation.task_ids[0].task_uuid))
+ ),
+ )
+ )
+
+ return context, response
+
+ async with self._state_manager.transactionally(
+ context,
+ self.tasks_dispatcher,
+ aborted_type=User.ListCountersAborted,
+ ) as transaction:
+
+ async with self._state_manager.writer(
+ context,
+ self._servicer.__state_type__,
+ self.tasks_dispatcher,
+ transaction=transaction,
+ authorize=self._maybe_authorize(
+ method_name='reboot.ping.UserMethods.ListCounters',
+ headers=context._headers,
+ auth=context.auth,
+ request=request,
+ ),
+ from_constructor=False,
+ requires_constructor=True
+ ) as (state, writer):
+
+ task = await UserServicerTasks(
+ context=context,
+ state_ref=context._state_ref,
+ ).ListCounters(
+ schedule=context._headers.task_schedule,
+ )
+
+ effects = IMPORT_reboot_aio_state_managers.Effects(
+ response=response,
+ state=state,
+ tasks=[task],
+ )
+
+ assert effects.tasks is not None
+
+ await writer.complete(effects)
+
+ assert grpc_context is not None
+
+ grpc_context.set_trailing_metadata(
+ grpc_context.trailing_metadata() +
+ (
+ (
+ IMPORT_reboot_aio_headers.TASK_ID_UUID,
+ str(IMPORT_uuid.UUID(bytes=task.task_id.task_uuid))
+ ),
+ )
+ )
+
+ return context, response
+
+ return context, response
+
+
+ # Entrypoint for non-reactive network calls (i.e. typical gRPC calls).
+ async def ListCounters(
+ self,
+ request: google.protobuf.empty_pb2.Empty,
+ grpc_context: IMPORT_grpc.aio.ServicerContext,
+ ) -> reboot.ping.ping_api_pb2.UserListCountersResponse:
+ headers = IMPORT_reboot_aio_headers.Headers.from_grpc_context(grpc_context)
+ assert headers.application_id is not None # Guaranteed by `Headers`.
+
+ # Confirm whether this is the right server to be serving this
+ # request.
+ try:
+ authoritative_server = self.placement_client.server_for_actor(
+ headers.application_id,
+ headers.state_ref,
+ )
+ except IMPORT_reboot_aio_placement.UnknownApplicationError:
+ # It's possible that the user did indeed type an application ID
+ # that doesn't exist, but it's also quite possible that this
+ # request reached us before the placement planner had gossipped
+ # out the information about which applications exist (we see
+ # this e.g. after `rbt dev`'s chaos monkey restarts). For that
+ # reason, abort with a retryable error.
+ await grpc_context.abort(
+ IMPORT_grpc.StatusCode.UNAVAILABLE,
+ f"Application '{headers.application_id}' not found. If you "
+ "are confident the application exists, this may be because "
+ "the system is still starting.",
+ )
+ raise # Unreachable but necessary for mypy.
+ if authoritative_server != self.server_id:
+ # This is NOT the correct server. Fail.
+ await grpc_context.abort(
+ IMPORT_grpc.StatusCode.UNAVAILABLE,
+ f"Server '{self.server_id}' is not authoritative for this "
+ f"request; server '{authoritative_server}' is.",
+ )
+ raise # Unreachable but necessary for mypy.
+
+ @IMPORT_reboot_aio_internals_middleware.maybe_run_function_twice_to_validate_effects
+ async def _run(
+ validating_effects: bool,
+ ) -> reboot.ping.ping_api_pb2.UserListCountersResponse:
+ context: IMPORT_typing.Optional[IMPORT_reboot_aio_contexts.Context] = None
+ try:
+ if headers.task_schedule is not None:
+ context, response = await self._schedule_ListCounters(
+ headers=headers,
+ request=request,
+ grpc_context=grpc_context,
+ )
+ return response
+
+ context = self.create_context(
+ headers=headers,
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User'),
+ method='ListCounters',
+ context_type=IMPORT_reboot_aio_contexts.ReaderContext,
+ )
+ assert context is not None
+
+ return await self._ListCounters(
+ request,
+ context,
+ validating_effects=validating_effects,
+ grpc_context=grpc_context,
+ )
+ except IMPORT_reboot_aio_contexts.EffectValidationRetry:
+ # Doing effect validation, just let this propagate.
+ raise
+ except IMPORT_reboot.aio.aborted.Aborted as aborted:
+ status = IMPORT_rpc_status_sync.to_status(aborted.to_status())
+ # Need to add transaction participants here because
+ # calling `grpc_context.abort_with_status()` will
+ # ignore any other trailing metadata. Only propagate
+ # transaction participants metadata if the caller cares.
+ # Callers that care are those that are themselves transactions.
+ # It's important to not just send this information to everyone;
+ # some clients can't tolerate trailers, see:
+ # https://github.com/reboot-dev/mono/issues/5081
+ if context is not None and headers.transaction_ids is not None:
+ assert context.transaction_id is not None
+ status = status._replace(
+ trailing_metadata=status.trailing_metadata + context.participants.to_grpc_metadata(
+ read_only_aware=headers.coordinator_read_only_aware,
+ )
+ )
+ await grpc_context.abort_with_status(status)
+ raise # Unreachable but necessary for mypy.
+ except IMPORT_asyncio.CancelledError:
+ # It's pretty normal for an RPC to be cancelled; it's not useful to
+ # print a stack trace.
+ raise
+ except BaseException as exception:
+ # Print the exception stack trace for easier debugging. Note
+ # that we don't include the stack trace in an error message
+ # for the same reason that gRPC doesn't do so by default,
+ # see https://github.com/grpc/grpc/issues/14897, but since this
+ # should only get logged on the server side it is safe.
+ logger.warning(
+ 'Unhandled exception\n' +
+ ''.join(IMPORT_traceback.format_exc() if IMPORT_reboot_nodejs_python.should_print_stacktrace() else [f"{type(exception).__name__}: {exception}"])
+ )
+
+ # Re-raise the exception for gRPC to handle!
+ #
+ # TODO: gRPC will print a stack trace from this
+ # exception which we don't want if we're executing via
+ # Node.js.
+ raise
+ finally:
+ # Propagate transaction participants, if the caller cares.
+ # Callers that care are those that are themselves transactions.
+ # It's important to not just send this information to everyone;
+ # some clients can't tolerate trailers, see:
+ # https://github.com/reboot-dev/mono/issues/5081
+ if context is not None and headers.transaction_ids is not None:
+ assert context.transaction_id is not None
+ grpc_context.set_trailing_metadata(
+ grpc_context.trailing_metadata() +
+ context.participants.to_grpc_metadata(
+ read_only_aware=headers.coordinator_read_only_aware,
+ )
+ )
+
+ with IMPORT_reboot_aio_tracing.context_from_headers(headers):
+ return await _run()
+
+ async def __Whoami(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: reboot.ping.ping_api_pb2.User,
+ request: google.protobuf.empty_pb2.Empty,
+ *,
+ validating_effects: bool,
+ ) -> reboot.ping.ping_api_pb2.UserWhoamiResponse:
+ try:
+ typed_state: User.State = UserFromProto(state, is_initial_state=(context.state_id in states_being_constructed))
+
+ if ongoing_transaction_state_key(context) in ongoing_transaction_states:
+ typed_state = ongoing_transaction_states[ongoing_transaction_state_key(context)].model_copy(deep=True)
+ response = (
+ await self._servicer._Whoami(
+ context=context,
+ state=typed_state,
+ request=request
+ )
+ )
+
+
+ UserToProto(typed_state, state)
+
+ IMPORT_reboot_aio_types.assert_type(
+ response,
+ [reboot.ping.ping_api_pb2.UserWhoamiResponse],
+ )
+ self.maybe_raise_effect_validation_retry(
+ logger=logger,
+ idempotency_manager=context,
+ method_name='User.Whoami',
+ validating_effects=validating_effects,
+ context=context,
+ )
+ return response
+ except IMPORT_reboot_aio_contexts.EffectValidationRetry:
+ # Doing effect validation, just let this propagate.
+ raise
+ except IMPORT_reboot.aio.aborted.Aborted as aborted:
+ # If the caller aborted due to a retryable error, just
+ # propagate the aborted instead of propagating `Unknown`
+ # so that a client can transparently retry.
+ if IMPORT_reboot.aio.aborted.is_retryable(aborted):
+ raise aborted
+ # Log any _unhandled_ abort stack traces to make it
+ # easier for debugging.
+ #
+ # NOTE: we don't log if we're a task as it will be logged
+ # in `public/reboot/aio/internals/tasks_dispatcher.py` instead.
+ aborted_type: IMPORT_typing.Optional[type] = None
+ aborted_type = User.WhoamiAborted
+ if isinstance(aborted, IMPORT_reboot.aio.aborted.SystemAborted):
+ # Not logging when within `node` as we already log there.
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ f"Unhandled (in 'reboot.ping.User.Whoami') {aborted}; propagating as 'Unknown'\n" +
+ ''.join(IMPORT_traceback.format_exception(aborted))
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ # TODO(benh): consider whether or not we want to
+ # include the 'package.service.method' which may
+ # get concatenated together forming a kind of
+ # "stack trace"; while it's super helpful for
+ # debugging, it does expose implementation
+ # information.
+ message=f"unhandled (in 'reboot.ping.User.Whoami') {aborted}"
+ )
+ else:
+ if (
+ aborted_type is not None and
+ not isinstance(aborted, aborted_type) and
+ aborted_type.is_declared_error(aborted.error)
+ ):
+ # We propagate declared errors that might have
+ # come from another call, i.e., we might have an
+ # `Aborted` but not for this method but the
+ # `Aborted` that we have has an error that this
+ # method declared. This allows a developer to
+ # simply add the declared error to their `.proto`
+ # file rather than having to catch and re-raise
+ # the error with their own aborted type.
+ if context.task is None:
+ logger.warning(
+ f"Propagating unhandled but declared error (in 'reboot.ping.User.Whoami') {aborted}"
+ )
+ elif (
+ aborted_type is None or
+ not isinstance(aborted, aborted_type)
+ ):
+ # Not logging when within `node` as we already log there.
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ f"Unhandled (in 'reboot.ping.User.Whoami') {aborted}; propagating as 'Unknown'\n" +
+ ''.join(IMPORT_traceback.format_exception(aborted))
+ )
+ # If this wasn't a declared error than we
+ # propagate it as `Unknown`.
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ # TODO(benh): consider whether or not we want to
+ # include the 'package.service.method' which may
+ # get concatenated together forming a kind of
+ # "stack trace"; while it's super helpful for
+ # debugging, it does expose implementation
+ # information.
+ message=f"unhandled (in 'reboot.ping.User.Whoami') {aborted}"
+ )
+
+ raise
+ except IMPORT_asyncio.CancelledError:
+ # It's pretty normal for an RPC to be cancelled; it's not useful to
+ # print a stack trace.
+ raise
+ except IMPORT_google_protobuf_message.DecodeError as decode_error:
+ # We usually see this error when we are trying to construct a proto
+ # message which is too deeply nested: protobuf has a limit of 100
+ # nested messages. See the limits here:
+ # https://protobuf.dev/programming-guides/proto-limits/
+
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ "Unhandled (in 'reboot.ping.User.Whoami') "
+ f"{type(decode_error).__name__}{': ' + str(decode_error) if len(str(decode_error)) > 0 else ''}; "
+ "This is usually caused by a deeply nested protobuf message, which is not supported by protobuf.\n"
+ "See the limits here: https://protobuf.dev/programming-guides/proto-limits/" +
+ ''.join(IMPORT_traceback.format_exception(decode_error))
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ message=f"unhandled (in 'reboot.ping.User.Whoami') {decode_error}; "
+ "This is usually caused by a deeply nested protobuf message, which is not supported by protobuf.\n"
+ "See the limits here: https://protobuf.dev/programming-guides/proto-limits/"
+ )
+ except BaseException as exception:
+ # Not logging when within `node` as we already log there.
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ "Unhandled (in 'reboot.ping.User.Whoami') "
+ f"{type(exception).__name__}{': ' + str(exception) if len(str(exception)) > 0 else ''}; "
+ "propagating as 'Unknown'\n" +
+ ''.join(IMPORT_traceback.format_exception(exception))
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ # TODO(benh): consider whether or not we want to
+ # include the 'package.service.method' which may
+ # get concatenated together forming a kind of
+ # "stack trace"; while it's super helpful for
+ # debugging, it does expose implementation
+ # information.
+ message=f"unhandled (in 'reboot.ping.User.Whoami') {type(exception).__name__}: {exception}"
+ )
+ finally:
+ pass
+
+ @IMPORT_reboot_aio_tracing.function_span(
+ # We expect an `EffectValidationRetry` exception; that's not an error.
+ set_status_on_exception=False
+ )
+ async def _Whoami(
+ self,
+ request: google.protobuf.empty_pb2.Empty,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ *,
+ validating_effects: bool,
+ grpc_context: IMPORT_typing.Optional[IMPORT_grpc.aio.ServicerContext] = None,
+ ) -> reboot.ping.ping_api_pb2.UserWhoamiResponse:
+ # Try to verify the token if a token verifier exists.
+ context.auth = await self._maybe_verify_token(
+ headers=context._headers, method='Whoami'
+ )
+
+ # Try and "preload" to opportunistically load both state and
+ # idempotent mutations from the database in a single
+ # round-trip. This is fire-and-forget; subsequent calls to
+ # `_load()` and `check_for_idempotent_mutation()` should find
+ # the preloaded data and skip their own calls to the database.
+ #
+ # TODO: a tradeoff we're making here is that we load
+ # idempotent mutations even if this is just a reader and we
+ # don't need them. Consider only preloading for writers and
+ # transactions if loading idempotent mutations is
+ # unnecessarily slow (and we haven't optimized that via a
+ # database stored bloom filter).
+ #
+ # We do this _after_ verifying the token so it is not a
+ # denial-of-service attack vector, i.e., only callers with
+ # valid tokens may trigger preloads on the database.
+ self._state_manager.preload(
+ context.state_type_name,
+ context._state_ref,
+ )
+
+
+ async with self._state_manager.transactionally(
+ context,
+ self.tasks_dispatcher,
+ aborted_type=User.WhoamiAborted,
+ ) as transaction:
+ authorizer = self._maybe_authorize(
+ method_name='reboot.ping.UserMethods.Whoami',
+ headers=context._headers,
+ auth=context.auth,
+ request=request,
+ )
+ async with self._state_manager.reader(
+ context,
+ self._servicer.__state_type__,
+ authorize=authorizer,
+ ) as state:
+ response = await self.__Whoami(
+ context,
+ state,
+ request,
+ validating_effects=validating_effects,
+ )
+ return response
+
+ async def _schedule_Whoami(
+ self,
+ *,
+ request: google.protobuf.empty_pb2.Empty,
+ headers: IMPORT_reboot_aio_headers.Headers,
+ grpc_context: IMPORT_grpc.aio.ServicerContext,
+ ) -> tuple[IMPORT_reboot_aio_contexts.WriterContext, reboot.ping.ping_api_pb2.UserWhoamiResponse]:
+ context: IMPORT_reboot_aio_contexts.WriterContext = self.create_context(
+ headers=headers,
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User'),
+ method='Whoami',
+ context_type=IMPORT_reboot_aio_contexts.WriterContext,
+ )
+ response = reboot.ping.ping_api_pb2.UserWhoamiResponse()
+
+ # Try to verify the token if a token verifier exists.
+ context.auth = await self._maybe_verify_token(
+ headers=headers, method='Whoami'
+ )
+
+ # Try and "preload" to opportunistically load both state and
+ # idempotent mutations from the database in a single
+ # round-trip. This is fire-and-forget; subsequent calls to
+ # `_load()` and `check_for_idempotent_mutation()` should find
+ # the preloaded data and skip their own calls to the database.
+ #
+ # TODO: a tradeoff we're making here is that we load
+ # idempotent mutations even if this is just a reader and we
+ # don't need them. Consider only preloading for writers and
+ # transactions if loading idempotent mutations is
+ # unnecessarily slow (and we haven't optimized that via a
+ # database stored bloom filter).
+ #
+ # We do this _after_ verifying the token so it is not a
+ # denial-of-service attack vector, i.e., only callers with
+ # valid tokens may trigger preloads on the database.
+ self._state_manager.preload(
+ context.state_type_name,
+ context._state_ref,
+ )
+
+ # Check if we already have performed this schedule! Note that
+ # we need to do this for all kinds of methods because this is
+ # effectively a mutation (actually a `writer`, see below).
+ #
+ # We do this _before_ calling 'transactionally()' because
+ # if this call is for a transaction method _and_ we've
+ # already performed the transaction then we don't want to
+ # become a transaction participant (again) we just want to
+ # return the transaction's response.
+ idempotent_mutation = await self._state_manager.check_for_idempotent_mutation(
+ context
+ )
+
+ if idempotent_mutation is not None:
+ response.ParseFromString(idempotent_mutation.response)
+
+ # We should have only scheduled a single task!
+ assert len(idempotent_mutation.task_ids) == 1
+ assert grpc_context is not None
+ grpc_context.set_trailing_metadata(
+ grpc_context.trailing_metadata() +
+ (
+ (
+ IMPORT_reboot_aio_headers.TASK_ID_UUID,
+ str(IMPORT_uuid.UUID(bytes=idempotent_mutation.task_ids[0].task_uuid))
+ ),
+ )
+ )
+
+ return context, response
+
+ async with self._state_manager.transactionally(
+ context,
+ self.tasks_dispatcher,
+ aborted_type=User.WhoamiAborted,
+ ) as transaction:
+
+ async with self._state_manager.writer(
+ context,
+ self._servicer.__state_type__,
+ self.tasks_dispatcher,
+ transaction=transaction,
+ authorize=self._maybe_authorize(
+ method_name='reboot.ping.UserMethods.Whoami',
+ headers=context._headers,
+ auth=context.auth,
+ request=request,
+ ),
+ from_constructor=False,
+ requires_constructor=True
+ ) as (state, writer):
+
+ task = await UserServicerTasks(
+ context=context,
+ state_ref=context._state_ref,
+ ).Whoami(
+ schedule=context._headers.task_schedule,
+ )
+
+ effects = IMPORT_reboot_aio_state_managers.Effects(
+ response=response,
+ state=state,
+ tasks=[task],
+ )
+
+ assert effects.tasks is not None
+
+ await writer.complete(effects)
+
+ assert grpc_context is not None
+
+ grpc_context.set_trailing_metadata(
+ grpc_context.trailing_metadata() +
+ (
+ (
+ IMPORT_reboot_aio_headers.TASK_ID_UUID,
+ str(IMPORT_uuid.UUID(bytes=task.task_id.task_uuid))
+ ),
+ )
+ )
+
+ return context, response
+
+ return context, response
+
+
+ # Entrypoint for non-reactive network calls (i.e. typical gRPC calls).
+ async def Whoami(
+ self,
+ request: google.protobuf.empty_pb2.Empty,
+ grpc_context: IMPORT_grpc.aio.ServicerContext,
+ ) -> reboot.ping.ping_api_pb2.UserWhoamiResponse:
+ headers = IMPORT_reboot_aio_headers.Headers.from_grpc_context(grpc_context)
+ assert headers.application_id is not None # Guaranteed by `Headers`.
+
+ # Confirm whether this is the right server to be serving this
+ # request.
+ try:
+ authoritative_server = self.placement_client.server_for_actor(
+ headers.application_id,
+ headers.state_ref,
+ )
+ except IMPORT_reboot_aio_placement.UnknownApplicationError:
+ # It's possible that the user did indeed type an application ID
+ # that doesn't exist, but it's also quite possible that this
+ # request reached us before the placement planner had gossipped
+ # out the information about which applications exist (we see
+ # this e.g. after `rbt dev`'s chaos monkey restarts). For that
+ # reason, abort with a retryable error.
+ await grpc_context.abort(
+ IMPORT_grpc.StatusCode.UNAVAILABLE,
+ f"Application '{headers.application_id}' not found. If you "
+ "are confident the application exists, this may be because "
+ "the system is still starting.",
+ )
+ raise # Unreachable but necessary for mypy.
+ if authoritative_server != self.server_id:
+ # This is NOT the correct server. Fail.
+ await grpc_context.abort(
+ IMPORT_grpc.StatusCode.UNAVAILABLE,
+ f"Server '{self.server_id}' is not authoritative for this "
+ f"request; server '{authoritative_server}' is.",
+ )
+ raise # Unreachable but necessary for mypy.
+
+ @IMPORT_reboot_aio_internals_middleware.maybe_run_function_twice_to_validate_effects
+ async def _run(
+ validating_effects: bool,
+ ) -> reboot.ping.ping_api_pb2.UserWhoamiResponse:
+ context: IMPORT_typing.Optional[IMPORT_reboot_aio_contexts.Context] = None
+ try:
+ if headers.task_schedule is not None:
+ context, response = await self._schedule_Whoami(
+ headers=headers,
+ request=request,
+ grpc_context=grpc_context,
+ )
+ return response
+
+ context = self.create_context(
+ headers=headers,
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User'),
+ method='Whoami',
+ context_type=IMPORT_reboot_aio_contexts.ReaderContext,
+ )
+ assert context is not None
+
+ return await self._Whoami(
+ request,
+ context,
+ validating_effects=validating_effects,
+ grpc_context=grpc_context,
+ )
+ except IMPORT_reboot_aio_contexts.EffectValidationRetry:
+ # Doing effect validation, just let this propagate.
+ raise
+ except IMPORT_reboot.aio.aborted.Aborted as aborted:
+ status = IMPORT_rpc_status_sync.to_status(aborted.to_status())
+ # Need to add transaction participants here because
+ # calling `grpc_context.abort_with_status()` will
+ # ignore any other trailing metadata. Only propagate
+ # transaction participants metadata if the caller cares.
+ # Callers that care are those that are themselves transactions.
+ # It's important to not just send this information to everyone;
+ # some clients can't tolerate trailers, see:
+ # https://github.com/reboot-dev/mono/issues/5081
+ if context is not None and headers.transaction_ids is not None:
+ assert context.transaction_id is not None
+ status = status._replace(
+ trailing_metadata=status.trailing_metadata + context.participants.to_grpc_metadata(
+ read_only_aware=headers.coordinator_read_only_aware,
+ )
+ )
+ await grpc_context.abort_with_status(status)
+ raise # Unreachable but necessary for mypy.
+ except IMPORT_asyncio.CancelledError:
+ # It's pretty normal for an RPC to be cancelled; it's not useful to
+ # print a stack trace.
+ raise
+ except BaseException as exception:
+ # Print the exception stack trace for easier debugging. Note
+ # that we don't include the stack trace in an error message
+ # for the same reason that gRPC doesn't do so by default,
+ # see https://github.com/grpc/grpc/issues/14897, but since this
+ # should only get logged on the server side it is safe.
+ logger.warning(
+ 'Unhandled exception\n' +
+ ''.join(IMPORT_traceback.format_exc() if IMPORT_reboot_nodejs_python.should_print_stacktrace() else [f"{type(exception).__name__}: {exception}"])
+ )
+
+ # Re-raise the exception for gRPC to handle!
+ #
+ # TODO: gRPC will print a stack trace from this
+ # exception which we don't want if we're executing via
+ # Node.js.
+ raise
+ finally:
+ # Propagate transaction participants, if the caller cares.
+ # Callers that care are those that are themselves transactions.
+ # It's important to not just send this information to everyone;
+ # some clients can't tolerate trailers, see:
+ # https://github.com/reboot-dev/mono/issues/5081
+ if context is not None and headers.transaction_ids is not None:
+ assert context.transaction_id is not None
+ grpc_context.set_trailing_metadata(
+ grpc_context.trailing_metadata() +
+ context.participants.to_grpc_metadata(
+ read_only_aware=headers.coordinator_read_only_aware,
+ )
+ )
+
+ with IMPORT_reboot_aio_tracing.context_from_headers(headers):
+ return await _run()
+
+ async def __Create(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext,
+ state: reboot.ping.ping_api_pb2.User,
+ request: google.protobuf.empty_pb2.Empty,
+ *,
+ validating_effects: bool,
+ ) -> google.protobuf.empty_pb2.Empty:
+ try:
+ states_being_constructed.add(context.state_id)
+ typed_state: User.State = UserFromProto(state, is_initial_state=(context.state_id in states_being_constructed))
+
+ # TODO: assert that there are no ongoing transactions for this state.
+ #
+ # The `typed_state` should be already validated above, so we can
+ # just store it here.
+ ongoing_transaction_states[ongoing_transaction_state_key(context)] = typed_state
+ response = (
+ await self._servicer._Create(
+ context=context,
+ state=typed_state,
+ request=request
+ )
+ )
+
+
+ UserToProto(typed_state, state)
+
+ IMPORT_reboot_aio_types.assert_type(
+ response,
+ [google.protobuf.empty_pb2.Empty],
+ )
+ self.maybe_raise_effect_validation_retry(
+ logger=logger,
+ idempotency_manager=context,
+ method_name='User.Create',
+ validating_effects=validating_effects,
+ context=context,
+ )
+ return response
+ except IMPORT_reboot_aio_contexts.EffectValidationRetry:
+ # Doing effect validation, just let this propagate.
+ raise
+ except IMPORT_reboot.aio.aborted.Aborted as aborted:
+ # If the caller aborted due to a retryable error, just
+ # propagate the aborted instead of propagating `Unknown`
+ # so that a client can transparently retry.
+ if IMPORT_reboot.aio.aborted.is_retryable(aborted):
+ raise aborted
+ # Log any _unhandled_ abort stack traces to make it
+ # easier for debugging.
+ #
+ # NOTE: we don't log if we're a task as it will be logged
+ # in `public/reboot/aio/internals/tasks_dispatcher.py` instead.
+ aborted_type: IMPORT_typing.Optional[type] = None
+ aborted_type = User.CreateAborted
+ if isinstance(aborted, IMPORT_reboot.aio.aborted.SystemAborted):
+ # Not logging when within `node` as we already log there.
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ f"Unhandled (in 'reboot.ping.User.Create') {aborted}; propagating as 'Unknown'\n" +
+ ''.join(IMPORT_traceback.format_exception(aborted))
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ # TODO(benh): consider whether or not we want to
+ # include the 'package.service.method' which may
+ # get concatenated together forming a kind of
+ # "stack trace"; while it's super helpful for
+ # debugging, it does expose implementation
+ # information.
+ message=f"unhandled (in 'reboot.ping.User.Create') {aborted}"
+ )
+ else:
+ if (
+ aborted_type is not None and
+ not isinstance(aborted, aborted_type) and
+ aborted_type.is_declared_error(aborted.error)
+ ):
+ # We propagate declared errors that might have
+ # come from another call, i.e., we might have an
+ # `Aborted` but not for this method but the
+ # `Aborted` that we have has an error that this
+ # method declared. This allows a developer to
+ # simply add the declared error to their `.proto`
+ # file rather than having to catch and re-raise
+ # the error with their own aborted type.
+ if context.task is None:
+ logger.warning(
+ f"Propagating unhandled but declared error (in 'reboot.ping.User.Create') {aborted}"
+ )
+ elif (
+ aborted_type is None or
+ not isinstance(aborted, aborted_type)
+ ):
+ # Not logging when within `node` as we already log there.
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ f"Unhandled (in 'reboot.ping.User.Create') {aborted}; propagating as 'Unknown'\n" +
+ ''.join(IMPORT_traceback.format_exception(aborted))
+ )
+ # If this wasn't a declared error than we
+ # propagate it as `Unknown`.
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ # TODO(benh): consider whether or not we want to
+ # include the 'package.service.method' which may
+ # get concatenated together forming a kind of
+ # "stack trace"; while it's super helpful for
+ # debugging, it does expose implementation
+ # information.
+ message=f"unhandled (in 'reboot.ping.User.Create') {aborted}"
+ )
+
+ raise
+ except IMPORT_asyncio.CancelledError:
+ # It's pretty normal for an RPC to be cancelled; it's not useful to
+ # print a stack trace.
+ raise
+ except IMPORT_google_protobuf_message.DecodeError as decode_error:
+ # We usually see this error when we are trying to construct a proto
+ # message which is too deeply nested: protobuf has a limit of 100
+ # nested messages. See the limits here:
+ # https://protobuf.dev/programming-guides/proto-limits/
+
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ "Unhandled (in 'reboot.ping.User.Create') "
+ f"{type(decode_error).__name__}{': ' + str(decode_error) if len(str(decode_error)) > 0 else ''}; "
+ "This is usually caused by a deeply nested protobuf message, which is not supported by protobuf.\n"
+ "See the limits here: https://protobuf.dev/programming-guides/proto-limits/" +
+ ''.join(IMPORT_traceback.format_exception(decode_error))
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ message=f"unhandled (in 'reboot.ping.User.Create') {decode_error}; "
+ "This is usually caused by a deeply nested protobuf message, which is not supported by protobuf.\n"
+ "See the limits here: https://protobuf.dev/programming-guides/proto-limits/"
+ )
+ except BaseException as exception:
+ # Not logging when within `node` as we already log there.
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ "Unhandled (in 'reboot.ping.User.Create') "
+ f"{type(exception).__name__}{': ' + str(exception) if len(str(exception)) > 0 else ''}; "
+ "propagating as 'Unknown'\n" +
+ ''.join(IMPORT_traceback.format_exception(exception))
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ # TODO(benh): consider whether or not we want to
+ # include the 'package.service.method' which may
+ # get concatenated together forming a kind of
+ # "stack trace"; while it's super helpful for
+ # debugging, it does expose implementation
+ # information.
+ message=f"unhandled (in 'reboot.ping.User.Create') {type(exception).__name__}: {exception}"
+ )
+ finally:
+ states_being_constructed.remove(context.state_id)
+ if ongoing_transaction_state_key(context) in ongoing_transaction_states:
+ del ongoing_transaction_states[ongoing_transaction_state_key(context)]
+
+ @IMPORT_reboot_aio_tracing.function_span(
+ # We expect an `EffectValidationRetry` exception; that's not an error.
+ set_status_on_exception=False
+ )
+ async def _Create(
+ self,
+ request: google.protobuf.empty_pb2.Empty,
+ context: IMPORT_reboot_aio_contexts.TransactionContext,
+ *,
+ validating_effects: bool,
+ grpc_context: IMPORT_typing.Optional[IMPORT_grpc.aio.ServicerContext] = None,
+ ) -> google.protobuf.empty_pb2.Empty:
+ # Try to verify the token if a token verifier exists.
+ context.auth = await self._maybe_verify_token(
+ headers=context._headers, method='Create'
+ )
+
+ # Try and "preload" to opportunistically load both state and
+ # idempotent mutations from the database in a single
+ # round-trip. This is fire-and-forget; subsequent calls to
+ # `_load()` and `check_for_idempotent_mutation()` should find
+ # the preloaded data and skip their own calls to the database.
+ #
+ # TODO: a tradeoff we're making here is that we load
+ # idempotent mutations even if this is just a reader and we
+ # don't need them. Consider only preloading for writers and
+ # transactions if loading idempotent mutations is
+ # unnecessarily slow (and we haven't optimized that via a
+ # database stored bloom filter).
+ #
+ # We do this _after_ verifying the token so it is not a
+ # denial-of-service attack vector, i.e., only callers with
+ # valid tokens may trigger preloads on the database.
+ self._state_manager.preload(
+ context.state_type_name,
+ context._state_ref,
+ )
+
+ # Check if we already have performed this mutation!
+ #
+ # We do this _before_ calling 'transactionally()' because
+ # if this call is for a transaction method _and_ we've
+ # already performed the transaction then we don't want to
+ # become a transaction participant (again) we just want to
+ # return the transaction's response.
+ idempotent_mutation = await self._state_manager.check_for_idempotent_mutation(
+ context
+ )
+
+ if idempotent_mutation is not None:
+ response = google.protobuf.empty_pb2.Empty()
+ response.ParseFromString(idempotent_mutation.response)
+ return response
+
+ async with self._state_manager.transactionally(
+ context,
+ self.tasks_dispatcher,
+ aborted_type=User.CreateAborted,
+ ) as transaction:
+ assert transaction is not None
+ async with self._state_manager.transaction(
+ context,
+ self._servicer.__state_type__,
+ transaction,
+ authorize=self._maybe_authorize(
+ method_name='reboot.ping.UserMethods.Create',
+ headers=context._headers,
+ auth=context.auth,
+ request=request,
+ ),
+ from_constructor=True,
+ requires_constructor=True
+ ) as (state, complete):
+
+ response = await self.__Create(
+ context,
+ state,
+ request,
+ validating_effects=validating_effects,
+ )
+
+ await complete(
+ IMPORT_reboot_aio_state_managers.Effects(
+ state=state,
+ response=response,
+ )
+ )
+ return response
+
+ async def _schedule_Create(
+ self,
+ *,
+ request: google.protobuf.empty_pb2.Empty,
+ headers: IMPORT_reboot_aio_headers.Headers,
+ grpc_context: IMPORT_grpc.aio.ServicerContext,
+ ) -> tuple[IMPORT_reboot_aio_contexts.WriterContext, google.protobuf.empty_pb2.Empty]:
+ context: IMPORT_reboot_aio_contexts.WriterContext = self.create_context(
+ headers=headers,
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User'),
+ method='Create',
+ context_type=IMPORT_reboot_aio_contexts.WriterContext,
+ )
+ response = google.protobuf.empty_pb2.Empty()
+
+ # Try to verify the token if a token verifier exists.
+ context.auth = await self._maybe_verify_token(
+ headers=headers, method='Create'
+ )
+
+ # Try and "preload" to opportunistically load both state and
+ # idempotent mutations from the database in a single
+ # round-trip. This is fire-and-forget; subsequent calls to
+ # `_load()` and `check_for_idempotent_mutation()` should find
+ # the preloaded data and skip their own calls to the database.
+ #
+ # TODO: a tradeoff we're making here is that we load
+ # idempotent mutations even if this is just a reader and we
+ # don't need them. Consider only preloading for writers and
+ # transactions if loading idempotent mutations is
+ # unnecessarily slow (and we haven't optimized that via a
+ # database stored bloom filter).
+ #
+ # We do this _after_ verifying the token so it is not a
+ # denial-of-service attack vector, i.e., only callers with
+ # valid tokens may trigger preloads on the database.
+ self._state_manager.preload(
+ context.state_type_name,
+ context._state_ref,
+ )
+
+ # Check if we already have performed this schedule! Note that
+ # we need to do this for all kinds of methods because this is
+ # effectively a mutation (actually a `writer`, see below).
+ #
+ # We do this _before_ calling 'transactionally()' because
+ # if this call is for a transaction method _and_ we've
+ # already performed the transaction then we don't want to
+ # become a transaction participant (again) we just want to
+ # return the transaction's response.
+ idempotent_mutation = await self._state_manager.check_for_idempotent_mutation(
+ context
+ )
+
+ if idempotent_mutation is not None:
+ response.ParseFromString(idempotent_mutation.response)
+
+ # We should have only scheduled a single task!
+ assert len(idempotent_mutation.task_ids) == 1
+ assert grpc_context is not None
+ grpc_context.set_trailing_metadata(
+ grpc_context.trailing_metadata() +
+ (
+ (
+ IMPORT_reboot_aio_headers.TASK_ID_UUID,
+ str(IMPORT_uuid.UUID(bytes=idempotent_mutation.task_ids[0].task_uuid))
+ ),
+ )
+ )
+
+ return context, response
+
+ async with self._state_manager.transactionally(
+ context,
+ self.tasks_dispatcher,
+ aborted_type=User.CreateAborted,
+ ) as transaction:
+
+ async with self._state_manager.writer(
+ context,
+ self._servicer.__state_type__,
+ self.tasks_dispatcher,
+ transaction=transaction,
+ authorize=self._maybe_authorize(
+ method_name='reboot.ping.UserMethods.Create',
+ headers=context._headers,
+ auth=context.auth,
+ request=request,
+ ),
+ from_constructor=True,
+ requires_constructor=True
+ ) as (state, writer):
+
+ task = await UserServicerTasks(
+ context=context,
+ state_ref=context._state_ref,
+ ).Create(
+ schedule=context._headers.task_schedule,
+ )
+
+ effects = IMPORT_reboot_aio_state_managers.Effects(
+ response=response,
+ state=state,
+ tasks=[task],
+ )
+
+ assert effects.tasks is not None
+
+ await writer.complete(effects)
+
+ assert grpc_context is not None
+
+ grpc_context.set_trailing_metadata(
+ grpc_context.trailing_metadata() +
+ (
+ (
+ IMPORT_reboot_aio_headers.TASK_ID_UUID,
+ str(IMPORT_uuid.UUID(bytes=task.task_id.task_uuid))
+ ),
+ )
+ )
+
+ return context, response
+
+ return context, response
+
+
+ # Entrypoint for non-reactive network calls (i.e. typical gRPC calls).
+ async def Create(
+ self,
+ request: google.protobuf.empty_pb2.Empty,
+ grpc_context: IMPORT_grpc.aio.ServicerContext,
+ ) -> google.protobuf.empty_pb2.Empty:
+ headers = IMPORT_reboot_aio_headers.Headers.from_grpc_context(grpc_context)
+ assert headers.application_id is not None # Guaranteed by `Headers`.
+
+ # Confirm whether this is the right server to be serving this
+ # request.
+ try:
+ authoritative_server = self.placement_client.server_for_actor(
+ headers.application_id,
+ headers.state_ref,
+ )
+ except IMPORT_reboot_aio_placement.UnknownApplicationError:
+ # It's possible that the user did indeed type an application ID
+ # that doesn't exist, but it's also quite possible that this
+ # request reached us before the placement planner had gossipped
+ # out the information about which applications exist (we see
+ # this e.g. after `rbt dev`'s chaos monkey restarts). For that
+ # reason, abort with a retryable error.
+ await grpc_context.abort(
+ IMPORT_grpc.StatusCode.UNAVAILABLE,
+ f"Application '{headers.application_id}' not found. If you "
+ "are confident the application exists, this may be because "
+ "the system is still starting.",
+ )
+ raise # Unreachable but necessary for mypy.
+ if authoritative_server != self.server_id:
+ # This is NOT the correct server. Fail.
+ await grpc_context.abort(
+ IMPORT_grpc.StatusCode.UNAVAILABLE,
+ f"Server '{self.server_id}' is not authoritative for this "
+ f"request; server '{authoritative_server}' is.",
+ )
+ raise # Unreachable but necessary for mypy.
+
+ @IMPORT_reboot_aio_internals_middleware.maybe_run_function_twice_to_validate_effects
+ async def _run(
+ validating_effects: bool,
+ ) -> google.protobuf.empty_pb2.Empty:
+ context: IMPORT_typing.Optional[IMPORT_reboot_aio_contexts.Context] = None
+ try:
+ if headers.task_schedule is not None:
+ context, response = await self._schedule_Create(
+ headers=headers,
+ request=request,
+ grpc_context=grpc_context,
+ )
+ return response
+
+ context = self.create_context(
+ headers=headers,
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User'),
+ method='Create',
+ context_type=IMPORT_reboot_aio_contexts.TransactionContext,
+ )
+ assert context is not None
+
+ return await self._Create(
+ request,
+ context,
+ validating_effects=validating_effects,
+ grpc_context=grpc_context,
+ )
+ except IMPORT_reboot_aio_contexts.EffectValidationRetry:
+ # Doing effect validation, just let this propagate.
+ raise
+ except IMPORT_reboot.aio.aborted.Aborted as aborted:
+ status = IMPORT_rpc_status_sync.to_status(aborted.to_status())
+ # Need to add transaction participants here because
+ # calling `grpc_context.abort_with_status()` will
+ # ignore any other trailing metadata. Only propagate
+ # transaction participants metadata if the caller cares.
+ # Callers that care are those that are themselves transactions.
+ # It's important to not just send this information to everyone;
+ # some clients can't tolerate trailers, see:
+ # https://github.com/reboot-dev/mono/issues/5081
+ if context is not None and headers.transaction_ids is not None:
+ assert context.transaction_id is not None
+ status = status._replace(
+ trailing_metadata=status.trailing_metadata + context.participants.to_grpc_metadata(
+ read_only_aware=headers.coordinator_read_only_aware,
+ )
+ )
+ await grpc_context.abort_with_status(status)
+ raise # Unreachable but necessary for mypy.
+ except IMPORT_asyncio.CancelledError:
+ # It's pretty normal for an RPC to be cancelled; it's not useful to
+ # print a stack trace.
+ raise
+ except BaseException as exception:
+ # Print the exception stack trace for easier debugging. Note
+ # that we don't include the stack trace in an error message
+ # for the same reason that gRPC doesn't do so by default,
+ # see https://github.com/grpc/grpc/issues/14897, but since this
+ # should only get logged on the server side it is safe.
+ logger.warning(
+ 'Unhandled exception\n' +
+ ''.join(IMPORT_traceback.format_exc() if IMPORT_reboot_nodejs_python.should_print_stacktrace() else [f"{type(exception).__name__}: {exception}"])
+ )
+
+ # Re-raise the exception for gRPC to handle!
+ #
+ # TODO: gRPC will print a stack trace from this
+ # exception which we don't want if we're executing via
+ # Node.js.
+ raise
+ finally:
+ # Propagate transaction participants, if the caller cares.
+ # Callers that care are those that are themselves transactions.
+ # It's important to not just send this information to everyone;
+ # some clients can't tolerate trailers, see:
+ # https://github.com/reboot-dev/mono/issues/5081
+ if context is not None and headers.transaction_ids is not None:
+ assert context.transaction_id is not None
+ grpc_context.set_trailing_metadata(
+ grpc_context.trailing_metadata() +
+ context.participants.to_grpc_metadata(
+ read_only_aware=headers.coordinator_read_only_aware,
+ )
+ )
+
+ with IMPORT_reboot_aio_tracing.context_from_headers(headers):
+ return await _run()
+
+ def _maybe_authorize(
+ self,
+ *,
+ method_name: str,
+ headers: IMPORT_reboot_aio_headers.Headers,
+ auth: IMPORT_typing.Optional[IMPORT_reboot_aio_auth.Auth],
+ request: IMPORT_typing.Optional[UserRequestTypes] = None,
+ ) -> IMPORT_typing.Optional[IMPORT_typing.Callable[[IMPORT_typing.Optional[UserStateType]], IMPORT_typing.Awaitable[None]]]:
+ """Returns a function to check authorization for the given method.
+
+ Raises `PermissionDenied` in case Authorizer is present but the request
+ is not authorized.
+ """
+ # To authorize internal calls, we use an internal magic token.
+ if headers.bearer_token == __internal_magic_token__:
+ return None
+
+ assert self._authorizer is not None
+
+ async def authorize(state: IMPORT_typing.Optional[UserStateType]) -> None:
+ # Create context for the authorizer. This is a `ReaderContext`
+ # independently of the calling context.
+ with self.use_context(
+ headers=(
+ # Get headers suitable for doing authorization.
+ headers.copy_for_token_verification_and_authorization()
+ ),
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User'),
+ method=method_name,
+ context_type=IMPORT_reboot_aio_contexts.ReaderContext,
+ ) as context:
+ context.auth = auth
+
+ # Get the authorizer decision.
+ authorization_decision = await self._authorizer.authorize(
+ method_name=method_name,
+ context=context,
+ state=state,
+ request=request,
+ )
+
+ # Enforce correct authorizer decision type.
+ try:
+ IMPORT_reboot_aio_types.assert_type(
+ authorization_decision,
+ [
+ IMPORT_rbt_v1alpha1.errors_pb2.Ok,
+ IMPORT_rbt_v1alpha1.errors_pb2.Unauthenticated,
+ IMPORT_rbt_v1alpha1.errors_pb2.PermissionDenied,
+ ]
+ )
+ except TypeError as e:
+ # Retyping.cast the exception to provide more context.
+ authorizer_type = f"{type(self._authorizer).__module__}.{type(self._authorizer).__name__}"
+ raise TypeError(
+ f"Authorizer '{authorizer_type}' "
+ f"returned unexpected type '{type(authorization_decision).__name__}' "
+ f"for method '{method_name}' on "
+ f"`reboot.ping.User('{headers.state_ref.id}')`"
+ ) from e
+
+ # If the decision is not `True`, raise a `SystemAborted` with either a
+ # `PermissionDenied` error (in case of `False`) or an `Unauthenticated`
+ # error.
+ if not isinstance(authorization_decision, IMPORT_rbt_v1alpha1.errors_pb2.Ok):
+ if isinstance(authorization_decision, IMPORT_rbt_v1alpha1.errors_pb2.Unauthenticated):
+ logger.warning(
+ f"Unauthenticated call to '{method_name}' on "
+ f"`reboot.ping.User('{headers.state_ref.id}')`"
+ )
+
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ authorization_decision,
+ message=
+ f"You are not authorized to call '{method_name}' on "
+ f"`reboot.ping.User('{headers.state_ref.id}')`"
+ )
+
+ return authorize
+
+ async def _maybe_verify_token(
+ self,
+ *,
+ headers: IMPORT_reboot_aio_headers.Headers,
+ method: str,
+ ) -> IMPORT_typing.Optional[IMPORT_reboot_aio_auth.Auth]:
+ """Verify the bearer token and if a token verifier is present.
+
+ Returns the (optional) `reboot.aio.auth.Auth` object
+ produced by the token verifier if the token can be verified.
+ """
+ if self._token_verifier is not None:
+ if headers.bearer_token == __internal_magic_token__:
+ return None
+
+ with self.use_context(
+ headers=(
+ # Get headers suitable for doing token verification.
+ headers.copy_for_token_verification_and_authorization()
+ ),
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User'),
+ method=method,
+ context_type=IMPORT_reboot_aio_contexts.ReaderContext,
+ ) as context:
+ result = await self._token_verifier.verify_token(
+ context=context,
+ token=headers.bearer_token,
+ )
+ if isinstance(result, IMPORT_rbt_v1alpha1.errors_pb2.Unauthenticated):
+ raise IMPORT_reboot_aio_aborted.SystemAborted(
+ result,
+ message=result.message or None,
+ )
+ return result
+
+ return None
+
+class CounterServicerMiddleware(IMPORT_reboot_aio_internals_middleware.Middleware):
+
+ def __init__(
+ self,
+ *,
+ servicer: CounterBaseServicer,
+ application_id: IMPORT_reboot_aio_types.ApplicationId,
+ server_id: IMPORT_reboot_aio_types.ServerId,
+ state_manager: IMPORT_reboot_aio_state_managers.StateManager,
+ placement_client: IMPORT_reboot_aio_placement.PlacementClient,
+ channel_manager: IMPORT_reboot_aio_internals_channel_manager._ChannelManager,
+ tasks_cache: IMPORT_reboot_aio_internals_tasks_cache.TasksCache,
+ token_verifier: IMPORT_typing.Optional[IMPORT_reboot_aio_auth_token_verifiers.TokenVerifier],
+ effect_validation: IMPORT_reboot_aio_contexts.EffectValidation,
+ ready: IMPORT_asyncio.Event,
+ ):
+ super().__init__(
+ application_id=application_id,
+ server_id=server_id,
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ service_names = [
+ IMPORT_reboot_aio_types.ServiceName("reboot.ping.CounterMethods"),
+ ],
+ placement_client=placement_client,
+ channel_manager=channel_manager,
+ effect_validation=effect_validation,
+ get_database_timestamp_ms=lambda: state_manager.latest_timestamp_ms,
+ )
+
+ self._servicer = servicer
+ self._state_manager = state_manager
+ self.tasks_dispatcher = IMPORT_reboot_aio_internals_tasks_dispatcher.TasksDispatcher(
+ application_id=application_id,
+ dispatch=self.dispatch,
+ tasks_cache=tasks_cache,
+ ready=ready,
+ complete_task=self._state_manager.complete_task,
+ )
+
+ # Store the type of each method's request so that stored requests can be
+ # deserialized into the correct type.
+ self.request_type_by_method_name: dict[str, type[IMPORT_google_protobuf_message.Message]] = {
+ 'Create': reboot.ping.ping_api_pb2.CounterCreateRequest,
+ 'Increment': google.protobuf.empty_pb2.Empty,
+ 'Value': google.protobuf.empty_pb2.Empty,
+ 'Description': google.protobuf.empty_pb2.Empty,
+ }
+
+ # Get authorizer, if any, converting from a rule if necessary.
+ def convert_authorizer_rule_if_necessary(
+ authorizer_or_rule: IMPORT_typing.Optional[
+ IMPORT_reboot_aio_auth_authorizers.Authorizer | IMPORT_reboot_aio_auth_authorizers.AuthorizerRule
+ ]
+ ) -> IMPORT_reboot_aio_auth_authorizers.Authorizer:
+
+ # If no authorizer or rule is provided, return the default
+ # authorizer which allows if app internal or allows if in
+ # dev mode (and logs some warnings to help the user
+ # realize where they are missing authorization).
+ if authorizer_or_rule is None:
+ return IMPORT_reboot_aio_auth_authorizers.DefaultAuthorizer(
+ 'Counter',
+ is_user_type=False,
+ )
+
+ if isinstance(authorizer_or_rule, IMPORT_reboot_aio_auth_authorizers.AuthorizerRule):
+ return CounterAuthorizer(
+ _default=authorizer_or_rule
+ )
+
+ return authorizer_or_rule
+
+ self._authorizer = convert_authorizer_rule_if_necessary(
+ servicer.authorizer()
+ )
+
+ # Create token verifier.
+ self._token_verifier: IMPORT_typing.Optional[IMPORT_reboot_aio_auth_token_verifiers.TokenVerifier] = (
+ servicer.token_verifier() or token_verifier
+ )
+
+ # Since users specify errors as proto messages they can't raise them
+ # directly - to do so they have to use the `Aborted` wrapper, which will
+ # hold the original proto message. On errors we'll need to check whether
+ # such wrappers hold a proto message for a specified error, so we can
+ # avoid retrying tasks that complete with a specified error.
+ self._specified_errors_by_service_method_name: dict[str, list[str]] = {
+ }
+
+
+ def add_to_server(self, server: IMPORT_grpc.aio.Server) -> None:
+ reboot.ping.ping_api_pb2_grpc.add_CounterMethodsServicer_to_server(
+ self, server
+ )
+
+ async def inspect(self, state_ref: IMPORT_reboot_aio_types.StateRef) -> IMPORT_typing.AsyncIterator[IMPORT_google_protobuf_message.Message]:
+ """Implementation of `Middleware.inspect()`."""
+ context = self.create_context(
+ headers=IMPORT_reboot_aio_headers.Headers(
+ application_id=self.application_id,
+ state_ref=state_ref,
+ ),
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ method="inspect",
+ context_type=IMPORT_reboot_aio_contexts.ReaderContext,
+ )
+
+ async with self._state_manager.streaming_reader_idempotency_key(
+ context,
+ self._servicer.__state_type__,
+ authorize=None,
+ ) as states:
+ async for (state, idempotency_key) in states:
+ yield state
+
+ async def react_query(
+ self,
+ headers: IMPORT_reboot_aio_headers.Headers,
+ method: str,
+ request_bytes: bytes,
+ ) -> IMPORT_typing.AsyncIterator[tuple[IMPORT_typing.Optional[IMPORT_google_protobuf_message.Message], list[IMPORT_uuid.UUID]]]:
+ """Returns the response of calling 'method' given a message
+ deserialized from the provided 'request_bytes' for each state
+ update that creates a different response.
+
+ # The caller (react.py) should have already ensured that this server
+ # is authoritative for this traffic.
+ assert self.placement_client.server_for_actor(
+ headers.application_id,
+ headers.state_ref,
+ ) == self._server_id
+
+ NOTE: only unary reader methods are supported."""
+ # Need to define these up here since we can only do that once.
+ last_response: IMPORT_typing.Optional[IMPORT_google_protobuf_message.Message] = None
+ aggregated_idempotency_keys: list[IMPORT_uuid.UUID] = []
+ if method == 'Create':
+ # Invariant here is that users should not have called this
+ # directly but only through code generated React
+ # components which should not have been generated except
+ # for valid method candidates.
+ logger.warning(
+ "Got a React query request with an invalid method name: "
+ f"Method '{method}' is invalid for servicer Counter."
+ "\n"
+ "Do you have a browser tab open for an older version "
+ "of this application, or for a different application all together?"
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.InvalidMethod(),
+ message=
+ f"Method '{method}' is invalid"
+ )
+ yield # Necessary for type checking.
+ elif method == 'Increment':
+ # Invariant here is that users should not have called this
+ # directly but only through code generated React
+ # components which should not have been generated except
+ # for valid method candidates.
+ logger.warning(
+ "Got a React query request with an invalid method name: "
+ f"Method '{method}' is invalid for servicer Counter."
+ "\n"
+ "Do you have a browser tab open for an older version "
+ "of this application, or for a different application all together?"
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.InvalidMethod(),
+ message=
+ f"Method '{method}' is invalid"
+ )
+ yield # Necessary for type checking.
+ elif method == 'Value':
+
+ context = self.create_context(
+ headers=headers,
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ method='Value',
+ context_type=IMPORT_reboot_aio_contexts.ReaderContext,
+ )
+
+ with IMPORT_reboot_aio_tracing.span(
+ state_name=f"{context.state_type_name}('{context.state_id}')",
+ span_name="Value() (reactively)",
+ # The naming above matches Python, but not TypeScript.
+ python_specific=True,
+ level=IMPORT_reboot_aio_tracing.TraceLevel.CUSTOMER,
+ ):
+ context.auth = await self._maybe_verify_token(
+ headers=headers, method='Value'
+ )
+
+ request = google.protobuf.empty_pb2.Empty()
+ request.ParseFromString(request_bytes)
+
+ async with self._state_manager.reactively(
+ context,
+ self._servicer.__state_type__,
+ authorize=self._maybe_authorize(
+ method_name='reboot.ping.CounterMethods.Value',
+ headers=headers,
+ auth=context.auth,
+ request=request,
+ ),
+ ) as states:
+ async for (state, idempotency_keys) in states:
+
+ aggregated_idempotency_keys.extend(idempotency_keys)
+
+ # Note: This does not do any defensive copying currently:
+ # see https://github.com/reboot-dev/respect/issues/2636.
+ @IMPORT_reboot_aio_internals_middleware.maybe_run_function_twice_to_validate_effects
+ async def run__Value(validating_effects: bool) -> IMPORT_google_protobuf_message.Message:
+ return await self.__Value(
+ context,
+ state,
+ request,
+ validating_effects=validating_effects,
+ )
+
+ response = await run__Value()
+
+ if last_response != response:
+ yield (response, aggregated_idempotency_keys)
+ last_response = response
+ else:
+ yield (None, aggregated_idempotency_keys)
+
+ aggregated_idempotency_keys.clear()
+ elif method == 'Description':
+
+ context = self.create_context(
+ headers=headers,
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ method='Description',
+ context_type=IMPORT_reboot_aio_contexts.ReaderContext,
+ )
+
+ with IMPORT_reboot_aio_tracing.span(
+ state_name=f"{context.state_type_name}('{context.state_id}')",
+ span_name="Description() (reactively)",
+ # The naming above matches Python, but not TypeScript.
+ python_specific=True,
+ level=IMPORT_reboot_aio_tracing.TraceLevel.CUSTOMER,
+ ):
+ context.auth = await self._maybe_verify_token(
+ headers=headers, method='Description'
+ )
+
+ request = google.protobuf.empty_pb2.Empty()
+ request.ParseFromString(request_bytes)
+
+ async with self._state_manager.reactively(
+ context,
+ self._servicer.__state_type__,
+ authorize=self._maybe_authorize(
+ method_name='reboot.ping.CounterMethods.Description',
+ headers=headers,
+ auth=context.auth,
+ request=request,
+ ),
+ ) as states:
+ async for (state, idempotency_keys) in states:
+
+ aggregated_idempotency_keys.extend(idempotency_keys)
+
+ # Note: This does not do any defensive copying currently:
+ # see https://github.com/reboot-dev/respect/issues/2636.
+ @IMPORT_reboot_aio_internals_middleware.maybe_run_function_twice_to_validate_effects
+ async def run__Description(validating_effects: bool) -> IMPORT_google_protobuf_message.Message:
+ return await self.__Description(
+ context,
+ state,
+ request,
+ validating_effects=validating_effects,
+ )
+
+ response = await run__Description()
+
+ if last_response != response:
+ yield (response, aggregated_idempotency_keys)
+ last_response = response
+ else:
+ yield (None, aggregated_idempotency_keys)
+
+ aggregated_idempotency_keys.clear()
+ else:
+ logger.warning(
+ "Got a React query request with an invalid method name: "
+ f"Method '{method}' is invalid for servicer Counter."
+ "\n"
+ "Do you have a browser tab open for an older version "
+ "of this application, or for a different application all together?"
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.InvalidMethod(),
+ message=
+ f"Method '{method}' not found"
+ )
+ yield # Unreachable but necessary for mypy.
+
+ async def react_mutate(
+ self,
+ headers: IMPORT_reboot_aio_headers.Headers,
+ method: str,
+ request_bytes: bytes,
+ ) -> IMPORT_google_protobuf_message.Message:
+ """Returns the response of calling 'method' given a message
+ deserialized from the provided 'request_bytes'."""
+ if method == 'Create':
+ request = reboot.ping.ping_api_pb2.CounterCreateRequest()
+ request.ParseFromString(request_bytes)
+
+ # NOTE: we automatically retry mutations that come through
+ # React when we get a `IMPORT_grpc.StatusCode.UNAVAILABLE` to
+ # match the retry logic we do in the React code generated
+ # to handle lack/loss of connectivity.
+ #
+ # TODO(benh): revisit this decision if we ever see reason
+ # to call `react_mutate()` from any place other than where
+ # we're executing React (e.g., browser, next.js server
+ # component, etc).
+ call_backoff = IMPORT_reboot_aio_backoff.Backoff()
+ while True:
+ # We make a full-fledged gRPC call, so that if this traffic
+ # was misrouted (i.e. this server is not authoritative
+ # for the state), it will now go to the right place. The
+ # receiving middleware will handle things like effect
+ # validation and so forth.
+ assert headers.application_id is not None # Guaranteed by `Headers`.
+ stub = reboot.ping.ping_api_pb2_grpc.CounterMethodsStub(
+ self.channel_manager.get_channel_to(
+ self.placement_client.address_for_actor(
+ headers.application_id,
+ headers.state_ref,
+ )
+ )
+ )
+ call = stub.Create(
+ request=request,
+ metadata=headers.to_grpc_metadata(),
+ )
+ try:
+ return await call
+ except IMPORT_grpc.aio.AioRpcError as error:
+ if error.code() == IMPORT_grpc.StatusCode.UNAVAILABLE:
+ await call_backoff()
+ continue
+
+ # Reconstitute the error that the server threw, if it was a declared error.
+ status = await IMPORT_rpc_status_async.from_call(call)
+ if status is not None:
+ raise Counter.CreateAborted.from_status(
+ status
+ ) from None
+ raise Counter.CreateAborted.from_grpc_aio_rpc_error(
+ error
+ ) from None
+
+ elif method == 'Increment':
+ request = google.protobuf.empty_pb2.Empty()
+ request.ParseFromString(request_bytes)
+
+ # NOTE: we automatically retry mutations that come through
+ # React when we get a `IMPORT_grpc.StatusCode.UNAVAILABLE` to
+ # match the retry logic we do in the React code generated
+ # to handle lack/loss of connectivity.
+ #
+ # TODO(benh): revisit this decision if we ever see reason
+ # to call `react_mutate()` from any place other than where
+ # we're executing React (e.g., browser, next.js server
+ # component, etc).
+ call_backoff = IMPORT_reboot_aio_backoff.Backoff()
+ while True:
+ # We make a full-fledged gRPC call, so that if this traffic
+ # was misrouted (i.e. this server is not authoritative
+ # for the state), it will now go to the right place. The
+ # receiving middleware will handle things like effect
+ # validation and so forth.
+ assert headers.application_id is not None # Guaranteed by `Headers`.
+ stub = reboot.ping.ping_api_pb2_grpc.CounterMethodsStub(
+ self.channel_manager.get_channel_to(
+ self.placement_client.address_for_actor(
+ headers.application_id,
+ headers.state_ref,
+ )
+ )
+ )
+ call = stub.Increment(
+ request=request,
+ metadata=headers.to_grpc_metadata(),
+ )
+ try:
+ return await call
+ except IMPORT_grpc.aio.AioRpcError as error:
+ if error.code() == IMPORT_grpc.StatusCode.UNAVAILABLE:
+ await call_backoff()
+ continue
+
+ # Reconstitute the error that the server threw, if it was a declared error.
+ status = await IMPORT_rpc_status_async.from_call(call)
+ if status is not None:
+ raise Counter.IncrementAborted.from_status(
+ status
+ ) from None
+ raise Counter.IncrementAborted.from_grpc_aio_rpc_error(
+ error
+ ) from None
+
+ elif method == 'Value':
+ # Invariant here is that users should not have called this
+ # directly but only through code generated React
+ # components which should not have been generated except
+ # for valid method candidates.
+ logger.warning(
+ "Got a react mutate request with an invalid method name: "
+ "Method 'Value' is invalid for servicer Counter."
+ "\n"
+ "Do you have an old browser tab still open for an older version "
+ "of this application, or a different application all together?"
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.InvalidMethod(),
+ message=f"Method '{method}' is invalid"
+ )
+ elif method == 'Description':
+ # Invariant here is that users should not have called this
+ # directly but only through code generated React
+ # components which should not have been generated except
+ # for valid method candidates.
+ logger.warning(
+ "Got a react mutate request with an invalid method name: "
+ "Method 'Description' is invalid for servicer Counter."
+ "\n"
+ "Do you have an old browser tab still open for an older version "
+ "of this application, or a different application all together?"
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.InvalidMethod(),
+ message=f"Method '{method}' is invalid"
+ )
+ else:
+ logger.warning(
+ "Got a react mutate request with an invalid method name: "
+ f"Method '{method}' is invalid for servicer Counter."
+ "\n"
+ "Do you have an old browser tab still open for an older version "
+ "of this application, or a different application all together?"
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.InvalidMethod(),
+ message=
+ f"Method '{method}' not found"
+ )
+
+ async def dispatch(
+ self,
+ task: IMPORT_reboot_aio_tasks.TaskEffect,
+ *,
+ only_validate: bool = False,
+ on_loop_iteration: IMPORT_reboot_aio_internals_tasks_dispatcher.OnLoopIterationCallable = (lambda iteration, next_iteration_schedule: None),
+ ) -> IMPORT_reboot_aio_internals_tasks_dispatcher.TaskResponseOrStatus:
+ """Dispatches the tasks to execute unless 'only_validate' is set to
+ true, in which case just ensures that the task actually exists.
+ Note that this function will be called *by* tasks_dispatcher; it will
+ not itself call into tasks_dispatcher."""
+
+ if 'Create' == task.method_name:
+ if only_validate:
+ # TODO(benh): validate 'task.request' is correct type.
+ return (google.protobuf.empty_pb2.Empty(), None)
+
+ # Use an inline method to create a new scope, so that we can use
+ # variable names like `context` and `effects` in multiple branches
+ # in this code (notably when there are multiple task types) without
+ # hitting a mypy error that the variable's type is not consistent.
+ async def run_Create(
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ *,
+ validating_effects: bool = False,
+ ):
+ async with self._state_manager.task_workflow(
+ context,
+ task,
+ on_loop_iteration=on_loop_iteration,
+ validating_effects=validating_effects,
+ ) as complete:
+ try:
+ response = await (CounterWorkflowStub(
+ context=context,
+ state_ref=context._state_ref,
+ ).Create(
+ CounterCreateRequestFromProto(IMPORT_typing.cast(reboot.ping.ping_api_pb2.CounterCreateRequest, task.request)),
+ bearer_token=__internal_magic_token__,
+ idempotency=IMPORT_reboot_aio_idempotency.Idempotency(
+ alias=f'Task {IMPORT_uuid.UUID(bytes=task.task_id.task_uuid)}',
+ ),
+ ))
+ await complete(task, (response, None))
+ return (response, None)
+ except IMPORT_asyncio.CancelledError:
+ # Do not retry a task if it was cancelled by a caller.
+ if self.tasks_dispatcher.is_task_cancelled(task.task_id.task_uuid):
+ result = (
+ None,
+ IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Cancelled(),
+ ).to_status(),
+ )
+ await complete(task, result)
+ return result
+ else:
+ raise
+ except IMPORT_reboot.aio.aborted.Aborted as aborted:
+ error_type = f'{aborted._protobuf_error.__class__.__module__}.{aborted._protobuf_error.__class__.__qualname__}'
+ # Do not retry a task if the error was specified in the
+ # proto file.
+ if error_type in self._specified_errors_by_service_method_name.get('reboot.ping.CounterMethods.Create', []):
+ result = (None, aborted.to_status())
+ await complete(task, result)
+ return result
+ raise
+
+
+ return await run_Create(
+ self.create_context(
+ headers=IMPORT_reboot_aio_headers.Headers(
+ application_id=self.application_id,
+ state_ref=IMPORT_reboot_aio_types.StateRef(task.task_id.state_ref),
+ ),
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ method='Create',
+ context_type=IMPORT_reboot_aio_contexts.WorkflowContext,
+ task=task,
+ # Propagate state manager and state type so that
+ # `until()` calls (via `WorkflowContext.retry_reactively_until()`)
+ # can enter `reactively()` scoped to each `until()`
+ # invocation.
+ reactively_state_manager=self._state_manager,
+ reactively_state_type=(
+ self._servicer.__state_type__
+ ),
+ )
+ )
+ elif 'Increment' == task.method_name:
+ if only_validate:
+ # TODO(benh): validate 'task.request' is correct type.
+ return (reboot.ping.ping_api_pb2.CounterIncrementResponse(), None)
+
+ # Use an inline method to create a new scope, so that we can use
+ # variable names like `context` and `effects` in multiple branches
+ # in this code (notably when there are multiple task types) without
+ # hitting a mypy error that the variable's type is not consistent.
+ async def run_Increment(
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ *,
+ validating_effects: bool = False,
+ ):
+ async with self._state_manager.task_workflow(
+ context,
+ task,
+ on_loop_iteration=on_loop_iteration,
+ validating_effects=validating_effects,
+ ) as complete:
+ try:
+ response = await (CounterWorkflowStub(
+ context=context,
+ state_ref=context._state_ref,
+ ).Increment(
+ bearer_token=__internal_magic_token__,
+ idempotency=IMPORT_reboot_aio_idempotency.Idempotency(
+ alias=f'Task {IMPORT_uuid.UUID(bytes=task.task_id.task_uuid)}',
+ ),
+ ))
+ await complete(task, (response, None))
+ return (response, None)
+ except IMPORT_asyncio.CancelledError:
+ # Do not retry a task if it was cancelled by a caller.
+ if self.tasks_dispatcher.is_task_cancelled(task.task_id.task_uuid):
+ result = (
+ None,
+ IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Cancelled(),
+ ).to_status(),
+ )
+ await complete(task, result)
+ return result
+ else:
+ raise
+ except IMPORT_reboot.aio.aborted.Aborted as aborted:
+ error_type = f'{aborted._protobuf_error.__class__.__module__}.{aborted._protobuf_error.__class__.__qualname__}'
+ # Do not retry a task if the error was specified in the
+ # proto file.
+ if error_type in self._specified_errors_by_service_method_name.get('reboot.ping.CounterMethods.Increment', []):
+ result = (None, aborted.to_status())
+ await complete(task, result)
+ return result
+ raise
+
+
+ return await run_Increment(
+ self.create_context(
+ headers=IMPORT_reboot_aio_headers.Headers(
+ application_id=self.application_id,
+ state_ref=IMPORT_reboot_aio_types.StateRef(task.task_id.state_ref),
+ ),
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ method='Increment',
+ context_type=IMPORT_reboot_aio_contexts.WorkflowContext,
+ task=task,
+ # Propagate state manager and state type so that
+ # `until()` calls (via `WorkflowContext.retry_reactively_until()`)
+ # can enter `reactively()` scoped to each `until()`
+ # invocation.
+ reactively_state_manager=self._state_manager,
+ reactively_state_type=(
+ self._servicer.__state_type__
+ ),
+ )
+ )
+ elif 'Value' == task.method_name:
+ if only_validate:
+ # TODO(benh): validate 'task.request' is correct type.
+ return (reboot.ping.ping_api_pb2.CounterValueResponse(), None)
+
+ # Use an inline method to create a new scope, so that we can use
+ # variable names like `context` and `effects` in multiple branches
+ # in this code (notably when there are multiple task types) without
+ # hitting a mypy error that the variable's type is not consistent.
+ async def run_Value(
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ *,
+ validating_effects: bool = False,
+ ):
+ async with self._state_manager.task_workflow(
+ context,
+ task,
+ on_loop_iteration=on_loop_iteration,
+ validating_effects=validating_effects,
+ ) as complete:
+ try:
+ response = await (CounterWorkflowStub(
+ context=context,
+ state_ref=context._state_ref,
+ ).Value(
+ bearer_token=__internal_magic_token__,
+ ))
+ await complete(task, (response, None))
+ return (response, None)
+ except IMPORT_asyncio.CancelledError:
+ # Do not retry a task if it was cancelled by a caller.
+ if self.tasks_dispatcher.is_task_cancelled(task.task_id.task_uuid):
+ result = (
+ None,
+ IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Cancelled(),
+ ).to_status(),
+ )
+ await complete(task, result)
+ return result
+ else:
+ raise
+ except IMPORT_reboot.aio.aborted.Aborted as aborted:
+ error_type = f'{aborted._protobuf_error.__class__.__module__}.{aborted._protobuf_error.__class__.__qualname__}'
+ # Do not retry a task if the error was specified in the
+ # proto file.
+ if error_type in self._specified_errors_by_service_method_name.get('reboot.ping.CounterMethods.Value', []):
+ result = (None, aborted.to_status())
+ await complete(task, result)
+ return result
+ raise
+
+
+ return await run_Value(
+ self.create_context(
+ headers=IMPORT_reboot_aio_headers.Headers(
+ application_id=self.application_id,
+ state_ref=IMPORT_reboot_aio_types.StateRef(task.task_id.state_ref),
+ ),
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ method='Value',
+ context_type=IMPORT_reboot_aio_contexts.WorkflowContext,
+ task=task,
+ # Propagate state manager and state type so that
+ # `until()` calls (via `WorkflowContext.retry_reactively_until()`)
+ # can enter `reactively()` scoped to each `until()`
+ # invocation.
+ reactively_state_manager=self._state_manager,
+ reactively_state_type=(
+ self._servicer.__state_type__
+ ),
+ )
+ )
+ elif 'Description' == task.method_name:
+ if only_validate:
+ # TODO(benh): validate 'task.request' is correct type.
+ return (reboot.ping.ping_api_pb2.CounterDescriptionResponse(), None)
+
+ # Use an inline method to create a new scope, so that we can use
+ # variable names like `context` and `effects` in multiple branches
+ # in this code (notably when there are multiple task types) without
+ # hitting a mypy error that the variable's type is not consistent.
+ async def run_Description(
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ *,
+ validating_effects: bool = False,
+ ):
+ async with self._state_manager.task_workflow(
+ context,
+ task,
+ on_loop_iteration=on_loop_iteration,
+ validating_effects=validating_effects,
+ ) as complete:
+ try:
+ response = await (CounterWorkflowStub(
+ context=context,
+ state_ref=context._state_ref,
+ ).Description(
+ bearer_token=__internal_magic_token__,
+ ))
+ await complete(task, (response, None))
+ return (response, None)
+ except IMPORT_asyncio.CancelledError:
+ # Do not retry a task if it was cancelled by a caller.
+ if self.tasks_dispatcher.is_task_cancelled(task.task_id.task_uuid):
+ result = (
+ None,
+ IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Cancelled(),
+ ).to_status(),
+ )
+ await complete(task, result)
+ return result
+ else:
+ raise
+ except IMPORT_reboot.aio.aborted.Aborted as aborted:
+ error_type = f'{aborted._protobuf_error.__class__.__module__}.{aborted._protobuf_error.__class__.__qualname__}'
+ # Do not retry a task if the error was specified in the
+ # proto file.
+ if error_type in self._specified_errors_by_service_method_name.get('reboot.ping.CounterMethods.Description', []):
+ result = (None, aborted.to_status())
+ await complete(task, result)
+ return result
+ raise
+
+
+ return await run_Description(
+ self.create_context(
+ headers=IMPORT_reboot_aio_headers.Headers(
+ application_id=self.application_id,
+ state_ref=IMPORT_reboot_aio_types.StateRef(task.task_id.state_ref),
+ ),
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ method='Description',
+ context_type=IMPORT_reboot_aio_contexts.WorkflowContext,
+ task=task,
+ # Propagate state manager and state type so that
+ # `until()` calls (via `WorkflowContext.retry_reactively_until()`)
+ # can enter `reactively()` scoped to each `until()`
+ # invocation.
+ reactively_state_manager=self._state_manager,
+ reactively_state_type=(
+ self._servicer.__state_type__
+ ),
+ )
+ )
+
+ # There are no tasks for this service.
+ start_or_validate = "start" if not only_validate else "validate"
+ raise RuntimeError(
+ f"Attempted to {start_or_validate} task '{task.method_name}' "
+ f"on 'Counter' which does not exist"
+ )
+
+ # Counter specific methods:
+ async def __Create(
+ self,
+ context: IMPORT_reboot_aio_contexts.WriterContext,
+ state: reboot.ping.ping_api_pb2.Counter,
+ request: reboot.ping.ping_api_pb2.CounterCreateRequest,
+ *,
+ validating_effects: bool,
+ ) -> Counter.CreateEffects:
+ try:
+ states_being_constructed.add(context.state_id)
+ typed_state: Counter.State = CounterFromProto(state, is_initial_state=(context.state_id in states_being_constructed))
+
+ if ongoing_transaction_state_key(context) in ongoing_transaction_states:
+ typed_state = ongoing_transaction_states[ongoing_transaction_state_key(context)].model_copy(deep=True)
+ response = (
+ await self._servicer._Create(
+ context=context,
+ state=typed_state,
+ request=request
+ )
+ )
+
+ # TODO: it's premature to overwrite the state now given that the
+ # writer might still "fail" and an error will get propagated back
+ # to the ongoing transaction which will still see the effects of
+ # this writer. What we should be doing instead is creating a
+ # callback API that we invoke only after a writer completes
+ # that lets us update the state _reference_ then.
+ if ongoing_transaction_state_key(context) in ongoing_transaction_states:
+ for field, value in typed_state.model_dump().items():
+ setattr(ongoing_transaction_states[ongoing_transaction_state_key(context)], field, value)
+
+ CounterToProto(typed_state, state)
+
+ IMPORT_reboot_aio_types.assert_type(
+ response,
+ [google.protobuf.empty_pb2.Empty],
+ )
+ self.maybe_raise_effect_validation_retry(
+ logger=logger,
+ idempotency_manager=context,
+ method_name='Counter.Create',
+ validating_effects=validating_effects,
+ context=context,
+ )
+ return Counter.CreateEffects(
+ state=state,
+ response=response,
+ tasks=context._tasks,
+ _colocated_upserts=context._colocated_upserts,
+ )
+ except IMPORT_reboot_aio_contexts.EffectValidationRetry:
+ # Doing effect validation, just let this propagate.
+ raise
+ except IMPORT_reboot.aio.aborted.Aborted as aborted:
+ # If the caller aborted due to a retryable error, just
+ # propagate the aborted instead of propagating `Unknown`
+ # so that a client can transparently retry.
+ if IMPORT_reboot.aio.aborted.is_retryable(aborted):
+ raise aborted
+ # Log any _unhandled_ abort stack traces to make it
+ # easier for debugging.
+ #
+ # NOTE: we don't log if we're a task as it will be logged
+ # in `public/reboot/aio/internals/tasks_dispatcher.py` instead.
+ aborted_type: IMPORT_typing.Optional[type] = None
+ aborted_type = Counter.CreateAborted
+ if isinstance(aborted, IMPORT_reboot.aio.aborted.SystemAborted):
+ # Not logging when within `node` as we already log there.
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ f"Unhandled (in 'reboot.ping.Counter.Create') {aborted}; propagating as 'Unknown'\n" +
+ ''.join(IMPORT_traceback.format_exception(aborted))
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ # TODO(benh): consider whether or not we want to
+ # include the 'package.service.method' which may
+ # get concatenated together forming a kind of
+ # "stack trace"; while it's super helpful for
+ # debugging, it does expose implementation
+ # information.
+ message=f"unhandled (in 'reboot.ping.Counter.Create') {aborted}"
+ )
+ else:
+ if (
+ aborted_type is not None and
+ not isinstance(aborted, aborted_type) and
+ aborted_type.is_declared_error(aborted.error)
+ ):
+ # We propagate declared errors that might have
+ # come from another call, i.e., we might have an
+ # `Aborted` but not for this method but the
+ # `Aborted` that we have has an error that this
+ # method declared. This allows a developer to
+ # simply add the declared error to their `.proto`
+ # file rather than having to catch and re-raise
+ # the error with their own aborted type.
+ if context.task is None:
+ logger.warning(
+ f"Propagating unhandled but declared error (in 'reboot.ping.Counter.Create') {aborted}"
+ )
+ elif (
+ aborted_type is None or
+ not isinstance(aborted, aborted_type)
+ ):
+ # Not logging when within `node` as we already log there.
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ f"Unhandled (in 'reboot.ping.Counter.Create') {aborted}; propagating as 'Unknown'\n" +
+ ''.join(IMPORT_traceback.format_exception(aborted))
+ )
+ # If this wasn't a declared error than we
+ # propagate it as `Unknown`.
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ # TODO(benh): consider whether or not we want to
+ # include the 'package.service.method' which may
+ # get concatenated together forming a kind of
+ # "stack trace"; while it's super helpful for
+ # debugging, it does expose implementation
+ # information.
+ message=f"unhandled (in 'reboot.ping.Counter.Create') {aborted}"
+ )
+
+ raise
+ except IMPORT_asyncio.CancelledError:
+ # It's pretty normal for an RPC to be cancelled; it's not useful to
+ # print a stack trace.
+ raise
+ except IMPORT_google_protobuf_message.DecodeError as decode_error:
+ # We usually see this error when we are trying to construct a proto
+ # message which is too deeply nested: protobuf has a limit of 100
+ # nested messages. See the limits here:
+ # https://protobuf.dev/programming-guides/proto-limits/
+
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ "Unhandled (in 'reboot.ping.Counter.Create') "
+ f"{type(decode_error).__name__}{': ' + str(decode_error) if len(str(decode_error)) > 0 else ''}; "
+ "This is usually caused by a deeply nested protobuf message, which is not supported by protobuf.\n"
+ "See the limits here: https://protobuf.dev/programming-guides/proto-limits/" +
+ ''.join(IMPORT_traceback.format_exception(decode_error))
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ message=f"unhandled (in 'reboot.ping.Counter.Create') {decode_error}; "
+ "This is usually caused by a deeply nested protobuf message, which is not supported by protobuf.\n"
+ "See the limits here: https://protobuf.dev/programming-guides/proto-limits/"
+ )
+ except BaseException as exception:
+ # Not logging when within `node` as we already log there.
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ "Unhandled (in 'reboot.ping.Counter.Create') "
+ f"{type(exception).__name__}{': ' + str(exception) if len(str(exception)) > 0 else ''}; "
+ "propagating as 'Unknown'\n" +
+ ''.join(IMPORT_traceback.format_exception(exception))
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ # TODO(benh): consider whether or not we want to
+ # include the 'package.service.method' which may
+ # get concatenated together forming a kind of
+ # "stack trace"; while it's super helpful for
+ # debugging, it does expose implementation
+ # information.
+ message=f"unhandled (in 'reboot.ping.Counter.Create') {type(exception).__name__}: {exception}"
+ )
+ finally:
+ states_being_constructed.remove(context.state_id)
+
+ @IMPORT_reboot_aio_tracing.function_span(
+ # We expect an `EffectValidationRetry` exception; that's not an error.
+ set_status_on_exception=False
+ )
+ async def _Create(
+ self,
+ request: reboot.ping.ping_api_pb2.CounterCreateRequest,
+ context: IMPORT_reboot_aio_contexts.WriterContext,
+ *,
+ validating_effects: bool,
+ grpc_context: IMPORT_typing.Optional[IMPORT_grpc.aio.ServicerContext] = None,
+ ) -> google.protobuf.empty_pb2.Empty:
+ # Try to verify the token if a token verifier exists.
+ context.auth = await self._maybe_verify_token(
+ headers=context._headers, method='Create'
+ )
+
+ # Try and "preload" to opportunistically load both state and
+ # idempotent mutations from the database in a single
+ # round-trip. This is fire-and-forget; subsequent calls to
+ # `_load()` and `check_for_idempotent_mutation()` should find
+ # the preloaded data and skip their own calls to the database.
+ #
+ # TODO: a tradeoff we're making here is that we load
+ # idempotent mutations even if this is just a reader and we
+ # don't need them. Consider only preloading for writers and
+ # transactions if loading idempotent mutations is
+ # unnecessarily slow (and we haven't optimized that via a
+ # database stored bloom filter).
+ #
+ # We do this _after_ verifying the token so it is not a
+ # denial-of-service attack vector, i.e., only callers with
+ # valid tokens may trigger preloads on the database.
+ self._state_manager.preload(
+ context.state_type_name,
+ context._state_ref,
+ )
+
+ # Check if we already have performed this mutation!
+ #
+ # We do this _before_ calling 'transactionally()' because
+ # if this call is for a transaction method _and_ we've
+ # already performed the transaction then we don't want to
+ # become a transaction participant (again) we just want to
+ # return the transaction's response.
+ idempotent_mutation = await self._state_manager.check_for_idempotent_mutation(
+ context
+ )
+
+ if idempotent_mutation is not None:
+ response = google.protobuf.empty_pb2.Empty()
+ response.ParseFromString(idempotent_mutation.response)
+ return response
+
+ async with self._state_manager.transactionally(
+ context,
+ self.tasks_dispatcher,
+ aborted_type=Counter.CreateAborted,
+ ) as transaction:
+ async with self._state_manager.writer(
+ context,
+ self._servicer.__state_type__,
+ self.tasks_dispatcher,
+ authorize=self._maybe_authorize(
+ method_name='reboot.ping.CounterMethods.Create',
+ headers=context._headers,
+ auth=context.auth,
+ request=request,
+ ),
+ transaction=transaction,
+ from_constructor=True,
+ requires_constructor=True,
+ ) as (state, writer):
+
+ effects = await self.__Create(
+ context,
+ state,
+ request,
+ validating_effects=validating_effects,
+ )
+
+ await writer.complete(effects)
+
+ # TODO: We need a single `Effects` superclass for all methods, so we
+ # would need to make it "partially" generic (with per-method subclasses
+ # filling out the rest of the generic parameters) in order to fix this.
+ return effects.response # type: ignore[return-value]
+
+ async def _schedule_Create(
+ self,
+ *,
+ request: reboot.ping.ping_api_pb2.CounterCreateRequest,
+ headers: IMPORT_reboot_aio_headers.Headers,
+ grpc_context: IMPORT_grpc.aio.ServicerContext,
+ ) -> tuple[IMPORT_reboot_aio_contexts.WriterContext, google.protobuf.empty_pb2.Empty]:
+ context: IMPORT_reboot_aio_contexts.WriterContext = self.create_context(
+ headers=headers,
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ method='Create',
+ context_type=IMPORT_reboot_aio_contexts.WriterContext,
+ )
+ response = google.protobuf.empty_pb2.Empty()
+
+ # Try to verify the token if a token verifier exists.
+ context.auth = await self._maybe_verify_token(
+ headers=headers, method='Create'
+ )
+
+ # Try and "preload" to opportunistically load both state and
+ # idempotent mutations from the database in a single
+ # round-trip. This is fire-and-forget; subsequent calls to
+ # `_load()` and `check_for_idempotent_mutation()` should find
+ # the preloaded data and skip their own calls to the database.
+ #
+ # TODO: a tradeoff we're making here is that we load
+ # idempotent mutations even if this is just a reader and we
+ # don't need them. Consider only preloading for writers and
+ # transactions if loading idempotent mutations is
+ # unnecessarily slow (and we haven't optimized that via a
+ # database stored bloom filter).
+ #
+ # We do this _after_ verifying the token so it is not a
+ # denial-of-service attack vector, i.e., only callers with
+ # valid tokens may trigger preloads on the database.
+ self._state_manager.preload(
+ context.state_type_name,
+ context._state_ref,
+ )
+
+ # Check if we already have performed this schedule! Note that
+ # we need to do this for all kinds of methods because this is
+ # effectively a mutation (actually a `writer`, see below).
+ #
+ # We do this _before_ calling 'transactionally()' because
+ # if this call is for a transaction method _and_ we've
+ # already performed the transaction then we don't want to
+ # become a transaction participant (again) we just want to
+ # return the transaction's response.
+ idempotent_mutation = await self._state_manager.check_for_idempotent_mutation(
+ context
+ )
+
+ if idempotent_mutation is not None:
+ response.ParseFromString(idempotent_mutation.response)
+
+ # We should have only scheduled a single task!
+ assert len(idempotent_mutation.task_ids) == 1
+ assert grpc_context is not None
+ grpc_context.set_trailing_metadata(
+ grpc_context.trailing_metadata() +
+ (
+ (
+ IMPORT_reboot_aio_headers.TASK_ID_UUID,
+ str(IMPORT_uuid.UUID(bytes=idempotent_mutation.task_ids[0].task_uuid))
+ ),
+ )
+ )
+
+ return context, response
+
+ async with self._state_manager.transactionally(
+ context,
+ self.tasks_dispatcher,
+ aborted_type=Counter.CreateAborted,
+ ) as transaction:
+
+ async with self._state_manager.writer(
+ context,
+ self._servicer.__state_type__,
+ self.tasks_dispatcher,
+ transaction=transaction,
+ authorize=self._maybe_authorize(
+ method_name='reboot.ping.CounterMethods.Create',
+ headers=context._headers,
+ auth=context.auth,
+ request=request,
+ ),
+ from_constructor=True,
+ requires_constructor=True
+ ) as (state, writer):
+
+ task = await CounterServicerTasks(
+ context=context,
+ state_ref=context._state_ref,
+ ).Create(
+ CounterCreateRequestFromProto(request),
+ schedule=context._headers.task_schedule,
+ )
+
+ effects = IMPORT_reboot_aio_state_managers.Effects(
+ response=response,
+ state=state,
+ tasks=[task],
+ )
+
+ assert effects.tasks is not None
+
+ await writer.complete(effects)
+
+ assert grpc_context is not None
+
+ grpc_context.set_trailing_metadata(
+ grpc_context.trailing_metadata() +
+ (
+ (
+ IMPORT_reboot_aio_headers.TASK_ID_UUID,
+ str(IMPORT_uuid.UUID(bytes=task.task_id.task_uuid))
+ ),
+ )
+ )
+
+ return context, response
+
+ return context, response
+
+
+ # Entrypoint for non-reactive network calls (i.e. typical gRPC calls).
+ async def Create(
+ self,
+ request: reboot.ping.ping_api_pb2.CounterCreateRequest,
+ grpc_context: IMPORT_grpc.aio.ServicerContext,
+ ) -> google.protobuf.empty_pb2.Empty:
+ headers = IMPORT_reboot_aio_headers.Headers.from_grpc_context(grpc_context)
+ assert headers.application_id is not None # Guaranteed by `Headers`.
+
+ # Confirm whether this is the right server to be serving this
+ # request.
+ try:
+ authoritative_server = self.placement_client.server_for_actor(
+ headers.application_id,
+ headers.state_ref,
+ )
+ except IMPORT_reboot_aio_placement.UnknownApplicationError:
+ # It's possible that the user did indeed type an application ID
+ # that doesn't exist, but it's also quite possible that this
+ # request reached us before the placement planner had gossipped
+ # out the information about which applications exist (we see
+ # this e.g. after `rbt dev`'s chaos monkey restarts). For that
+ # reason, abort with a retryable error.
+ await grpc_context.abort(
+ IMPORT_grpc.StatusCode.UNAVAILABLE,
+ f"Application '{headers.application_id}' not found. If you "
+ "are confident the application exists, this may be because "
+ "the system is still starting.",
+ )
+ raise # Unreachable but necessary for mypy.
+ if authoritative_server != self.server_id:
+ # This is NOT the correct server. Fail.
+ await grpc_context.abort(
+ IMPORT_grpc.StatusCode.UNAVAILABLE,
+ f"Server '{self.server_id}' is not authoritative for this "
+ f"request; server '{authoritative_server}' is.",
+ )
+ raise # Unreachable but necessary for mypy.
+
+ @IMPORT_reboot_aio_internals_middleware.maybe_run_function_twice_to_validate_effects
+ async def _run(
+ validating_effects: bool,
+ ) -> google.protobuf.empty_pb2.Empty:
+ context: IMPORT_typing.Optional[IMPORT_reboot_aio_contexts.Context] = None
+ try:
+ if headers.task_schedule is not None:
+ context, response = await self._schedule_Create(
+ headers=headers,
+ request=request,
+ grpc_context=grpc_context,
+ )
+ return response
+
+ context = self.create_context(
+ headers=headers,
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ method='Create',
+ context_type=IMPORT_reboot_aio_contexts.WriterContext,
+ )
+ assert context is not None
+
+ return await self._Create(
+ request,
+ context,
+ validating_effects=validating_effects,
+ grpc_context=grpc_context,
+ )
+ except IMPORT_reboot_aio_contexts.EffectValidationRetry:
+ # Doing effect validation, just let this propagate.
+ raise
+ except IMPORT_reboot.aio.aborted.Aborted as aborted:
+ status = IMPORT_rpc_status_sync.to_status(aborted.to_status())
+ # Need to add transaction participants here because
+ # calling `grpc_context.abort_with_status()` will
+ # ignore any other trailing metadata. Only propagate
+ # transaction participants metadata if the caller cares.
+ # Callers that care are those that are themselves transactions.
+ # It's important to not just send this information to everyone;
+ # some clients can't tolerate trailers, see:
+ # https://github.com/reboot-dev/mono/issues/5081
+ if context is not None and headers.transaction_ids is not None:
+ assert context.transaction_id is not None
+ status = status._replace(
+ trailing_metadata=status.trailing_metadata + context.participants.to_grpc_metadata(
+ read_only_aware=headers.coordinator_read_only_aware,
+ )
+ )
+ await grpc_context.abort_with_status(status)
+ raise # Unreachable but necessary for mypy.
+ except IMPORT_asyncio.CancelledError:
+ # It's pretty normal for an RPC to be cancelled; it's not useful to
+ # print a stack trace.
+ raise
+ except BaseException as exception:
+ # Print the exception stack trace for easier debugging. Note
+ # that we don't include the stack trace in an error message
+ # for the same reason that gRPC doesn't do so by default,
+ # see https://github.com/grpc/grpc/issues/14897, but since this
+ # should only get logged on the server side it is safe.
+ logger.warning(
+ 'Unhandled exception\n' +
+ ''.join(IMPORT_traceback.format_exc() if IMPORT_reboot_nodejs_python.should_print_stacktrace() else [f"{type(exception).__name__}: {exception}"])
+ )
+
+ # Re-raise the exception for gRPC to handle!
+ #
+ # TODO: gRPC will print a stack trace from this
+ # exception which we don't want if we're executing via
+ # Node.js.
+ raise
+ finally:
+ # Propagate transaction participants, if the caller cares.
+ # Callers that care are those that are themselves transactions.
+ # It's important to not just send this information to everyone;
+ # some clients can't tolerate trailers, see:
+ # https://github.com/reboot-dev/mono/issues/5081
+ if context is not None and headers.transaction_ids is not None:
+ assert context.transaction_id is not None
+ grpc_context.set_trailing_metadata(
+ grpc_context.trailing_metadata() +
+ context.participants.to_grpc_metadata(
+ read_only_aware=headers.coordinator_read_only_aware,
+ )
+ )
+
+ with IMPORT_reboot_aio_tracing.context_from_headers(headers):
+ return await _run()
+
+ async def __Increment(
+ self,
+ context: IMPORT_reboot_aio_contexts.WriterContext,
+ state: reboot.ping.ping_api_pb2.Counter,
+ request: google.protobuf.empty_pb2.Empty,
+ *,
+ validating_effects: bool,
+ ) -> Counter.IncrementEffects:
+ try:
+ typed_state: Counter.State = CounterFromProto(state, is_initial_state=(context.state_id in states_being_constructed))
+
+ if ongoing_transaction_state_key(context) in ongoing_transaction_states:
+ typed_state = ongoing_transaction_states[ongoing_transaction_state_key(context)].model_copy(deep=True)
+ response = (
+ await self._servicer._Increment(
+ context=context,
+ state=typed_state,
+ request=request
+ )
+ )
+
+ # TODO: it's premature to overwrite the state now given that the
+ # writer might still "fail" and an error will get propagated back
+ # to the ongoing transaction which will still see the effects of
+ # this writer. What we should be doing instead is creating a
+ # callback API that we invoke only after a writer completes
+ # that lets us update the state _reference_ then.
+ if ongoing_transaction_state_key(context) in ongoing_transaction_states:
+ for field, value in typed_state.model_dump().items():
+ setattr(ongoing_transaction_states[ongoing_transaction_state_key(context)], field, value)
+
+ CounterToProto(typed_state, state)
+
+ IMPORT_reboot_aio_types.assert_type(
+ response,
+ [reboot.ping.ping_api_pb2.CounterIncrementResponse],
+ )
+ self.maybe_raise_effect_validation_retry(
+ logger=logger,
+ idempotency_manager=context,
+ method_name='Counter.Increment',
+ validating_effects=validating_effects,
+ context=context,
+ )
+ return Counter.IncrementEffects(
+ state=state,
+ response=response,
+ tasks=context._tasks,
+ _colocated_upserts=context._colocated_upserts,
+ )
+ except IMPORT_reboot_aio_contexts.EffectValidationRetry:
+ # Doing effect validation, just let this propagate.
+ raise
+ except IMPORT_reboot.aio.aborted.Aborted as aborted:
+ # If the caller aborted due to a retryable error, just
+ # propagate the aborted instead of propagating `Unknown`
+ # so that a client can transparently retry.
+ if IMPORT_reboot.aio.aborted.is_retryable(aborted):
+ raise aborted
+ # Log any _unhandled_ abort stack traces to make it
+ # easier for debugging.
+ #
+ # NOTE: we don't log if we're a task as it will be logged
+ # in `public/reboot/aio/internals/tasks_dispatcher.py` instead.
+ aborted_type: IMPORT_typing.Optional[type] = None
+ aborted_type = Counter.IncrementAborted
+ if isinstance(aborted, IMPORT_reboot.aio.aborted.SystemAborted):
+ # Not logging when within `node` as we already log there.
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ f"Unhandled (in 'reboot.ping.Counter.Increment') {aborted}; propagating as 'Unknown'\n" +
+ ''.join(IMPORT_traceback.format_exception(aborted))
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ # TODO(benh): consider whether or not we want to
+ # include the 'package.service.method' which may
+ # get concatenated together forming a kind of
+ # "stack trace"; while it's super helpful for
+ # debugging, it does expose implementation
+ # information.
+ message=f"unhandled (in 'reboot.ping.Counter.Increment') {aborted}"
+ )
+ else:
+ if (
+ aborted_type is not None and
+ not isinstance(aborted, aborted_type) and
+ aborted_type.is_declared_error(aborted.error)
+ ):
+ # We propagate declared errors that might have
+ # come from another call, i.e., we might have an
+ # `Aborted` but not for this method but the
+ # `Aborted` that we have has an error that this
+ # method declared. This allows a developer to
+ # simply add the declared error to their `.proto`
+ # file rather than having to catch and re-raise
+ # the error with their own aborted type.
+ if context.task is None:
+ logger.warning(
+ f"Propagating unhandled but declared error (in 'reboot.ping.Counter.Increment') {aborted}"
+ )
+ elif (
+ aborted_type is None or
+ not isinstance(aborted, aborted_type)
+ ):
+ # Not logging when within `node` as we already log there.
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ f"Unhandled (in 'reboot.ping.Counter.Increment') {aborted}; propagating as 'Unknown'\n" +
+ ''.join(IMPORT_traceback.format_exception(aborted))
+ )
+ # If this wasn't a declared error than we
+ # propagate it as `Unknown`.
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ # TODO(benh): consider whether or not we want to
+ # include the 'package.service.method' which may
+ # get concatenated together forming a kind of
+ # "stack trace"; while it's super helpful for
+ # debugging, it does expose implementation
+ # information.
+ message=f"unhandled (in 'reboot.ping.Counter.Increment') {aborted}"
+ )
+
+ raise
+ except IMPORT_asyncio.CancelledError:
+ # It's pretty normal for an RPC to be cancelled; it's not useful to
+ # print a stack trace.
+ raise
+ except IMPORT_google_protobuf_message.DecodeError as decode_error:
+ # We usually see this error when we are trying to construct a proto
+ # message which is too deeply nested: protobuf has a limit of 100
+ # nested messages. See the limits here:
+ # https://protobuf.dev/programming-guides/proto-limits/
+
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ "Unhandled (in 'reboot.ping.Counter.Increment') "
+ f"{type(decode_error).__name__}{': ' + str(decode_error) if len(str(decode_error)) > 0 else ''}; "
+ "This is usually caused by a deeply nested protobuf message, which is not supported by protobuf.\n"
+ "See the limits here: https://protobuf.dev/programming-guides/proto-limits/" +
+ ''.join(IMPORT_traceback.format_exception(decode_error))
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ message=f"unhandled (in 'reboot.ping.Counter.Increment') {decode_error}; "
+ "This is usually caused by a deeply nested protobuf message, which is not supported by protobuf.\n"
+ "See the limits here: https://protobuf.dev/programming-guides/proto-limits/"
+ )
+ except BaseException as exception:
+ # Not logging when within `node` as we already log there.
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ "Unhandled (in 'reboot.ping.Counter.Increment') "
+ f"{type(exception).__name__}{': ' + str(exception) if len(str(exception)) > 0 else ''}; "
+ "propagating as 'Unknown'\n" +
+ ''.join(IMPORT_traceback.format_exception(exception))
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ # TODO(benh): consider whether or not we want to
+ # include the 'package.service.method' which may
+ # get concatenated together forming a kind of
+ # "stack trace"; while it's super helpful for
+ # debugging, it does expose implementation
+ # information.
+ message=f"unhandled (in 'reboot.ping.Counter.Increment') {type(exception).__name__}: {exception}"
+ )
+ finally:
+ pass
+
+ @IMPORT_reboot_aio_tracing.function_span(
+ # We expect an `EffectValidationRetry` exception; that's not an error.
+ set_status_on_exception=False
+ )
+ async def _Increment(
+ self,
+ request: google.protobuf.empty_pb2.Empty,
+ context: IMPORT_reboot_aio_contexts.WriterContext,
+ *,
+ validating_effects: bool,
+ grpc_context: IMPORT_typing.Optional[IMPORT_grpc.aio.ServicerContext] = None,
+ ) -> reboot.ping.ping_api_pb2.CounterIncrementResponse:
+ # Try to verify the token if a token verifier exists.
+ context.auth = await self._maybe_verify_token(
+ headers=context._headers, method='Increment'
+ )
+
+ # Try and "preload" to opportunistically load both state and
+ # idempotent mutations from the database in a single
+ # round-trip. This is fire-and-forget; subsequent calls to
+ # `_load()` and `check_for_idempotent_mutation()` should find
+ # the preloaded data and skip their own calls to the database.
+ #
+ # TODO: a tradeoff we're making here is that we load
+ # idempotent mutations even if this is just a reader and we
+ # don't need them. Consider only preloading for writers and
+ # transactions if loading idempotent mutations is
+ # unnecessarily slow (and we haven't optimized that via a
+ # database stored bloom filter).
+ #
+ # We do this _after_ verifying the token so it is not a
+ # denial-of-service attack vector, i.e., only callers with
+ # valid tokens may trigger preloads on the database.
+ self._state_manager.preload(
+ context.state_type_name,
+ context._state_ref,
+ )
+
+ # Check if we already have performed this mutation!
+ #
+ # We do this _before_ calling 'transactionally()' because
+ # if this call is for a transaction method _and_ we've
+ # already performed the transaction then we don't want to
+ # become a transaction participant (again) we just want to
+ # return the transaction's response.
+ idempotent_mutation = await self._state_manager.check_for_idempotent_mutation(
+ context
+ )
+
+ if idempotent_mutation is not None:
+ response = reboot.ping.ping_api_pb2.CounterIncrementResponse()
+ response.ParseFromString(idempotent_mutation.response)
+ return response
+
+ async with self._state_manager.transactionally(
+ context,
+ self.tasks_dispatcher,
+ aborted_type=Counter.IncrementAborted,
+ ) as transaction:
+ async with self._state_manager.writer(
+ context,
+ self._servicer.__state_type__,
+ self.tasks_dispatcher,
+ authorize=self._maybe_authorize(
+ method_name='reboot.ping.CounterMethods.Increment',
+ headers=context._headers,
+ auth=context.auth,
+ request=request,
+ ),
+ transaction=transaction,
+ from_constructor=False,
+ requires_constructor=True,
+ ) as (state, writer):
+
+ effects = await self.__Increment(
+ context,
+ state,
+ request,
+ validating_effects=validating_effects,
+ )
+
+ await writer.complete(effects)
+
+ # TODO: We need a single `Effects` superclass for all methods, so we
+ # would need to make it "partially" generic (with per-method subclasses
+ # filling out the rest of the generic parameters) in order to fix this.
+ return effects.response # type: ignore[return-value]
+
+ async def _schedule_Increment(
+ self,
+ *,
+ request: google.protobuf.empty_pb2.Empty,
+ headers: IMPORT_reboot_aio_headers.Headers,
+ grpc_context: IMPORT_grpc.aio.ServicerContext,
+ ) -> tuple[IMPORT_reboot_aio_contexts.WriterContext, reboot.ping.ping_api_pb2.CounterIncrementResponse]:
+ context: IMPORT_reboot_aio_contexts.WriterContext = self.create_context(
+ headers=headers,
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ method='Increment',
+ context_type=IMPORT_reboot_aio_contexts.WriterContext,
+ )
+ response = reboot.ping.ping_api_pb2.CounterIncrementResponse()
+
+ # Try to verify the token if a token verifier exists.
+ context.auth = await self._maybe_verify_token(
+ headers=headers, method='Increment'
+ )
+
+ # Try and "preload" to opportunistically load both state and
+ # idempotent mutations from the database in a single
+ # round-trip. This is fire-and-forget; subsequent calls to
+ # `_load()` and `check_for_idempotent_mutation()` should find
+ # the preloaded data and skip their own calls to the database.
+ #
+ # TODO: a tradeoff we're making here is that we load
+ # idempotent mutations even if this is just a reader and we
+ # don't need them. Consider only preloading for writers and
+ # transactions if loading idempotent mutations is
+ # unnecessarily slow (and we haven't optimized that via a
+ # database stored bloom filter).
+ #
+ # We do this _after_ verifying the token so it is not a
+ # denial-of-service attack vector, i.e., only callers with
+ # valid tokens may trigger preloads on the database.
+ self._state_manager.preload(
+ context.state_type_name,
+ context._state_ref,
+ )
+
+ # Check if we already have performed this schedule! Note that
+ # we need to do this for all kinds of methods because this is
+ # effectively a mutation (actually a `writer`, see below).
+ #
+ # We do this _before_ calling 'transactionally()' because
+ # if this call is for a transaction method _and_ we've
+ # already performed the transaction then we don't want to
+ # become a transaction participant (again) we just want to
+ # return the transaction's response.
+ idempotent_mutation = await self._state_manager.check_for_idempotent_mutation(
+ context
+ )
+
+ if idempotent_mutation is not None:
+ response.ParseFromString(idempotent_mutation.response)
+
+ # We should have only scheduled a single task!
+ assert len(idempotent_mutation.task_ids) == 1
+ assert grpc_context is not None
+ grpc_context.set_trailing_metadata(
+ grpc_context.trailing_metadata() +
+ (
+ (
+ IMPORT_reboot_aio_headers.TASK_ID_UUID,
+ str(IMPORT_uuid.UUID(bytes=idempotent_mutation.task_ids[0].task_uuid))
+ ),
+ )
+ )
+
+ return context, response
+
+ async with self._state_manager.transactionally(
+ context,
+ self.tasks_dispatcher,
+ aborted_type=Counter.IncrementAborted,
+ ) as transaction:
+
+ async with self._state_manager.writer(
+ context,
+ self._servicer.__state_type__,
+ self.tasks_dispatcher,
+ transaction=transaction,
+ authorize=self._maybe_authorize(
+ method_name='reboot.ping.CounterMethods.Increment',
+ headers=context._headers,
+ auth=context.auth,
+ request=request,
+ ),
+ from_constructor=False,
+ requires_constructor=True
+ ) as (state, writer):
+
+ task = await CounterServicerTasks(
+ context=context,
+ state_ref=context._state_ref,
+ ).Increment(
+ schedule=context._headers.task_schedule,
+ )
+
+ effects = IMPORT_reboot_aio_state_managers.Effects(
+ response=response,
+ state=state,
+ tasks=[task],
+ )
+
+ assert effects.tasks is not None
+
+ await writer.complete(effects)
+
+ assert grpc_context is not None
+
+ grpc_context.set_trailing_metadata(
+ grpc_context.trailing_metadata() +
+ (
+ (
+ IMPORT_reboot_aio_headers.TASK_ID_UUID,
+ str(IMPORT_uuid.UUID(bytes=task.task_id.task_uuid))
+ ),
+ )
+ )
+
+ return context, response
+
+ return context, response
+
+
+ # Entrypoint for non-reactive network calls (i.e. typical gRPC calls).
+ async def Increment(
+ self,
+ request: google.protobuf.empty_pb2.Empty,
+ grpc_context: IMPORT_grpc.aio.ServicerContext,
+ ) -> reboot.ping.ping_api_pb2.CounterIncrementResponse:
+ headers = IMPORT_reboot_aio_headers.Headers.from_grpc_context(grpc_context)
+ assert headers.application_id is not None # Guaranteed by `Headers`.
+
+ # Confirm whether this is the right server to be serving this
+ # request.
+ try:
+ authoritative_server = self.placement_client.server_for_actor(
+ headers.application_id,
+ headers.state_ref,
+ )
+ except IMPORT_reboot_aio_placement.UnknownApplicationError:
+ # It's possible that the user did indeed type an application ID
+ # that doesn't exist, but it's also quite possible that this
+ # request reached us before the placement planner had gossipped
+ # out the information about which applications exist (we see
+ # this e.g. after `rbt dev`'s chaos monkey restarts). For that
+ # reason, abort with a retryable error.
+ await grpc_context.abort(
+ IMPORT_grpc.StatusCode.UNAVAILABLE,
+ f"Application '{headers.application_id}' not found. If you "
+ "are confident the application exists, this may be because "
+ "the system is still starting.",
+ )
+ raise # Unreachable but necessary for mypy.
+ if authoritative_server != self.server_id:
+ # This is NOT the correct server. Fail.
+ await grpc_context.abort(
+ IMPORT_grpc.StatusCode.UNAVAILABLE,
+ f"Server '{self.server_id}' is not authoritative for this "
+ f"request; server '{authoritative_server}' is.",
+ )
+ raise # Unreachable but necessary for mypy.
+
+ @IMPORT_reboot_aio_internals_middleware.maybe_run_function_twice_to_validate_effects
+ async def _run(
+ validating_effects: bool,
+ ) -> reboot.ping.ping_api_pb2.CounterIncrementResponse:
+ context: IMPORT_typing.Optional[IMPORT_reboot_aio_contexts.Context] = None
+ try:
+ if headers.task_schedule is not None:
+ context, response = await self._schedule_Increment(
+ headers=headers,
+ request=request,
+ grpc_context=grpc_context,
+ )
+ return response
+
+ context = self.create_context(
+ headers=headers,
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ method='Increment',
+ context_type=IMPORT_reboot_aio_contexts.WriterContext,
+ )
+ assert context is not None
+
+ return await self._Increment(
+ request,
+ context,
+ validating_effects=validating_effects,
+ grpc_context=grpc_context,
+ )
+ except IMPORT_reboot_aio_contexts.EffectValidationRetry:
+ # Doing effect validation, just let this propagate.
+ raise
+ except IMPORT_reboot.aio.aborted.Aborted as aborted:
+ status = IMPORT_rpc_status_sync.to_status(aborted.to_status())
+ # Need to add transaction participants here because
+ # calling `grpc_context.abort_with_status()` will
+ # ignore any other trailing metadata. Only propagate
+ # transaction participants metadata if the caller cares.
+ # Callers that care are those that are themselves transactions.
+ # It's important to not just send this information to everyone;
+ # some clients can't tolerate trailers, see:
+ # https://github.com/reboot-dev/mono/issues/5081
+ if context is not None and headers.transaction_ids is not None:
+ assert context.transaction_id is not None
+ status = status._replace(
+ trailing_metadata=status.trailing_metadata + context.participants.to_grpc_metadata(
+ read_only_aware=headers.coordinator_read_only_aware,
+ )
+ )
+ await grpc_context.abort_with_status(status)
+ raise # Unreachable but necessary for mypy.
+ except IMPORT_asyncio.CancelledError:
+ # It's pretty normal for an RPC to be cancelled; it's not useful to
+ # print a stack trace.
+ raise
+ except BaseException as exception:
+ # Print the exception stack trace for easier debugging. Note
+ # that we don't include the stack trace in an error message
+ # for the same reason that gRPC doesn't do so by default,
+ # see https://github.com/grpc/grpc/issues/14897, but since this
+ # should only get logged on the server side it is safe.
+ logger.warning(
+ 'Unhandled exception\n' +
+ ''.join(IMPORT_traceback.format_exc() if IMPORT_reboot_nodejs_python.should_print_stacktrace() else [f"{type(exception).__name__}: {exception}"])
+ )
+
+ # Re-raise the exception for gRPC to handle!
+ #
+ # TODO: gRPC will print a stack trace from this
+ # exception which we don't want if we're executing via
+ # Node.js.
+ raise
+ finally:
+ # Propagate transaction participants, if the caller cares.
+ # Callers that care are those that are themselves transactions.
+ # It's important to not just send this information to everyone;
+ # some clients can't tolerate trailers, see:
+ # https://github.com/reboot-dev/mono/issues/5081
+ if context is not None and headers.transaction_ids is not None:
+ assert context.transaction_id is not None
+ grpc_context.set_trailing_metadata(
+ grpc_context.trailing_metadata() +
+ context.participants.to_grpc_metadata(
+ read_only_aware=headers.coordinator_read_only_aware,
+ )
+ )
+
+ with IMPORT_reboot_aio_tracing.context_from_headers(headers):
+ return await _run()
+
+ async def __Value(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: reboot.ping.ping_api_pb2.Counter,
+ request: google.protobuf.empty_pb2.Empty,
+ *,
+ validating_effects: bool,
+ ) -> reboot.ping.ping_api_pb2.CounterValueResponse:
+ try:
+ typed_state: Counter.State = CounterFromProto(state, is_initial_state=(context.state_id in states_being_constructed))
+
+ if ongoing_transaction_state_key(context) in ongoing_transaction_states:
+ typed_state = ongoing_transaction_states[ongoing_transaction_state_key(context)].model_copy(deep=True)
+ response = (
+ await self._servicer._Value(
+ context=context,
+ state=typed_state,
+ request=request
+ )
+ )
+
+
+ CounterToProto(typed_state, state)
+
+ IMPORT_reboot_aio_types.assert_type(
+ response,
+ [reboot.ping.ping_api_pb2.CounterValueResponse],
+ )
+ self.maybe_raise_effect_validation_retry(
+ logger=logger,
+ idempotency_manager=context,
+ method_name='Counter.Value',
+ validating_effects=validating_effects,
+ context=context,
+ )
+ return response
+ except IMPORT_reboot_aio_contexts.EffectValidationRetry:
+ # Doing effect validation, just let this propagate.
+ raise
+ except IMPORT_reboot.aio.aborted.Aborted as aborted:
+ # If the caller aborted due to a retryable error, just
+ # propagate the aborted instead of propagating `Unknown`
+ # so that a client can transparently retry.
+ if IMPORT_reboot.aio.aborted.is_retryable(aborted):
+ raise aborted
+ # Log any _unhandled_ abort stack traces to make it
+ # easier for debugging.
+ #
+ # NOTE: we don't log if we're a task as it will be logged
+ # in `public/reboot/aio/internals/tasks_dispatcher.py` instead.
+ aborted_type: IMPORT_typing.Optional[type] = None
+ aborted_type = Counter.ValueAborted
+ if isinstance(aborted, IMPORT_reboot.aio.aborted.SystemAborted):
+ # Not logging when within `node` as we already log there.
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ f"Unhandled (in 'reboot.ping.Counter.Value') {aborted}; propagating as 'Unknown'\n" +
+ ''.join(IMPORT_traceback.format_exception(aborted))
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ # TODO(benh): consider whether or not we want to
+ # include the 'package.service.method' which may
+ # get concatenated together forming a kind of
+ # "stack trace"; while it's super helpful for
+ # debugging, it does expose implementation
+ # information.
+ message=f"unhandled (in 'reboot.ping.Counter.Value') {aborted}"
+ )
+ else:
+ if (
+ aborted_type is not None and
+ not isinstance(aborted, aborted_type) and
+ aborted_type.is_declared_error(aborted.error)
+ ):
+ # We propagate declared errors that might have
+ # come from another call, i.e., we might have an
+ # `Aborted` but not for this method but the
+ # `Aborted` that we have has an error that this
+ # method declared. This allows a developer to
+ # simply add the declared error to their `.proto`
+ # file rather than having to catch and re-raise
+ # the error with their own aborted type.
+ if context.task is None:
+ logger.warning(
+ f"Propagating unhandled but declared error (in 'reboot.ping.Counter.Value') {aborted}"
+ )
+ elif (
+ aborted_type is None or
+ not isinstance(aborted, aborted_type)
+ ):
+ # Not logging when within `node` as we already log there.
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ f"Unhandled (in 'reboot.ping.Counter.Value') {aborted}; propagating as 'Unknown'\n" +
+ ''.join(IMPORT_traceback.format_exception(aborted))
+ )
+ # If this wasn't a declared error than we
+ # propagate it as `Unknown`.
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ # TODO(benh): consider whether or not we want to
+ # include the 'package.service.method' which may
+ # get concatenated together forming a kind of
+ # "stack trace"; while it's super helpful for
+ # debugging, it does expose implementation
+ # information.
+ message=f"unhandled (in 'reboot.ping.Counter.Value') {aborted}"
+ )
+
+ raise
+ except IMPORT_asyncio.CancelledError:
+ # It's pretty normal for an RPC to be cancelled; it's not useful to
+ # print a stack trace.
+ raise
+ except IMPORT_google_protobuf_message.DecodeError as decode_error:
+ # We usually see this error when we are trying to construct a proto
+ # message which is too deeply nested: protobuf has a limit of 100
+ # nested messages. See the limits here:
+ # https://protobuf.dev/programming-guides/proto-limits/
+
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ "Unhandled (in 'reboot.ping.Counter.Value') "
+ f"{type(decode_error).__name__}{': ' + str(decode_error) if len(str(decode_error)) > 0 else ''}; "
+ "This is usually caused by a deeply nested protobuf message, which is not supported by protobuf.\n"
+ "See the limits here: https://protobuf.dev/programming-guides/proto-limits/" +
+ ''.join(IMPORT_traceback.format_exception(decode_error))
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ message=f"unhandled (in 'reboot.ping.Counter.Value') {decode_error}; "
+ "This is usually caused by a deeply nested protobuf message, which is not supported by protobuf.\n"
+ "See the limits here: https://protobuf.dev/programming-guides/proto-limits/"
+ )
+ except BaseException as exception:
+ # Not logging when within `node` as we already log there.
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ "Unhandled (in 'reboot.ping.Counter.Value') "
+ f"{type(exception).__name__}{': ' + str(exception) if len(str(exception)) > 0 else ''}; "
+ "propagating as 'Unknown'\n" +
+ ''.join(IMPORT_traceback.format_exception(exception))
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ # TODO(benh): consider whether or not we want to
+ # include the 'package.service.method' which may
+ # get concatenated together forming a kind of
+ # "stack trace"; while it's super helpful for
+ # debugging, it does expose implementation
+ # information.
+ message=f"unhandled (in 'reboot.ping.Counter.Value') {type(exception).__name__}: {exception}"
+ )
+ finally:
+ pass
+
+ @IMPORT_reboot_aio_tracing.function_span(
+ # We expect an `EffectValidationRetry` exception; that's not an error.
+ set_status_on_exception=False
+ )
+ async def _Value(
+ self,
+ request: google.protobuf.empty_pb2.Empty,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ *,
+ validating_effects: bool,
+ grpc_context: IMPORT_typing.Optional[IMPORT_grpc.aio.ServicerContext] = None,
+ ) -> reboot.ping.ping_api_pb2.CounterValueResponse:
+ # Try to verify the token if a token verifier exists.
+ context.auth = await self._maybe_verify_token(
+ headers=context._headers, method='Value'
+ )
+
+ # Try and "preload" to opportunistically load both state and
+ # idempotent mutations from the database in a single
+ # round-trip. This is fire-and-forget; subsequent calls to
+ # `_load()` and `check_for_idempotent_mutation()` should find
+ # the preloaded data and skip their own calls to the database.
+ #
+ # TODO: a tradeoff we're making here is that we load
+ # idempotent mutations even if this is just a reader and we
+ # don't need them. Consider only preloading for writers and
+ # transactions if loading idempotent mutations is
+ # unnecessarily slow (and we haven't optimized that via a
+ # database stored bloom filter).
+ #
+ # We do this _after_ verifying the token so it is not a
+ # denial-of-service attack vector, i.e., only callers with
+ # valid tokens may trigger preloads on the database.
+ self._state_manager.preload(
+ context.state_type_name,
+ context._state_ref,
+ )
+
+
+ async with self._state_manager.transactionally(
+ context,
+ self.tasks_dispatcher,
+ aborted_type=Counter.ValueAborted,
+ ) as transaction:
+ authorizer = self._maybe_authorize(
+ method_name='reboot.ping.CounterMethods.Value',
+ headers=context._headers,
+ auth=context.auth,
+ request=request,
+ )
+ async with self._state_manager.reader(
+ context,
+ self._servicer.__state_type__,
+ authorize=authorizer,
+ ) as state:
+ response = await self.__Value(
+ context,
+ state,
+ request,
+ validating_effects=validating_effects,
+ )
+ return response
+
+ async def _schedule_Value(
+ self,
+ *,
+ request: google.protobuf.empty_pb2.Empty,
+ headers: IMPORT_reboot_aio_headers.Headers,
+ grpc_context: IMPORT_grpc.aio.ServicerContext,
+ ) -> tuple[IMPORT_reboot_aio_contexts.WriterContext, reboot.ping.ping_api_pb2.CounterValueResponse]:
+ context: IMPORT_reboot_aio_contexts.WriterContext = self.create_context(
+ headers=headers,
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ method='Value',
+ context_type=IMPORT_reboot_aio_contexts.WriterContext,
+ )
+ response = reboot.ping.ping_api_pb2.CounterValueResponse()
+
+ # Try to verify the token if a token verifier exists.
+ context.auth = await self._maybe_verify_token(
+ headers=headers, method='Value'
+ )
+
+ # Try and "preload" to opportunistically load both state and
+ # idempotent mutations from the database in a single
+ # round-trip. This is fire-and-forget; subsequent calls to
+ # `_load()` and `check_for_idempotent_mutation()` should find
+ # the preloaded data and skip their own calls to the database.
+ #
+ # TODO: a tradeoff we're making here is that we load
+ # idempotent mutations even if this is just a reader and we
+ # don't need them. Consider only preloading for writers and
+ # transactions if loading idempotent mutations is
+ # unnecessarily slow (and we haven't optimized that via a
+ # database stored bloom filter).
+ #
+ # We do this _after_ verifying the token so it is not a
+ # denial-of-service attack vector, i.e., only callers with
+ # valid tokens may trigger preloads on the database.
+ self._state_manager.preload(
+ context.state_type_name,
+ context._state_ref,
+ )
+
+ # Check if we already have performed this schedule! Note that
+ # we need to do this for all kinds of methods because this is
+ # effectively a mutation (actually a `writer`, see below).
+ #
+ # We do this _before_ calling 'transactionally()' because
+ # if this call is for a transaction method _and_ we've
+ # already performed the transaction then we don't want to
+ # become a transaction participant (again) we just want to
+ # return the transaction's response.
+ idempotent_mutation = await self._state_manager.check_for_idempotent_mutation(
+ context
+ )
+
+ if idempotent_mutation is not None:
+ response.ParseFromString(idempotent_mutation.response)
+
+ # We should have only scheduled a single task!
+ assert len(idempotent_mutation.task_ids) == 1
+ assert grpc_context is not None
+ grpc_context.set_trailing_metadata(
+ grpc_context.trailing_metadata() +
+ (
+ (
+ IMPORT_reboot_aio_headers.TASK_ID_UUID,
+ str(IMPORT_uuid.UUID(bytes=idempotent_mutation.task_ids[0].task_uuid))
+ ),
+ )
+ )
+
+ return context, response
+
+ async with self._state_manager.transactionally(
+ context,
+ self.tasks_dispatcher,
+ aborted_type=Counter.ValueAborted,
+ ) as transaction:
+
+ async with self._state_manager.writer(
+ context,
+ self._servicer.__state_type__,
+ self.tasks_dispatcher,
+ transaction=transaction,
+ authorize=self._maybe_authorize(
+ method_name='reboot.ping.CounterMethods.Value',
+ headers=context._headers,
+ auth=context.auth,
+ request=request,
+ ),
+ from_constructor=False,
+ requires_constructor=True
+ ) as (state, writer):
+
+ task = await CounterServicerTasks(
+ context=context,
+ state_ref=context._state_ref,
+ ).Value(
+ schedule=context._headers.task_schedule,
+ )
+
+ effects = IMPORT_reboot_aio_state_managers.Effects(
+ response=response,
+ state=state,
+ tasks=[task],
+ )
+
+ assert effects.tasks is not None
+
+ await writer.complete(effects)
+
+ assert grpc_context is not None
+
+ grpc_context.set_trailing_metadata(
+ grpc_context.trailing_metadata() +
+ (
+ (
+ IMPORT_reboot_aio_headers.TASK_ID_UUID,
+ str(IMPORT_uuid.UUID(bytes=task.task_id.task_uuid))
+ ),
+ )
+ )
+
+ return context, response
+
+ return context, response
+
+
+ # Entrypoint for non-reactive network calls (i.e. typical gRPC calls).
+ async def Value(
+ self,
+ request: google.protobuf.empty_pb2.Empty,
+ grpc_context: IMPORT_grpc.aio.ServicerContext,
+ ) -> reboot.ping.ping_api_pb2.CounterValueResponse:
+ headers = IMPORT_reboot_aio_headers.Headers.from_grpc_context(grpc_context)
+ assert headers.application_id is not None # Guaranteed by `Headers`.
+
+ # Confirm whether this is the right server to be serving this
+ # request.
+ try:
+ authoritative_server = self.placement_client.server_for_actor(
+ headers.application_id,
+ headers.state_ref,
+ )
+ except IMPORT_reboot_aio_placement.UnknownApplicationError:
+ # It's possible that the user did indeed type an application ID
+ # that doesn't exist, but it's also quite possible that this
+ # request reached us before the placement planner had gossipped
+ # out the information about which applications exist (we see
+ # this e.g. after `rbt dev`'s chaos monkey restarts). For that
+ # reason, abort with a retryable error.
+ await grpc_context.abort(
+ IMPORT_grpc.StatusCode.UNAVAILABLE,
+ f"Application '{headers.application_id}' not found. If you "
+ "are confident the application exists, this may be because "
+ "the system is still starting.",
+ )
+ raise # Unreachable but necessary for mypy.
+ if authoritative_server != self.server_id:
+ # This is NOT the correct server. Fail.
+ await grpc_context.abort(
+ IMPORT_grpc.StatusCode.UNAVAILABLE,
+ f"Server '{self.server_id}' is not authoritative for this "
+ f"request; server '{authoritative_server}' is.",
+ )
+ raise # Unreachable but necessary for mypy.
+
+ @IMPORT_reboot_aio_internals_middleware.maybe_run_function_twice_to_validate_effects
+ async def _run(
+ validating_effects: bool,
+ ) -> reboot.ping.ping_api_pb2.CounterValueResponse:
+ context: IMPORT_typing.Optional[IMPORT_reboot_aio_contexts.Context] = None
+ try:
+ if headers.task_schedule is not None:
+ context, response = await self._schedule_Value(
+ headers=headers,
+ request=request,
+ grpc_context=grpc_context,
+ )
+ return response
+
+ context = self.create_context(
+ headers=headers,
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ method='Value',
+ context_type=IMPORT_reboot_aio_contexts.ReaderContext,
+ )
+ assert context is not None
+
+ return await self._Value(
+ request,
+ context,
+ validating_effects=validating_effects,
+ grpc_context=grpc_context,
+ )
+ except IMPORT_reboot_aio_contexts.EffectValidationRetry:
+ # Doing effect validation, just let this propagate.
+ raise
+ except IMPORT_reboot.aio.aborted.Aborted as aborted:
+ status = IMPORT_rpc_status_sync.to_status(aborted.to_status())
+ # Need to add transaction participants here because
+ # calling `grpc_context.abort_with_status()` will
+ # ignore any other trailing metadata. Only propagate
+ # transaction participants metadata if the caller cares.
+ # Callers that care are those that are themselves transactions.
+ # It's important to not just send this information to everyone;
+ # some clients can't tolerate trailers, see:
+ # https://github.com/reboot-dev/mono/issues/5081
+ if context is not None and headers.transaction_ids is not None:
+ assert context.transaction_id is not None
+ status = status._replace(
+ trailing_metadata=status.trailing_metadata + context.participants.to_grpc_metadata(
+ read_only_aware=headers.coordinator_read_only_aware,
+ )
+ )
+ await grpc_context.abort_with_status(status)
+ raise # Unreachable but necessary for mypy.
+ except IMPORT_asyncio.CancelledError:
+ # It's pretty normal for an RPC to be cancelled; it's not useful to
+ # print a stack trace.
+ raise
+ except BaseException as exception:
+ # Print the exception stack trace for easier debugging. Note
+ # that we don't include the stack trace in an error message
+ # for the same reason that gRPC doesn't do so by default,
+ # see https://github.com/grpc/grpc/issues/14897, but since this
+ # should only get logged on the server side it is safe.
+ logger.warning(
+ 'Unhandled exception\n' +
+ ''.join(IMPORT_traceback.format_exc() if IMPORT_reboot_nodejs_python.should_print_stacktrace() else [f"{type(exception).__name__}: {exception}"])
+ )
+
+ # Re-raise the exception for gRPC to handle!
+ #
+ # TODO: gRPC will print a stack trace from this
+ # exception which we don't want if we're executing via
+ # Node.js.
+ raise
+ finally:
+ # Propagate transaction participants, if the caller cares.
+ # Callers that care are those that are themselves transactions.
+ # It's important to not just send this information to everyone;
+ # some clients can't tolerate trailers, see:
+ # https://github.com/reboot-dev/mono/issues/5081
+ if context is not None and headers.transaction_ids is not None:
+ assert context.transaction_id is not None
+ grpc_context.set_trailing_metadata(
+ grpc_context.trailing_metadata() +
+ context.participants.to_grpc_metadata(
+ read_only_aware=headers.coordinator_read_only_aware,
+ )
+ )
+
+ with IMPORT_reboot_aio_tracing.context_from_headers(headers):
+ return await _run()
+
+ async def __Description(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: reboot.ping.ping_api_pb2.Counter,
+ request: google.protobuf.empty_pb2.Empty,
+ *,
+ validating_effects: bool,
+ ) -> reboot.ping.ping_api_pb2.CounterDescriptionResponse:
+ try:
+ typed_state: Counter.State = CounterFromProto(state, is_initial_state=(context.state_id in states_being_constructed))
+
+ if ongoing_transaction_state_key(context) in ongoing_transaction_states:
+ typed_state = ongoing_transaction_states[ongoing_transaction_state_key(context)].model_copy(deep=True)
+ response = (
+ await self._servicer._Description(
+ context=context,
+ state=typed_state,
+ request=request
+ )
+ )
+
+
+ CounterToProto(typed_state, state)
+
+ IMPORT_reboot_aio_types.assert_type(
+ response,
+ [reboot.ping.ping_api_pb2.CounterDescriptionResponse],
+ )
+ self.maybe_raise_effect_validation_retry(
+ logger=logger,
+ idempotency_manager=context,
+ method_name='Counter.Description',
+ validating_effects=validating_effects,
+ context=context,
+ )
+ return response
+ except IMPORT_reboot_aio_contexts.EffectValidationRetry:
+ # Doing effect validation, just let this propagate.
+ raise
+ except IMPORT_reboot.aio.aborted.Aborted as aborted:
+ # If the caller aborted due to a retryable error, just
+ # propagate the aborted instead of propagating `Unknown`
+ # so that a client can transparently retry.
+ if IMPORT_reboot.aio.aborted.is_retryable(aborted):
+ raise aborted
+ # Log any _unhandled_ abort stack traces to make it
+ # easier for debugging.
+ #
+ # NOTE: we don't log if we're a task as it will be logged
+ # in `public/reboot/aio/internals/tasks_dispatcher.py` instead.
+ aborted_type: IMPORT_typing.Optional[type] = None
+ aborted_type = Counter.DescriptionAborted
+ if isinstance(aborted, IMPORT_reboot.aio.aborted.SystemAborted):
+ # Not logging when within `node` as we already log there.
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ f"Unhandled (in 'reboot.ping.Counter.Description') {aborted}; propagating as 'Unknown'\n" +
+ ''.join(IMPORT_traceback.format_exception(aborted))
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ # TODO(benh): consider whether or not we want to
+ # include the 'package.service.method' which may
+ # get concatenated together forming a kind of
+ # "stack trace"; while it's super helpful for
+ # debugging, it does expose implementation
+ # information.
+ message=f"unhandled (in 'reboot.ping.Counter.Description') {aborted}"
+ )
+ else:
+ if (
+ aborted_type is not None and
+ not isinstance(aborted, aborted_type) and
+ aborted_type.is_declared_error(aborted.error)
+ ):
+ # We propagate declared errors that might have
+ # come from another call, i.e., we might have an
+ # `Aborted` but not for this method but the
+ # `Aborted` that we have has an error that this
+ # method declared. This allows a developer to
+ # simply add the declared error to their `.proto`
+ # file rather than having to catch and re-raise
+ # the error with their own aborted type.
+ if context.task is None:
+ logger.warning(
+ f"Propagating unhandled but declared error (in 'reboot.ping.Counter.Description') {aborted}"
+ )
+ elif (
+ aborted_type is None or
+ not isinstance(aborted, aborted_type)
+ ):
+ # Not logging when within `node` as we already log there.
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ f"Unhandled (in 'reboot.ping.Counter.Description') {aborted}; propagating as 'Unknown'\n" +
+ ''.join(IMPORT_traceback.format_exception(aborted))
+ )
+ # If this wasn't a declared error than we
+ # propagate it as `Unknown`.
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ # TODO(benh): consider whether or not we want to
+ # include the 'package.service.method' which may
+ # get concatenated together forming a kind of
+ # "stack trace"; while it's super helpful for
+ # debugging, it does expose implementation
+ # information.
+ message=f"unhandled (in 'reboot.ping.Counter.Description') {aborted}"
+ )
+
+ raise
+ except IMPORT_asyncio.CancelledError:
+ # It's pretty normal for an RPC to be cancelled; it's not useful to
+ # print a stack trace.
+ raise
+ except IMPORT_google_protobuf_message.DecodeError as decode_error:
+ # We usually see this error when we are trying to construct a proto
+ # message which is too deeply nested: protobuf has a limit of 100
+ # nested messages. See the limits here:
+ # https://protobuf.dev/programming-guides/proto-limits/
+
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ "Unhandled (in 'reboot.ping.Counter.Description') "
+ f"{type(decode_error).__name__}{': ' + str(decode_error) if len(str(decode_error)) > 0 else ''}; "
+ "This is usually caused by a deeply nested protobuf message, which is not supported by protobuf.\n"
+ "See the limits here: https://protobuf.dev/programming-guides/proto-limits/" +
+ ''.join(IMPORT_traceback.format_exception(decode_error))
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ message=f"unhandled (in 'reboot.ping.Counter.Description') {decode_error}; "
+ "This is usually caused by a deeply nested protobuf message, which is not supported by protobuf.\n"
+ "See the limits here: https://protobuf.dev/programming-guides/proto-limits/"
+ )
+ except BaseException as exception:
+ # Not logging when within `node` as we already log there.
+ if IMPORT_reboot_nodejs_python.should_print_stacktrace():
+ logger.warning(
+ "Unhandled (in 'reboot.ping.Counter.Description') "
+ f"{type(exception).__name__}{': ' + str(exception) if len(str(exception)) > 0 else ''}; "
+ "propagating as 'Unknown'\n" +
+ ''.join(IMPORT_traceback.format_exception(exception))
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ # TODO(benh): consider whether or not we want to
+ # include the 'package.service.method' which may
+ # get concatenated together forming a kind of
+ # "stack trace"; while it's super helpful for
+ # debugging, it does expose implementation
+ # information.
+ message=f"unhandled (in 'reboot.ping.Counter.Description') {type(exception).__name__}: {exception}"
+ )
+ finally:
+ pass
+
+ @IMPORT_reboot_aio_tracing.function_span(
+ # We expect an `EffectValidationRetry` exception; that's not an error.
+ set_status_on_exception=False
+ )
+ async def _Description(
+ self,
+ request: google.protobuf.empty_pb2.Empty,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ *,
+ validating_effects: bool,
+ grpc_context: IMPORT_typing.Optional[IMPORT_grpc.aio.ServicerContext] = None,
+ ) -> reboot.ping.ping_api_pb2.CounterDescriptionResponse:
+ # Try to verify the token if a token verifier exists.
+ context.auth = await self._maybe_verify_token(
+ headers=context._headers, method='Description'
+ )
+
+ # Try and "preload" to opportunistically load both state and
+ # idempotent mutations from the database in a single
+ # round-trip. This is fire-and-forget; subsequent calls to
+ # `_load()` and `check_for_idempotent_mutation()` should find
+ # the preloaded data and skip their own calls to the database.
+ #
+ # TODO: a tradeoff we're making here is that we load
+ # idempotent mutations even if this is just a reader and we
+ # don't need them. Consider only preloading for writers and
+ # transactions if loading idempotent mutations is
+ # unnecessarily slow (and we haven't optimized that via a
+ # database stored bloom filter).
+ #
+ # We do this _after_ verifying the token so it is not a
+ # denial-of-service attack vector, i.e., only callers with
+ # valid tokens may trigger preloads on the database.
+ self._state_manager.preload(
+ context.state_type_name,
+ context._state_ref,
+ )
+
+
+ async with self._state_manager.transactionally(
+ context,
+ self.tasks_dispatcher,
+ aborted_type=Counter.DescriptionAborted,
+ ) as transaction:
+ authorizer = self._maybe_authorize(
+ method_name='reboot.ping.CounterMethods.Description',
+ headers=context._headers,
+ auth=context.auth,
+ request=request,
+ )
+ async with self._state_manager.reader(
+ context,
+ self._servicer.__state_type__,
+ authorize=authorizer,
+ ) as state:
+ response = await self.__Description(
+ context,
+ state,
+ request,
+ validating_effects=validating_effects,
+ )
+ return response
+
+ async def _schedule_Description(
+ self,
+ *,
+ request: google.protobuf.empty_pb2.Empty,
+ headers: IMPORT_reboot_aio_headers.Headers,
+ grpc_context: IMPORT_grpc.aio.ServicerContext,
+ ) -> tuple[IMPORT_reboot_aio_contexts.WriterContext, reboot.ping.ping_api_pb2.CounterDescriptionResponse]:
+ context: IMPORT_reboot_aio_contexts.WriterContext = self.create_context(
+ headers=headers,
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ method='Description',
+ context_type=IMPORT_reboot_aio_contexts.WriterContext,
+ )
+ response = reboot.ping.ping_api_pb2.CounterDescriptionResponse()
+
+ # Try to verify the token if a token verifier exists.
+ context.auth = await self._maybe_verify_token(
+ headers=headers, method='Description'
+ )
+
+ # Try and "preload" to opportunistically load both state and
+ # idempotent mutations from the database in a single
+ # round-trip. This is fire-and-forget; subsequent calls to
+ # `_load()` and `check_for_idempotent_mutation()` should find
+ # the preloaded data and skip their own calls to the database.
+ #
+ # TODO: a tradeoff we're making here is that we load
+ # idempotent mutations even if this is just a reader and we
+ # don't need them. Consider only preloading for writers and
+ # transactions if loading idempotent mutations is
+ # unnecessarily slow (and we haven't optimized that via a
+ # database stored bloom filter).
+ #
+ # We do this _after_ verifying the token so it is not a
+ # denial-of-service attack vector, i.e., only callers with
+ # valid tokens may trigger preloads on the database.
+ self._state_manager.preload(
+ context.state_type_name,
+ context._state_ref,
+ )
+
+ # Check if we already have performed this schedule! Note that
+ # we need to do this for all kinds of methods because this is
+ # effectively a mutation (actually a `writer`, see below).
+ #
+ # We do this _before_ calling 'transactionally()' because
+ # if this call is for a transaction method _and_ we've
+ # already performed the transaction then we don't want to
+ # become a transaction participant (again) we just want to
+ # return the transaction's response.
+ idempotent_mutation = await self._state_manager.check_for_idempotent_mutation(
+ context
+ )
+
+ if idempotent_mutation is not None:
+ response.ParseFromString(idempotent_mutation.response)
+
+ # We should have only scheduled a single task!
+ assert len(idempotent_mutation.task_ids) == 1
+ assert grpc_context is not None
+ grpc_context.set_trailing_metadata(
+ grpc_context.trailing_metadata() +
+ (
+ (
+ IMPORT_reboot_aio_headers.TASK_ID_UUID,
+ str(IMPORT_uuid.UUID(bytes=idempotent_mutation.task_ids[0].task_uuid))
+ ),
+ )
+ )
+
+ return context, response
+
+ async with self._state_manager.transactionally(
+ context,
+ self.tasks_dispatcher,
+ aborted_type=Counter.DescriptionAborted,
+ ) as transaction:
+
+ async with self._state_manager.writer(
+ context,
+ self._servicer.__state_type__,
+ self.tasks_dispatcher,
+ transaction=transaction,
+ authorize=self._maybe_authorize(
+ method_name='reboot.ping.CounterMethods.Description',
+ headers=context._headers,
+ auth=context.auth,
+ request=request,
+ ),
+ from_constructor=False,
+ requires_constructor=True
+ ) as (state, writer):
+
+ task = await CounterServicerTasks(
+ context=context,
+ state_ref=context._state_ref,
+ ).Description(
+ schedule=context._headers.task_schedule,
+ )
+
+ effects = IMPORT_reboot_aio_state_managers.Effects(
+ response=response,
+ state=state,
+ tasks=[task],
+ )
+
+ assert effects.tasks is not None
+
+ await writer.complete(effects)
+
+ assert grpc_context is not None
+
+ grpc_context.set_trailing_metadata(
+ grpc_context.trailing_metadata() +
+ (
+ (
+ IMPORT_reboot_aio_headers.TASK_ID_UUID,
+ str(IMPORT_uuid.UUID(bytes=task.task_id.task_uuid))
+ ),
+ )
+ )
+
+ return context, response
+
+ return context, response
+
+
+ # Entrypoint for non-reactive network calls (i.e. typical gRPC calls).
+ async def Description(
+ self,
+ request: google.protobuf.empty_pb2.Empty,
+ grpc_context: IMPORT_grpc.aio.ServicerContext,
+ ) -> reboot.ping.ping_api_pb2.CounterDescriptionResponse:
+ headers = IMPORT_reboot_aio_headers.Headers.from_grpc_context(grpc_context)
+ assert headers.application_id is not None # Guaranteed by `Headers`.
+
+ # Confirm whether this is the right server to be serving this
+ # request.
+ try:
+ authoritative_server = self.placement_client.server_for_actor(
+ headers.application_id,
+ headers.state_ref,
+ )
+ except IMPORT_reboot_aio_placement.UnknownApplicationError:
+ # It's possible that the user did indeed type an application ID
+ # that doesn't exist, but it's also quite possible that this
+ # request reached us before the placement planner had gossipped
+ # out the information about which applications exist (we see
+ # this e.g. after `rbt dev`'s chaos monkey restarts). For that
+ # reason, abort with a retryable error.
+ await grpc_context.abort(
+ IMPORT_grpc.StatusCode.UNAVAILABLE,
+ f"Application '{headers.application_id}' not found. If you "
+ "are confident the application exists, this may be because "
+ "the system is still starting.",
+ )
+ raise # Unreachable but necessary for mypy.
+ if authoritative_server != self.server_id:
+ # This is NOT the correct server. Fail.
+ await grpc_context.abort(
+ IMPORT_grpc.StatusCode.UNAVAILABLE,
+ f"Server '{self.server_id}' is not authoritative for this "
+ f"request; server '{authoritative_server}' is.",
+ )
+ raise # Unreachable but necessary for mypy.
+
+ @IMPORT_reboot_aio_internals_middleware.maybe_run_function_twice_to_validate_effects
+ async def _run(
+ validating_effects: bool,
+ ) -> reboot.ping.ping_api_pb2.CounterDescriptionResponse:
+ context: IMPORT_typing.Optional[IMPORT_reboot_aio_contexts.Context] = None
+ try:
+ if headers.task_schedule is not None:
+ context, response = await self._schedule_Description(
+ headers=headers,
+ request=request,
+ grpc_context=grpc_context,
+ )
+ return response
+
+ context = self.create_context(
+ headers=headers,
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ method='Description',
+ context_type=IMPORT_reboot_aio_contexts.ReaderContext,
+ )
+ assert context is not None
+
+ return await self._Description(
+ request,
+ context,
+ validating_effects=validating_effects,
+ grpc_context=grpc_context,
+ )
+ except IMPORT_reboot_aio_contexts.EffectValidationRetry:
+ # Doing effect validation, just let this propagate.
+ raise
+ except IMPORT_reboot.aio.aborted.Aborted as aborted:
+ status = IMPORT_rpc_status_sync.to_status(aborted.to_status())
+ # Need to add transaction participants here because
+ # calling `grpc_context.abort_with_status()` will
+ # ignore any other trailing metadata. Only propagate
+ # transaction participants metadata if the caller cares.
+ # Callers that care are those that are themselves transactions.
+ # It's important to not just send this information to everyone;
+ # some clients can't tolerate trailers, see:
+ # https://github.com/reboot-dev/mono/issues/5081
+ if context is not None and headers.transaction_ids is not None:
+ assert context.transaction_id is not None
+ status = status._replace(
+ trailing_metadata=status.trailing_metadata + context.participants.to_grpc_metadata(
+ read_only_aware=headers.coordinator_read_only_aware,
+ )
+ )
+ await grpc_context.abort_with_status(status)
+ raise # Unreachable but necessary for mypy.
+ except IMPORT_asyncio.CancelledError:
+ # It's pretty normal for an RPC to be cancelled; it's not useful to
+ # print a stack trace.
+ raise
+ except BaseException as exception:
+ # Print the exception stack trace for easier debugging. Note
+ # that we don't include the stack trace in an error message
+ # for the same reason that gRPC doesn't do so by default,
+ # see https://github.com/grpc/grpc/issues/14897, but since this
+ # should only get logged on the server side it is safe.
+ logger.warning(
+ 'Unhandled exception\n' +
+ ''.join(IMPORT_traceback.format_exc() if IMPORT_reboot_nodejs_python.should_print_stacktrace() else [f"{type(exception).__name__}: {exception}"])
+ )
+
+ # Re-raise the exception for gRPC to handle!
+ #
+ # TODO: gRPC will print a stack trace from this
+ # exception which we don't want if we're executing via
+ # Node.js.
+ raise
+ finally:
+ # Propagate transaction participants, if the caller cares.
+ # Callers that care are those that are themselves transactions.
+ # It's important to not just send this information to everyone;
+ # some clients can't tolerate trailers, see:
+ # https://github.com/reboot-dev/mono/issues/5081
+ if context is not None and headers.transaction_ids is not None:
+ assert context.transaction_id is not None
+ grpc_context.set_trailing_metadata(
+ grpc_context.trailing_metadata() +
+ context.participants.to_grpc_metadata(
+ read_only_aware=headers.coordinator_read_only_aware,
+ )
+ )
+
+ with IMPORT_reboot_aio_tracing.context_from_headers(headers):
+ return await _run()
+
+ def _maybe_authorize(
+ self,
+ *,
+ method_name: str,
+ headers: IMPORT_reboot_aio_headers.Headers,
+ auth: IMPORT_typing.Optional[IMPORT_reboot_aio_auth.Auth],
+ request: IMPORT_typing.Optional[CounterRequestTypes] = None,
+ ) -> IMPORT_typing.Optional[IMPORT_typing.Callable[[IMPORT_typing.Optional[CounterStateType]], IMPORT_typing.Awaitable[None]]]:
+ """Returns a function to check authorization for the given method.
+
+ Raises `PermissionDenied` in case Authorizer is present but the request
+ is not authorized.
+ """
+ # To authorize internal calls, we use an internal magic token.
+ if headers.bearer_token == __internal_magic_token__:
+ return None
+
+ assert self._authorizer is not None
+
+ async def authorize(state: IMPORT_typing.Optional[CounterStateType]) -> None:
+ # Create context for the authorizer. This is a `ReaderContext`
+ # independently of the calling context.
+ with self.use_context(
+ headers=(
+ # Get headers suitable for doing authorization.
+ headers.copy_for_token_verification_and_authorization()
+ ),
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ method=method_name,
+ context_type=IMPORT_reboot_aio_contexts.ReaderContext,
+ ) as context:
+ context.auth = auth
+
+ # Get the authorizer decision.
+ authorization_decision = await self._authorizer.authorize(
+ method_name=method_name,
+ context=context,
+ state=state,
+ request=request,
+ )
+
+ # Enforce correct authorizer decision type.
+ try:
+ IMPORT_reboot_aio_types.assert_type(
+ authorization_decision,
+ [
+ IMPORT_rbt_v1alpha1.errors_pb2.Ok,
+ IMPORT_rbt_v1alpha1.errors_pb2.Unauthenticated,
+ IMPORT_rbt_v1alpha1.errors_pb2.PermissionDenied,
+ ]
+ )
+ except TypeError as e:
+ # Retyping.cast the exception to provide more context.
+ authorizer_type = f"{type(self._authorizer).__module__}.{type(self._authorizer).__name__}"
+ raise TypeError(
+ f"Authorizer '{authorizer_type}' "
+ f"returned unexpected type '{type(authorization_decision).__name__}' "
+ f"for method '{method_name}' on "
+ f"`reboot.ping.Counter('{headers.state_ref.id}')`"
+ ) from e
+
+ # If the decision is not `True`, raise a `SystemAborted` with either a
+ # `PermissionDenied` error (in case of `False`) or an `Unauthenticated`
+ # error.
+ if not isinstance(authorization_decision, IMPORT_rbt_v1alpha1.errors_pb2.Ok):
+ if isinstance(authorization_decision, IMPORT_rbt_v1alpha1.errors_pb2.Unauthenticated):
+ logger.warning(
+ f"Unauthenticated call to '{method_name}' on "
+ f"`reboot.ping.Counter('{headers.state_ref.id}')`"
+ )
+
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ authorization_decision,
+ message=
+ f"You are not authorized to call '{method_name}' on "
+ f"`reboot.ping.Counter('{headers.state_ref.id}')`"
+ )
+
+ return authorize
+
+ async def _maybe_verify_token(
+ self,
+ *,
+ headers: IMPORT_reboot_aio_headers.Headers,
+ method: str,
+ ) -> IMPORT_typing.Optional[IMPORT_reboot_aio_auth.Auth]:
+ """Verify the bearer token and if a token verifier is present.
+
+ Returns the (optional) `reboot.aio.auth.Auth` object
+ produced by the token verifier if the token can be verified.
+ """
+ if self._token_verifier is not None:
+ if headers.bearer_token == __internal_magic_token__:
+ return None
+
+ with self.use_context(
+ headers=(
+ # Get headers suitable for doing token verification.
+ headers.copy_for_token_verification_and_authorization()
+ ),
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ method=method,
+ context_type=IMPORT_reboot_aio_contexts.ReaderContext,
+ ) as context:
+ result = await self._token_verifier.verify_token(
+ context=context,
+ token=headers.bearer_token,
+ )
+ if isinstance(result, IMPORT_rbt_v1alpha1.errors_pb2.Unauthenticated):
+ raise IMPORT_reboot_aio_aborted.SystemAborted(
+ result,
+ message=result.message or None,
+ )
+ return result
+
+ return None
+
+
+############################ Client Stubs ############################
+# This section is relevant for clients accessing a Reboot service. Since
+# servicers are themselves often clients also, this code is generated for
+# them also.
+
+
+class _PingStub(IMPORT_reboot_aio_stubs.Stub):
+
+ __state_type_name__ = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping')
+
+ def __init__(
+ self,
+ *,
+ context: IMPORT_reboot_aio_contexts.Context | IMPORT_reboot_aio_external.ExternalContext,
+ state_ref: IMPORT_reboot_aio_types.StateRef,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ):
+ # Within a Reboot context we do not pass on the caller's bearer token, as that might
+ # have security implications - we cannot simply trust any service we are calling with
+ # the user's credentials. Instead, the developer can rely on the default app-internal
+ # auth, or override that and set an explicit bearer token.
+ #
+ # In the case of `ExternalContext`, however, its `bearer_token` was set specifically
+ # by the developer for the purpose of making these calls.
+ caller_id: IMPORT_typing.Optional[IMPORT_reboot_aio_caller_id.CallerID] = None
+ if isinstance(context, IMPORT_reboot_aio_external.ExternalContext):
+ # Note that only `ExternalContext` even has a `bearer_token` field.
+ bearer_token = context.bearer_token
+ # If the creator of the `ExternalContext` set an explicit caller ID, obey it.
+ caller_id = context.caller_id
+ else:
+ caller_id = IMPORT_reboot_aio_caller_id.CallerID(
+ application_id=context.application_id,
+ )
+
+ super().__init__(
+ channel_manager=context.channel_manager,
+ idempotency_manager=context,
+ state_ref=state_ref,
+ context=context if isinstance(context, IMPORT_reboot_aio_contexts.Context) else None,
+ bearer_token=bearer_token,
+ caller_id=caller_id,
+ )
+
+ # All the channels for all services of this state will go to the same
+ # place, so we can just get a single channel and share it across all
+ # stubs.
+ channel = self._channel_manager.get_channel_to_state(
+ self.__state_type_name__, state_ref
+ )
+ self._reboot_ping_pingmethods_stub = reboot.ping.ping_api_pb2_grpc.PingMethodsStub(channel)
+
+
+class PingReaderStub(_PingStub):
+
+ def __init__(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext | IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ *,
+ state_ref: IMPORT_reboot_aio_types.StateRef,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ):
+ IMPORT_reboot_aio_types.assert_type(context, [IMPORT_reboot_aio_contexts.ReaderContext, IMPORT_reboot_aio_contexts.WriterContext, IMPORT_reboot_aio_contexts.TransactionContext, IMPORT_reboot_aio_contexts.WorkflowContext, IMPORT_reboot_aio_external.ExternalContext])
+ super().__init__(
+ context=context,
+ state_ref=state_ref,
+ bearer_token=bearer_token,
+ )
+
+ # Ping specific methods:
+
+
+ async def Describe(
+ self,
+ *,
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ ) -> reboot.ping.ping_api_pb2.PingDescribeResponse:
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping')
+ service_name = IMPORT_reboot_aio_types.ServiceName('reboot.ping.PingMethods')
+ method = 'Describe'
+
+ proto_request = PingDescribeRequestToProto(
+ )
+
+ async def call():
+ async with self._call(
+ state_type_name,
+ service_name,
+ method,
+ self._reboot_ping_pingmethods_stub.Describe,
+ proto_request,
+ unary=True,
+ reader=True,
+ response_type=reboot.ping.ping_api_pb2.PingDescribeResponse,
+ aborted_type=Ping.DescribeAborted,
+ metadata=metadata,
+ bearer_token=bearer_token,
+ ) as call:
+ assert isinstance(call, IMPORT_typing.Awaitable)
+ return await call
+
+ if isinstance(self._context, IMPORT_reboot_aio_contexts.WorkflowContext):
+ # Use the idempotency manager to make sure that this
+ # reader is being called following the rules.
+ with self._context.idempotently(
+ state_type_name=state_type_name,
+ state_ref=self._headers.state_ref,
+ service_name=service_name,
+ method=method,
+ mutation=False,
+ request=proto_request,
+ metadata=metadata,
+ idempotency=idempotency,
+ # Only need to pass `aborted_type` for mutations.
+ aborted_type=None,
+ ) as idempotency_key:
+ assert idempotency is not None
+ # Check if this reader is from an `.always()` and if
+ # so, don't memoize!
+ if idempotency.always:
+ return await call()
+
+ assert idempotency_key is not None
+ return await IMPORT_reboot_aio_workflows.at_least_once(
+ (
+ # TODO: for easier debugging include the
+ # original alias (or generated alias in the
+ # case of `.per_iteration()` w/o an alias)
+ # instead of just `idempotency_key`.
+ f'{ service_name }.{ method } ({str(idempotency_key)})',
+ # NOTE: we want this to be `PER_WORKFLOW`
+ # because any per iteration concerns should
+ # have already been taken care of by caller
+ # using `.per_iteration()`.
+ IMPORT_reboot_aio_workflows.PER_WORKFLOW
+ ),
+ self._context,
+ call,
+ type=reboot.ping.ping_api_pb2.PingDescribeResponse,
+ )
+ return await call()
+
+ async def NumPings(
+ self,
+ *,
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ ) -> reboot.ping.ping_api_pb2.PingNumPingsResponse:
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping')
+ service_name = IMPORT_reboot_aio_types.ServiceName('reboot.ping.PingMethods')
+ method = 'NumPings'
+
+ proto_request = PingNumPingsRequestToProto(
+ )
+
+ async def call():
+ async with self._call(
+ state_type_name,
+ service_name,
+ method,
+ self._reboot_ping_pingmethods_stub.NumPings,
+ proto_request,
+ unary=True,
+ reader=True,
+ response_type=reboot.ping.ping_api_pb2.PingNumPingsResponse,
+ aborted_type=Ping.NumPingsAborted,
+ metadata=metadata,
+ bearer_token=bearer_token,
+ ) as call:
+ assert isinstance(call, IMPORT_typing.Awaitable)
+ return await call
+
+ if isinstance(self._context, IMPORT_reboot_aio_contexts.WorkflowContext):
+ # Use the idempotency manager to make sure that this
+ # reader is being called following the rules.
+ with self._context.idempotently(
+ state_type_name=state_type_name,
+ state_ref=self._headers.state_ref,
+ service_name=service_name,
+ method=method,
+ mutation=False,
+ request=proto_request,
+ metadata=metadata,
+ idempotency=idempotency,
+ # Only need to pass `aborted_type` for mutations.
+ aborted_type=None,
+ ) as idempotency_key:
+ assert idempotency is not None
+ # Check if this reader is from an `.always()` and if
+ # so, don't memoize!
+ if idempotency.always:
+ return await call()
+
+ assert idempotency_key is not None
+ return await IMPORT_reboot_aio_workflows.at_least_once(
+ (
+ # TODO: for easier debugging include the
+ # original alias (or generated alias in the
+ # case of `.per_iteration()` w/o an alias)
+ # instead of just `idempotency_key`.
+ f'{ service_name }.{ method } ({str(idempotency_key)})',
+ # NOTE: we want this to be `PER_WORKFLOW`
+ # because any per iteration concerns should
+ # have already been taken care of by caller
+ # using `.per_iteration()`.
+ IMPORT_reboot_aio_workflows.PER_WORKFLOW
+ ),
+ self._context,
+ call,
+ type=reboot.ping.ping_api_pb2.PingNumPingsResponse,
+ )
+ return await call()
+
+
+
+class PingWriterStub(_PingStub):
+
+ def __init__(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ *,
+ state_ref: IMPORT_reboot_aio_types.StateRef,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ):
+ IMPORT_reboot_aio_types.assert_type(context, [IMPORT_reboot_aio_contexts.TransactionContext, IMPORT_reboot_aio_contexts.WorkflowContext, IMPORT_reboot_aio_external.ExternalContext])
+ super().__init__(
+ context=context,
+ state_ref=state_ref,
+ bearer_token=bearer_token,
+ )
+
+ # Ping specific methods:
+
+
+ async def Describe(
+ self,
+ *,
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ) -> reboot.ping.ping_api_pb2.PingDescribeResponse:
+ async with self._call(
+ IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping'),
+ IMPORT_reboot_aio_types.ServiceName('reboot.ping.PingMethods'),
+ 'Describe',
+ self._reboot_ping_pingmethods_stub.Describe,
+ PingDescribeRequestToProto(
+ ),
+ unary=True,
+ reader=True,
+ response_type=reboot.ping.ping_api_pb2.PingDescribeResponse,
+ aborted_type=Ping.DescribeAborted,
+ metadata=metadata,
+ bearer_token=bearer_token,
+ ) as call:
+ assert isinstance(call, IMPORT_typing.Awaitable), type(call)
+ return await call
+
+ async def NumPings(
+ self,
+ *,
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ) -> reboot.ping.ping_api_pb2.PingNumPingsResponse:
+ async with self._call(
+ IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping'),
+ IMPORT_reboot_aio_types.ServiceName('reboot.ping.PingMethods'),
+ 'NumPings',
+ self._reboot_ping_pingmethods_stub.NumPings,
+ PingNumPingsRequestToProto(
+ ),
+ unary=True,
+ reader=True,
+ response_type=reboot.ping.ping_api_pb2.PingNumPingsResponse,
+ aborted_type=Ping.NumPingsAborted,
+ metadata=metadata,
+ bearer_token=bearer_token,
+ ) as call:
+ assert isinstance(call, IMPORT_typing.Awaitable), type(call)
+ return await call
+
+
+class PingWorkflowStub(_PingStub):
+
+ def __init__(
+ self,
+ *,
+ context: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ state_ref: IMPORT_reboot_aio_types.StateRef,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ):
+ IMPORT_reboot_aio_types.assert_type(context, [IMPORT_reboot_aio_contexts.TransactionContext, IMPORT_reboot_aio_contexts.WorkflowContext, IMPORT_reboot_aio_external.ExternalContext])
+ super().__init__(
+ context=context,
+ state_ref=state_ref,
+ bearer_token=bearer_token,
+ )
+
+ # Ping specific methods:
+ async def DoPing(
+ self,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ *,
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ) -> reboot.ping.ping_api_pb2.PingDoPingResponse:
+ idempotency_key: IMPORT_typing.Optional[IMPORT_uuid.UUID]
+ proto_request = PingDoPingRequestToProto(
+ )
+
+ with self._idempotency_manager.idempotently(
+ state_type_name=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping'),
+ state_ref=self._headers.state_ref,
+ service_name=IMPORT_reboot_aio_types.ServiceName('reboot.ping.PingMethods'),
+ method='DoPing',
+ mutation=True,
+ request=proto_request,
+ metadata=metadata,
+ idempotency=idempotency,
+ aborted_type=Ping.DoPingAborted,
+ ) as idempotency_key:
+ async with self._call(
+ IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping'),
+ IMPORT_reboot_aio_types.ServiceName('reboot.ping.PingMethods'),
+ 'DoPing',
+ self._reboot_ping_pingmethods_stub.DoPing,
+ proto_request,
+ unary=True,
+ reader=False,
+ response_type=reboot.ping.ping_api_pb2.PingDoPingResponse,
+ aborted_type=Ping.DoPingAborted,
+ metadata=metadata,
+ idempotency_key=idempotency_key,
+ per_iteration=idempotency.per_iteration if idempotency is not None else False,
+ always=idempotency.always if idempotency is not None else False,
+ bearer_token=bearer_token,
+ ) as call:
+ assert isinstance(call, IMPORT_typing.Awaitable), type(call)
+ return await call
+
+
+ async def Describe(
+ self,
+ *,
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ) -> reboot.ping.ping_api_pb2.PingDescribeResponse:
+ async with self._call(
+ IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping'),
+ IMPORT_reboot_aio_types.ServiceName('reboot.ping.PingMethods'),
+ 'Describe',
+ self._reboot_ping_pingmethods_stub.Describe,
+ PingDescribeRequestToProto(
+ ),
+ unary=True,
+ reader=True,
+ response_type=reboot.ping.ping_api_pb2.PingDescribeResponse,
+ aborted_type=Ping.DescribeAborted,
+ metadata=metadata,
+ bearer_token=bearer_token,
+ ) as call:
+ assert isinstance(call, IMPORT_typing.Awaitable), type(call)
+ return await call
+
+ async def NumPings(
+ self,
+ *,
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ) -> reboot.ping.ping_api_pb2.PingNumPingsResponse:
+ async with self._call(
+ IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping'),
+ IMPORT_reboot_aio_types.ServiceName('reboot.ping.PingMethods'),
+ 'NumPings',
+ self._reboot_ping_pingmethods_stub.NumPings,
+ PingNumPingsRequestToProto(
+ ),
+ unary=True,
+ reader=True,
+ response_type=reboot.ping.ping_api_pb2.PingNumPingsResponse,
+ aborted_type=Ping.NumPingsAborted,
+ metadata=metadata,
+ bearer_token=bearer_token,
+ ) as call:
+ assert isinstance(call, IMPORT_typing.Awaitable), type(call)
+ return await call
+
+
+
+class PingTasksStub(_PingStub):
+
+ def __init__(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ *,
+ state_ref: IMPORT_reboot_aio_types.StateRef,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ):
+ IMPORT_reboot_aio_types.assert_type(context, [IMPORT_reboot_aio_contexts.TransactionContext, IMPORT_reboot_aio_contexts.WorkflowContext, IMPORT_reboot_aio_external.ExternalContext])
+ super().__init__(
+ context=context,
+ state_ref=state_ref,
+ bearer_token=bearer_token,
+ )
+
+ # Ping specific methods:
+ async def DoPing(
+ self,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ *,
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ idempotency_key: IMPORT_typing.Optional[IMPORT_uuid.UUID]
+ proto_request = PingDoPingRequestToProto(
+ )
+
+ with self._idempotency_manager.idempotently(
+ state_type_name=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping'),
+ state_ref=self._headers.state_ref,
+ service_name=IMPORT_reboot_aio_types.ServiceName('reboot.ping.PingMethods'),
+ method='DoPing',
+ mutation=True,
+ request=proto_request,
+ metadata=metadata,
+ idempotency=idempotency,
+ aborted_type=Ping.DoPingAborted,
+ ) as idempotency_key:
+ async with self._call(
+ IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping'),
+ IMPORT_reboot_aio_types.ServiceName('reboot.ping.PingMethods'),
+ 'DoPing',
+ self._reboot_ping_pingmethods_stub.DoPing,
+ proto_request,
+ unary=True,
+ reader=False,
+ response_type=reboot.ping.ping_api_pb2.PingDoPingResponse,
+ aborted_type=Ping.DoPingAborted,
+ metadata=metadata,
+ idempotency_key=idempotency_key,
+ per_iteration=idempotency.per_iteration if idempotency is not None else False,
+ always=idempotency.always if idempotency is not None else False,
+ bearer_token=bearer_token,
+ ) as call:
+ assert isinstance(call, IMPORT_typing.Awaitable), type(call)
+ await call
+ for (key, value) in await call.trailing_metadata(): # type: ignore[misc, attr-defined]
+ if key == IMPORT_reboot_aio_headers.TASK_ID_UUID:
+ return IMPORT_rbt_v1alpha1.tasks_pb2.TaskId(
+ state_type=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping'),
+ state_ref=self._headers.state_ref.to_str(),
+ task_uuid=IMPORT_uuid.UUID(value).bytes,
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Internal(),
+ message='Trailing metadata missing for task schedule',
+ )
+ async def DoPingPeriodically(
+ self,
+ request: Ping.DoPingPeriodicallyRequest,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ *,
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ idempotency_key: IMPORT_typing.Optional[IMPORT_uuid.UUID]
+ proto_request = PingDoPingPeriodicallyRequestToProto(
+ request,
+ )
+
+ with self._idempotency_manager.idempotently(
+ state_type_name=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping'),
+ state_ref=self._headers.state_ref,
+ service_name=IMPORT_reboot_aio_types.ServiceName('reboot.ping.PingMethods'),
+ method='DoPingPeriodically',
+ mutation=True,
+ request=proto_request,
+ metadata=metadata,
+ idempotency=idempotency,
+ aborted_type=Ping.DoPingPeriodicallyAborted,
+ ) as idempotency_key:
+ async with self._call(
+ IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping'),
+ IMPORT_reboot_aio_types.ServiceName('reboot.ping.PingMethods'),
+ 'DoPingPeriodically',
+ self._reboot_ping_pingmethods_stub.DoPingPeriodically,
+ proto_request,
+ unary=True,
+ reader=False,
+ response_type=reboot.ping.ping_api_pb2.PingDoPingPeriodicallyResponse,
+ aborted_type=Ping.DoPingPeriodicallyAborted,
+ metadata=metadata,
+ idempotency_key=idempotency_key,
+ per_iteration=idempotency.per_iteration if idempotency is not None else False,
+ always=idempotency.always if idempotency is not None else False,
+ bearer_token=bearer_token,
+ ) as call:
+ assert isinstance(call, IMPORT_typing.Awaitable), type(call)
+ await call
+ for (key, value) in await call.trailing_metadata(): # type: ignore[misc, attr-defined]
+ if key == IMPORT_reboot_aio_headers.TASK_ID_UUID:
+ return IMPORT_rbt_v1alpha1.tasks_pb2.TaskId(
+ state_type=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping'),
+ state_ref=self._headers.state_ref.to_str(),
+ task_uuid=IMPORT_uuid.UUID(value).bytes,
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Internal(),
+ message='Trailing metadata missing for task schedule',
+ )
+ async def Describe(
+ self,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ *,
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ idempotency_key: IMPORT_typing.Optional[IMPORT_uuid.UUID]
+ proto_request = PingDescribeRequestToProto(
+ )
+
+ with self._idempotency_manager.idempotently(
+ state_type_name=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping'),
+ state_ref=self._headers.state_ref,
+ service_name=IMPORT_reboot_aio_types.ServiceName('reboot.ping.PingMethods'),
+ method='Describe',
+ mutation=True,
+ request=proto_request,
+ metadata=metadata,
+ idempotency=idempotency,
+ aborted_type=Ping.DescribeAborted,
+ ) as idempotency_key:
+ async with self._call(
+ IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping'),
+ IMPORT_reboot_aio_types.ServiceName('reboot.ping.PingMethods'),
+ 'Describe',
+ self._reboot_ping_pingmethods_stub.Describe,
+ proto_request,
+ unary=True,
+ reader=False,
+ response_type=reboot.ping.ping_api_pb2.PingDescribeResponse,
+ aborted_type=Ping.DescribeAborted,
+ metadata=metadata,
+ idempotency_key=idempotency_key,
+ per_iteration=idempotency.per_iteration if idempotency is not None else False,
+ always=idempotency.always if idempotency is not None else False,
+ bearer_token=bearer_token,
+ ) as call:
+ assert isinstance(call, IMPORT_typing.Awaitable), type(call)
+ await call
+ for (key, value) in await call.trailing_metadata(): # type: ignore[misc, attr-defined]
+ if key == IMPORT_reboot_aio_headers.TASK_ID_UUID:
+ return IMPORT_rbt_v1alpha1.tasks_pb2.TaskId(
+ state_type=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping'),
+ state_ref=self._headers.state_ref.to_str(),
+ task_uuid=IMPORT_uuid.UUID(value).bytes,
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Internal(),
+ message='Trailing metadata missing for task schedule',
+ )
+ async def NumPings(
+ self,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ *,
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ idempotency_key: IMPORT_typing.Optional[IMPORT_uuid.UUID]
+ proto_request = PingNumPingsRequestToProto(
+ )
+
+ with self._idempotency_manager.idempotently(
+ state_type_name=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping'),
+ state_ref=self._headers.state_ref,
+ service_name=IMPORT_reboot_aio_types.ServiceName('reboot.ping.PingMethods'),
+ method='NumPings',
+ mutation=True,
+ request=proto_request,
+ metadata=metadata,
+ idempotency=idempotency,
+ aborted_type=Ping.NumPingsAborted,
+ ) as idempotency_key:
+ async with self._call(
+ IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping'),
+ IMPORT_reboot_aio_types.ServiceName('reboot.ping.PingMethods'),
+ 'NumPings',
+ self._reboot_ping_pingmethods_stub.NumPings,
+ proto_request,
+ unary=True,
+ reader=False,
+ response_type=reboot.ping.ping_api_pb2.PingNumPingsResponse,
+ aborted_type=Ping.NumPingsAborted,
+ metadata=metadata,
+ idempotency_key=idempotency_key,
+ per_iteration=idempotency.per_iteration if idempotency is not None else False,
+ always=idempotency.always if idempotency is not None else False,
+ bearer_token=bearer_token,
+ ) as call:
+ assert isinstance(call, IMPORT_typing.Awaitable), type(call)
+ await call
+ for (key, value) in await call.trailing_metadata(): # type: ignore[misc, attr-defined]
+ if key == IMPORT_reboot_aio_headers.TASK_ID_UUID:
+ return IMPORT_rbt_v1alpha1.tasks_pb2.TaskId(
+ state_type=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping'),
+ state_ref=self._headers.state_ref.to_str(),
+ task_uuid=IMPORT_uuid.UUID(value).bytes,
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Internal(),
+ message='Trailing metadata missing for task schedule',
+ )
+
+
+class PingServicerTasks:
+
+ _context: IMPORT_reboot_aio_contexts.WriterContext
+
+ def __init__(
+ self,
+ context: IMPORT_reboot_aio_contexts.WriterContext,
+ *,
+ state_ref: IMPORT_reboot_aio_types.StateRef,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ):
+ IMPORT_reboot_aio_types.assert_type(context, [IMPORT_reboot_aio_contexts.WriterContext])
+ self._context = context
+ self._state_ref = state_ref
+
+ # Ping specific methods:
+ async def DoPing(
+ self,
+ *,
+ schedule: IMPORT_typing.Optional[IMPORT_datetime_datetime | IMPORT_datetime_timedelta] = None,
+ ) -> IMPORT_reboot_aio_tasks.TaskEffect:
+ schedule = ensure_has_timezone(when=schedule)
+ task = IMPORT_reboot_aio_tasks.TaskEffect(
+ state_type=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping'),
+ state_ref=self._state_ref,
+ method_name='DoPing',
+ request=PingDoPingRequestToProto(
+ ),
+ schedule=(IMPORT_reboot_time_DateTimeWithTimeZone.now() + schedule) if isinstance(
+ schedule, IMPORT_datetime_timedelta
+ ) else schedule,
+ )
+
+ self._context._tasks.append(task)
+
+ return task
+
+ async def DoPingPeriodically(
+ self,
+ request: Ping.DoPingPeriodicallyRequest,
+ *,
+ schedule: IMPORT_typing.Optional[IMPORT_datetime_datetime | IMPORT_datetime_timedelta] = None,
+ ) -> IMPORT_reboot_aio_tasks.TaskEffect:
+ schedule = ensure_has_timezone(when=schedule)
+ task = IMPORT_reboot_aio_tasks.TaskEffect(
+ state_type=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping'),
+ state_ref=self._state_ref,
+ method_name='DoPingPeriodically',
+ request=PingDoPingPeriodicallyRequestToProto(
+ request,
+ ),
+ schedule=(IMPORT_reboot_time_DateTimeWithTimeZone.now() + schedule) if isinstance(
+ schedule, IMPORT_datetime_timedelta
+ ) else schedule,
+ )
+
+ self._context._tasks.append(task)
+
+ return task
+
+ async def Describe(
+ self,
+ *,
+ schedule: IMPORT_typing.Optional[IMPORT_datetime_datetime | IMPORT_datetime_timedelta] = None,
+ ) -> IMPORT_reboot_aio_tasks.TaskEffect:
+ schedule = ensure_has_timezone(when=schedule)
+ task = IMPORT_reboot_aio_tasks.TaskEffect(
+ state_type=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping'),
+ state_ref=self._state_ref,
+ method_name='Describe',
+ request=PingDescribeRequestToProto(
+ ),
+ schedule=(IMPORT_reboot_time_DateTimeWithTimeZone.now() + schedule) if isinstance(
+ schedule, IMPORT_datetime_timedelta
+ ) else schedule,
+ )
+
+ self._context._tasks.append(task)
+
+ return task
+
+ async def NumPings(
+ self,
+ *,
+ schedule: IMPORT_typing.Optional[IMPORT_datetime_datetime | IMPORT_datetime_timedelta] = None,
+ ) -> IMPORT_reboot_aio_tasks.TaskEffect:
+ schedule = ensure_has_timezone(when=schedule)
+ task = IMPORT_reboot_aio_tasks.TaskEffect(
+ state_type=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping'),
+ state_ref=self._state_ref,
+ method_name='NumPings',
+ request=PingNumPingsRequestToProto(
+ ),
+ schedule=(IMPORT_reboot_time_DateTimeWithTimeZone.now() + schedule) if isinstance(
+ schedule, IMPORT_datetime_timedelta
+ ) else schedule,
+ )
+
+ self._context._tasks.append(task)
+
+ return task
+
+
+class _PongStub(IMPORT_reboot_aio_stubs.Stub):
+
+ __state_type_name__ = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Pong')
+
+ def __init__(
+ self,
+ *,
+ context: IMPORT_reboot_aio_contexts.Context | IMPORT_reboot_aio_external.ExternalContext,
+ state_ref: IMPORT_reboot_aio_types.StateRef,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ):
+ # Within a Reboot context we do not pass on the caller's bearer token, as that might
+ # have security implications - we cannot simply trust any service we are calling with
+ # the user's credentials. Instead, the developer can rely on the default app-internal
+ # auth, or override that and set an explicit bearer token.
+ #
+ # In the case of `ExternalContext`, however, its `bearer_token` was set specifically
+ # by the developer for the purpose of making these calls.
+ caller_id: IMPORT_typing.Optional[IMPORT_reboot_aio_caller_id.CallerID] = None
+ if isinstance(context, IMPORT_reboot_aio_external.ExternalContext):
+ # Note that only `ExternalContext` even has a `bearer_token` field.
+ bearer_token = context.bearer_token
+ # If the creator of the `ExternalContext` set an explicit caller ID, obey it.
+ caller_id = context.caller_id
+ else:
+ caller_id = IMPORT_reboot_aio_caller_id.CallerID(
+ application_id=context.application_id,
+ )
+
+ super().__init__(
+ channel_manager=context.channel_manager,
+ idempotency_manager=context,
+ state_ref=state_ref,
+ context=context if isinstance(context, IMPORT_reboot_aio_contexts.Context) else None,
+ bearer_token=bearer_token,
+ caller_id=caller_id,
+ )
+
+ # All the channels for all services of this state will go to the same
+ # place, so we can just get a single channel and share it across all
+ # stubs.
+ channel = self._channel_manager.get_channel_to_state(
+ self.__state_type_name__, state_ref
+ )
+ self._reboot_ping_pongmethods_stub = reboot.ping.ping_api_pb2_grpc.PongMethodsStub(channel)
+
+
+class PongReaderStub(_PongStub):
+
+ def __init__(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext | IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ *,
+ state_ref: IMPORT_reboot_aio_types.StateRef,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ):
+ IMPORT_reboot_aio_types.assert_type(context, [IMPORT_reboot_aio_contexts.ReaderContext, IMPORT_reboot_aio_contexts.WriterContext, IMPORT_reboot_aio_contexts.TransactionContext, IMPORT_reboot_aio_contexts.WorkflowContext, IMPORT_reboot_aio_external.ExternalContext])
+ super().__init__(
+ context=context,
+ state_ref=state_ref,
+ bearer_token=bearer_token,
+ )
+
+ # Pong specific methods:
+
+ async def NumPongs(
+ self,
+ *,
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ ) -> reboot.ping.ping_api_pb2.PongNumPongsResponse:
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Pong')
+ service_name = IMPORT_reboot_aio_types.ServiceName('reboot.ping.PongMethods')
+ method = 'NumPongs'
+
+ proto_request = PongNumPongsRequestToProto(
+ )
+
+ async def call():
+ async with self._call(
+ state_type_name,
+ service_name,
+ method,
+ self._reboot_ping_pongmethods_stub.NumPongs,
+ proto_request,
+ unary=True,
+ reader=True,
+ response_type=reboot.ping.ping_api_pb2.PongNumPongsResponse,
+ aborted_type=Pong.NumPongsAborted,
+ metadata=metadata,
+ bearer_token=bearer_token,
+ ) as call:
+ assert isinstance(call, IMPORT_typing.Awaitable)
+ return await call
+
+ if isinstance(self._context, IMPORT_reboot_aio_contexts.WorkflowContext):
+ # Use the idempotency manager to make sure that this
+ # reader is being called following the rules.
+ with self._context.idempotently(
+ state_type_name=state_type_name,
+ state_ref=self._headers.state_ref,
+ service_name=service_name,
+ method=method,
+ mutation=False,
+ request=proto_request,
+ metadata=metadata,
+ idempotency=idempotency,
+ # Only need to pass `aborted_type` for mutations.
+ aborted_type=None,
+ ) as idempotency_key:
+ assert idempotency is not None
+ # Check if this reader is from an `.always()` and if
+ # so, don't memoize!
+ if idempotency.always:
+ return await call()
+
+ assert idempotency_key is not None
+ return await IMPORT_reboot_aio_workflows.at_least_once(
+ (
+ # TODO: for easier debugging include the
+ # original alias (or generated alias in the
+ # case of `.per_iteration()` w/o an alias)
+ # instead of just `idempotency_key`.
+ f'{ service_name }.{ method } ({str(idempotency_key)})',
+ # NOTE: we want this to be `PER_WORKFLOW`
+ # because any per iteration concerns should
+ # have already been taken care of by caller
+ # using `.per_iteration()`.
+ IMPORT_reboot_aio_workflows.PER_WORKFLOW
+ ),
+ self._context,
+ call,
+ type=reboot.ping.ping_api_pb2.PongNumPongsResponse,
+ )
+ return await call()
+
+
+
+class PongWriterStub(_PongStub):
+
+ def __init__(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ *,
+ state_ref: IMPORT_reboot_aio_types.StateRef,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ):
+ IMPORT_reboot_aio_types.assert_type(context, [IMPORT_reboot_aio_contexts.TransactionContext, IMPORT_reboot_aio_contexts.WorkflowContext, IMPORT_reboot_aio_external.ExternalContext])
+ super().__init__(
+ context=context,
+ state_ref=state_ref,
+ bearer_token=bearer_token,
+ )
+
+ # Pong specific methods:
+ async def DoPong(
+ self,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ *,
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ) -> reboot.ping.ping_api_pb2.PongDoPongResponse:
+ proto_request = PongDoPongRequestToProto(
+ )
+ with self._idempotency_manager.idempotently(
+ state_type_name=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Pong'),
+ state_ref=self._headers.state_ref,
+ service_name=IMPORT_reboot_aio_types.ServiceName('reboot.ping.PongMethods'),
+ method='DoPong',
+ mutation=True,
+ request=proto_request,
+ metadata=metadata,
+ idempotency=idempotency,
+ aborted_type=Pong.DoPongAborted,
+ ) as idempotency_key:
+ async with self._call(
+ IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Pong'),
+ IMPORT_reboot_aio_types.ServiceName('reboot.ping.PongMethods'),
+ 'DoPong',
+ self._reboot_ping_pongmethods_stub.DoPong,
+ proto_request,
+ unary=True,
+ reader=False,
+ response_type=reboot.ping.ping_api_pb2.PongDoPongResponse,
+ aborted_type=Pong.DoPongAborted,
+ metadata=metadata,
+ idempotency_key=idempotency_key,
+ per_iteration=idempotency.per_iteration if idempotency is not None else False,
+ always=idempotency.always if idempotency is not None else False,
+ bearer_token=bearer_token,
+ ) as call:
+ assert isinstance(call, IMPORT_typing.Awaitable), type(call)
+ return await call
+
+ async def NumPongs(
+ self,
+ *,
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ) -> reboot.ping.ping_api_pb2.PongNumPongsResponse:
+ async with self._call(
+ IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Pong'),
+ IMPORT_reboot_aio_types.ServiceName('reboot.ping.PongMethods'),
+ 'NumPongs',
+ self._reboot_ping_pongmethods_stub.NumPongs,
+ PongNumPongsRequestToProto(
+ ),
+ unary=True,
+ reader=True,
+ response_type=reboot.ping.ping_api_pb2.PongNumPongsResponse,
+ aborted_type=Pong.NumPongsAborted,
+ metadata=metadata,
+ bearer_token=bearer_token,
+ ) as call:
+ assert isinstance(call, IMPORT_typing.Awaitable), type(call)
+ return await call
+
+
+class PongWorkflowStub(_PongStub):
+
+ def __init__(
+ self,
+ *,
+ context: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ state_ref: IMPORT_reboot_aio_types.StateRef,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ):
+ IMPORT_reboot_aio_types.assert_type(context, [IMPORT_reboot_aio_contexts.TransactionContext, IMPORT_reboot_aio_contexts.WorkflowContext, IMPORT_reboot_aio_external.ExternalContext])
+ super().__init__(
+ context=context,
+ state_ref=state_ref,
+ bearer_token=bearer_token,
+ )
+
+ # Pong specific methods:
+ async def DoPong(
+ self,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ *,
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ) -> reboot.ping.ping_api_pb2.PongDoPongResponse:
+ idempotency_key: IMPORT_typing.Optional[IMPORT_uuid.UUID]
+ proto_request = PongDoPongRequestToProto(
+ )
+
+ with self._idempotency_manager.idempotently(
+ state_type_name=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Pong'),
+ state_ref=self._headers.state_ref,
+ service_name=IMPORT_reboot_aio_types.ServiceName('reboot.ping.PongMethods'),
+ method='DoPong',
+ mutation=True,
+ request=proto_request,
+ metadata=metadata,
+ idempotency=idempotency,
+ aborted_type=Pong.DoPongAborted,
+ ) as idempotency_key:
+ async with self._call(
+ IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Pong'),
+ IMPORT_reboot_aio_types.ServiceName('reboot.ping.PongMethods'),
+ 'DoPong',
+ self._reboot_ping_pongmethods_stub.DoPong,
+ proto_request,
+ unary=True,
+ reader=False,
+ response_type=reboot.ping.ping_api_pb2.PongDoPongResponse,
+ aborted_type=Pong.DoPongAborted,
+ metadata=metadata,
+ idempotency_key=idempotency_key,
+ per_iteration=idempotency.per_iteration if idempotency is not None else False,
+ always=idempotency.always if idempotency is not None else False,
+ bearer_token=bearer_token,
+ ) as call:
+ assert isinstance(call, IMPORT_typing.Awaitable), type(call)
+ return await call
+
+ async def NumPongs(
+ self,
+ *,
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ) -> reboot.ping.ping_api_pb2.PongNumPongsResponse:
+ async with self._call(
+ IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Pong'),
+ IMPORT_reboot_aio_types.ServiceName('reboot.ping.PongMethods'),
+ 'NumPongs',
+ self._reboot_ping_pongmethods_stub.NumPongs,
+ PongNumPongsRequestToProto(
+ ),
+ unary=True,
+ reader=True,
+ response_type=reboot.ping.ping_api_pb2.PongNumPongsResponse,
+ aborted_type=Pong.NumPongsAborted,
+ metadata=metadata,
+ bearer_token=bearer_token,
+ ) as call:
+ assert isinstance(call, IMPORT_typing.Awaitable), type(call)
+ return await call
+
+
+
+class PongTasksStub(_PongStub):
+
+ def __init__(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ *,
+ state_ref: IMPORT_reboot_aio_types.StateRef,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ):
+ IMPORT_reboot_aio_types.assert_type(context, [IMPORT_reboot_aio_contexts.TransactionContext, IMPORT_reboot_aio_contexts.WorkflowContext, IMPORT_reboot_aio_external.ExternalContext])
+ super().__init__(
+ context=context,
+ state_ref=state_ref,
+ bearer_token=bearer_token,
+ )
+
+ # Pong specific methods:
+ async def DoPong(
+ self,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ *,
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ idempotency_key: IMPORT_typing.Optional[IMPORT_uuid.UUID]
+ proto_request = PongDoPongRequestToProto(
+ )
+
+ with self._idempotency_manager.idempotently(
+ state_type_name=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Pong'),
+ state_ref=self._headers.state_ref,
+ service_name=IMPORT_reboot_aio_types.ServiceName('reboot.ping.PongMethods'),
+ method='DoPong',
+ mutation=True,
+ request=proto_request,
+ metadata=metadata,
+ idempotency=idempotency,
+ aborted_type=Pong.DoPongAborted,
+ ) as idempotency_key:
+ async with self._call(
+ IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Pong'),
+ IMPORT_reboot_aio_types.ServiceName('reboot.ping.PongMethods'),
+ 'DoPong',
+ self._reboot_ping_pongmethods_stub.DoPong,
+ proto_request,
+ unary=True,
+ reader=False,
+ response_type=reboot.ping.ping_api_pb2.PongDoPongResponse,
+ aborted_type=Pong.DoPongAborted,
+ metadata=metadata,
+ idempotency_key=idempotency_key,
+ per_iteration=idempotency.per_iteration if idempotency is not None else False,
+ always=idempotency.always if idempotency is not None else False,
+ bearer_token=bearer_token,
+ ) as call:
+ assert isinstance(call, IMPORT_typing.Awaitable), type(call)
+ await call
+ for (key, value) in await call.trailing_metadata(): # type: ignore[misc, attr-defined]
+ if key == IMPORT_reboot_aio_headers.TASK_ID_UUID:
+ return IMPORT_rbt_v1alpha1.tasks_pb2.TaskId(
+ state_type=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Pong'),
+ state_ref=self._headers.state_ref.to_str(),
+ task_uuid=IMPORT_uuid.UUID(value).bytes,
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Internal(),
+ message='Trailing metadata missing for task schedule',
+ )
+ async def NumPongs(
+ self,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ *,
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ idempotency_key: IMPORT_typing.Optional[IMPORT_uuid.UUID]
+ proto_request = PongNumPongsRequestToProto(
+ )
+
+ with self._idempotency_manager.idempotently(
+ state_type_name=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Pong'),
+ state_ref=self._headers.state_ref,
+ service_name=IMPORT_reboot_aio_types.ServiceName('reboot.ping.PongMethods'),
+ method='NumPongs',
+ mutation=True,
+ request=proto_request,
+ metadata=metadata,
+ idempotency=idempotency,
+ aborted_type=Pong.NumPongsAborted,
+ ) as idempotency_key:
+ async with self._call(
+ IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Pong'),
+ IMPORT_reboot_aio_types.ServiceName('reboot.ping.PongMethods'),
+ 'NumPongs',
+ self._reboot_ping_pongmethods_stub.NumPongs,
+ proto_request,
+ unary=True,
+ reader=False,
+ response_type=reboot.ping.ping_api_pb2.PongNumPongsResponse,
+ aborted_type=Pong.NumPongsAborted,
+ metadata=metadata,
+ idempotency_key=idempotency_key,
+ per_iteration=idempotency.per_iteration if idempotency is not None else False,
+ always=idempotency.always if idempotency is not None else False,
+ bearer_token=bearer_token,
+ ) as call:
+ assert isinstance(call, IMPORT_typing.Awaitable), type(call)
+ await call
+ for (key, value) in await call.trailing_metadata(): # type: ignore[misc, attr-defined]
+ if key == IMPORT_reboot_aio_headers.TASK_ID_UUID:
+ return IMPORT_rbt_v1alpha1.tasks_pb2.TaskId(
+ state_type=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Pong'),
+ state_ref=self._headers.state_ref.to_str(),
+ task_uuid=IMPORT_uuid.UUID(value).bytes,
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Internal(),
+ message='Trailing metadata missing for task schedule',
+ )
+
+
+class PongServicerTasks:
+
+ _context: IMPORT_reboot_aio_contexts.WriterContext
+
+ def __init__(
+ self,
+ context: IMPORT_reboot_aio_contexts.WriterContext,
+ *,
+ state_ref: IMPORT_reboot_aio_types.StateRef,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ):
+ IMPORT_reboot_aio_types.assert_type(context, [IMPORT_reboot_aio_contexts.WriterContext])
+ self._context = context
+ self._state_ref = state_ref
+
+ # Pong specific methods:
+ async def DoPong(
+ self,
+ *,
+ schedule: IMPORT_typing.Optional[IMPORT_datetime_datetime | IMPORT_datetime_timedelta] = None,
+ ) -> IMPORT_reboot_aio_tasks.TaskEffect:
+ schedule = ensure_has_timezone(when=schedule)
+ task = IMPORT_reboot_aio_tasks.TaskEffect(
+ state_type=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Pong'),
+ state_ref=self._state_ref,
+ method_name='DoPong',
+ request=PongDoPongRequestToProto(
+ ),
+ schedule=(IMPORT_reboot_time_DateTimeWithTimeZone.now() + schedule) if isinstance(
+ schedule, IMPORT_datetime_timedelta
+ ) else schedule,
+ )
+
+ self._context._tasks.append(task)
+
+ return task
+
+ async def NumPongs(
+ self,
+ *,
+ schedule: IMPORT_typing.Optional[IMPORT_datetime_datetime | IMPORT_datetime_timedelta] = None,
+ ) -> IMPORT_reboot_aio_tasks.TaskEffect:
+ schedule = ensure_has_timezone(when=schedule)
+ task = IMPORT_reboot_aio_tasks.TaskEffect(
+ state_type=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Pong'),
+ state_ref=self._state_ref,
+ method_name='NumPongs',
+ request=PongNumPongsRequestToProto(
+ ),
+ schedule=(IMPORT_reboot_time_DateTimeWithTimeZone.now() + schedule) if isinstance(
+ schedule, IMPORT_datetime_timedelta
+ ) else schedule,
+ )
+
+ self._context._tasks.append(task)
+
+ return task
+
+
+class _UserStub(IMPORT_reboot_aio_stubs.Stub):
+
+ __state_type_name__ = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User')
+
+ def __init__(
+ self,
+ *,
+ context: IMPORT_reboot_aio_contexts.Context | IMPORT_reboot_aio_external.ExternalContext,
+ state_ref: IMPORT_reboot_aio_types.StateRef,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ):
+ # Within a Reboot context we do not pass on the caller's bearer token, as that might
+ # have security implications - we cannot simply trust any service we are calling with
+ # the user's credentials. Instead, the developer can rely on the default app-internal
+ # auth, or override that and set an explicit bearer token.
+ #
+ # In the case of `ExternalContext`, however, its `bearer_token` was set specifically
+ # by the developer for the purpose of making these calls.
+ caller_id: IMPORT_typing.Optional[IMPORT_reboot_aio_caller_id.CallerID] = None
+ if isinstance(context, IMPORT_reboot_aio_external.ExternalContext):
+ # Note that only `ExternalContext` even has a `bearer_token` field.
+ bearer_token = context.bearer_token
+ # If the creator of the `ExternalContext` set an explicit caller ID, obey it.
+ caller_id = context.caller_id
+ else:
+ caller_id = IMPORT_reboot_aio_caller_id.CallerID(
+ application_id=context.application_id,
+ )
+
+ super().__init__(
+ channel_manager=context.channel_manager,
+ idempotency_manager=context,
+ state_ref=state_ref,
+ context=context if isinstance(context, IMPORT_reboot_aio_contexts.Context) else None,
+ bearer_token=bearer_token,
+ caller_id=caller_id,
+ )
+
+ # All the channels for all services of this state will go to the same
+ # place, so we can just get a single channel and share it across all
+ # stubs.
+ channel = self._channel_manager.get_channel_to_state(
+ self.__state_type_name__, state_ref
+ )
+ self._reboot_ping_usermethods_stub = reboot.ping.ping_api_pb2_grpc.UserMethodsStub(channel)
+
+
+class UserReaderStub(_UserStub):
+
+ def __init__(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext | IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ *,
+ state_ref: IMPORT_reboot_aio_types.StateRef,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ):
+ IMPORT_reboot_aio_types.assert_type(context, [IMPORT_reboot_aio_contexts.ReaderContext, IMPORT_reboot_aio_contexts.WriterContext, IMPORT_reboot_aio_contexts.TransactionContext, IMPORT_reboot_aio_contexts.WorkflowContext, IMPORT_reboot_aio_external.ExternalContext])
+ super().__init__(
+ context=context,
+ state_ref=state_ref,
+ bearer_token=bearer_token,
+ )
+
+ # User specific methods:
+
+ async def ListCounters(
+ self,
+ *,
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ ) -> reboot.ping.ping_api_pb2.UserListCountersResponse:
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User')
+ service_name = IMPORT_reboot_aio_types.ServiceName('reboot.ping.UserMethods')
+ method = 'ListCounters'
+
+ proto_request = UserListCountersRequestToProto(
+ )
+
+ async def call():
+ async with self._call(
+ state_type_name,
+ service_name,
+ method,
+ self._reboot_ping_usermethods_stub.ListCounters,
+ proto_request,
+ unary=True,
+ reader=True,
+ response_type=reboot.ping.ping_api_pb2.UserListCountersResponse,
+ aborted_type=User.ListCountersAborted,
+ metadata=metadata,
+ bearer_token=bearer_token,
+ ) as call:
+ assert isinstance(call, IMPORT_typing.Awaitable)
+ return await call
+
+ if isinstance(self._context, IMPORT_reboot_aio_contexts.WorkflowContext):
+ # Use the idempotency manager to make sure that this
+ # reader is being called following the rules.
+ with self._context.idempotently(
+ state_type_name=state_type_name,
+ state_ref=self._headers.state_ref,
+ service_name=service_name,
+ method=method,
+ mutation=False,
+ request=proto_request,
+ metadata=metadata,
+ idempotency=idempotency,
+ # Only need to pass `aborted_type` for mutations.
+ aborted_type=None,
+ ) as idempotency_key:
+ assert idempotency is not None
+ # Check if this reader is from an `.always()` and if
+ # so, don't memoize!
+ if idempotency.always:
+ return await call()
+
+ assert idempotency_key is not None
+ return await IMPORT_reboot_aio_workflows.at_least_once(
+ (
+ # TODO: for easier debugging include the
+ # original alias (or generated alias in the
+ # case of `.per_iteration()` w/o an alias)
+ # instead of just `idempotency_key`.
+ f'{ service_name }.{ method } ({str(idempotency_key)})',
+ # NOTE: we want this to be `PER_WORKFLOW`
+ # because any per iteration concerns should
+ # have already been taken care of by caller
+ # using `.per_iteration()`.
+ IMPORT_reboot_aio_workflows.PER_WORKFLOW
+ ),
+ self._context,
+ call,
+ type=reboot.ping.ping_api_pb2.UserListCountersResponse,
+ )
+ return await call()
+
+ async def Whoami(
+ self,
+ *,
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ ) -> reboot.ping.ping_api_pb2.UserWhoamiResponse:
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User')
+ service_name = IMPORT_reboot_aio_types.ServiceName('reboot.ping.UserMethods')
+ method = 'Whoami'
+
+ proto_request = UserWhoamiRequestToProto(
+ )
+
+ async def call():
+ async with self._call(
+ state_type_name,
+ service_name,
+ method,
+ self._reboot_ping_usermethods_stub.Whoami,
+ proto_request,
+ unary=True,
+ reader=True,
+ response_type=reboot.ping.ping_api_pb2.UserWhoamiResponse,
+ aborted_type=User.WhoamiAborted,
+ metadata=metadata,
+ bearer_token=bearer_token,
+ ) as call:
+ assert isinstance(call, IMPORT_typing.Awaitable)
+ return await call
+
+ if isinstance(self._context, IMPORT_reboot_aio_contexts.WorkflowContext):
+ # Use the idempotency manager to make sure that this
+ # reader is being called following the rules.
+ with self._context.idempotently(
+ state_type_name=state_type_name,
+ state_ref=self._headers.state_ref,
+ service_name=service_name,
+ method=method,
+ mutation=False,
+ request=proto_request,
+ metadata=metadata,
+ idempotency=idempotency,
+ # Only need to pass `aborted_type` for mutations.
+ aborted_type=None,
+ ) as idempotency_key:
+ assert idempotency is not None
+ # Check if this reader is from an `.always()` and if
+ # so, don't memoize!
+ if idempotency.always:
+ return await call()
+
+ assert idempotency_key is not None
+ return await IMPORT_reboot_aio_workflows.at_least_once(
+ (
+ # TODO: for easier debugging include the
+ # original alias (or generated alias in the
+ # case of `.per_iteration()` w/o an alias)
+ # instead of just `idempotency_key`.
+ f'{ service_name }.{ method } ({str(idempotency_key)})',
+ # NOTE: we want this to be `PER_WORKFLOW`
+ # because any per iteration concerns should
+ # have already been taken care of by caller
+ # using `.per_iteration()`.
+ IMPORT_reboot_aio_workflows.PER_WORKFLOW
+ ),
+ self._context,
+ call,
+ type=reboot.ping.ping_api_pb2.UserWhoamiResponse,
+ )
+ return await call()
+
+
+
+
+class UserWriterStub(_UserStub):
+
+ def __init__(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ *,
+ state_ref: IMPORT_reboot_aio_types.StateRef,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ):
+ IMPORT_reboot_aio_types.assert_type(context, [IMPORT_reboot_aio_contexts.TransactionContext, IMPORT_reboot_aio_contexts.WorkflowContext, IMPORT_reboot_aio_external.ExternalContext])
+ super().__init__(
+ context=context,
+ state_ref=state_ref,
+ bearer_token=bearer_token,
+ )
+
+ # User specific methods:
+
+ async def ListCounters(
+ self,
+ *,
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ) -> reboot.ping.ping_api_pb2.UserListCountersResponse:
+ async with self._call(
+ IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User'),
+ IMPORT_reboot_aio_types.ServiceName('reboot.ping.UserMethods'),
+ 'ListCounters',
+ self._reboot_ping_usermethods_stub.ListCounters,
+ UserListCountersRequestToProto(
+ ),
+ unary=True,
+ reader=True,
+ response_type=reboot.ping.ping_api_pb2.UserListCountersResponse,
+ aborted_type=User.ListCountersAborted,
+ metadata=metadata,
+ bearer_token=bearer_token,
+ ) as call:
+ assert isinstance(call, IMPORT_typing.Awaitable), type(call)
+ return await call
+
+ async def Whoami(
+ self,
+ *,
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ) -> reboot.ping.ping_api_pb2.UserWhoamiResponse:
+ async with self._call(
+ IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User'),
+ IMPORT_reboot_aio_types.ServiceName('reboot.ping.UserMethods'),
+ 'Whoami',
+ self._reboot_ping_usermethods_stub.Whoami,
+ UserWhoamiRequestToProto(
+ ),
+ unary=True,
+ reader=True,
+ response_type=reboot.ping.ping_api_pb2.UserWhoamiResponse,
+ aborted_type=User.WhoamiAborted,
+ metadata=metadata,
+ bearer_token=bearer_token,
+ ) as call:
+ assert isinstance(call, IMPORT_typing.Awaitable), type(call)
+ return await call
+
+
+
+class UserWorkflowStub(_UserStub):
+
+ def __init__(
+ self,
+ *,
+ context: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ state_ref: IMPORT_reboot_aio_types.StateRef,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ):
+ IMPORT_reboot_aio_types.assert_type(context, [IMPORT_reboot_aio_contexts.TransactionContext, IMPORT_reboot_aio_contexts.WorkflowContext, IMPORT_reboot_aio_external.ExternalContext])
+ super().__init__(
+ context=context,
+ state_ref=state_ref,
+ bearer_token=bearer_token,
+ )
+
+ # User specific methods:
+ async def CreateCounter(
+ self,
+ request: User.CreateCounterRequest,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ *,
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ) -> reboot.ping.ping_api_pb2.UserCreateCounterResponse:
+ idempotency_key: IMPORT_typing.Optional[IMPORT_uuid.UUID]
+ proto_request = UserCreateCounterRequestToProto(
+ request,
+ )
+
+ with self._idempotency_manager.idempotently(
+ state_type_name=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User'),
+ state_ref=self._headers.state_ref,
+ service_name=IMPORT_reboot_aio_types.ServiceName('reboot.ping.UserMethods'),
+ method='CreateCounter',
+ mutation=True,
+ request=proto_request,
+ metadata=metadata,
+ idempotency=idempotency,
+ aborted_type=User.CreateCounterAborted,
+ ) as idempotency_key:
+ async with self._call(
+ IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User'),
+ IMPORT_reboot_aio_types.ServiceName('reboot.ping.UserMethods'),
+ 'CreateCounter',
+ self._reboot_ping_usermethods_stub.CreateCounter,
+ proto_request,
+ unary=True,
+ reader=False,
+ response_type=reboot.ping.ping_api_pb2.UserCreateCounterResponse,
+ aborted_type=User.CreateCounterAborted,
+ metadata=metadata,
+ idempotency_key=idempotency_key,
+ per_iteration=idempotency.per_iteration if idempotency is not None else False,
+ always=idempotency.always if idempotency is not None else False,
+ bearer_token=bearer_token,
+ ) as call:
+ assert isinstance(call, IMPORT_typing.Awaitable), type(call)
+ return await call
+
+ async def ListCounters(
+ self,
+ *,
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ) -> reboot.ping.ping_api_pb2.UserListCountersResponse:
+ async with self._call(
+ IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User'),
+ IMPORT_reboot_aio_types.ServiceName('reboot.ping.UserMethods'),
+ 'ListCounters',
+ self._reboot_ping_usermethods_stub.ListCounters,
+ UserListCountersRequestToProto(
+ ),
+ unary=True,
+ reader=True,
+ response_type=reboot.ping.ping_api_pb2.UserListCountersResponse,
+ aborted_type=User.ListCountersAborted,
+ metadata=metadata,
+ bearer_token=bearer_token,
+ ) as call:
+ assert isinstance(call, IMPORT_typing.Awaitable), type(call)
+ return await call
+
+ async def Whoami(
+ self,
+ *,
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ) -> reboot.ping.ping_api_pb2.UserWhoamiResponse:
+ async with self._call(
+ IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User'),
+ IMPORT_reboot_aio_types.ServiceName('reboot.ping.UserMethods'),
+ 'Whoami',
+ self._reboot_ping_usermethods_stub.Whoami,
+ UserWhoamiRequestToProto(
+ ),
+ unary=True,
+ reader=True,
+ response_type=reboot.ping.ping_api_pb2.UserWhoamiResponse,
+ aborted_type=User.WhoamiAborted,
+ metadata=metadata,
+ bearer_token=bearer_token,
+ ) as call:
+ assert isinstance(call, IMPORT_typing.Awaitable), type(call)
+ return await call
+
+ async def Create(
+ self,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ *,
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ) -> google.protobuf.empty_pb2.Empty:
+ idempotency_key: IMPORT_typing.Optional[IMPORT_uuid.UUID]
+ proto_request = UserCreateRequestToProto(
+ )
+
+ with self._idempotency_manager.idempotently(
+ state_type_name=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User'),
+ state_ref=self._headers.state_ref,
+ service_name=IMPORT_reboot_aio_types.ServiceName('reboot.ping.UserMethods'),
+ method='Create',
+ mutation=True,
+ request=proto_request,
+ metadata=metadata,
+ idempotency=idempotency,
+ aborted_type=User.CreateAborted,
+ ) as idempotency_key:
+ async with self._call(
+ IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User'),
+ IMPORT_reboot_aio_types.ServiceName('reboot.ping.UserMethods'),
+ 'Create',
+ self._reboot_ping_usermethods_stub.Create,
+ proto_request,
+ unary=True,
+ reader=False,
+ response_type=google.protobuf.empty_pb2.Empty,
+ aborted_type=User.CreateAborted,
+ metadata=metadata,
+ idempotency_key=idempotency_key,
+ per_iteration=idempotency.per_iteration if idempotency is not None else False,
+ always=idempotency.always if idempotency is not None else False,
+ bearer_token=bearer_token,
+ ) as call:
+ assert isinstance(call, IMPORT_typing.Awaitable), type(call)
+ return await call
+
+
+
+class UserTasksStub(_UserStub):
+
+ def __init__(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ *,
+ state_ref: IMPORT_reboot_aio_types.StateRef,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ):
+ IMPORT_reboot_aio_types.assert_type(context, [IMPORT_reboot_aio_contexts.TransactionContext, IMPORT_reboot_aio_contexts.WorkflowContext, IMPORT_reboot_aio_external.ExternalContext])
+ super().__init__(
+ context=context,
+ state_ref=state_ref,
+ bearer_token=bearer_token,
+ )
+
+ # User specific methods:
+ async def CreateCounter(
+ self,
+ request: User.CreateCounterRequest,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ *,
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ idempotency_key: IMPORT_typing.Optional[IMPORT_uuid.UUID]
+ proto_request = UserCreateCounterRequestToProto(
+ request,
+ )
+
+ with self._idempotency_manager.idempotently(
+ state_type_name=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User'),
+ state_ref=self._headers.state_ref,
+ service_name=IMPORT_reboot_aio_types.ServiceName('reboot.ping.UserMethods'),
+ method='CreateCounter',
+ mutation=True,
+ request=proto_request,
+ metadata=metadata,
+ idempotency=idempotency,
+ aborted_type=User.CreateCounterAborted,
+ ) as idempotency_key:
+ async with self._call(
+ IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User'),
+ IMPORT_reboot_aio_types.ServiceName('reboot.ping.UserMethods'),
+ 'CreateCounter',
+ self._reboot_ping_usermethods_stub.CreateCounter,
+ proto_request,
+ unary=True,
+ reader=False,
+ response_type=reboot.ping.ping_api_pb2.UserCreateCounterResponse,
+ aborted_type=User.CreateCounterAborted,
+ metadata=metadata,
+ idempotency_key=idempotency_key,
+ per_iteration=idempotency.per_iteration if idempotency is not None else False,
+ always=idempotency.always if idempotency is not None else False,
+ bearer_token=bearer_token,
+ ) as call:
+ assert isinstance(call, IMPORT_typing.Awaitable), type(call)
+ await call
+ for (key, value) in await call.trailing_metadata(): # type: ignore[misc, attr-defined]
+ if key == IMPORT_reboot_aio_headers.TASK_ID_UUID:
+ return IMPORT_rbt_v1alpha1.tasks_pb2.TaskId(
+ state_type=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User'),
+ state_ref=self._headers.state_ref.to_str(),
+ task_uuid=IMPORT_uuid.UUID(value).bytes,
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Internal(),
+ message='Trailing metadata missing for task schedule',
+ )
+ async def ListCounters(
+ self,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ *,
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ idempotency_key: IMPORT_typing.Optional[IMPORT_uuid.UUID]
+ proto_request = UserListCountersRequestToProto(
+ )
+
+ with self._idempotency_manager.idempotently(
+ state_type_name=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User'),
+ state_ref=self._headers.state_ref,
+ service_name=IMPORT_reboot_aio_types.ServiceName('reboot.ping.UserMethods'),
+ method='ListCounters',
+ mutation=True,
+ request=proto_request,
+ metadata=metadata,
+ idempotency=idempotency,
+ aborted_type=User.ListCountersAborted,
+ ) as idempotency_key:
+ async with self._call(
+ IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User'),
+ IMPORT_reboot_aio_types.ServiceName('reboot.ping.UserMethods'),
+ 'ListCounters',
+ self._reboot_ping_usermethods_stub.ListCounters,
+ proto_request,
+ unary=True,
+ reader=False,
+ response_type=reboot.ping.ping_api_pb2.UserListCountersResponse,
+ aborted_type=User.ListCountersAborted,
+ metadata=metadata,
+ idempotency_key=idempotency_key,
+ per_iteration=idempotency.per_iteration if idempotency is not None else False,
+ always=idempotency.always if idempotency is not None else False,
+ bearer_token=bearer_token,
+ ) as call:
+ assert isinstance(call, IMPORT_typing.Awaitable), type(call)
+ await call
+ for (key, value) in await call.trailing_metadata(): # type: ignore[misc, attr-defined]
+ if key == IMPORT_reboot_aio_headers.TASK_ID_UUID:
+ return IMPORT_rbt_v1alpha1.tasks_pb2.TaskId(
+ state_type=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User'),
+ state_ref=self._headers.state_ref.to_str(),
+ task_uuid=IMPORT_uuid.UUID(value).bytes,
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Internal(),
+ message='Trailing metadata missing for task schedule',
+ )
+ async def Whoami(
+ self,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ *,
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ idempotency_key: IMPORT_typing.Optional[IMPORT_uuid.UUID]
+ proto_request = UserWhoamiRequestToProto(
+ )
+
+ with self._idempotency_manager.idempotently(
+ state_type_name=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User'),
+ state_ref=self._headers.state_ref,
+ service_name=IMPORT_reboot_aio_types.ServiceName('reboot.ping.UserMethods'),
+ method='Whoami',
+ mutation=True,
+ request=proto_request,
+ metadata=metadata,
+ idempotency=idempotency,
+ aborted_type=User.WhoamiAborted,
+ ) as idempotency_key:
+ async with self._call(
+ IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User'),
+ IMPORT_reboot_aio_types.ServiceName('reboot.ping.UserMethods'),
+ 'Whoami',
+ self._reboot_ping_usermethods_stub.Whoami,
+ proto_request,
+ unary=True,
+ reader=False,
+ response_type=reboot.ping.ping_api_pb2.UserWhoamiResponse,
+ aborted_type=User.WhoamiAborted,
+ metadata=metadata,
+ idempotency_key=idempotency_key,
+ per_iteration=idempotency.per_iteration if idempotency is not None else False,
+ always=idempotency.always if idempotency is not None else False,
+ bearer_token=bearer_token,
+ ) as call:
+ assert isinstance(call, IMPORT_typing.Awaitable), type(call)
+ await call
+ for (key, value) in await call.trailing_metadata(): # type: ignore[misc, attr-defined]
+ if key == IMPORT_reboot_aio_headers.TASK_ID_UUID:
+ return IMPORT_rbt_v1alpha1.tasks_pb2.TaskId(
+ state_type=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User'),
+ state_ref=self._headers.state_ref.to_str(),
+ task_uuid=IMPORT_uuid.UUID(value).bytes,
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Internal(),
+ message='Trailing metadata missing for task schedule',
+ )
+
+
+class UserServicerTasks:
+
+ _context: IMPORT_reboot_aio_contexts.WriterContext
+
+ def __init__(
+ self,
+ context: IMPORT_reboot_aio_contexts.WriterContext,
+ *,
+ state_ref: IMPORT_reboot_aio_types.StateRef,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ):
+ IMPORT_reboot_aio_types.assert_type(context, [IMPORT_reboot_aio_contexts.WriterContext])
+ self._context = context
+ self._state_ref = state_ref
+
+ # User specific methods:
+ async def CreateCounter(
+ self,
+ request: User.CreateCounterRequest,
+ *,
+ schedule: IMPORT_typing.Optional[IMPORT_datetime_datetime | IMPORT_datetime_timedelta] = None,
+ ) -> IMPORT_reboot_aio_tasks.TaskEffect:
+ schedule = ensure_has_timezone(when=schedule)
+ task = IMPORT_reboot_aio_tasks.TaskEffect(
+ state_type=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User'),
+ state_ref=self._state_ref,
+ method_name='CreateCounter',
+ request=UserCreateCounterRequestToProto(
+ request,
+ ),
+ schedule=(IMPORT_reboot_time_DateTimeWithTimeZone.now() + schedule) if isinstance(
+ schedule, IMPORT_datetime_timedelta
+ ) else schedule,
+ )
+
+ self._context._tasks.append(task)
+
+ return task
+
+ async def ListCounters(
+ self,
+ *,
+ schedule: IMPORT_typing.Optional[IMPORT_datetime_datetime | IMPORT_datetime_timedelta] = None,
+ ) -> IMPORT_reboot_aio_tasks.TaskEffect:
+ schedule = ensure_has_timezone(when=schedule)
+ task = IMPORT_reboot_aio_tasks.TaskEffect(
+ state_type=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User'),
+ state_ref=self._state_ref,
+ method_name='ListCounters',
+ request=UserListCountersRequestToProto(
+ ),
+ schedule=(IMPORT_reboot_time_DateTimeWithTimeZone.now() + schedule) if isinstance(
+ schedule, IMPORT_datetime_timedelta
+ ) else schedule,
+ )
+
+ self._context._tasks.append(task)
+
+ return task
+
+ async def Whoami(
+ self,
+ *,
+ schedule: IMPORT_typing.Optional[IMPORT_datetime_datetime | IMPORT_datetime_timedelta] = None,
+ ) -> IMPORT_reboot_aio_tasks.TaskEffect:
+ schedule = ensure_has_timezone(when=schedule)
+ task = IMPORT_reboot_aio_tasks.TaskEffect(
+ state_type=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User'),
+ state_ref=self._state_ref,
+ method_name='Whoami',
+ request=UserWhoamiRequestToProto(
+ ),
+ schedule=(IMPORT_reboot_time_DateTimeWithTimeZone.now() + schedule) if isinstance(
+ schedule, IMPORT_datetime_timedelta
+ ) else schedule,
+ )
+
+ self._context._tasks.append(task)
+
+ return task
+
+ async def Create(
+ self,
+ *,
+ schedule: IMPORT_typing.Optional[IMPORT_datetime_datetime | IMPORT_datetime_timedelta] = None,
+ ) -> IMPORT_reboot_aio_tasks.TaskEffect:
+ schedule = ensure_has_timezone(when=schedule)
+ task = IMPORT_reboot_aio_tasks.TaskEffect(
+ state_type=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User'),
+ state_ref=self._state_ref,
+ method_name='Create',
+ request=UserCreateRequestToProto(
+ ),
+ schedule=(IMPORT_reboot_time_DateTimeWithTimeZone.now() + schedule) if isinstance(
+ schedule, IMPORT_datetime_timedelta
+ ) else schedule,
+ )
+
+ self._context._tasks.append(task)
+
+ return task
+
+
+class _CounterStub(IMPORT_reboot_aio_stubs.Stub):
+
+ __state_type_name__ = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter')
+
+ def __init__(
+ self,
+ *,
+ context: IMPORT_reboot_aio_contexts.Context | IMPORT_reboot_aio_external.ExternalContext,
+ state_ref: IMPORT_reboot_aio_types.StateRef,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ):
+ # Within a Reboot context we do not pass on the caller's bearer token, as that might
+ # have security implications - we cannot simply trust any service we are calling with
+ # the user's credentials. Instead, the developer can rely on the default app-internal
+ # auth, or override that and set an explicit bearer token.
+ #
+ # In the case of `ExternalContext`, however, its `bearer_token` was set specifically
+ # by the developer for the purpose of making these calls.
+ caller_id: IMPORT_typing.Optional[IMPORT_reboot_aio_caller_id.CallerID] = None
+ if isinstance(context, IMPORT_reboot_aio_external.ExternalContext):
+ # Note that only `ExternalContext` even has a `bearer_token` field.
+ bearer_token = context.bearer_token
+ # If the creator of the `ExternalContext` set an explicit caller ID, obey it.
+ caller_id = context.caller_id
+ else:
+ caller_id = IMPORT_reboot_aio_caller_id.CallerID(
+ application_id=context.application_id,
+ )
+
+ super().__init__(
+ channel_manager=context.channel_manager,
+ idempotency_manager=context,
+ state_ref=state_ref,
+ context=context if isinstance(context, IMPORT_reboot_aio_contexts.Context) else None,
+ bearer_token=bearer_token,
+ caller_id=caller_id,
+ )
+
+ # All the channels for all services of this state will go to the same
+ # place, so we can just get a single channel and share it across all
+ # stubs.
+ channel = self._channel_manager.get_channel_to_state(
+ self.__state_type_name__, state_ref
+ )
+ self._reboot_ping_countermethods_stub = reboot.ping.ping_api_pb2_grpc.CounterMethodsStub(channel)
+
+
+class CounterReaderStub(_CounterStub):
+
+ def __init__(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext | IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ *,
+ state_ref: IMPORT_reboot_aio_types.StateRef,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ):
+ IMPORT_reboot_aio_types.assert_type(context, [IMPORT_reboot_aio_contexts.ReaderContext, IMPORT_reboot_aio_contexts.WriterContext, IMPORT_reboot_aio_contexts.TransactionContext, IMPORT_reboot_aio_contexts.WorkflowContext, IMPORT_reboot_aio_external.ExternalContext])
+ super().__init__(
+ context=context,
+ state_ref=state_ref,
+ bearer_token=bearer_token,
+ )
+
+ # Counter specific methods:
+
+
+ async def Value(
+ self,
+ *,
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ ) -> reboot.ping.ping_api_pb2.CounterValueResponse:
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter')
+ service_name = IMPORT_reboot_aio_types.ServiceName('reboot.ping.CounterMethods')
+ method = 'Value'
+
+ proto_request = CounterValueRequestToProto(
+ )
+
+ async def call():
+ async with self._call(
+ state_type_name,
+ service_name,
+ method,
+ self._reboot_ping_countermethods_stub.Value,
+ proto_request,
+ unary=True,
+ reader=True,
+ response_type=reboot.ping.ping_api_pb2.CounterValueResponse,
+ aborted_type=Counter.ValueAborted,
+ metadata=metadata,
+ bearer_token=bearer_token,
+ ) as call:
+ assert isinstance(call, IMPORT_typing.Awaitable)
+ return await call
+
+ if isinstance(self._context, IMPORT_reboot_aio_contexts.WorkflowContext):
+ # Use the idempotency manager to make sure that this
+ # reader is being called following the rules.
+ with self._context.idempotently(
+ state_type_name=state_type_name,
+ state_ref=self._headers.state_ref,
+ service_name=service_name,
+ method=method,
+ mutation=False,
+ request=proto_request,
+ metadata=metadata,
+ idempotency=idempotency,
+ # Only need to pass `aborted_type` for mutations.
+ aborted_type=None,
+ ) as idempotency_key:
+ assert idempotency is not None
+ # Check if this reader is from an `.always()` and if
+ # so, don't memoize!
+ if idempotency.always:
+ return await call()
+
+ assert idempotency_key is not None
+ return await IMPORT_reboot_aio_workflows.at_least_once(
+ (
+ # TODO: for easier debugging include the
+ # original alias (or generated alias in the
+ # case of `.per_iteration()` w/o an alias)
+ # instead of just `idempotency_key`.
+ f'{ service_name }.{ method } ({str(idempotency_key)})',
+ # NOTE: we want this to be `PER_WORKFLOW`
+ # because any per iteration concerns should
+ # have already been taken care of by caller
+ # using `.per_iteration()`.
+ IMPORT_reboot_aio_workflows.PER_WORKFLOW
+ ),
+ self._context,
+ call,
+ type=reboot.ping.ping_api_pb2.CounterValueResponse,
+ )
+ return await call()
+
+ async def Description(
+ self,
+ *,
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ ) -> reboot.ping.ping_api_pb2.CounterDescriptionResponse:
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter')
+ service_name = IMPORT_reboot_aio_types.ServiceName('reboot.ping.CounterMethods')
+ method = 'Description'
+
+ proto_request = CounterDescriptionRequestToProto(
+ )
+
+ async def call():
+ async with self._call(
+ state_type_name,
+ service_name,
+ method,
+ self._reboot_ping_countermethods_stub.Description,
+ proto_request,
+ unary=True,
+ reader=True,
+ response_type=reboot.ping.ping_api_pb2.CounterDescriptionResponse,
+ aborted_type=Counter.DescriptionAborted,
+ metadata=metadata,
+ bearer_token=bearer_token,
+ ) as call:
+ assert isinstance(call, IMPORT_typing.Awaitable)
+ return await call
+
+ if isinstance(self._context, IMPORT_reboot_aio_contexts.WorkflowContext):
+ # Use the idempotency manager to make sure that this
+ # reader is being called following the rules.
+ with self._context.idempotently(
+ state_type_name=state_type_name,
+ state_ref=self._headers.state_ref,
+ service_name=service_name,
+ method=method,
+ mutation=False,
+ request=proto_request,
+ metadata=metadata,
+ idempotency=idempotency,
+ # Only need to pass `aborted_type` for mutations.
+ aborted_type=None,
+ ) as idempotency_key:
+ assert idempotency is not None
+ # Check if this reader is from an `.always()` and if
+ # so, don't memoize!
+ if idempotency.always:
+ return await call()
+
+ assert idempotency_key is not None
+ return await IMPORT_reboot_aio_workflows.at_least_once(
+ (
+ # TODO: for easier debugging include the
+ # original alias (or generated alias in the
+ # case of `.per_iteration()` w/o an alias)
+ # instead of just `idempotency_key`.
+ f'{ service_name }.{ method } ({str(idempotency_key)})',
+ # NOTE: we want this to be `PER_WORKFLOW`
+ # because any per iteration concerns should
+ # have already been taken care of by caller
+ # using `.per_iteration()`.
+ IMPORT_reboot_aio_workflows.PER_WORKFLOW
+ ),
+ self._context,
+ call,
+ type=reboot.ping.ping_api_pb2.CounterDescriptionResponse,
+ )
+ return await call()
+
+
+
+class CounterWriterStub(_CounterStub):
+
+ def __init__(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ *,
+ state_ref: IMPORT_reboot_aio_types.StateRef,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ):
+ IMPORT_reboot_aio_types.assert_type(context, [IMPORT_reboot_aio_contexts.TransactionContext, IMPORT_reboot_aio_contexts.WorkflowContext, IMPORT_reboot_aio_external.ExternalContext])
+ super().__init__(
+ context=context,
+ state_ref=state_ref,
+ bearer_token=bearer_token,
+ )
+
+ # Counter specific methods:
+ async def Create(
+ self,
+ request: Counter.CreateRequest,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ *,
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ) -> google.protobuf.empty_pb2.Empty:
+ proto_request = CounterCreateRequestToProto(
+ request,
+ )
+ with self._idempotency_manager.idempotently(
+ state_type_name=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ state_ref=self._headers.state_ref,
+ service_name=IMPORT_reboot_aio_types.ServiceName('reboot.ping.CounterMethods'),
+ method='Create',
+ mutation=True,
+ request=proto_request,
+ metadata=metadata,
+ idempotency=idempotency,
+ aborted_type=Counter.CreateAborted,
+ ) as idempotency_key:
+ async with self._call(
+ IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ IMPORT_reboot_aio_types.ServiceName('reboot.ping.CounterMethods'),
+ 'Create',
+ self._reboot_ping_countermethods_stub.Create,
+ proto_request,
+ unary=True,
+ reader=False,
+ response_type=google.protobuf.empty_pb2.Empty,
+ aborted_type=Counter.CreateAborted,
+ metadata=metadata,
+ idempotency_key=idempotency_key,
+ per_iteration=idempotency.per_iteration if idempotency is not None else False,
+ always=idempotency.always if idempotency is not None else False,
+ bearer_token=bearer_token,
+ ) as call:
+ assert isinstance(call, IMPORT_typing.Awaitable), type(call)
+ return await call
+
+ async def Increment(
+ self,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ *,
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ) -> reboot.ping.ping_api_pb2.CounterIncrementResponse:
+ proto_request = CounterIncrementRequestToProto(
+ )
+ with self._idempotency_manager.idempotently(
+ state_type_name=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ state_ref=self._headers.state_ref,
+ service_name=IMPORT_reboot_aio_types.ServiceName('reboot.ping.CounterMethods'),
+ method='Increment',
+ mutation=True,
+ request=proto_request,
+ metadata=metadata,
+ idempotency=idempotency,
+ aborted_type=Counter.IncrementAborted,
+ ) as idempotency_key:
+ async with self._call(
+ IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ IMPORT_reboot_aio_types.ServiceName('reboot.ping.CounterMethods'),
+ 'Increment',
+ self._reboot_ping_countermethods_stub.Increment,
+ proto_request,
+ unary=True,
+ reader=False,
+ response_type=reboot.ping.ping_api_pb2.CounterIncrementResponse,
+ aborted_type=Counter.IncrementAborted,
+ metadata=metadata,
+ idempotency_key=idempotency_key,
+ per_iteration=idempotency.per_iteration if idempotency is not None else False,
+ always=idempotency.always if idempotency is not None else False,
+ bearer_token=bearer_token,
+ ) as call:
+ assert isinstance(call, IMPORT_typing.Awaitable), type(call)
+ return await call
+
+ async def Value(
+ self,
+ *,
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ) -> reboot.ping.ping_api_pb2.CounterValueResponse:
+ async with self._call(
+ IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ IMPORT_reboot_aio_types.ServiceName('reboot.ping.CounterMethods'),
+ 'Value',
+ self._reboot_ping_countermethods_stub.Value,
+ CounterValueRequestToProto(
+ ),
+ unary=True,
+ reader=True,
+ response_type=reboot.ping.ping_api_pb2.CounterValueResponse,
+ aborted_type=Counter.ValueAborted,
+ metadata=metadata,
+ bearer_token=bearer_token,
+ ) as call:
+ assert isinstance(call, IMPORT_typing.Awaitable), type(call)
+ return await call
+
+ async def Description(
+ self,
+ *,
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ) -> reboot.ping.ping_api_pb2.CounterDescriptionResponse:
+ async with self._call(
+ IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ IMPORT_reboot_aio_types.ServiceName('reboot.ping.CounterMethods'),
+ 'Description',
+ self._reboot_ping_countermethods_stub.Description,
+ CounterDescriptionRequestToProto(
+ ),
+ unary=True,
+ reader=True,
+ response_type=reboot.ping.ping_api_pb2.CounterDescriptionResponse,
+ aborted_type=Counter.DescriptionAborted,
+ metadata=metadata,
+ bearer_token=bearer_token,
+ ) as call:
+ assert isinstance(call, IMPORT_typing.Awaitable), type(call)
+ return await call
+
+
+class CounterWorkflowStub(_CounterStub):
+
+ def __init__(
+ self,
+ *,
+ context: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ state_ref: IMPORT_reboot_aio_types.StateRef,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ):
+ IMPORT_reboot_aio_types.assert_type(context, [IMPORT_reboot_aio_contexts.TransactionContext, IMPORT_reboot_aio_contexts.WorkflowContext, IMPORT_reboot_aio_external.ExternalContext])
+ super().__init__(
+ context=context,
+ state_ref=state_ref,
+ bearer_token=bearer_token,
+ )
+
+ # Counter specific methods:
+ async def Create(
+ self,
+ request: Counter.CreateRequest,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ *,
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ) -> google.protobuf.empty_pb2.Empty:
+ idempotency_key: IMPORT_typing.Optional[IMPORT_uuid.UUID]
+ proto_request = CounterCreateRequestToProto(
+ request,
+ )
+
+ with self._idempotency_manager.idempotently(
+ state_type_name=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ state_ref=self._headers.state_ref,
+ service_name=IMPORT_reboot_aio_types.ServiceName('reboot.ping.CounterMethods'),
+ method='Create',
+ mutation=True,
+ request=proto_request,
+ metadata=metadata,
+ idempotency=idempotency,
+ aborted_type=Counter.CreateAborted,
+ ) as idempotency_key:
+ async with self._call(
+ IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ IMPORT_reboot_aio_types.ServiceName('reboot.ping.CounterMethods'),
+ 'Create',
+ self._reboot_ping_countermethods_stub.Create,
+ proto_request,
+ unary=True,
+ reader=False,
+ response_type=google.protobuf.empty_pb2.Empty,
+ aborted_type=Counter.CreateAborted,
+ metadata=metadata,
+ idempotency_key=idempotency_key,
+ per_iteration=idempotency.per_iteration if idempotency is not None else False,
+ always=idempotency.always if idempotency is not None else False,
+ bearer_token=bearer_token,
+ ) as call:
+ assert isinstance(call, IMPORT_typing.Awaitable), type(call)
+ return await call
+
+ async def Increment(
+ self,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ *,
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ) -> reboot.ping.ping_api_pb2.CounterIncrementResponse:
+ idempotency_key: IMPORT_typing.Optional[IMPORT_uuid.UUID]
+ proto_request = CounterIncrementRequestToProto(
+ )
+
+ with self._idempotency_manager.idempotently(
+ state_type_name=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ state_ref=self._headers.state_ref,
+ service_name=IMPORT_reboot_aio_types.ServiceName('reboot.ping.CounterMethods'),
+ method='Increment',
+ mutation=True,
+ request=proto_request,
+ metadata=metadata,
+ idempotency=idempotency,
+ aborted_type=Counter.IncrementAborted,
+ ) as idempotency_key:
+ async with self._call(
+ IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ IMPORT_reboot_aio_types.ServiceName('reboot.ping.CounterMethods'),
+ 'Increment',
+ self._reboot_ping_countermethods_stub.Increment,
+ proto_request,
+ unary=True,
+ reader=False,
+ response_type=reboot.ping.ping_api_pb2.CounterIncrementResponse,
+ aborted_type=Counter.IncrementAborted,
+ metadata=metadata,
+ idempotency_key=idempotency_key,
+ per_iteration=idempotency.per_iteration if idempotency is not None else False,
+ always=idempotency.always if idempotency is not None else False,
+ bearer_token=bearer_token,
+ ) as call:
+ assert isinstance(call, IMPORT_typing.Awaitable), type(call)
+ return await call
+
+ async def Value(
+ self,
+ *,
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ) -> reboot.ping.ping_api_pb2.CounterValueResponse:
+ async with self._call(
+ IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ IMPORT_reboot_aio_types.ServiceName('reboot.ping.CounterMethods'),
+ 'Value',
+ self._reboot_ping_countermethods_stub.Value,
+ CounterValueRequestToProto(
+ ),
+ unary=True,
+ reader=True,
+ response_type=reboot.ping.ping_api_pb2.CounterValueResponse,
+ aborted_type=Counter.ValueAborted,
+ metadata=metadata,
+ bearer_token=bearer_token,
+ ) as call:
+ assert isinstance(call, IMPORT_typing.Awaitable), type(call)
+ return await call
+
+ async def Description(
+ self,
+ *,
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ) -> reboot.ping.ping_api_pb2.CounterDescriptionResponse:
+ async with self._call(
+ IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ IMPORT_reboot_aio_types.ServiceName('reboot.ping.CounterMethods'),
+ 'Description',
+ self._reboot_ping_countermethods_stub.Description,
+ CounterDescriptionRequestToProto(
+ ),
+ unary=True,
+ reader=True,
+ response_type=reboot.ping.ping_api_pb2.CounterDescriptionResponse,
+ aborted_type=Counter.DescriptionAborted,
+ metadata=metadata,
+ bearer_token=bearer_token,
+ ) as call:
+ assert isinstance(call, IMPORT_typing.Awaitable), type(call)
+ return await call
+
+
+
+class CounterTasksStub(_CounterStub):
+
+ def __init__(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ *,
+ state_ref: IMPORT_reboot_aio_types.StateRef,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ):
+ IMPORT_reboot_aio_types.assert_type(context, [IMPORT_reboot_aio_contexts.TransactionContext, IMPORT_reboot_aio_contexts.WorkflowContext, IMPORT_reboot_aio_external.ExternalContext])
+ super().__init__(
+ context=context,
+ state_ref=state_ref,
+ bearer_token=bearer_token,
+ )
+
+ # Counter specific methods:
+ async def Increment(
+ self,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ *,
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ idempotency_key: IMPORT_typing.Optional[IMPORT_uuid.UUID]
+ proto_request = CounterIncrementRequestToProto(
+ )
+
+ with self._idempotency_manager.idempotently(
+ state_type_name=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ state_ref=self._headers.state_ref,
+ service_name=IMPORT_reboot_aio_types.ServiceName('reboot.ping.CounterMethods'),
+ method='Increment',
+ mutation=True,
+ request=proto_request,
+ metadata=metadata,
+ idempotency=idempotency,
+ aborted_type=Counter.IncrementAborted,
+ ) as idempotency_key:
+ async with self._call(
+ IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ IMPORT_reboot_aio_types.ServiceName('reboot.ping.CounterMethods'),
+ 'Increment',
+ self._reboot_ping_countermethods_stub.Increment,
+ proto_request,
+ unary=True,
+ reader=False,
+ response_type=reboot.ping.ping_api_pb2.CounterIncrementResponse,
+ aborted_type=Counter.IncrementAborted,
+ metadata=metadata,
+ idempotency_key=idempotency_key,
+ per_iteration=idempotency.per_iteration if idempotency is not None else False,
+ always=idempotency.always if idempotency is not None else False,
+ bearer_token=bearer_token,
+ ) as call:
+ assert isinstance(call, IMPORT_typing.Awaitable), type(call)
+ await call
+ for (key, value) in await call.trailing_metadata(): # type: ignore[misc, attr-defined]
+ if key == IMPORT_reboot_aio_headers.TASK_ID_UUID:
+ return IMPORT_rbt_v1alpha1.tasks_pb2.TaskId(
+ state_type=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ state_ref=self._headers.state_ref.to_str(),
+ task_uuid=IMPORT_uuid.UUID(value).bytes,
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Internal(),
+ message='Trailing metadata missing for task schedule',
+ )
+ async def Value(
+ self,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ *,
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ idempotency_key: IMPORT_typing.Optional[IMPORT_uuid.UUID]
+ proto_request = CounterValueRequestToProto(
+ )
+
+ with self._idempotency_manager.idempotently(
+ state_type_name=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ state_ref=self._headers.state_ref,
+ service_name=IMPORT_reboot_aio_types.ServiceName('reboot.ping.CounterMethods'),
+ method='Value',
+ mutation=True,
+ request=proto_request,
+ metadata=metadata,
+ idempotency=idempotency,
+ aborted_type=Counter.ValueAborted,
+ ) as idempotency_key:
+ async with self._call(
+ IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ IMPORT_reboot_aio_types.ServiceName('reboot.ping.CounterMethods'),
+ 'Value',
+ self._reboot_ping_countermethods_stub.Value,
+ proto_request,
+ unary=True,
+ reader=False,
+ response_type=reboot.ping.ping_api_pb2.CounterValueResponse,
+ aborted_type=Counter.ValueAborted,
+ metadata=metadata,
+ idempotency_key=idempotency_key,
+ per_iteration=idempotency.per_iteration if idempotency is not None else False,
+ always=idempotency.always if idempotency is not None else False,
+ bearer_token=bearer_token,
+ ) as call:
+ assert isinstance(call, IMPORT_typing.Awaitable), type(call)
+ await call
+ for (key, value) in await call.trailing_metadata(): # type: ignore[misc, attr-defined]
+ if key == IMPORT_reboot_aio_headers.TASK_ID_UUID:
+ return IMPORT_rbt_v1alpha1.tasks_pb2.TaskId(
+ state_type=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ state_ref=self._headers.state_ref.to_str(),
+ task_uuid=IMPORT_uuid.UUID(value).bytes,
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Internal(),
+ message='Trailing metadata missing for task schedule',
+ )
+ async def Description(
+ self,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ *,
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ idempotency_key: IMPORT_typing.Optional[IMPORT_uuid.UUID]
+ proto_request = CounterDescriptionRequestToProto(
+ )
+
+ with self._idempotency_manager.idempotently(
+ state_type_name=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ state_ref=self._headers.state_ref,
+ service_name=IMPORT_reboot_aio_types.ServiceName('reboot.ping.CounterMethods'),
+ method='Description',
+ mutation=True,
+ request=proto_request,
+ metadata=metadata,
+ idempotency=idempotency,
+ aborted_type=Counter.DescriptionAborted,
+ ) as idempotency_key:
+ async with self._call(
+ IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ IMPORT_reboot_aio_types.ServiceName('reboot.ping.CounterMethods'),
+ 'Description',
+ self._reboot_ping_countermethods_stub.Description,
+ proto_request,
+ unary=True,
+ reader=False,
+ response_type=reboot.ping.ping_api_pb2.CounterDescriptionResponse,
+ aborted_type=Counter.DescriptionAborted,
+ metadata=metadata,
+ idempotency_key=idempotency_key,
+ per_iteration=idempotency.per_iteration if idempotency is not None else False,
+ always=idempotency.always if idempotency is not None else False,
+ bearer_token=bearer_token,
+ ) as call:
+ assert isinstance(call, IMPORT_typing.Awaitable), type(call)
+ await call
+ for (key, value) in await call.trailing_metadata(): # type: ignore[misc, attr-defined]
+ if key == IMPORT_reboot_aio_headers.TASK_ID_UUID:
+ return IMPORT_rbt_v1alpha1.tasks_pb2.TaskId(
+ state_type=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ state_ref=self._headers.state_ref.to_str(),
+ task_uuid=IMPORT_uuid.UUID(value).bytes,
+ )
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Internal(),
+ message='Trailing metadata missing for task schedule',
+ )
+
+
+class CounterServicerTasks:
+
+ _context: IMPORT_reboot_aio_contexts.WriterContext
+
+ def __init__(
+ self,
+ context: IMPORT_reboot_aio_contexts.WriterContext,
+ *,
+ state_ref: IMPORT_reboot_aio_types.StateRef,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ):
+ IMPORT_reboot_aio_types.assert_type(context, [IMPORT_reboot_aio_contexts.WriterContext])
+ self._context = context
+ self._state_ref = state_ref
+
+ # Counter specific methods:
+ async def Create(
+ self,
+ request: Counter.CreateRequest,
+ *,
+ schedule: IMPORT_typing.Optional[IMPORT_datetime_datetime | IMPORT_datetime_timedelta] = None,
+ ) -> IMPORT_reboot_aio_tasks.TaskEffect:
+ schedule = ensure_has_timezone(when=schedule)
+ task = IMPORT_reboot_aio_tasks.TaskEffect(
+ state_type=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ state_ref=self._state_ref,
+ method_name='Create',
+ request=CounterCreateRequestToProto(
+ request,
+ ),
+ schedule=(IMPORT_reboot_time_DateTimeWithTimeZone.now() + schedule) if isinstance(
+ schedule, IMPORT_datetime_timedelta
+ ) else schedule,
+ )
+
+ self._context._tasks.append(task)
+
+ return task
+
+ async def Increment(
+ self,
+ *,
+ schedule: IMPORT_typing.Optional[IMPORT_datetime_datetime | IMPORT_datetime_timedelta] = None,
+ ) -> IMPORT_reboot_aio_tasks.TaskEffect:
+ schedule = ensure_has_timezone(when=schedule)
+ task = IMPORT_reboot_aio_tasks.TaskEffect(
+ state_type=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ state_ref=self._state_ref,
+ method_name='Increment',
+ request=CounterIncrementRequestToProto(
+ ),
+ schedule=(IMPORT_reboot_time_DateTimeWithTimeZone.now() + schedule) if isinstance(
+ schedule, IMPORT_datetime_timedelta
+ ) else schedule,
+ )
+
+ self._context._tasks.append(task)
+
+ return task
+
+ async def Value(
+ self,
+ *,
+ schedule: IMPORT_typing.Optional[IMPORT_datetime_datetime | IMPORT_datetime_timedelta] = None,
+ ) -> IMPORT_reboot_aio_tasks.TaskEffect:
+ schedule = ensure_has_timezone(when=schedule)
+ task = IMPORT_reboot_aio_tasks.TaskEffect(
+ state_type=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ state_ref=self._state_ref,
+ method_name='Value',
+ request=CounterValueRequestToProto(
+ ),
+ schedule=(IMPORT_reboot_time_DateTimeWithTimeZone.now() + schedule) if isinstance(
+ schedule, IMPORT_datetime_timedelta
+ ) else schedule,
+ )
+
+ self._context._tasks.append(task)
+
+ return task
+
+ async def Description(
+ self,
+ *,
+ schedule: IMPORT_typing.Optional[IMPORT_datetime_datetime | IMPORT_datetime_timedelta] = None,
+ ) -> IMPORT_reboot_aio_tasks.TaskEffect:
+ schedule = ensure_has_timezone(when=schedule)
+ task = IMPORT_reboot_aio_tasks.TaskEffect(
+ state_type=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ state_ref=self._state_ref,
+ method_name='Description',
+ request=CounterDescriptionRequestToProto(
+ ),
+ schedule=(IMPORT_reboot_time_DateTimeWithTimeZone.now() + schedule) if isinstance(
+ schedule, IMPORT_datetime_timedelta
+ ) else schedule,
+ )
+
+ self._context._tasks.append(task)
+
+ return task
+
+
+
+############################ Authorizers ############################
+# Relevant to servicers; irrelevant to clients.
+
+PingStateType: IMPORT_typing.TypeAlias = reboot.ping.ping_api_pb2.Ping
+PingRequestTypes: IMPORT_typing.TypeAlias = \
+ google.protobuf.empty_pb2.Empty \
+ | reboot.ping.ping_api_pb2.PingDoPingPeriodicallyRequest
+
+class PingAuthorizer(
+ IMPORT_reboot_aio_auth_authorizers.Authorizer[PingStateType, PingRequestTypes],
+):
+ StateType: IMPORT_typing.TypeAlias = PingStateType
+ RequestTypes: IMPORT_typing.TypeAlias = PingRequestTypes
+ Decision: IMPORT_typing.TypeAlias = IMPORT_reboot_aio_auth_authorizers.Authorizer.Decision
+
+ def __init__(
+ self,
+ *,
+ DoPing: IMPORT_typing.Optional[
+ IMPORT_reboot_aio_auth_authorizers.AuthorizerRule[
+ Ping.State,
+ None
+ ]
+ ] = None,
+ do_ping: IMPORT_typing.Optional[
+ IMPORT_reboot_aio_auth_authorizers.AuthorizerRule[
+ Ping.State,
+ None
+ ]
+ ] = None,
+ DoPingPeriodically: IMPORT_typing.Optional[
+ IMPORT_reboot_aio_auth_authorizers.AuthorizerRule[
+ Ping.State,
+ Ping.DoPingPeriodicallyRequest,
+ ]
+ ] = None,
+ do_ping_periodically: IMPORT_typing.Optional[
+ IMPORT_reboot_aio_auth_authorizers.AuthorizerRule[
+ Ping.State,
+ Ping.DoPingPeriodicallyRequest,
+ ]
+ ] = None,
+ Describe: IMPORT_typing.Optional[
+ IMPORT_reboot_aio_auth_authorizers.AuthorizerRule[
+ Ping.State,
+ None
+ ]
+ ] = None,
+ describe: IMPORT_typing.Optional[
+ IMPORT_reboot_aio_auth_authorizers.AuthorizerRule[
+ Ping.State,
+ None
+ ]
+ ] = None,
+ NumPings: IMPORT_typing.Optional[
+ IMPORT_reboot_aio_auth_authorizers.AuthorizerRule[
+ Ping.State,
+ None
+ ]
+ ] = None,
+ num_pings: IMPORT_typing.Optional[
+ IMPORT_reboot_aio_auth_authorizers.AuthorizerRule[
+ Ping.State,
+ None
+ ]
+ ] = None,
+ # NOTE: using `_` prefix for `_default` so as not to collide
+ # with any method names since a prefixed `_` is forbidden by
+ # our protoc plugins.
+ _default: IMPORT_reboot_aio_auth_authorizers.AuthorizerRule[
+ reboot.ping.ping_api_pb2.Ping,
+ IMPORT_google_protobuf_message.Message,
+ ] = IMPORT_reboot_aio_auth_authorizers.allow_if(
+ all=[IMPORT_reboot_aio_auth_authorizers.is_app_internal],
+ ),
+ ):
+ if do_ping is not None and DoPing is not None:
+ raise ValueError(
+ f"Cannot specify both 'DoPing' and 'do_ping' authorizer rules"
+ )
+ self._do_ping = do_ping or DoPing
+ if do_ping_periodically is not None and DoPingPeriodically is not None:
+ raise ValueError(
+ f"Cannot specify both 'DoPingPeriodically' and 'do_ping_periodically' authorizer rules"
+ )
+ self._do_ping_periodically = do_ping_periodically or DoPingPeriodically
+ if describe is not None and Describe is not None:
+ raise ValueError(
+ f"Cannot specify both 'Describe' and 'describe' authorizer rules"
+ )
+ self._describe = describe or Describe
+ if num_pings is not None and NumPings is not None:
+ raise ValueError(
+ f"Cannot specify both 'NumPings' and 'num_pings' authorizer rules"
+ )
+ self._num_pings = num_pings or NumPings
+ self.__default = _default
+
+ async def authorize(
+ self,
+ *,
+ method_name: str,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: IMPORT_typing.Optional[PingStateType],
+ request: IMPORT_typing.Optional[PingRequestTypes],
+ **kwargs,
+ ) -> IMPORT_reboot_aio_auth_authorizers.Authorizer.Decision:
+ if method_name == 'reboot.ping.PingMethods.DoPing':
+ return await self.DoPing(
+ context=context,
+ state=PingFromProto(state, is_initial_state=(context.state_id in states_being_constructed)) if state is not None else state,
+ )
+ elif method_name == 'reboot.ping.PingMethods.DoPingPeriodically':
+ return await self.DoPingPeriodically(
+ context=context,
+ state=PingFromProto(state, is_initial_state=(context.state_id in states_being_constructed)) if state is not None else state,
+ request=PingDoPingPeriodicallyRequestFromProto(request) if request is not None else request,
+ )
+ elif method_name == 'reboot.ping.PingMethods.Describe':
+ return await self.Describe(
+ context=context,
+ state=PingFromProto(state, is_initial_state=(context.state_id in states_being_constructed)) if state is not None else state,
+ )
+ elif method_name == 'reboot.ping.PingMethods.NumPings':
+ return await self.NumPings(
+ context=context,
+ state=PingFromProto(state, is_initial_state=(context.state_id in states_being_constructed)) if state is not None else state,
+ )
+ else:
+ return IMPORT_rbt_v1alpha1.errors_pb2.PermissionDenied()
+
+ # For 'reboot.ping.PingMethods.DoPing'.
+ async def DoPing(
+ self,
+ *,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: Ping.State,
+ ) -> IMPORT_reboot_aio_auth_authorizers.Authorizer.Decision:
+ return await (self._do_ping or self.__default).execute(
+ context=context,
+ state=state,
+ # TODO: Don't pass 'None' if user specified 'request=None'
+ # in the Pydantic API. We need to support dropping 'request'
+ # argument in the default 'AuthorizerRule's before.
+ request=None,
+ )
+
+ # For 'reboot.ping.PingMethods.DoPingPeriodically'.
+ async def DoPingPeriodically(
+ self,
+ *,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: Ping.State,
+ request: Ping.DoPingPeriodicallyRequest,
+ ) -> IMPORT_reboot_aio_auth_authorizers.Authorizer.Decision:
+ return await (self._do_ping_periodically or self.__default).execute(
+ context=context,
+ state=state,
+ request=request,
+ )
+
+ # For 'reboot.ping.PingMethods.Describe'.
+ async def Describe(
+ self,
+ *,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: Ping.State,
+ ) -> IMPORT_reboot_aio_auth_authorizers.Authorizer.Decision:
+ return await (self._describe or self.__default).execute(
+ context=context,
+ state=state,
+ # TODO: Don't pass 'None' if user specified 'request=None'
+ # in the Pydantic API. We need to support dropping 'request'
+ # argument in the default 'AuthorizerRule's before.
+ request=None,
+ )
+
+ # For 'reboot.ping.PingMethods.NumPings'.
+ async def NumPings(
+ self,
+ *,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: Ping.State,
+ ) -> IMPORT_reboot_aio_auth_authorizers.Authorizer.Decision:
+ return await (self._num_pings or self.__default).execute(
+ context=context,
+ state=state,
+ # TODO: Don't pass 'None' if user specified 'request=None'
+ # in the Pydantic API. We need to support dropping 'request'
+ # argument in the default 'AuthorizerRule's before.
+ request=None,
+ )
+
+
+PongStateType: IMPORT_typing.TypeAlias = reboot.ping.ping_api_pb2.Pong
+PongRequestTypes: IMPORT_typing.TypeAlias = \
+ google.protobuf.empty_pb2.Empty
+
+class PongAuthorizer(
+ IMPORT_reboot_aio_auth_authorizers.Authorizer[PongStateType, PongRequestTypes],
+):
+ StateType: IMPORT_typing.TypeAlias = PongStateType
+ RequestTypes: IMPORT_typing.TypeAlias = PongRequestTypes
+ Decision: IMPORT_typing.TypeAlias = IMPORT_reboot_aio_auth_authorizers.Authorizer.Decision
+
+ def __init__(
+ self,
+ *,
+ DoPong: IMPORT_typing.Optional[
+ IMPORT_reboot_aio_auth_authorizers.AuthorizerRule[
+ Pong.State,
+ None
+ ]
+ ] = None,
+ do_pong: IMPORT_typing.Optional[
+ IMPORT_reboot_aio_auth_authorizers.AuthorizerRule[
+ Pong.State,
+ None
+ ]
+ ] = None,
+ NumPongs: IMPORT_typing.Optional[
+ IMPORT_reboot_aio_auth_authorizers.AuthorizerRule[
+ Pong.State,
+ None
+ ]
+ ] = None,
+ num_pongs: IMPORT_typing.Optional[
+ IMPORT_reboot_aio_auth_authorizers.AuthorizerRule[
+ Pong.State,
+ None
+ ]
+ ] = None,
+ # NOTE: using `_` prefix for `_default` so as not to collide
+ # with any method names since a prefixed `_` is forbidden by
+ # our protoc plugins.
+ _default: IMPORT_reboot_aio_auth_authorizers.AuthorizerRule[
+ reboot.ping.ping_api_pb2.Pong,
+ IMPORT_google_protobuf_message.Message,
+ ] = IMPORT_reboot_aio_auth_authorizers.allow_if(
+ all=[IMPORT_reboot_aio_auth_authorizers.is_app_internal],
+ ),
+ ):
+ if do_pong is not None and DoPong is not None:
+ raise ValueError(
+ f"Cannot specify both 'DoPong' and 'do_pong' authorizer rules"
+ )
+ self._do_pong = do_pong or DoPong
+ if num_pongs is not None and NumPongs is not None:
+ raise ValueError(
+ f"Cannot specify both 'NumPongs' and 'num_pongs' authorizer rules"
+ )
+ self._num_pongs = num_pongs or NumPongs
+ self.__default = _default
+
+ async def authorize(
+ self,
+ *,
+ method_name: str,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: IMPORT_typing.Optional[PongStateType],
+ request: IMPORT_typing.Optional[PongRequestTypes],
+ **kwargs,
+ ) -> IMPORT_reboot_aio_auth_authorizers.Authorizer.Decision:
+ if method_name == 'reboot.ping.PongMethods.DoPong':
+ return await self.DoPong(
+ context=context,
+ state=PongFromProto(state, is_initial_state=(context.state_id in states_being_constructed)) if state is not None else state,
+ )
+ elif method_name == 'reboot.ping.PongMethods.NumPongs':
+ return await self.NumPongs(
+ context=context,
+ state=PongFromProto(state, is_initial_state=(context.state_id in states_being_constructed)) if state is not None else state,
+ )
+ else:
+ return IMPORT_rbt_v1alpha1.errors_pb2.PermissionDenied()
+
+ # For 'reboot.ping.PongMethods.DoPong'.
+ async def DoPong(
+ self,
+ *,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: Pong.State,
+ ) -> IMPORT_reboot_aio_auth_authorizers.Authorizer.Decision:
+ return await (self._do_pong or self.__default).execute(
+ context=context,
+ state=state,
+ # TODO: Don't pass 'None' if user specified 'request=None'
+ # in the Pydantic API. We need to support dropping 'request'
+ # argument in the default 'AuthorizerRule's before.
+ request=None,
+ )
+
+ # For 'reboot.ping.PongMethods.NumPongs'.
+ async def NumPongs(
+ self,
+ *,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: Pong.State,
+ ) -> IMPORT_reboot_aio_auth_authorizers.Authorizer.Decision:
+ return await (self._num_pongs or self.__default).execute(
+ context=context,
+ state=state,
+ # TODO: Don't pass 'None' if user specified 'request=None'
+ # in the Pydantic API. We need to support dropping 'request'
+ # argument in the default 'AuthorizerRule's before.
+ request=None,
+ )
+
+
+UserStateType: IMPORT_typing.TypeAlias = reboot.ping.ping_api_pb2.User
+UserRequestTypes: IMPORT_typing.TypeAlias = \
+ reboot.ping.ping_api_pb2.UserCreateCounterRequest \
+ | google.protobuf.empty_pb2.Empty
+
+class UserAuthorizer(
+ IMPORT_reboot_aio_auth_authorizers.Authorizer[UserStateType, UserRequestTypes],
+):
+ StateType: IMPORT_typing.TypeAlias = UserStateType
+ RequestTypes: IMPORT_typing.TypeAlias = UserRequestTypes
+ Decision: IMPORT_typing.TypeAlias = IMPORT_reboot_aio_auth_authorizers.Authorizer.Decision
+
+ def __init__(
+ self,
+ *,
+ CreateCounter: IMPORT_typing.Optional[
+ IMPORT_reboot_aio_auth_authorizers.AuthorizerRule[
+ User.State,
+ User.CreateCounterRequest,
+ ]
+ ] = None,
+ create_counter: IMPORT_typing.Optional[
+ IMPORT_reboot_aio_auth_authorizers.AuthorizerRule[
+ User.State,
+ User.CreateCounterRequest,
+ ]
+ ] = None,
+ ListCounters: IMPORT_typing.Optional[
+ IMPORT_reboot_aio_auth_authorizers.AuthorizerRule[
+ User.State,
+ None
+ ]
+ ] = None,
+ list_counters: IMPORT_typing.Optional[
+ IMPORT_reboot_aio_auth_authorizers.AuthorizerRule[
+ User.State,
+ None
+ ]
+ ] = None,
+ Whoami: IMPORT_typing.Optional[
+ IMPORT_reboot_aio_auth_authorizers.AuthorizerRule[
+ User.State,
+ None
+ ]
+ ] = None,
+ whoami: IMPORT_typing.Optional[
+ IMPORT_reboot_aio_auth_authorizers.AuthorizerRule[
+ User.State,
+ None
+ ]
+ ] = None,
+ Create: IMPORT_typing.Optional[
+ IMPORT_reboot_aio_auth_authorizers.AuthorizerRule[
+ User.State,
+ None
+ ]
+ ] = None,
+ create: IMPORT_typing.Optional[
+ IMPORT_reboot_aio_auth_authorizers.AuthorizerRule[
+ User.State,
+ None
+ ]
+ ] = None,
+ # NOTE: using `_` prefix for `_default` so as not to collide
+ # with any method names since a prefixed `_` is forbidden by
+ # our protoc plugins.
+ _default: IMPORT_reboot_aio_auth_authorizers.AuthorizerRule[
+ reboot.ping.ping_api_pb2.User,
+ IMPORT_google_protobuf_message.Message,
+ ] = IMPORT_reboot_aio_auth_authorizers.allow_if(
+ any=[
+ IMPORT_reboot_aio_auth_authorizers.state_id_is_user_id,
+ IMPORT_reboot_aio_auth_authorizers.is_app_internal,
+ ],
+ ),
+ ):
+ if create_counter is not None and CreateCounter is not None:
+ raise ValueError(
+ f"Cannot specify both 'CreateCounter' and 'create_counter' authorizer rules"
+ )
+ self._create_counter = create_counter or CreateCounter
+ if list_counters is not None and ListCounters is not None:
+ raise ValueError(
+ f"Cannot specify both 'ListCounters' and 'list_counters' authorizer rules"
+ )
+ self._list_counters = list_counters or ListCounters
+ if whoami is not None and Whoami is not None:
+ raise ValueError(
+ f"Cannot specify both 'Whoami' and 'whoami' authorizer rules"
+ )
+ self._whoami = whoami or Whoami
+ if create is not None and Create is not None:
+ raise ValueError(
+ f"Cannot specify both 'Create' and 'create' authorizer rules"
+ )
+ self._create = create or Create
+ self.__default = _default
+
+ async def authorize(
+ self,
+ *,
+ method_name: str,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: IMPORT_typing.Optional[UserStateType],
+ request: IMPORT_typing.Optional[UserRequestTypes],
+ **kwargs,
+ ) -> IMPORT_reboot_aio_auth_authorizers.Authorizer.Decision:
+ if method_name == 'reboot.ping.UserMethods.CreateCounter':
+ return await self.CreateCounter(
+ context=context,
+ state=UserFromProto(state, is_initial_state=(context.state_id in states_being_constructed)) if state is not None else state,
+ request=UserCreateCounterRequestFromProto(request) if request is not None else request,
+ )
+ elif method_name == 'reboot.ping.UserMethods.ListCounters':
+ return await self.ListCounters(
+ context=context,
+ state=UserFromProto(state, is_initial_state=(context.state_id in states_being_constructed)) if state is not None else state,
+ )
+ elif method_name == 'reboot.ping.UserMethods.Whoami':
+ return await self.Whoami(
+ context=context,
+ state=UserFromProto(state, is_initial_state=(context.state_id in states_being_constructed)) if state is not None else state,
+ )
+ elif method_name == 'reboot.ping.UserMethods.Create':
+ return await self.Create(
+ context=context,
+ )
+ else:
+ return IMPORT_rbt_v1alpha1.errors_pb2.PermissionDenied()
+
+ # For 'reboot.ping.UserMethods.CreateCounter'.
+ async def CreateCounter(
+ self,
+ *,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: User.State,
+ request: User.CreateCounterRequest,
+ ) -> IMPORT_reboot_aio_auth_authorizers.Authorizer.Decision:
+ return await (self._create_counter or self.__default).execute(
+ context=context,
+ state=state,
+ request=request,
+ )
+
+ # For 'reboot.ping.UserMethods.ListCounters'.
+ async def ListCounters(
+ self,
+ *,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: User.State,
+ ) -> IMPORT_reboot_aio_auth_authorizers.Authorizer.Decision:
+ return await (self._list_counters or self.__default).execute(
+ context=context,
+ state=state,
+ # TODO: Don't pass 'None' if user specified 'request=None'
+ # in the Pydantic API. We need to support dropping 'request'
+ # argument in the default 'AuthorizerRule's before.
+ request=None,
+ )
+
+ # For 'reboot.ping.UserMethods.Whoami'.
+ async def Whoami(
+ self,
+ *,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: User.State,
+ ) -> IMPORT_reboot_aio_auth_authorizers.Authorizer.Decision:
+ return await (self._whoami or self.__default).execute(
+ context=context,
+ state=state,
+ # TODO: Don't pass 'None' if user specified 'request=None'
+ # in the Pydantic API. We need to support dropping 'request'
+ # argument in the default 'AuthorizerRule's before.
+ request=None,
+ )
+
+ # For 'reboot.ping.UserMethods.Create'.
+ async def Create(
+ self,
+ *,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ ) -> IMPORT_reboot_aio_auth_authorizers.Authorizer.Decision:
+ return await (self._create or self.__default).execute(
+ context=context,
+ state=None,
+ # TODO: Don't pass 'None' if user specified 'request=None'
+ # in the Pydantic API. We need to support dropping 'request'
+ # argument in the default 'AuthorizerRule's before.
+ request=None,
+ )
+
+
+CounterStateType: IMPORT_typing.TypeAlias = reboot.ping.ping_api_pb2.Counter
+CounterRequestTypes: IMPORT_typing.TypeAlias = \
+ reboot.ping.ping_api_pb2.CounterCreateRequest \
+ | google.protobuf.empty_pb2.Empty
+
+class CounterAuthorizer(
+ IMPORT_reboot_aio_auth_authorizers.Authorizer[CounterStateType, CounterRequestTypes],
+):
+ StateType: IMPORT_typing.TypeAlias = CounterStateType
+ RequestTypes: IMPORT_typing.TypeAlias = CounterRequestTypes
+ Decision: IMPORT_typing.TypeAlias = IMPORT_reboot_aio_auth_authorizers.Authorizer.Decision
+
+ def __init__(
+ self,
+ *,
+ Create: IMPORT_typing.Optional[
+ IMPORT_reboot_aio_auth_authorizers.AuthorizerRule[
+ Counter.State,
+ Counter.CreateRequest,
+ ]
+ ] = None,
+ create: IMPORT_typing.Optional[
+ IMPORT_reboot_aio_auth_authorizers.AuthorizerRule[
+ Counter.State,
+ Counter.CreateRequest,
+ ]
+ ] = None,
+ Increment: IMPORT_typing.Optional[
+ IMPORT_reboot_aio_auth_authorizers.AuthorizerRule[
+ Counter.State,
+ None
+ ]
+ ] = None,
+ increment: IMPORT_typing.Optional[
+ IMPORT_reboot_aio_auth_authorizers.AuthorizerRule[
+ Counter.State,
+ None
+ ]
+ ] = None,
+ Value: IMPORT_typing.Optional[
+ IMPORT_reboot_aio_auth_authorizers.AuthorizerRule[
+ Counter.State,
+ None
+ ]
+ ] = None,
+ value: IMPORT_typing.Optional[
+ IMPORT_reboot_aio_auth_authorizers.AuthorizerRule[
+ Counter.State,
+ None
+ ]
+ ] = None,
+ Description: IMPORT_typing.Optional[
+ IMPORT_reboot_aio_auth_authorizers.AuthorizerRule[
+ Counter.State,
+ None
+ ]
+ ] = None,
+ description: IMPORT_typing.Optional[
+ IMPORT_reboot_aio_auth_authorizers.AuthorizerRule[
+ Counter.State,
+ None
+ ]
+ ] = None,
+ # NOTE: using `_` prefix for `_default` so as not to collide
+ # with any method names since a prefixed `_` is forbidden by
+ # our protoc plugins.
+ _default: IMPORT_reboot_aio_auth_authorizers.AuthorizerRule[
+ reboot.ping.ping_api_pb2.Counter,
+ IMPORT_google_protobuf_message.Message,
+ ] = IMPORT_reboot_aio_auth_authorizers.allow_if(
+ all=[IMPORT_reboot_aio_auth_authorizers.is_app_internal],
+ ),
+ ):
+ if create is not None and Create is not None:
+ raise ValueError(
+ f"Cannot specify both 'Create' and 'create' authorizer rules"
+ )
+ self._create = create or Create
+ if increment is not None and Increment is not None:
+ raise ValueError(
+ f"Cannot specify both 'Increment' and 'increment' authorizer rules"
+ )
+ self._increment = increment or Increment
+ if value is not None and Value is not None:
+ raise ValueError(
+ f"Cannot specify both 'Value' and 'value' authorizer rules"
+ )
+ self._value = value or Value
+ if description is not None and Description is not None:
+ raise ValueError(
+ f"Cannot specify both 'Description' and 'description' authorizer rules"
+ )
+ self._description = description or Description
+ self.__default = _default
+
+ async def authorize(
+ self,
+ *,
+ method_name: str,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: IMPORT_typing.Optional[CounterStateType],
+ request: IMPORT_typing.Optional[CounterRequestTypes],
+ **kwargs,
+ ) -> IMPORT_reboot_aio_auth_authorizers.Authorizer.Decision:
+ if method_name == 'reboot.ping.CounterMethods.Create':
+ return await self.Create(
+ context=context,
+ request=CounterCreateRequestFromProto(request) if request is not None else request,
+ )
+ elif method_name == 'reboot.ping.CounterMethods.Increment':
+ return await self.Increment(
+ context=context,
+ state=CounterFromProto(state, is_initial_state=(context.state_id in states_being_constructed)) if state is not None else state,
+ )
+ elif method_name == 'reboot.ping.CounterMethods.Value':
+ return await self.Value(
+ context=context,
+ state=CounterFromProto(state, is_initial_state=(context.state_id in states_being_constructed)) if state is not None else state,
+ )
+ elif method_name == 'reboot.ping.CounterMethods.Description':
+ return await self.Description(
+ context=context,
+ state=CounterFromProto(state, is_initial_state=(context.state_id in states_being_constructed)) if state is not None else state,
+ )
+ else:
+ return IMPORT_rbt_v1alpha1.errors_pb2.PermissionDenied()
+
+ # For 'reboot.ping.CounterMethods.Create'.
+ async def Create(
+ self,
+ *,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ request: Counter.CreateRequest,
+ ) -> IMPORT_reboot_aio_auth_authorizers.Authorizer.Decision:
+ return await (self._create or self.__default).execute(
+ context=context,
+ state=None,
+ request=request,
+ )
+
+ # For 'reboot.ping.CounterMethods.Increment'.
+ async def Increment(
+ self,
+ *,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: Counter.State,
+ ) -> IMPORT_reboot_aio_auth_authorizers.Authorizer.Decision:
+ return await (self._increment or self.__default).execute(
+ context=context,
+ state=state,
+ # TODO: Don't pass 'None' if user specified 'request=None'
+ # in the Pydantic API. We need to support dropping 'request'
+ # argument in the default 'AuthorizerRule's before.
+ request=None,
+ )
+
+ # For 'reboot.ping.CounterMethods.Value'.
+ async def Value(
+ self,
+ *,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: Counter.State,
+ ) -> IMPORT_reboot_aio_auth_authorizers.Authorizer.Decision:
+ return await (self._value or self.__default).execute(
+ context=context,
+ state=state,
+ # TODO: Don't pass 'None' if user specified 'request=None'
+ # in the Pydantic API. We need to support dropping 'request'
+ # argument in the default 'AuthorizerRule's before.
+ request=None,
+ )
+
+ # For 'reboot.ping.CounterMethods.Description'.
+ async def Description(
+ self,
+ *,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: Counter.State,
+ ) -> IMPORT_reboot_aio_auth_authorizers.Authorizer.Decision:
+ return await (self._description or self.__default).execute(
+ context=context,
+ state=state,
+ # TODO: Don't pass 'None' if user specified 'request=None'
+ # in the Pydantic API. We need to support dropping 'request'
+ # argument in the default 'AuthorizerRule's before.
+ request=None,
+ )
+
+
+
+############################ Reboot Servicers ############################
+# Base classes for server-side implementations of Reboot servicers.
+# Irrelevant to clients.
+
+class PingBaseServicer(IMPORT_reboot_aio_servicers.Servicer):
+ Authorizer: IMPORT_typing.TypeAlias = PingAuthorizer
+ # Node-backed servicers attach a JS bridge object at runtime. We declare it
+ # on the base class so mypy accepts accesses when a servicer is typed as
+ # `PingBaseServicer` (e.g. in classmethod workflows).
+ _js_servicer_reference: IMPORT_typing.Any
+
+ __servicer__: IMPORT_contextvars.ContextVar[IMPORT_typing.Optional[PingBaseServicer]] = IMPORT_contextvars.ContextVar(
+ 'Provides access to a servicer in the current asyncio context. '
+ 'We need that to be able to do inline writes and reads inside '
+ 'a workflow',
+ default=None,
+ )
+
+ __service_names__ = [
+ IMPORT_reboot_aio_types.ServiceName('reboot.ping.PingMethods'),
+ ]
+ __state_type_name__ = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping')
+ __state_type__ = reboot.ping.ping_api_pb2.Ping
+ __file_descriptor__ = reboot.ping.ping_api_pb2.DESCRIPTOR
+
+ def __init__(self):
+ super().__init__()
+ # NOTE: need to hold on to the middleware so we can do inline
+ # writes (see 'self.write(...)').
+ #
+ # Because '_middleware' is not really private this does mean
+ # users may do possibly dangerous things, but this is no more
+ # likely given they could have already overridden
+ # 'create_middleware()'.
+ self._middleware: IMPORT_typing.Optional[PingServicerMiddleware] = None
+
+ def create_middleware(
+ self,
+ *,
+ application_id: IMPORT_reboot_aio_types.ApplicationId,
+ server_id: IMPORT_reboot_aio_types.ServerId,
+ state_manager: IMPORT_reboot_aio_state_managers.StateManager,
+ placement_client: IMPORT_reboot_aio_placement.PlacementClient,
+ channel_manager: IMPORT_reboot_aio_internals_channel_manager._ChannelManager,
+ tasks_cache: IMPORT_reboot_aio_internals_tasks_cache.TasksCache,
+ token_verifier: IMPORT_typing.Optional[IMPORT_reboot_aio_auth_token_verifiers.TokenVerifier],
+ effect_validation: IMPORT_reboot_aio_contexts.EffectValidation,
+ ready: IMPORT_asyncio.Event,
+ ) -> PingServicerMiddleware:
+ self._middleware = PingServicerMiddleware(
+ servicer=self,
+ application_id=application_id,
+ server_id=server_id,
+ state_manager=state_manager,
+ placement_client=placement_client,
+ channel_manager=channel_manager,
+ tasks_cache=tasks_cache,
+ token_verifier=token_verifier,
+ effect_validation=effect_validation,
+ ready=ready,
+ )
+ return self._middleware
+
+ def authorizer(self) -> IMPORT_typing.Optional[IMPORT_reboot_aio_auth_authorizers.Authorizer | IMPORT_reboot_aio_auth_authorizers.AuthorizerRule]:
+ return None
+
+ def token_verifier(self) -> IMPORT_typing.Optional[IMPORT_reboot_aio_auth_token_verifiers.TokenVerifier]:
+ return None
+
+ _is_auto_construct = False
+
+
+ @staticmethod
+ def _mcp_tool_names() -> list[str]:
+ """Return the MCP tool names this servicer registers."""
+ return [
+ "ping_do_ping",
+ "ping_num_pings",
+ "ping_show_pinger",
+ ]
+
+ @staticmethod
+ def _add_mcp(
+ mcp: IMPORT_mcp_server_fastmcp.FastMCP,
+ auto_construct_state_type_full_names: list[IMPORT_reboot_aio_types.StateTypeName],
+ ) -> None:
+ """Register MCP tools and resources for Ping."""
+
+ # Tool for 'reboot.ping.PingMethods.DoPing'.
+ @mcp.tool(
+ name="ping_do_ping",
+ title="DoPing",
+ description="Invoke DoPing on Ping.",
+ )
+ async def ping_do_ping_tool(
+ ctx: IMPORT_mcp_server_fastmcp.Context,
+ ping_id: str,
+ ):
+ reboot_context = IMPORT_reboot_mcp_context.get_reboot_context(ctx)
+ ref = Ping.ref(ping_id)
+ return await ref.do_ping(reboot_context)
+
+
+ # Tool for 'reboot.ping.PingMethods.NumPings'.
+ @mcp.tool(
+ name="ping_num_pings",
+ title="NumPings",
+ description="Invoke NumPings on Ping.",
+ )
+ async def ping_num_pings_tool(
+ ctx: IMPORT_mcp_server_fastmcp.Context,
+ ping_id: str,
+ ):
+ reboot_context = IMPORT_reboot_mcp_context.get_reboot_context(ctx)
+ ref = Ping.ref(ping_id)
+ return await ref.num_pings(reboot_context)
+
+
+ # Map UI method names to web app paths.
+ _ui_app_paths: dict[str, str] = {
+ "show_pinger": "web/ui/pinger",
+ }
+
+ # Optional artifact path overrides (only populated
+ # when `artifact_path` is explicitly set).
+ _ui_artifact_paths: dict[str, str] = {
+ }
+
+ # Cache-bust tokens: content hash of each UI's built
+ # `index.html`, or a per-process random fallback if the
+ # artifact is missing. Embedded in the tool's
+ # `_meta.ui.resourceUri` below and in the `@mcp.resource`
+ # URI template so hosts that aggressively cache MCP
+ # resource reads (e.g., ChatGPT) see a fresh URI after
+ # each rebuild. Computed once at servicer setup since
+ # `--config=dist` restarts the process on deploy. The
+ # helper falls back to a per-process token (and logs)
+ # rather than raising if the project root can't be
+ # located — deploys with unusual filesystem layouts
+ # still start cleanly.
+ _ui_cache_busts: dict[str, str] = (
+ IMPORT_reboot_mcp_ui.compute_ui_cache_busts(
+ __file__,
+ _ui_app_paths,
+ _ui_artifact_paths,
+ )
+ )
+
+ # Register each UI tool's recompute inputs in the
+ # server-local registry. The patched `FastMCP.list_tools`
+ # in `reboot.mcp.factories` reads this on every
+ # `tools/list` call and rewrites `_meta.ui.resourceUri`
+ # with a freshly-hashed token so clients that re-list on
+ # new sessions (Claude, MCPJam) pick up dist-file changes
+ # without a server restart. Kept server-local — never
+ # rides on the wire in `_meta`.
+ IMPORT_reboot_mcp_ui.register_ui_tool_for_cache_bust(
+ tool_name="ping_show_pinger",
+ caller_file=__file__,
+ ui_path=_ui_app_paths["show_pinger"],
+ ui_name="show_pinger",
+ artifact_path=_ui_artifact_paths.get("show_pinger"),
+ uri_prefix=(
+ "ui://ping/"
+ "show_pinger/"
+ ),
+ )
+
+ # UI tools — allow AI to trigger loading of UIs.
+ @mcp.tool(
+ name="ping_show_pinger",
+ title="Show Ping Counter",
+ description="Interactive UI for the Ping's counter.",
+ meta={
+ "ui": {
+ "resourceUri": (
+ "ui://ping/show_pinger/"
+ + _ui_cache_busts["show_pinger"]
+ ),
+ },
+ },
+ )
+ async def ping_show_pinger_tool(
+ ctx: IMPORT_mcp_server_fastmcp.Context,
+ ping_id: str,
+ ) -> dict:
+ _ids: dict[IMPORT_reboot_aio_types.StateTypeName, str | None] = {
+ IMPORT_reboot_aio_types.StateTypeName("reboot.ping.Ping"): ping_id,
+ }
+ # Include all auto-construct IDs.
+ for _auto_construct_full_name in auto_construct_state_type_full_names:
+ _ids[_auto_construct_full_name] = (
+ IMPORT_reboot_mcp_context.get_mcp_user_id(ctx))
+ result: dict = {"ids": _ids}
+
+ # Invariant: the MCP server has a bearer token for this tool
+ # call because opening a UI requires an authenticated
+ # session. This is the only place where the token can be
+ # passed to the iframe, since the iframe has no other access
+ # to the MCP session credentials. The token is read by
+ # `McpConnector` from `ontoolresult` and forwarded to
+ # `RebootClientProvider` for all Reboot calls inside the
+ # app. This tool is also invoked by `McpConnector` to
+ # refresh an expired token, so the field must always be
+ # present when a token is available.
+ reboot_context = IMPORT_reboot_mcp_context.get_reboot_context(ctx)
+ if reboot_context is not None and reboot_context.bearer_token is not None:
+ result["bearer_token"] = reboot_context.bearer_token
+ return result
+
+ def _get_reboot_url_from_request(
+ ctx: IMPORT_mcp_server_fastmcp.Context,
+ ) -> str:
+ """Extract Reboot URL from request headers."""
+ request = ctx.request_context.request
+ if request is None:
+ raise RuntimeError(
+ "No HTTP request in MCP context"
+ )
+ host = (
+ request.headers.get("x-forwarded-host")
+ or request.headers.get("host")
+ )
+ if not host:
+ raise RuntimeError(
+ "No host header in request"
+ )
+ scheme = (
+ request.headers.get("x-forwarded-proto")
+ or ("https" if request.url.scheme == "https"
+ else "http")
+ )
+ return f"{scheme}://{host}"
+
+ @mcp.resource(
+ "ui://ping/{ui}/{cache_bust}",
+ name="ping-ui",
+ title="Ping UI",
+ description="Ping UIs.",
+ mime_type="text/html;profile=mcp-app",
+ )
+ def ping_ui_resource(
+ ctx: IMPORT_mcp_server_fastmcp.Context,
+ ui: str,
+ cache_bust: str,
+ ) -> str:
+ # `cache_bust` comes from the URI's trailing
+ # segment: the content-hash token that `list_tools`
+ # wrote into the tool's `_meta.ui.resourceUri`. We
+ # pass it to `ui_html` so the per-process HTML
+ # cache keys on it — same token = cache hit,
+ # changed token (because the dist file changed) =
+ # cache miss = fresh read.
+ reboot_url = _get_reboot_url_from_request(ctx)
+ if ui not in _ui_app_paths:
+ raise ValueError(f"Unknown UI: {ui}")
+ return IMPORT_reboot_mcp_ui.ui_html(
+ _ui_app_paths[ui],
+ reboot_url,
+ ui_name=ui,
+ artifact_path=_ui_artifact_paths.get(ui),
+ cache_bust=cache_bust,
+ )
+
+ pass # End of _add_mcp.
+
+
+ def ref(
+ self,
+ *,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ) -> Ping.WeakReference[Ping.WeakReference._WriterSchedule]:
+ context = IMPORT_reboot_aio_contexts.Context.get()
+
+ if context is None:
+ raise RuntimeError(
+ 'Missing asyncio context variable `context`; '
+ 'are you using this class without Reboot?'
+ )
+
+ return Ping.WeakReference(
+ # TODO(https://github.com/reboot-dev/mono/issues/3226): add support for calling other applications.
+ # For now this always stays within the application that creates the context.
+ application_id=None,
+ state_id=context._state_ref.id,
+ schedule_type=Ping.WeakReference._WriterSchedule,
+ # If the user didn't specify a bearer token we may still end up using the app-internal bearer token,
+ # but that's decided at the time of the call.
+ bearer_token=bearer_token,
+ servicer=self,
+ )
+
+ class Effects(IMPORT_reboot_aio_state_managers.Effects):
+ def __init__(
+ self,
+ *,
+ state: reboot.ping.ping_api_pb2.Ping,
+ response: IMPORT_typing.Optional[IMPORT_google_protobuf_message.Message] = None,
+ tasks: IMPORT_typing.Optional[list[IMPORT_reboot_aio_tasks.TaskEffect]] = None,
+ _colocated_upserts: IMPORT_typing.Optional[list[tuple[str, IMPORT_typing.Optional[bytes]]]] = None,
+ ):
+ IMPORT_reboot_aio_types.assert_type(state, [reboot.ping.ping_api_pb2.Ping])
+
+ super().__init__(state=state, response=response, tasks=tasks, _colocated_upserts=_colocated_upserts)
+
+
+
+
+
+
+ InlineWriterCallableResult = IMPORT_typing.TypeVar('InlineWriterCallableResult', covariant=True)
+
+ class InlineWriterCallable(IMPORT_typing.Protocol[InlineWriterCallableResult]):
+ async def __call__(
+ self,
+ state: reboot.ping.ping_api_pb2.Ping
+ ) -> PingBaseServicer.InlineWriterCallableResult:
+ ...
+
+ class WorkflowState:
+
+ def __init__(
+ self,
+ servicer,
+ ):
+ self._servicer = servicer
+
+ async def read(
+ self, context: IMPORT_reboot_aio_contexts.WorkflowContext
+ ) -> Ping.State:
+ """Read the current state within a workflow."""
+ return await (
+ self.always() if context.within_until()
+ else (
+ self.per_iteration() if context.within_loop()
+ else self.per_workflow()
+ )
+ ).read(context)
+
+ async def write(
+ self,
+ idempotency_alias: str,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ writer: PingBaseServicer.InlineWriterCallable[PingBaseServicer.InlineWriterCallableResult],
+ __options__: IMPORT_reboot_aio_call.Options = IMPORT_reboot_aio_call.Options(),
+ *,
+ type: IMPORT_reboot_aio_workflows.Type | IMPORT_reboot_aio_workflows._Unset = IMPORT_reboot_aio_workflows._UNSET,
+ ) -> PingBaseServicer.InlineWriterCallableResult:
+ """Perform an "inline write" within a workflow.
+
+ `type=` is optional: when omitted, `writer`'s return
+ annotation is used (falling back to `type(None)` if
+ there is none).
+ """
+ type_t, _ = IMPORT_reboot_aio_workflows._resolve_callable_return_type(
+ writer, type,
+ )
+ return await (
+ self.per_iteration(idempotency_alias) if context.within_loop()
+ else self.per_workflow(idempotency_alias)
+ ).write(
+ context, writer, __options__, type=type_t
+ )
+
+ class _Idempotently:
+
+ def __init__(
+ self,
+ *,
+ servicer: PingBaseServicer,
+ alias: IMPORT_typing.Optional[str],
+ how: IMPORT_reboot_aio_workflows.How,
+ ):
+ self._servicer = servicer
+ self._alias = alias
+ self._how = how
+
+ async def read(
+ self, context: IMPORT_reboot_aio_contexts.WorkflowContext
+ ) -> Ping.State:
+ """Read the current state within a workflow."""
+ return await self._read(
+ self._servicer,
+ context.idempotency(
+ how=IMPORT_reboot_aio_idempotency.ALWAYS,
+ ) if self._how == IMPORT_reboot_aio_workflows.ALWAYS else context.idempotency(
+ alias=self._alias,
+ how=self._how,
+ ),
+ context,
+ )
+
+ @staticmethod
+ async def _read(
+ servicer: PingBaseServicer,
+ idempotency: IMPORT_reboot_aio_idempotency.Idempotency,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ ) -> Ping.State:
+ """Read the current state within a workflow."""
+ IMPORT_reboot_aio_types.assert_type(context, [IMPORT_reboot_aio_contexts.WorkflowContext])
+
+ if servicer._middleware is None:
+ raise RuntimeError(
+ 'Reboot middleware was not created; '
+ 'are you using this class without Reboot?'
+ )
+
+ async def read() -> reboot.ping.ping_api_pb2.Ping:
+ assert servicer._middleware is not None
+ return await servicer._middleware._state_manager.read(
+ context, servicer.__state_type__
+ )
+
+ if idempotency.always:
+ return PingFromProto(await read())
+
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping')
+
+ # Use the idempotency manager to make sure that this
+ # reader is being called following the rules.
+ with context.idempotently(
+ state_type_name=state_type_name,
+ state_ref=context._state_ref,
+ # Not calling a method so `service_name`,
+ # `method`, `request`, etc are irrelevant.
+ service_name=None,
+ method=None,
+ mutation=False,
+ request=None,
+ metadata=None,
+ idempotency=idempotency,
+ # Only need to pass `aborted_type` for mutations.
+ aborted_type=None,
+ ) as idempotency_key:
+ assert idempotency_key is not None
+ protobuf_state = await IMPORT_reboot_aio_workflows.at_least_once(
+ (
+ # TODO: for easier debugging include the
+ # original alias (or generated alias in
+ # the case of `.per_iteration()` w/o an
+ # alias) instead of just
+ # `idempotency_key`.
+ f"inline reader of '{ state_type_name }' ({str(idempotency_key)})",
+ # NOTE: we want this to be `PER_WORKFLOW`
+ # because any per iteration concerns
+ # should have already been taken care of
+ # by caller using `.per_iteration()`.
+ IMPORT_reboot_aio_workflows.PER_WORKFLOW
+ ),
+ context,
+ read,
+ type=reboot.ping.ping_api_pb2.Ping,
+ )
+
+ return PingFromProto(protobuf_state)
+
+ async def write(
+ self,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ writer: PingBaseServicer.InlineWriterCallable[PingBaseServicer.InlineWriterCallableResult],
+ __options__: IMPORT_reboot_aio_call.Options = IMPORT_reboot_aio_call.Options(),
+ *,
+ type: IMPORT_reboot_aio_workflows.Type | IMPORT_reboot_aio_workflows._Unset = IMPORT_reboot_aio_workflows._UNSET,
+ check_type: bool = True,
+ ) -> PingBaseServicer.InlineWriterCallableResult:
+ # `type=` is optional: when omitted, `writer`'s
+ # return annotation is used (falling back to
+ # `type(None)` when there is none).
+ type_t, _ = IMPORT_reboot_aio_workflows._resolve_callable_return_type(
+ writer, type,
+ )
+ return await self._write(
+ context,
+ writer,
+ __options__,
+ type_result=type_t,
+ check_type=check_type,
+ )
+
+ async def _write(
+ self,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ writer: PingBaseServicer.InlineWriterCallable[PingBaseServicer.InlineWriterCallableResult],
+ __options__: IMPORT_reboot_aio_call.Options = IMPORT_reboot_aio_call.Options(),
+ *,
+ type_result: IMPORT_reboot_aio_workflows.TypeT,
+ check_type: bool,
+ ) -> PingBaseServicer.InlineWriterCallableResult:
+ unidempotently = self._how == IMPORT_reboot_aio_workflows.ALWAYS
+ idempotency = (
+ context.idempotency(
+ how=IMPORT_reboot_aio_idempotency.ALWAYS,
+ ) if unidempotently else context.idempotency(
+ alias=self._alias,
+ how=self._how,
+ )
+ )
+
+ return await self._write_validating_effects(
+ self._servicer,
+ idempotency,
+ context,
+ writer,
+ __options__,
+ type_result=type_result,
+ check_type=check_type,
+ unidempotently=unidempotently,
+ checkpoint=context.checkpoint(),
+ )
+
+ @staticmethod
+ @IMPORT_reboot_aio_internals_middleware.maybe_run_function_twice_to_validate_effects
+ async def _write_validating_effects(
+ validating_effects: bool,
+ servicer: PingBaseServicer,
+ idempotency: IMPORT_reboot_aio_idempotency.Idempotency,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ writer: PingBaseServicer.InlineWriterCallable[PingBaseServicer.InlineWriterCallableResult],
+ __options__: IMPORT_reboot_aio_call.Options = IMPORT_reboot_aio_call.Options(),
+ *,
+ type_result: IMPORT_reboot_aio_workflows.TypeT,
+ check_type: bool,
+ unidempotently: bool,
+ checkpoint: IMPORT_reboot_aio_idempotency.Checkpoint,
+ ) -> PingBaseServicer.InlineWriterCallableResult:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+
+ IMPORT_reboot_aio_types.assert_type(context, [IMPORT_reboot_aio_contexts.WorkflowContext])
+
+ if servicer._middleware is None:
+ raise RuntimeError(
+ 'Reboot middleware was not created; '
+ 'are you using this class without Reboot?'
+ )
+
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+
+ if __options__ is not None:
+ if __options__.metadata is not None:
+ metadata = __options__.metadata
+
+ if metadata is None:
+ metadata = ()
+
+ headers = IMPORT_reboot_aio_headers.Headers(
+ application_id=context.application_id,
+ state_ref=context._state_ref,
+ caller_id=IMPORT_reboot_aio_caller_id.CallerID(
+ application_id=context.application_id,
+ ),
+ workflow_id=context.workflow_id,
+ # Only set `workflow_iteration` for
+ # `per_iteration` calls so that the
+ # mutation is stored/recovered with
+ # iteration scoping.
+ workflow_iteration=(
+ context.workflow_iteration
+ if idempotency.per_iteration else None
+ ),
+ )
+
+ metadata += headers.to_grpc_metadata()
+
+ idempotency_key: IMPORT_typing.Optional[IMPORT_uuid.UUID]
+ with context.idempotently(
+ state_type_name=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping'),
+ state_ref=context._state_ref,
+ service_name=None, # Indicates an inline writer.
+ method=None, # Indicates an inline writer.
+ mutation=True,
+ request=None, # Indicates an inline writer.
+ metadata=metadata,
+ idempotency=idempotency,
+ aborted_type=None, # Indicates an inline writer.
+ ) as idempotency_key:
+
+ if any(t[0] == IMPORT_reboot_aio_headers.IDEMPOTENCY_KEY_HEADER for t in metadata):
+ raise ValueError(
+ f"Do not set '{IMPORT_reboot_aio_headers.IDEMPOTENCY_KEY_HEADER}' metadata yourself"
+ )
+
+ if idempotency_key is not None:
+ metadata += (
+ (IMPORT_reboot_aio_headers.IDEMPOTENCY_KEY_HEADER, str(idempotency_key)),
+ )
+
+ with servicer._middleware.use_context(
+ headers=IMPORT_reboot_aio_headers.Headers.from_grpc_metadata(metadata),
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping'),
+ method='inline writer',
+ context_type=IMPORT_reboot_aio_contexts.WriterContext,
+ ) as writer_context:
+ # Check if we already have performed this mutation!
+ #
+ # We do this _before_ calling 'transactionally()' because
+ # if this call is for a transaction method _and_ we've
+ # already performed the transaction then we don't want to
+ # become a transaction participant (again) we just want to
+ # return the transaction's response.
+ idempotent_mutation = (
+ await servicer._middleware._state_manager.check_for_idempotent_mutation(
+ writer_context
+ )
+ )
+
+ if idempotent_mutation is not None:
+ assert len(idempotent_mutation.response) != 0
+ response = IMPORT_google_protobuf_wrappers_pb2.BytesValue()
+ response.ParseFromString(idempotent_mutation.response)
+ result: PingBaseServicer.InlineWriterCallableResult = IMPORT_pickle.loads(response.value)
+
+ if check_type and not isinstance(result, IMPORT_reboot_aio_workflows._isinstance_type(type_result)):
+ raise TypeError(
+ f"Stored result of type '{type(result).__name__}' from 'writer' "
+ f"is not of expected type '{IMPORT_reboot_aio_workflows._format_type(type_result)}'; have you changed "
+ "the 'type' that you expect after having stored a result?"
+ )
+
+ return result
+
+ async with servicer._middleware._state_manager.transactionally(
+ writer_context,
+ servicer._middleware.tasks_dispatcher,
+ aborted_type=None,
+ ) as transaction:
+ async with servicer._middleware._state_manager.writer(
+ writer_context,
+ servicer.__state_type__,
+ servicer._middleware.tasks_dispatcher,
+ # TODO: Decide if we want to do any kind of authorization for inline
+ # writers otherwise passing `None` here is fine.
+ authorize=None,
+ transaction=transaction,
+ ) as (protobuf_state, state_manager_writer):
+ # Serialize the state so we can see if it changed.
+ serialized_state = protobuf_state.SerializeToString(
+ deterministic=True,
+ )
+
+ typed_state = PingFromProto(protobuf_state)
+
+ result = await writer(state=typed_state)
+
+ PingToProto(typed_state, protobuf_state)
+
+ if check_type and not isinstance(result, IMPORT_reboot_aio_workflows._isinstance_type(type_result)):
+ raise TypeError(
+ f"Result of type '{type(result).__name__}' from 'writer' is "
+ f"not of expected type '{IMPORT_reboot_aio_workflows._format_type(type_result)}'; "
+ "did you specify an incorrect 'type'?"
+ )
+
+ task: IMPORT_typing.Optional[IMPORT_reboot_aio_tasks.TaskEffect] = context.task
+
+ assert task is not None, (
+ "Should always have a task when running a `workflow`"
+ )
+
+ method_name = f"Ping.{task.method_name} inline writer"
+
+ if idempotency.alias is not None:
+ method_name += " with idempotency alias '" + idempotency.alias + "'"
+ elif idempotency.key is not None:
+ method_name += " with idempotency key=" + str(idempotency.key)
+
+ servicer._middleware.maybe_raise_effect_validation_retry(
+ logger=logger,
+ idempotency_manager=context,
+ method_name=method_name,
+ validating_effects=validating_effects,
+ context=context,
+ checkpoint=checkpoint,
+ )
+
+ # We don't pass the context to the
+ # writer, so we don't expect there to
+ # be any scheduled tasks!
+ assert len(context._tasks) == 0
+
+ effects = IMPORT_reboot_aio_state_managers.Effects(
+ state=(
+ # Pass `None` if the state hasn't changed!
+ protobuf_state if serialized_state != protobuf_state.SerializeToString(
+ deterministic=True,
+ )
+ else None
+ ),
+ response=IMPORT_google_protobuf_wrappers_pb2.BytesValue(
+ value=IMPORT_pickle.dumps(result)
+ ),
+ )
+
+ await state_manager_writer.complete(effects)
+
+ return result
+
+ def per_workflow(self, alias: IMPORT_typing.Optional[str] = None):
+ return PingBaseServicer.WorkflowState._Idempotently(
+ servicer=self._servicer,
+ alias=alias,
+ how=IMPORT_reboot_aio_workflows.PER_WORKFLOW,
+ )
+
+ def per_iteration(self, alias: IMPORT_typing.Optional[str] = None):
+ return PingBaseServicer.WorkflowState._Idempotently(
+ servicer=self._servicer,
+ alias=alias,
+ how=IMPORT_reboot_aio_workflows.PER_ITERATION,
+ )
+
+ class _Always:
+ """Helper class for providing better types for `write` that don't
+ require passing `type` or `check_type`."""
+
+ def __init__(
+ self,
+ *,
+ servicer: PingBaseServicer,
+ ):
+ self._servicer = servicer
+
+ async def read(
+ self, context: IMPORT_reboot_aio_contexts.WorkflowContext
+ ) -> Ping.State:
+ return await PingBaseServicer.WorkflowState._Idempotently(
+ servicer=self._servicer,
+ alias=None,
+ how=IMPORT_reboot_aio_workflows.ALWAYS,
+ ).read(context)
+
+ async def write(
+ self,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ writer: PingBaseServicer.InlineWriterCallable[PingBaseServicer.InlineWriterCallableResult],
+ __options__: IMPORT_reboot_aio_call.Options = IMPORT_reboot_aio_call.Options(),
+ ) -> PingBaseServicer.InlineWriterCallableResult:
+ return await PingBaseServicer.WorkflowState._Idempotently(
+ servicer=self._servicer,
+ alias=None,
+ how=IMPORT_reboot_aio_workflows.ALWAYS,
+ )._write(
+ context,
+ writer,
+ __options__,
+ type_result=type(None),
+ check_type=False,
+ )
+
+ def always(self):
+ return PingBaseServicer.WorkflowState._Always(
+ servicer=self._servicer,
+ )
+
+ # For 'reboot.ping.PingMethods.DoPing'.
+ @IMPORT_abc_abstractmethod
+ async def _DoPing(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext,
+ state: Ping.State,
+ request: google.protobuf.empty_pb2.Empty,
+ ) -> reboot.ping.ping_api_pb2.PingDoPingResponse:
+ raise NotImplementedError
+
+ # For 'reboot.ping.PingMethods.DoPingPeriodically'.
+ @IMPORT_abc_abstractmethod
+ async def _DoPingPeriodically(
+ self,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ request: reboot.ping.ping_api_pb2.PingDoPingPeriodicallyRequest,
+ ) -> reboot.ping.ping_api_pb2.PingDoPingPeriodicallyResponse:
+ raise NotImplementedError
+
+ # For 'reboot.ping.PingMethods.Describe'.
+ @IMPORT_abc_abstractmethod
+ async def _Describe(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: Ping.State,
+ request: google.protobuf.empty_pb2.Empty,
+ ) -> reboot.ping.ping_api_pb2.PingDescribeResponse:
+ raise NotImplementedError
+
+ # For 'reboot.ping.PingMethods.NumPings'.
+ @IMPORT_abc_abstractmethod
+ async def _NumPings(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: Ping.State,
+ request: google.protobuf.empty_pb2.Empty,
+ ) -> reboot.ping.ping_api_pb2.PingNumPingsResponse:
+ raise NotImplementedError
+
+
+
+class PingSingletonServicer(PingBaseServicer):
+
+ @property
+ def state(self):
+ return PingBaseServicer.WorkflowState(
+ servicer=self
+ )
+
+ # For 'reboot.ping.PingMethods.DoPing'.
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that new code that
+ # doesn't implement it continues to work.
+ # TODO: make it abstractmethod when renaming is done.
+ async def DoPing(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext,
+ state: Ping.State,
+ ) -> Ping.DoPingResponse:
+ # During the migration from 'PascalCase' to 'snake_case' method
+ # naming in Python servicers, we call the 'snake_case' version
+ # by default, so new names will do the correct thing making the
+ # code to be backwards compatible for some time and if a servicer
+ # overrides the 'PascalCase' version - it will override that
+ # method and will just work.
+ return await self.do_ping(
+ context,
+ state,
+ )
+
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that existing code that
+ # doesn't implement it continues to work.
+ async def do_ping(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext,
+ state: Ping.State,
+ ) -> Ping.DoPingResponse:
+ raise NotImplementedError
+
+ # For 'reboot.ping.PingMethods.DoPingPeriodically'.
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that new code that
+ # doesn't implement it continues to work.
+ # TODO: make it abstractmethod when renaming is done.
+ @classmethod
+ async def DoPingPeriodically(
+ cls,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ request: Ping.DoPingPeriodicallyRequest,
+ ) -> Ping.DoPingPeriodicallyResponse:
+ # During the migration from 'PascalCase' to 'snake_case' method
+ # naming in Python servicers, we call the 'snake_case' version
+ # by default, so new names will do the correct thing making the
+ # code to be backwards compatible for some time and if a servicer
+ # overrides the 'PascalCase' version - it will override that
+ # method and will just work.
+ return await cls.do_ping_periodically(
+ context,
+ request,
+ )
+
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that existing code that
+ # doesn't implement it continues to work.
+ @classmethod
+ async def do_ping_periodically(
+ cls,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ request: Ping.DoPingPeriodicallyRequest,
+ ) -> Ping.DoPingPeriodicallyResponse:
+ raise NotImplementedError
+
+ # For 'reboot.ping.PingMethods.Describe'.
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that new code that
+ # doesn't implement it continues to work.
+ # TODO: make it abstractmethod when renaming is done.
+ async def Describe(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: Ping.State,
+ ) -> Ping.DescribeResponse:
+ # During the migration from 'PascalCase' to 'snake_case' method
+ # naming in Python servicers, we call the 'snake_case' version
+ # by default, so new names will do the correct thing making the
+ # code to be backwards compatible for some time and if a servicer
+ # overrides the 'PascalCase' version - it will override that
+ # method and will just work.
+ return await self.describe(
+ context,
+ state,
+ )
+
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that existing code that
+ # doesn't implement it continues to work.
+ # TODO: make it abstractmethod when renaming is done.
+ async def describe(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: Ping.State,
+ ) -> Ping.DescribeResponse:
+ raise NotImplementedError
+
+ # For 'reboot.ping.PingMethods.NumPings'.
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that new code that
+ # doesn't implement it continues to work.
+ # TODO: make it abstractmethod when renaming is done.
+ async def NumPings(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: Ping.State,
+ ) -> Ping.NumPingsResponse:
+ # During the migration from 'PascalCase' to 'snake_case' method
+ # naming in Python servicers, we call the 'snake_case' version
+ # by default, so new names will do the correct thing making the
+ # code to be backwards compatible for some time and if a servicer
+ # overrides the 'PascalCase' version - it will override that
+ # method and will just work.
+ return await self.num_pings(
+ context,
+ state,
+ )
+
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that existing code that
+ # doesn't implement it continues to work.
+ # TODO: make it abstractmethod when renaming is done.
+ async def num_pings(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: Ping.State,
+ ) -> Ping.NumPingsResponse:
+ raise NotImplementedError
+
+
+ # For 'reboot.ping.PingMethods.DoPing'.
+ async def _DoPing(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext,
+ state: Ping.State,
+ request: google.protobuf.empty_pb2.Empty,
+ ) -> reboot.ping.ping_api_pb2.PingDoPingResponse:
+ # Wrap the call to the developer's method in a `span` so that it
+ # is traced using its fully-qualified Python name.
+ with IMPORT_reboot_aio_tracing.span(
+ state_name=f"reboot.ping.Ping('{context.state_id}')",
+ span_name=f"{IMPORT_reboot_aio_tracing.qualified_type_name(self)}.DoPing()",
+ level=IMPORT_reboot_aio_tracing.TraceLevel.CUSTOMER,
+ python_specific=True,
+ ):
+ return PingDoPingResponseToProto(
+ await self.DoPing(
+ context,
+ state,
+ )
+ )
+
+
+ # For 'reboot.ping.PingMethods.DoPingPeriodically'.
+ async def _DoPingPeriodically(
+ self,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ request: reboot.ping.ping_api_pb2.PingDoPingPeriodicallyRequest,
+ ) -> reboot.ping.ping_api_pb2.PingDoPingPeriodicallyResponse:
+ # Wrap the call to the developer's method in a `span` so that it
+ # is traced using its fully-qualified Python name.
+ with IMPORT_reboot_aio_tracing.span(
+ state_name=f"reboot.ping.Ping('{context.state_id}')",
+ span_name=f"{IMPORT_reboot_aio_tracing.qualified_type_name(self)}.DoPingPeriodically()",
+ level=IMPORT_reboot_aio_tracing.TraceLevel.CUSTOMER,
+ python_specific=True,
+ ):
+ typed_request = PingDoPingPeriodicallyRequestFromProto(request)
+ return PingDoPingPeriodicallyResponseToProto(
+ await self.DoPingPeriodically(
+ context,
+ typed_request,
+ )
+ )
+
+
+ # For 'reboot.ping.PingMethods.Describe'.
+ async def _Describe(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: Ping.State,
+ request: google.protobuf.empty_pb2.Empty,
+ ) -> reboot.ping.ping_api_pb2.PingDescribeResponse:
+ # Wrap the call to the developer's method in a `span` so that it
+ # is traced using its fully-qualified Python name.
+ with IMPORT_reboot_aio_tracing.span(
+ state_name=f"reboot.ping.Ping('{context.state_id}')",
+ span_name=f"{IMPORT_reboot_aio_tracing.qualified_type_name(self)}.Describe()",
+ level=IMPORT_reboot_aio_tracing.TraceLevel.CUSTOMER,
+ python_specific=True,
+ ):
+ response = (
+ self.Describe(
+ context,
+ state,
+ )
+ )
+ return PingDescribeResponseToProto(await response)
+
+ # For 'reboot.ping.PingMethods.NumPings'.
+ async def _NumPings(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: Ping.State,
+ request: google.protobuf.empty_pb2.Empty,
+ ) -> reboot.ping.ping_api_pb2.PingNumPingsResponse:
+ # Wrap the call to the developer's method in a `span` so that it
+ # is traced using its fully-qualified Python name.
+ with IMPORT_reboot_aio_tracing.span(
+ state_name=f"reboot.ping.Ping('{context.state_id}')",
+ span_name=f"{IMPORT_reboot_aio_tracing.qualified_type_name(self)}.NumPings()",
+ level=IMPORT_reboot_aio_tracing.TraceLevel.CUSTOMER,
+ python_specific=True,
+ ):
+ response = (
+ self.NumPings(
+ context,
+ state,
+ )
+ )
+ return PingNumPingsResponseToProto(await response)
+
+
+
+class PingServicer(PingBaseServicer):
+
+ _state: IMPORT_contextvars.ContextVar[
+ IMPORT_typing.Optional[Ping.State]
+ ] = IMPORT_contextvars.ContextVar(
+ 'Provides access to state for each call, i.e., there may be '
+ 'multiple readers executing concurrently but each might have '
+ 'a different `state`',
+ default=None,
+ )
+
+ # An instance of the derived class for each state.
+ _instances: dict[str, PingServicer] = {}
+
+ def _instance(self, state_id: str):
+ instances = PingServicer._instances
+ instance = instances.get(state_id)
+ if instance is None:
+ instance = self.__class__()
+ instance._middleware = self._middleware
+ instances[state_id] = instance
+ return instance
+
+ @property
+ def state(self) -> Ping.State:
+ state = PingServicer._state.get()
+ if state is None:
+ raise RuntimeError(
+ "`state` property is only relevant within a `Servicer` method"
+ )
+ return state
+
+ @state.setter
+ def state(self, new_state: Ping.State):
+ state = PingServicer._state.get()
+ if state is None:
+ raise RuntimeError(
+ "`state` property is only relevant within a `Servicer` method"
+ )
+ for field_name, field_value in new_state.model_dump().items():
+ setattr(state, field_name, field_value)
+
+ # For 'reboot.ping.PingMethods.DoPing'.
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that new code that
+ # doesn't implement it continues to work.
+ # TODO: make it abstractmethod when renaming is done.
+ async def DoPing(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext,
+ ) -> Ping.DoPingResponse:
+ # During the migration from 'PascalCase' to 'snake_case' method
+ # naming in Python servicers, we call the 'snake_case' version
+ # by default, so new names will do the correct thing making the
+ # code to be backwards compatible for some time and if a servicer
+ # overrides the 'PascalCase' version - it will override that
+ # method and will just work.
+ return await self.do_ping(
+ context,
+ )
+
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that existing code that
+ # doesn't implement it continues to work.
+ async def do_ping(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext,
+ ) -> Ping.DoPingResponse:
+ raise NotImplementedError
+
+ # For 'reboot.ping.PingMethods.DoPingPeriodically'.
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that new code that
+ # doesn't implement it continues to work.
+ # TODO: make it abstractmethod when renaming is done.
+ @classmethod
+ async def DoPingPeriodically(
+ cls,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ request: Ping.DoPingPeriodicallyRequest,
+ ) -> Ping.DoPingPeriodicallyResponse:
+ # During the migration from 'PascalCase' to 'snake_case' method
+ # naming in Python servicers, we call the 'snake_case' version
+ # by default, so new names will do the correct thing making the
+ # code to be backwards compatible for some time and if a servicer
+ # overrides the 'PascalCase' version - it will override that
+ # method and will just work.
+ return await cls.do_ping_periodically(
+ context,
+ request,
+ )
+
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that existing code that
+ # doesn't implement it continues to work.
+ @classmethod
+ async def do_ping_periodically(
+ cls,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ request: Ping.DoPingPeriodicallyRequest,
+ ) -> Ping.DoPingPeriodicallyResponse:
+ raise NotImplementedError
+
+ # For 'reboot.ping.PingMethods.Describe'.
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that new code that
+ # doesn't implement it continues to work.
+ # TODO: make it abstractmethod when renaming is done.
+ async def Describe(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ ) -> Ping.DescribeResponse:
+ # During the migration from 'PascalCase' to 'snake_case' method
+ # naming in Python servicers, we call the 'snake_case' version
+ # by default, so new names will do the correct thing making the
+ # code to be backwards compatible for some time and if a servicer
+ # overrides the 'PascalCase' version - it will override that
+ # method and will just work.
+ return await self.describe(
+ context,
+ )
+
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that existing code that
+ # doesn't implement it continues to work.
+ async def describe(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ ) -> Ping.DescribeResponse:
+ raise NotImplementedError
+
+ # For 'reboot.ping.PingMethods.NumPings'.
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that new code that
+ # doesn't implement it continues to work.
+ # TODO: make it abstractmethod when renaming is done.
+ async def NumPings(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ ) -> Ping.NumPingsResponse:
+ # During the migration from 'PascalCase' to 'snake_case' method
+ # naming in Python servicers, we call the 'snake_case' version
+ # by default, so new names will do the correct thing making the
+ # code to be backwards compatible for some time and if a servicer
+ # overrides the 'PascalCase' version - it will override that
+ # method and will just work.
+ return await self.num_pings(
+ context,
+ )
+
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that existing code that
+ # doesn't implement it continues to work.
+ async def num_pings(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ ) -> Ping.NumPingsResponse:
+ raise NotImplementedError
+
+
+ # For 'reboot.ping.PingMethods.DoPing'.
+ async def _DoPing(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext,
+ state: Ping.State,
+ request: google.protobuf.empty_pb2.Empty,
+ ) -> reboot.ping.ping_api_pb2.PingDoPingResponse:
+ # We should have an asyncio task and thus context per request,
+ # let's confirm this assumption by making sure that
+ # `_state is None`.
+ assert PingServicer._state.get() is None
+ PingServicer._state.set(state)
+ try:
+ # Wrap the call to the developer's method in a `span` so that it
+ # is traced using its fully-qualified Python name.
+ instance = self._instance(context.state_id)
+ with IMPORT_reboot_aio_tracing.span(
+ state_name=f"reboot.ping.Ping('{context.state_id}')",
+ span_name=f"{IMPORT_reboot_aio_tracing.qualified_type_name(instance)}.DoPing()",
+ level=IMPORT_reboot_aio_tracing.TraceLevel.CUSTOMER,
+ python_specific=True,
+ ):
+
+ return PingDoPingResponseToProto(
+ await instance.DoPing(
+ context,
+ )
+ )
+ finally:
+ PingServicer._state.set(None)
+
+ # For 'reboot.ping.PingMethods.DoPingPeriodically'.
+ async def _DoPingPeriodically(
+ self,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ request: reboot.ping.ping_api_pb2.PingDoPingPeriodicallyRequest,
+ ) -> reboot.ping.ping_api_pb2.PingDoPingPeriodicallyResponse:
+ # Wrap the call to the developer's method in a `span` so that it
+ # is traced using its fully-qualified Python name.
+ instance = self._instance(context.state_id)
+ with IMPORT_reboot_aio_tracing.span(
+ state_name=f"reboot.ping.Ping('{context.state_id}')",
+ span_name=f"{IMPORT_reboot_aio_tracing.qualified_type_name(instance)}.DoPingPeriodically()",
+ level=IMPORT_reboot_aio_tracing.TraceLevel.CUSTOMER,
+ python_specific=True,
+ ):
+ typed_request = PingDoPingPeriodicallyRequestFromProto(request)
+
+ return PingDoPingPeriodicallyResponseToProto(
+ await instance.DoPingPeriodically(
+ context,
+ typed_request,
+ )
+ )
+
+ # For 'reboot.ping.PingMethods.Describe'.
+ async def _Describe(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: Ping.State,
+ request: google.protobuf.empty_pb2.Empty,
+ ) -> reboot.ping.ping_api_pb2.PingDescribeResponse:
+ # We should have an asyncio task and thus context per request,
+ # let's confirm this assumption by making sure that
+ # `_state is None`.
+ assert PingServicer._state.get() is None
+ PingServicer._state.set(state)
+ try:
+ # Wrap the call to the developer's method in a `span` so that it
+ # is traced using its fully-qualified Python name.
+ instance = self._instance(context.state_id)
+ with IMPORT_reboot_aio_tracing.span(
+ state_name=f"reboot.ping.Ping('{context.state_id}')",
+ span_name=f"{IMPORT_reboot_aio_tracing.qualified_type_name(instance)}.Describe()",
+ level=IMPORT_reboot_aio_tracing.TraceLevel.CUSTOMER,
+ python_specific=True,
+ ):
+
+ return PingDescribeResponseToProto(
+ await instance.Describe(
+ context,
+ )
+ )
+ finally:
+ PingServicer._state.set(None)
+
+ # For 'reboot.ping.PingMethods.NumPings'.
+ async def _NumPings(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: Ping.State,
+ request: google.protobuf.empty_pb2.Empty,
+ ) -> reboot.ping.ping_api_pb2.PingNumPingsResponse:
+ # We should have an asyncio task and thus context per request,
+ # let's confirm this assumption by making sure that
+ # `_state is None`.
+ assert PingServicer._state.get() is None
+ PingServicer._state.set(state)
+ try:
+ # Wrap the call to the developer's method in a `span` so that it
+ # is traced using its fully-qualified Python name.
+ instance = self._instance(context.state_id)
+ with IMPORT_reboot_aio_tracing.span(
+ state_name=f"reboot.ping.Ping('{context.state_id}')",
+ span_name=f"{IMPORT_reboot_aio_tracing.qualified_type_name(instance)}.NumPings()",
+ level=IMPORT_reboot_aio_tracing.TraceLevel.CUSTOMER,
+ python_specific=True,
+ ):
+
+ return PingNumPingsResponseToProto(
+ await instance.NumPings(
+ context,
+ )
+ )
+ finally:
+ PingServicer._state.set(None)
+
+
+class PongBaseServicer(IMPORT_reboot_aio_servicers.Servicer):
+ Authorizer: IMPORT_typing.TypeAlias = PongAuthorizer
+ # Node-backed servicers attach a JS bridge object at runtime. We declare it
+ # on the base class so mypy accepts accesses when a servicer is typed as
+ # `PongBaseServicer` (e.g. in classmethod workflows).
+ _js_servicer_reference: IMPORT_typing.Any
+
+ __servicer__: IMPORT_contextvars.ContextVar[IMPORT_typing.Optional[PongBaseServicer]] = IMPORT_contextvars.ContextVar(
+ 'Provides access to a servicer in the current asyncio context. '
+ 'We need that to be able to do inline writes and reads inside '
+ 'a workflow',
+ default=None,
+ )
+
+ __service_names__ = [
+ IMPORT_reboot_aio_types.ServiceName('reboot.ping.PongMethods'),
+ ]
+ __state_type_name__ = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Pong')
+ __state_type__ = reboot.ping.ping_api_pb2.Pong
+ __file_descriptor__ = reboot.ping.ping_api_pb2.DESCRIPTOR
+
+ def __init__(self):
+ super().__init__()
+ # NOTE: need to hold on to the middleware so we can do inline
+ # writes (see 'self.write(...)').
+ #
+ # Because '_middleware' is not really private this does mean
+ # users may do possibly dangerous things, but this is no more
+ # likely given they could have already overridden
+ # 'create_middleware()'.
+ self._middleware: IMPORT_typing.Optional[PongServicerMiddleware] = None
+
+ def create_middleware(
+ self,
+ *,
+ application_id: IMPORT_reboot_aio_types.ApplicationId,
+ server_id: IMPORT_reboot_aio_types.ServerId,
+ state_manager: IMPORT_reboot_aio_state_managers.StateManager,
+ placement_client: IMPORT_reboot_aio_placement.PlacementClient,
+ channel_manager: IMPORT_reboot_aio_internals_channel_manager._ChannelManager,
+ tasks_cache: IMPORT_reboot_aio_internals_tasks_cache.TasksCache,
+ token_verifier: IMPORT_typing.Optional[IMPORT_reboot_aio_auth_token_verifiers.TokenVerifier],
+ effect_validation: IMPORT_reboot_aio_contexts.EffectValidation,
+ ready: IMPORT_asyncio.Event,
+ ) -> PongServicerMiddleware:
+ self._middleware = PongServicerMiddleware(
+ servicer=self,
+ application_id=application_id,
+ server_id=server_id,
+ state_manager=state_manager,
+ placement_client=placement_client,
+ channel_manager=channel_manager,
+ tasks_cache=tasks_cache,
+ token_verifier=token_verifier,
+ effect_validation=effect_validation,
+ ready=ready,
+ )
+ return self._middleware
+
+ def authorizer(self) -> IMPORT_typing.Optional[IMPORT_reboot_aio_auth_authorizers.Authorizer | IMPORT_reboot_aio_auth_authorizers.AuthorizerRule]:
+ return None
+
+ def token_verifier(self) -> IMPORT_typing.Optional[IMPORT_reboot_aio_auth_token_verifiers.TokenVerifier]:
+ return None
+
+ _is_auto_construct = False
+
+
+
+
+ def ref(
+ self,
+ *,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ) -> Pong.WeakReference[Pong.WeakReference._WriterSchedule]:
+ context = IMPORT_reboot_aio_contexts.Context.get()
+
+ if context is None:
+ raise RuntimeError(
+ 'Missing asyncio context variable `context`; '
+ 'are you using this class without Reboot?'
+ )
+
+ return Pong.WeakReference(
+ # TODO(https://github.com/reboot-dev/mono/issues/3226): add support for calling other applications.
+ # For now this always stays within the application that creates the context.
+ application_id=None,
+ state_id=context._state_ref.id,
+ schedule_type=Pong.WeakReference._WriterSchedule,
+ # If the user didn't specify a bearer token we may still end up using the app-internal bearer token,
+ # but that's decided at the time of the call.
+ bearer_token=bearer_token,
+ servicer=self,
+ )
+
+ class Effects(IMPORT_reboot_aio_state_managers.Effects):
+ def __init__(
+ self,
+ *,
+ state: reboot.ping.ping_api_pb2.Pong,
+ response: IMPORT_typing.Optional[IMPORT_google_protobuf_message.Message] = None,
+ tasks: IMPORT_typing.Optional[list[IMPORT_reboot_aio_tasks.TaskEffect]] = None,
+ _colocated_upserts: IMPORT_typing.Optional[list[tuple[str, IMPORT_typing.Optional[bytes]]]] = None,
+ ):
+ IMPORT_reboot_aio_types.assert_type(state, [reboot.ping.ping_api_pb2.Pong])
+
+ super().__init__(state=state, response=response, tasks=tasks, _colocated_upserts=_colocated_upserts)
+
+ # For 'reboot.ping.PongMethods.DoPong'.
+ class DoPongEffects(Effects):
+ def __init__(
+ self,
+ *,
+ state: reboot.ping.ping_api_pb2.Pong,
+ response: reboot.ping.ping_api_pb2.PongDoPongResponse,
+ tasks: IMPORT_typing.Optional[list[IMPORT_reboot_aio_tasks.TaskEffect]] = None,
+ _colocated_upserts: IMPORT_typing.Optional[list[tuple[str, IMPORT_typing.Optional[bytes]]]] = None,
+ ):
+ IMPORT_reboot_aio_types.assert_type(state, [reboot.ping.ping_api_pb2.Pong])
+ IMPORT_reboot_aio_types.assert_type(response, [reboot.ping.ping_api_pb2.PongDoPongResponse])
+
+ super().__init__(state=state, response=response, tasks=tasks, _colocated_upserts=_colocated_upserts)
+
+
+
+
+ InlineWriterCallableResult = IMPORT_typing.TypeVar('InlineWriterCallableResult', covariant=True)
+
+ class InlineWriterCallable(IMPORT_typing.Protocol[InlineWriterCallableResult]):
+ async def __call__(
+ self,
+ state: reboot.ping.ping_api_pb2.Pong
+ ) -> PongBaseServicer.InlineWriterCallableResult:
+ ...
+
+ class WorkflowState:
+
+ def __init__(
+ self,
+ servicer,
+ ):
+ self._servicer = servicer
+
+ async def read(
+ self, context: IMPORT_reboot_aio_contexts.WorkflowContext
+ ) -> Pong.State:
+ """Read the current state within a workflow."""
+ return await (
+ self.always() if context.within_until()
+ else (
+ self.per_iteration() if context.within_loop()
+ else self.per_workflow()
+ )
+ ).read(context)
+
+ async def write(
+ self,
+ idempotency_alias: str,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ writer: PongBaseServicer.InlineWriterCallable[PongBaseServicer.InlineWriterCallableResult],
+ __options__: IMPORT_reboot_aio_call.Options = IMPORT_reboot_aio_call.Options(),
+ *,
+ type: IMPORT_reboot_aio_workflows.Type | IMPORT_reboot_aio_workflows._Unset = IMPORT_reboot_aio_workflows._UNSET,
+ ) -> PongBaseServicer.InlineWriterCallableResult:
+ """Perform an "inline write" within a workflow.
+
+ `type=` is optional: when omitted, `writer`'s return
+ annotation is used (falling back to `type(None)` if
+ there is none).
+ """
+ type_t, _ = IMPORT_reboot_aio_workflows._resolve_callable_return_type(
+ writer, type,
+ )
+ return await (
+ self.per_iteration(idempotency_alias) if context.within_loop()
+ else self.per_workflow(idempotency_alias)
+ ).write(
+ context, writer, __options__, type=type_t
+ )
+
+ class _Idempotently:
+
+ def __init__(
+ self,
+ *,
+ servicer: PongBaseServicer,
+ alias: IMPORT_typing.Optional[str],
+ how: IMPORT_reboot_aio_workflows.How,
+ ):
+ self._servicer = servicer
+ self._alias = alias
+ self._how = how
+
+ async def read(
+ self, context: IMPORT_reboot_aio_contexts.WorkflowContext
+ ) -> Pong.State:
+ """Read the current state within a workflow."""
+ return await self._read(
+ self._servicer,
+ context.idempotency(
+ how=IMPORT_reboot_aio_idempotency.ALWAYS,
+ ) if self._how == IMPORT_reboot_aio_workflows.ALWAYS else context.idempotency(
+ alias=self._alias,
+ how=self._how,
+ ),
+ context,
+ )
+
+ @staticmethod
+ async def _read(
+ servicer: PongBaseServicer,
+ idempotency: IMPORT_reboot_aio_idempotency.Idempotency,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ ) -> Pong.State:
+ """Read the current state within a workflow."""
+ IMPORT_reboot_aio_types.assert_type(context, [IMPORT_reboot_aio_contexts.WorkflowContext])
+
+ if servicer._middleware is None:
+ raise RuntimeError(
+ 'Reboot middleware was not created; '
+ 'are you using this class without Reboot?'
+ )
+
+ async def read() -> reboot.ping.ping_api_pb2.Pong:
+ assert servicer._middleware is not None
+ return await servicer._middleware._state_manager.read(
+ context, servicer.__state_type__
+ )
+
+ if idempotency.always:
+ return PongFromProto(await read())
+
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Pong')
+
+ # Use the idempotency manager to make sure that this
+ # reader is being called following the rules.
+ with context.idempotently(
+ state_type_name=state_type_name,
+ state_ref=context._state_ref,
+ # Not calling a method so `service_name`,
+ # `method`, `request`, etc are irrelevant.
+ service_name=None,
+ method=None,
+ mutation=False,
+ request=None,
+ metadata=None,
+ idempotency=idempotency,
+ # Only need to pass `aborted_type` for mutations.
+ aborted_type=None,
+ ) as idempotency_key:
+ assert idempotency_key is not None
+ protobuf_state = await IMPORT_reboot_aio_workflows.at_least_once(
+ (
+ # TODO: for easier debugging include the
+ # original alias (or generated alias in
+ # the case of `.per_iteration()` w/o an
+ # alias) instead of just
+ # `idempotency_key`.
+ f"inline reader of '{ state_type_name }' ({str(idempotency_key)})",
+ # NOTE: we want this to be `PER_WORKFLOW`
+ # because any per iteration concerns
+ # should have already been taken care of
+ # by caller using `.per_iteration()`.
+ IMPORT_reboot_aio_workflows.PER_WORKFLOW
+ ),
+ context,
+ read,
+ type=reboot.ping.ping_api_pb2.Pong,
+ )
+
+ return PongFromProto(protobuf_state)
+
+ async def write(
+ self,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ writer: PongBaseServicer.InlineWriterCallable[PongBaseServicer.InlineWriterCallableResult],
+ __options__: IMPORT_reboot_aio_call.Options = IMPORT_reboot_aio_call.Options(),
+ *,
+ type: IMPORT_reboot_aio_workflows.Type | IMPORT_reboot_aio_workflows._Unset = IMPORT_reboot_aio_workflows._UNSET,
+ check_type: bool = True,
+ ) -> PongBaseServicer.InlineWriterCallableResult:
+ # `type=` is optional: when omitted, `writer`'s
+ # return annotation is used (falling back to
+ # `type(None)` when there is none).
+ type_t, _ = IMPORT_reboot_aio_workflows._resolve_callable_return_type(
+ writer, type,
+ )
+ return await self._write(
+ context,
+ writer,
+ __options__,
+ type_result=type_t,
+ check_type=check_type,
+ )
+
+ async def _write(
+ self,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ writer: PongBaseServicer.InlineWriterCallable[PongBaseServicer.InlineWriterCallableResult],
+ __options__: IMPORT_reboot_aio_call.Options = IMPORT_reboot_aio_call.Options(),
+ *,
+ type_result: IMPORT_reboot_aio_workflows.TypeT,
+ check_type: bool,
+ ) -> PongBaseServicer.InlineWriterCallableResult:
+ unidempotently = self._how == IMPORT_reboot_aio_workflows.ALWAYS
+ idempotency = (
+ context.idempotency(
+ how=IMPORT_reboot_aio_idempotency.ALWAYS,
+ ) if unidempotently else context.idempotency(
+ alias=self._alias,
+ how=self._how,
+ )
+ )
+
+ return await self._write_validating_effects(
+ self._servicer,
+ idempotency,
+ context,
+ writer,
+ __options__,
+ type_result=type_result,
+ check_type=check_type,
+ unidempotently=unidempotently,
+ checkpoint=context.checkpoint(),
+ )
+
+ @staticmethod
+ @IMPORT_reboot_aio_internals_middleware.maybe_run_function_twice_to_validate_effects
+ async def _write_validating_effects(
+ validating_effects: bool,
+ servicer: PongBaseServicer,
+ idempotency: IMPORT_reboot_aio_idempotency.Idempotency,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ writer: PongBaseServicer.InlineWriterCallable[PongBaseServicer.InlineWriterCallableResult],
+ __options__: IMPORT_reboot_aio_call.Options = IMPORT_reboot_aio_call.Options(),
+ *,
+ type_result: IMPORT_reboot_aio_workflows.TypeT,
+ check_type: bool,
+ unidempotently: bool,
+ checkpoint: IMPORT_reboot_aio_idempotency.Checkpoint,
+ ) -> PongBaseServicer.InlineWriterCallableResult:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+
+ IMPORT_reboot_aio_types.assert_type(context, [IMPORT_reboot_aio_contexts.WorkflowContext])
+
+ if servicer._middleware is None:
+ raise RuntimeError(
+ 'Reboot middleware was not created; '
+ 'are you using this class without Reboot?'
+ )
+
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+
+ if __options__ is not None:
+ if __options__.metadata is not None:
+ metadata = __options__.metadata
+
+ if metadata is None:
+ metadata = ()
+
+ headers = IMPORT_reboot_aio_headers.Headers(
+ application_id=context.application_id,
+ state_ref=context._state_ref,
+ caller_id=IMPORT_reboot_aio_caller_id.CallerID(
+ application_id=context.application_id,
+ ),
+ workflow_id=context.workflow_id,
+ # Only set `workflow_iteration` for
+ # `per_iteration` calls so that the
+ # mutation is stored/recovered with
+ # iteration scoping.
+ workflow_iteration=(
+ context.workflow_iteration
+ if idempotency.per_iteration else None
+ ),
+ )
+
+ metadata += headers.to_grpc_metadata()
+
+ idempotency_key: IMPORT_typing.Optional[IMPORT_uuid.UUID]
+ with context.idempotently(
+ state_type_name=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Pong'),
+ state_ref=context._state_ref,
+ service_name=None, # Indicates an inline writer.
+ method=None, # Indicates an inline writer.
+ mutation=True,
+ request=None, # Indicates an inline writer.
+ metadata=metadata,
+ idempotency=idempotency,
+ aborted_type=None, # Indicates an inline writer.
+ ) as idempotency_key:
+
+ if any(t[0] == IMPORT_reboot_aio_headers.IDEMPOTENCY_KEY_HEADER for t in metadata):
+ raise ValueError(
+ f"Do not set '{IMPORT_reboot_aio_headers.IDEMPOTENCY_KEY_HEADER}' metadata yourself"
+ )
+
+ if idempotency_key is not None:
+ metadata += (
+ (IMPORT_reboot_aio_headers.IDEMPOTENCY_KEY_HEADER, str(idempotency_key)),
+ )
+
+ with servicer._middleware.use_context(
+ headers=IMPORT_reboot_aio_headers.Headers.from_grpc_metadata(metadata),
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Pong'),
+ method='inline writer',
+ context_type=IMPORT_reboot_aio_contexts.WriterContext,
+ ) as writer_context:
+ # Check if we already have performed this mutation!
+ #
+ # We do this _before_ calling 'transactionally()' because
+ # if this call is for a transaction method _and_ we've
+ # already performed the transaction then we don't want to
+ # become a transaction participant (again) we just want to
+ # return the transaction's response.
+ idempotent_mutation = (
+ await servicer._middleware._state_manager.check_for_idempotent_mutation(
+ writer_context
+ )
+ )
+
+ if idempotent_mutation is not None:
+ assert len(idempotent_mutation.response) != 0
+ response = IMPORT_google_protobuf_wrappers_pb2.BytesValue()
+ response.ParseFromString(idempotent_mutation.response)
+ result: PongBaseServicer.InlineWriterCallableResult = IMPORT_pickle.loads(response.value)
+
+ if check_type and not isinstance(result, IMPORT_reboot_aio_workflows._isinstance_type(type_result)):
+ raise TypeError(
+ f"Stored result of type '{type(result).__name__}' from 'writer' "
+ f"is not of expected type '{IMPORT_reboot_aio_workflows._format_type(type_result)}'; have you changed "
+ "the 'type' that you expect after having stored a result?"
+ )
+
+ return result
+
+ async with servicer._middleware._state_manager.transactionally(
+ writer_context,
+ servicer._middleware.tasks_dispatcher,
+ aborted_type=None,
+ ) as transaction:
+ async with servicer._middleware._state_manager.writer(
+ writer_context,
+ servicer.__state_type__,
+ servicer._middleware.tasks_dispatcher,
+ # TODO: Decide if we want to do any kind of authorization for inline
+ # writers otherwise passing `None` here is fine.
+ authorize=None,
+ transaction=transaction,
+ ) as (protobuf_state, state_manager_writer):
+ # Serialize the state so we can see if it changed.
+ serialized_state = protobuf_state.SerializeToString(
+ deterministic=True,
+ )
+
+ typed_state = PongFromProto(protobuf_state)
+
+ result = await writer(state=typed_state)
+
+ PongToProto(typed_state, protobuf_state)
+
+ if check_type and not isinstance(result, IMPORT_reboot_aio_workflows._isinstance_type(type_result)):
+ raise TypeError(
+ f"Result of type '{type(result).__name__}' from 'writer' is "
+ f"not of expected type '{IMPORT_reboot_aio_workflows._format_type(type_result)}'; "
+ "did you specify an incorrect 'type'?"
+ )
+
+ task: IMPORT_typing.Optional[IMPORT_reboot_aio_tasks.TaskEffect] = context.task
+
+ assert task is not None, (
+ "Should always have a task when running a `workflow`"
+ )
+
+ method_name = f"Pong.{task.method_name} inline writer"
+
+ if idempotency.alias is not None:
+ method_name += " with idempotency alias '" + idempotency.alias + "'"
+ elif idempotency.key is not None:
+ method_name += " with idempotency key=" + str(idempotency.key)
+
+ servicer._middleware.maybe_raise_effect_validation_retry(
+ logger=logger,
+ idempotency_manager=context,
+ method_name=method_name,
+ validating_effects=validating_effects,
+ context=context,
+ checkpoint=checkpoint,
+ )
+
+ # We don't pass the context to the
+ # writer, so we don't expect there to
+ # be any scheduled tasks!
+ assert len(context._tasks) == 0
+
+ effects = IMPORT_reboot_aio_state_managers.Effects(
+ state=(
+ # Pass `None` if the state hasn't changed!
+ protobuf_state if serialized_state != protobuf_state.SerializeToString(
+ deterministic=True,
+ )
+ else None
+ ),
+ response=IMPORT_google_protobuf_wrappers_pb2.BytesValue(
+ value=IMPORT_pickle.dumps(result)
+ ),
+ )
+
+ await state_manager_writer.complete(effects)
+
+ return result
+
+ def per_workflow(self, alias: IMPORT_typing.Optional[str] = None):
+ return PongBaseServicer.WorkflowState._Idempotently(
+ servicer=self._servicer,
+ alias=alias,
+ how=IMPORT_reboot_aio_workflows.PER_WORKFLOW,
+ )
+
+ def per_iteration(self, alias: IMPORT_typing.Optional[str] = None):
+ return PongBaseServicer.WorkflowState._Idempotently(
+ servicer=self._servicer,
+ alias=alias,
+ how=IMPORT_reboot_aio_workflows.PER_ITERATION,
+ )
+
+ class _Always:
+ """Helper class for providing better types for `write` that don't
+ require passing `type` or `check_type`."""
+
+ def __init__(
+ self,
+ *,
+ servicer: PongBaseServicer,
+ ):
+ self._servicer = servicer
+
+ async def read(
+ self, context: IMPORT_reboot_aio_contexts.WorkflowContext
+ ) -> Pong.State:
+ return await PongBaseServicer.WorkflowState._Idempotently(
+ servicer=self._servicer,
+ alias=None,
+ how=IMPORT_reboot_aio_workflows.ALWAYS,
+ ).read(context)
+
+ async def write(
+ self,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ writer: PongBaseServicer.InlineWriterCallable[PongBaseServicer.InlineWriterCallableResult],
+ __options__: IMPORT_reboot_aio_call.Options = IMPORT_reboot_aio_call.Options(),
+ ) -> PongBaseServicer.InlineWriterCallableResult:
+ return await PongBaseServicer.WorkflowState._Idempotently(
+ servicer=self._servicer,
+ alias=None,
+ how=IMPORT_reboot_aio_workflows.ALWAYS,
+ )._write(
+ context,
+ writer,
+ __options__,
+ type_result=type(None),
+ check_type=False,
+ )
+
+ def always(self):
+ return PongBaseServicer.WorkflowState._Always(
+ servicer=self._servicer,
+ )
+
+ # For 'reboot.ping.PongMethods.DoPong'.
+ @IMPORT_abc_abstractmethod
+ async def _DoPong(
+ self,
+ context: IMPORT_reboot_aio_contexts.WriterContext,
+ state: Pong.State,
+ request: google.protobuf.empty_pb2.Empty,
+ ) -> reboot.ping.ping_api_pb2.PongDoPongResponse:
+ raise NotImplementedError
+
+ # For 'reboot.ping.PongMethods.NumPongs'.
+ @IMPORT_abc_abstractmethod
+ async def _NumPongs(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: Pong.State,
+ request: google.protobuf.empty_pb2.Empty,
+ ) -> reboot.ping.ping_api_pb2.PongNumPongsResponse:
+ raise NotImplementedError
+
+
+
+class PongSingletonServicer(PongBaseServicer):
+
+ @property
+ def state(self):
+ return PongBaseServicer.WorkflowState(
+ servicer=self
+ )
+
+ # For 'reboot.ping.PongMethods.DoPong'.
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that new code that
+ # doesn't implement it continues to work.
+ # TODO: make it abstractmethod when renaming is done.
+ async def DoPong(
+ self,
+ context: IMPORT_reboot_aio_contexts.WriterContext,
+ state: Pong.State,
+ ) -> Pong.DoPongResponse:
+ # During the migration from 'PascalCase' to 'snake_case' method
+ # naming in Python servicers, we call the 'snake_case' version
+ # by default, so new names will do the correct thing making the
+ # code to be backwards compatible for some time and if a servicer
+ # overrides the 'PascalCase' version - it will override that
+ # method and will just work.
+ return await self.do_pong(
+ context,
+ state,
+ )
+
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that existing code that
+ # doesn't implement it continues to work.
+ async def do_pong(
+ self,
+ context: IMPORT_reboot_aio_contexts.WriterContext,
+ state: reboot.ping.ping_api_pb2.Pong,
+ ) -> Pong.DoPongResponse:
+ raise NotImplementedError
+
+ # For 'reboot.ping.PongMethods.NumPongs'.
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that new code that
+ # doesn't implement it continues to work.
+ # TODO: make it abstractmethod when renaming is done.
+ async def NumPongs(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: Pong.State,
+ ) -> Pong.NumPongsResponse:
+ # During the migration from 'PascalCase' to 'snake_case' method
+ # naming in Python servicers, we call the 'snake_case' version
+ # by default, so new names will do the correct thing making the
+ # code to be backwards compatible for some time and if a servicer
+ # overrides the 'PascalCase' version - it will override that
+ # method and will just work.
+ return await self.num_pongs(
+ context,
+ state,
+ )
+
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that existing code that
+ # doesn't implement it continues to work.
+ # TODO: make it abstractmethod when renaming is done.
+ async def num_pongs(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: Pong.State,
+ ) -> Pong.NumPongsResponse:
+ raise NotImplementedError
+
+
+ # For 'reboot.ping.PongMethods.DoPong'.
+ async def _DoPong(
+ self,
+ context: IMPORT_reboot_aio_contexts.WriterContext,
+ state: Pong.State,
+ request: google.protobuf.empty_pb2.Empty,
+ ) -> reboot.ping.ping_api_pb2.PongDoPongResponse:
+ # Wrap the call to the developer's method in a `span` so that it
+ # is traced using its fully-qualified Python name.
+ with IMPORT_reboot_aio_tracing.span(
+ state_name=f"reboot.ping.Pong('{context.state_id}')",
+ span_name=f"{IMPORT_reboot_aio_tracing.qualified_type_name(self)}.DoPong()",
+ level=IMPORT_reboot_aio_tracing.TraceLevel.CUSTOMER,
+ python_specific=True,
+ ):
+ return PongDoPongResponseToProto(
+ await self.DoPong(
+ context,
+ state,
+ )
+ )
+
+
+ # For 'reboot.ping.PongMethods.NumPongs'.
+ async def _NumPongs(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: Pong.State,
+ request: google.protobuf.empty_pb2.Empty,
+ ) -> reboot.ping.ping_api_pb2.PongNumPongsResponse:
+ # Wrap the call to the developer's method in a `span` so that it
+ # is traced using its fully-qualified Python name.
+ with IMPORT_reboot_aio_tracing.span(
+ state_name=f"reboot.ping.Pong('{context.state_id}')",
+ span_name=f"{IMPORT_reboot_aio_tracing.qualified_type_name(self)}.NumPongs()",
+ level=IMPORT_reboot_aio_tracing.TraceLevel.CUSTOMER,
+ python_specific=True,
+ ):
+ response = (
+ self.NumPongs(
+ context,
+ state,
+ )
+ )
+ return PongNumPongsResponseToProto(await response)
+
+
+
+class PongServicer(PongBaseServicer):
+
+ _state: IMPORT_contextvars.ContextVar[
+ IMPORT_typing.Optional[Pong.State]
+ ] = IMPORT_contextvars.ContextVar(
+ 'Provides access to state for each call, i.e., there may be '
+ 'multiple readers executing concurrently but each might have '
+ 'a different `state`',
+ default=None,
+ )
+
+ # An instance of the derived class for each state.
+ _instances: dict[str, PongServicer] = {}
+
+ def _instance(self, state_id: str):
+ instances = PongServicer._instances
+ instance = instances.get(state_id)
+ if instance is None:
+ instance = self.__class__()
+ instance._middleware = self._middleware
+ instances[state_id] = instance
+ return instance
+
+ @property
+ def state(self) -> Pong.State:
+ state = PongServicer._state.get()
+ if state is None:
+ raise RuntimeError(
+ "`state` property is only relevant within a `Servicer` method"
+ )
+ return state
+
+ @state.setter
+ def state(self, new_state: Pong.State):
+ state = PongServicer._state.get()
+ if state is None:
+ raise RuntimeError(
+ "`state` property is only relevant within a `Servicer` method"
+ )
+ for field_name, field_value in new_state.model_dump().items():
+ setattr(state, field_name, field_value)
+
+ # For 'reboot.ping.PongMethods.DoPong'.
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that new code that
+ # doesn't implement it continues to work.
+ # TODO: make it abstractmethod when renaming is done.
+ async def DoPong(
+ self,
+ context: IMPORT_reboot_aio_contexts.WriterContext,
+ ) -> Pong.DoPongResponse:
+ # During the migration from 'PascalCase' to 'snake_case' method
+ # naming in Python servicers, we call the 'snake_case' version
+ # by default, so new names will do the correct thing making the
+ # code to be backwards compatible for some time and if a servicer
+ # overrides the 'PascalCase' version - it will override that
+ # method and will just work.
+ return await self.do_pong(
+ context,
+ )
+
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that existing code that
+ # doesn't implement it continues to work.
+ async def do_pong(
+ self,
+ context: IMPORT_reboot_aio_contexts.WriterContext,
+ ) -> Pong.DoPongResponse:
+ raise NotImplementedError
+
+ # For 'reboot.ping.PongMethods.NumPongs'.
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that new code that
+ # doesn't implement it continues to work.
+ # TODO: make it abstractmethod when renaming is done.
+ async def NumPongs(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ ) -> Pong.NumPongsResponse:
+ # During the migration from 'PascalCase' to 'snake_case' method
+ # naming in Python servicers, we call the 'snake_case' version
+ # by default, so new names will do the correct thing making the
+ # code to be backwards compatible for some time and if a servicer
+ # overrides the 'PascalCase' version - it will override that
+ # method and will just work.
+ return await self.num_pongs(
+ context,
+ )
+
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that existing code that
+ # doesn't implement it continues to work.
+ async def num_pongs(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ ) -> Pong.NumPongsResponse:
+ raise NotImplementedError
+
+
+ # For 'reboot.ping.PongMethods.DoPong'.
+ async def _DoPong(
+ self,
+ context: IMPORT_reboot_aio_contexts.WriterContext,
+ state: Pong.State,
+ request: google.protobuf.empty_pb2.Empty,
+ ) -> reboot.ping.ping_api_pb2.PongDoPongResponse:
+ # We should have an asyncio task and thus context per request,
+ # let's confirm this assumption by making sure that
+ # `_state is None`.
+ assert PongServicer._state.get() is None
+ PongServicer._state.set(state)
+ try:
+ # Wrap the call to the developer's method in a `span` so that it
+ # is traced using its fully-qualified Python name.
+ instance = self._instance(context.state_id)
+ with IMPORT_reboot_aio_tracing.span(
+ state_name=f"reboot.ping.Pong('{context.state_id}')",
+ span_name=f"{IMPORT_reboot_aio_tracing.qualified_type_name(instance)}.DoPong()",
+ level=IMPORT_reboot_aio_tracing.TraceLevel.CUSTOMER,
+ python_specific=True,
+ ):
+
+ return PongDoPongResponseToProto(
+ await instance.DoPong(
+ context,
+ )
+ )
+ finally:
+ PongServicer._state.set(None)
+
+ # For 'reboot.ping.PongMethods.NumPongs'.
+ async def _NumPongs(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: Pong.State,
+ request: google.protobuf.empty_pb2.Empty,
+ ) -> reboot.ping.ping_api_pb2.PongNumPongsResponse:
+ # We should have an asyncio task and thus context per request,
+ # let's confirm this assumption by making sure that
+ # `_state is None`.
+ assert PongServicer._state.get() is None
+ PongServicer._state.set(state)
+ try:
+ # Wrap the call to the developer's method in a `span` so that it
+ # is traced using its fully-qualified Python name.
+ instance = self._instance(context.state_id)
+ with IMPORT_reboot_aio_tracing.span(
+ state_name=f"reboot.ping.Pong('{context.state_id}')",
+ span_name=f"{IMPORT_reboot_aio_tracing.qualified_type_name(instance)}.NumPongs()",
+ level=IMPORT_reboot_aio_tracing.TraceLevel.CUSTOMER,
+ python_specific=True,
+ ):
+
+ return PongNumPongsResponseToProto(
+ await instance.NumPongs(
+ context,
+ )
+ )
+ finally:
+ PongServicer._state.set(None)
+
+
+class UserBaseServicer(IMPORT_reboot_aio_servicers.Servicer):
+ Authorizer: IMPORT_typing.TypeAlias = UserAuthorizer
+ # Node-backed servicers attach a JS bridge object at runtime. We declare it
+ # on the base class so mypy accepts accesses when a servicer is typed as
+ # `UserBaseServicer` (e.g. in classmethod workflows).
+ _js_servicer_reference: IMPORT_typing.Any
+
+ __servicer__: IMPORT_contextvars.ContextVar[IMPORT_typing.Optional[UserBaseServicer]] = IMPORT_contextvars.ContextVar(
+ 'Provides access to a servicer in the current asyncio context. '
+ 'We need that to be able to do inline writes and reads inside '
+ 'a workflow',
+ default=None,
+ )
+
+ __service_names__ = [
+ IMPORT_reboot_aio_types.ServiceName('reboot.ping.UserMethods'),
+ ]
+ __state_type_name__ = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User')
+ __state_type__ = reboot.ping.ping_api_pb2.User
+ __file_descriptor__ = reboot.ping.ping_api_pb2.DESCRIPTOR
+
+ def __init__(self):
+ super().__init__()
+ # NOTE: need to hold on to the middleware so we can do inline
+ # writes (see 'self.write(...)').
+ #
+ # Because '_middleware' is not really private this does mean
+ # users may do possibly dangerous things, but this is no more
+ # likely given they could have already overridden
+ # 'create_middleware()'.
+ self._middleware: IMPORT_typing.Optional[UserServicerMiddleware] = None
+
+ def create_middleware(
+ self,
+ *,
+ application_id: IMPORT_reboot_aio_types.ApplicationId,
+ server_id: IMPORT_reboot_aio_types.ServerId,
+ state_manager: IMPORT_reboot_aio_state_managers.StateManager,
+ placement_client: IMPORT_reboot_aio_placement.PlacementClient,
+ channel_manager: IMPORT_reboot_aio_internals_channel_manager._ChannelManager,
+ tasks_cache: IMPORT_reboot_aio_internals_tasks_cache.TasksCache,
+ token_verifier: IMPORT_typing.Optional[IMPORT_reboot_aio_auth_token_verifiers.TokenVerifier],
+ effect_validation: IMPORT_reboot_aio_contexts.EffectValidation,
+ ready: IMPORT_asyncio.Event,
+ ) -> UserServicerMiddleware:
+ self._middleware = UserServicerMiddleware(
+ servicer=self,
+ application_id=application_id,
+ server_id=server_id,
+ state_manager=state_manager,
+ placement_client=placement_client,
+ channel_manager=channel_manager,
+ tasks_cache=tasks_cache,
+ token_verifier=token_verifier,
+ effect_validation=effect_validation,
+ ready=ready,
+ )
+ return self._middleware
+
+ def authorizer(self) -> IMPORT_typing.Optional[IMPORT_reboot_aio_auth_authorizers.Authorizer | IMPORT_reboot_aio_auth_authorizers.AuthorizerRule]:
+ return None
+
+ def token_verifier(self) -> IMPORT_typing.Optional[IMPORT_reboot_aio_auth_token_verifiers.TokenVerifier]:
+ return None
+
+ _is_auto_construct = True
+
+
+ @staticmethod
+ def _mcp_tool_names() -> list[str]:
+ """Return the MCP tool names this servicer registers."""
+ return [
+ "create_counter",
+ "list_counters",
+ "whoami",
+ ]
+
+ @staticmethod
+ def _add_mcp(
+ mcp: IMPORT_mcp_server_fastmcp.FastMCP,
+ auto_construct_state_type_full_names: list[IMPORT_reboot_aio_types.StateTypeName],
+ ) -> None:
+ """Register MCP tools and resources for User."""
+
+ # Tool for 'reboot.ping.UserMethods.CreateCounter'.
+ @mcp.tool(
+ name="create_counter",
+ title="CreateCounter",
+ description="Create a new Counter with a description of what it counts. Returns the `counter_id`, which is not human-readable but should be passed to future tool calls that need it.",
+ )
+ async def create_counter_tool(
+ ctx: IMPORT_mcp_server_fastmcp.Context,
+ *,
+ request: User.CreateCounterRequest,
+ ):
+ reboot_context = IMPORT_reboot_mcp_context.get_reboot_context(ctx)
+ id = IMPORT_reboot_mcp_context.get_mcp_user_id(ctx)
+ # State is auto-constructed on session init;
+ # see `_auto_construct`.
+ ref = User.ref(id)
+ return await ref.create_counter(reboot_context, request)
+
+
+ # Tool for 'reboot.ping.UserMethods.ListCounters'.
+ @mcp.tool(
+ name="list_counters",
+ title="ListCounters",
+ description="List all counters created by this user. Returns `counter_id` and description for each. The `counter_id` is not human-readable, but use it when calling tools that take a `counter_id`.",
+ )
+ async def list_counters_tool(
+ ctx: IMPORT_mcp_server_fastmcp.Context,
+ ):
+ reboot_context = IMPORT_reboot_mcp_context.get_reboot_context(ctx)
+ id = IMPORT_reboot_mcp_context.get_mcp_user_id(ctx)
+ # State is auto-constructed on session init;
+ # see `_auto_construct`.
+ ref = User.ref(id)
+ return await ref.list_counters(reboot_context)
+
+
+ # Tool for 'reboot.ping.UserMethods.Whoami'.
+ @mcp.tool(
+ name="whoami",
+ title="Whoami",
+ description="Returns the authenticated user's ID.",
+ )
+ async def whoami_tool(
+ ctx: IMPORT_mcp_server_fastmcp.Context,
+ ):
+ reboot_context = IMPORT_reboot_mcp_context.get_reboot_context(ctx)
+ id = IMPORT_reboot_mcp_context.get_mcp_user_id(ctx)
+ # State is auto-constructed on session init;
+ # see `_auto_construct`.
+ ref = User.ref(id)
+ return await ref.whoami(reboot_context)
+
+ pass # End of _add_mcp.
+
+ @staticmethod
+ async def _auto_construct(
+ context: IMPORT_reboot_aio_external.ExternalContext,
+ state_id: str,
+ ) -> None:
+ """Auto-construct a `User` state."""
+ # Some states have `_auto_construct` called many times, e.g. a
+ # per-user state may get auto-constructed at the start of every
+ # MCP session. We derive a deterministic idempotency key from the
+ # state ID so that repeated calls (even from different contexts)
+ # are a NOOP. We prefer this over catching the
+ # `StateAlreadyExists` error that would otherwise result, since
+ # that logs an `ERROR` to user-visible logs.
+ idempotency_key = IMPORT_uuid.uuid5(
+ IMPORT_uuid.NAMESPACE_URL,
+ f"urn:dev.reboot:auto-construct:User:{state_id}",
+ )
+ await User.idempotently(
+ key=idempotency_key,
+ ).create(context, state_id)
+
+ def ref(
+ self,
+ *,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ) -> User.WeakReference[User.WeakReference._WriterSchedule]:
+ context = IMPORT_reboot_aio_contexts.Context.get()
+
+ if context is None:
+ raise RuntimeError(
+ 'Missing asyncio context variable `context`; '
+ 'are you using this class without Reboot?'
+ )
+
+ return User.WeakReference(
+ # TODO(https://github.com/reboot-dev/mono/issues/3226): add support for calling other applications.
+ # For now this always stays within the application that creates the context.
+ application_id=None,
+ state_id=context._state_ref.id,
+ schedule_type=User.WeakReference._WriterSchedule,
+ # If the user didn't specify a bearer token we may still end up using the app-internal bearer token,
+ # but that's decided at the time of the call.
+ bearer_token=bearer_token,
+ servicer=self,
+ )
+
+ class Effects(IMPORT_reboot_aio_state_managers.Effects):
+ def __init__(
+ self,
+ *,
+ state: reboot.ping.ping_api_pb2.User,
+ response: IMPORT_typing.Optional[IMPORT_google_protobuf_message.Message] = None,
+ tasks: IMPORT_typing.Optional[list[IMPORT_reboot_aio_tasks.TaskEffect]] = None,
+ _colocated_upserts: IMPORT_typing.Optional[list[tuple[str, IMPORT_typing.Optional[bytes]]]] = None,
+ ):
+ IMPORT_reboot_aio_types.assert_type(state, [reboot.ping.ping_api_pb2.User])
+
+ super().__init__(state=state, response=response, tasks=tasks, _colocated_upserts=_colocated_upserts)
+
+
+
+
+
+
+ InlineWriterCallableResult = IMPORT_typing.TypeVar('InlineWriterCallableResult', covariant=True)
+
+ class InlineWriterCallable(IMPORT_typing.Protocol[InlineWriterCallableResult]):
+ async def __call__(
+ self,
+ state: reboot.ping.ping_api_pb2.User
+ ) -> UserBaseServicer.InlineWriterCallableResult:
+ ...
+
+ class WorkflowState:
+
+ def __init__(
+ self,
+ servicer,
+ ):
+ self._servicer = servicer
+
+ async def read(
+ self, context: IMPORT_reboot_aio_contexts.WorkflowContext
+ ) -> User.State:
+ """Read the current state within a workflow."""
+ return await (
+ self.always() if context.within_until()
+ else (
+ self.per_iteration() if context.within_loop()
+ else self.per_workflow()
+ )
+ ).read(context)
+
+ async def write(
+ self,
+ idempotency_alias: str,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ writer: UserBaseServicer.InlineWriterCallable[UserBaseServicer.InlineWriterCallableResult],
+ __options__: IMPORT_reboot_aio_call.Options = IMPORT_reboot_aio_call.Options(),
+ *,
+ type: IMPORT_reboot_aio_workflows.Type | IMPORT_reboot_aio_workflows._Unset = IMPORT_reboot_aio_workflows._UNSET,
+ ) -> UserBaseServicer.InlineWriterCallableResult:
+ """Perform an "inline write" within a workflow.
+
+ `type=` is optional: when omitted, `writer`'s return
+ annotation is used (falling back to `type(None)` if
+ there is none).
+ """
+ type_t, _ = IMPORT_reboot_aio_workflows._resolve_callable_return_type(
+ writer, type,
+ )
+ return await (
+ self.per_iteration(idempotency_alias) if context.within_loop()
+ else self.per_workflow(idempotency_alias)
+ ).write(
+ context, writer, __options__, type=type_t
+ )
+
+ class _Idempotently:
+
+ def __init__(
+ self,
+ *,
+ servicer: UserBaseServicer,
+ alias: IMPORT_typing.Optional[str],
+ how: IMPORT_reboot_aio_workflows.How,
+ ):
+ self._servicer = servicer
+ self._alias = alias
+ self._how = how
+
+ async def read(
+ self, context: IMPORT_reboot_aio_contexts.WorkflowContext
+ ) -> User.State:
+ """Read the current state within a workflow."""
+ return await self._read(
+ self._servicer,
+ context.idempotency(
+ how=IMPORT_reboot_aio_idempotency.ALWAYS,
+ ) if self._how == IMPORT_reboot_aio_workflows.ALWAYS else context.idempotency(
+ alias=self._alias,
+ how=self._how,
+ ),
+ context,
+ )
+
+ @staticmethod
+ async def _read(
+ servicer: UserBaseServicer,
+ idempotency: IMPORT_reboot_aio_idempotency.Idempotency,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ ) -> User.State:
+ """Read the current state within a workflow."""
+ IMPORT_reboot_aio_types.assert_type(context, [IMPORT_reboot_aio_contexts.WorkflowContext])
+
+ if servicer._middleware is None:
+ raise RuntimeError(
+ 'Reboot middleware was not created; '
+ 'are you using this class without Reboot?'
+ )
+
+ async def read() -> reboot.ping.ping_api_pb2.User:
+ assert servicer._middleware is not None
+ return await servicer._middleware._state_manager.read(
+ context, servicer.__state_type__
+ )
+
+ if idempotency.always:
+ return UserFromProto(await read())
+
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User')
+
+ # Use the idempotency manager to make sure that this
+ # reader is being called following the rules.
+ with context.idempotently(
+ state_type_name=state_type_name,
+ state_ref=context._state_ref,
+ # Not calling a method so `service_name`,
+ # `method`, `request`, etc are irrelevant.
+ service_name=None,
+ method=None,
+ mutation=False,
+ request=None,
+ metadata=None,
+ idempotency=idempotency,
+ # Only need to pass `aborted_type` for mutations.
+ aborted_type=None,
+ ) as idempotency_key:
+ assert idempotency_key is not None
+ protobuf_state = await IMPORT_reboot_aio_workflows.at_least_once(
+ (
+ # TODO: for easier debugging include the
+ # original alias (or generated alias in
+ # the case of `.per_iteration()` w/o an
+ # alias) instead of just
+ # `idempotency_key`.
+ f"inline reader of '{ state_type_name }' ({str(idempotency_key)})",
+ # NOTE: we want this to be `PER_WORKFLOW`
+ # because any per iteration concerns
+ # should have already been taken care of
+ # by caller using `.per_iteration()`.
+ IMPORT_reboot_aio_workflows.PER_WORKFLOW
+ ),
+ context,
+ read,
+ type=reboot.ping.ping_api_pb2.User,
+ )
+
+ return UserFromProto(protobuf_state)
+
+ async def write(
+ self,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ writer: UserBaseServicer.InlineWriterCallable[UserBaseServicer.InlineWriterCallableResult],
+ __options__: IMPORT_reboot_aio_call.Options = IMPORT_reboot_aio_call.Options(),
+ *,
+ type: IMPORT_reboot_aio_workflows.Type | IMPORT_reboot_aio_workflows._Unset = IMPORT_reboot_aio_workflows._UNSET,
+ check_type: bool = True,
+ ) -> UserBaseServicer.InlineWriterCallableResult:
+ # `type=` is optional: when omitted, `writer`'s
+ # return annotation is used (falling back to
+ # `type(None)` when there is none).
+ type_t, _ = IMPORT_reboot_aio_workflows._resolve_callable_return_type(
+ writer, type,
+ )
+ return await self._write(
+ context,
+ writer,
+ __options__,
+ type_result=type_t,
+ check_type=check_type,
+ )
+
+ async def _write(
+ self,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ writer: UserBaseServicer.InlineWriterCallable[UserBaseServicer.InlineWriterCallableResult],
+ __options__: IMPORT_reboot_aio_call.Options = IMPORT_reboot_aio_call.Options(),
+ *,
+ type_result: IMPORT_reboot_aio_workflows.TypeT,
+ check_type: bool,
+ ) -> UserBaseServicer.InlineWriterCallableResult:
+ unidempotently = self._how == IMPORT_reboot_aio_workflows.ALWAYS
+ idempotency = (
+ context.idempotency(
+ how=IMPORT_reboot_aio_idempotency.ALWAYS,
+ ) if unidempotently else context.idempotency(
+ alias=self._alias,
+ how=self._how,
+ )
+ )
+
+ return await self._write_validating_effects(
+ self._servicer,
+ idempotency,
+ context,
+ writer,
+ __options__,
+ type_result=type_result,
+ check_type=check_type,
+ unidempotently=unidempotently,
+ checkpoint=context.checkpoint(),
+ )
+
+ @staticmethod
+ @IMPORT_reboot_aio_internals_middleware.maybe_run_function_twice_to_validate_effects
+ async def _write_validating_effects(
+ validating_effects: bool,
+ servicer: UserBaseServicer,
+ idempotency: IMPORT_reboot_aio_idempotency.Idempotency,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ writer: UserBaseServicer.InlineWriterCallable[UserBaseServicer.InlineWriterCallableResult],
+ __options__: IMPORT_reboot_aio_call.Options = IMPORT_reboot_aio_call.Options(),
+ *,
+ type_result: IMPORT_reboot_aio_workflows.TypeT,
+ check_type: bool,
+ unidempotently: bool,
+ checkpoint: IMPORT_reboot_aio_idempotency.Checkpoint,
+ ) -> UserBaseServicer.InlineWriterCallableResult:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+
+ IMPORT_reboot_aio_types.assert_type(context, [IMPORT_reboot_aio_contexts.WorkflowContext])
+
+ if servicer._middleware is None:
+ raise RuntimeError(
+ 'Reboot middleware was not created; '
+ 'are you using this class without Reboot?'
+ )
+
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+
+ if __options__ is not None:
+ if __options__.metadata is not None:
+ metadata = __options__.metadata
+
+ if metadata is None:
+ metadata = ()
+
+ headers = IMPORT_reboot_aio_headers.Headers(
+ application_id=context.application_id,
+ state_ref=context._state_ref,
+ caller_id=IMPORT_reboot_aio_caller_id.CallerID(
+ application_id=context.application_id,
+ ),
+ workflow_id=context.workflow_id,
+ # Only set `workflow_iteration` for
+ # `per_iteration` calls so that the
+ # mutation is stored/recovered with
+ # iteration scoping.
+ workflow_iteration=(
+ context.workflow_iteration
+ if idempotency.per_iteration else None
+ ),
+ )
+
+ metadata += headers.to_grpc_metadata()
+
+ idempotency_key: IMPORT_typing.Optional[IMPORT_uuid.UUID]
+ with context.idempotently(
+ state_type_name=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User'),
+ state_ref=context._state_ref,
+ service_name=None, # Indicates an inline writer.
+ method=None, # Indicates an inline writer.
+ mutation=True,
+ request=None, # Indicates an inline writer.
+ metadata=metadata,
+ idempotency=idempotency,
+ aborted_type=None, # Indicates an inline writer.
+ ) as idempotency_key:
+
+ if any(t[0] == IMPORT_reboot_aio_headers.IDEMPOTENCY_KEY_HEADER for t in metadata):
+ raise ValueError(
+ f"Do not set '{IMPORT_reboot_aio_headers.IDEMPOTENCY_KEY_HEADER}' metadata yourself"
+ )
+
+ if idempotency_key is not None:
+ metadata += (
+ (IMPORT_reboot_aio_headers.IDEMPOTENCY_KEY_HEADER, str(idempotency_key)),
+ )
+
+ with servicer._middleware.use_context(
+ headers=IMPORT_reboot_aio_headers.Headers.from_grpc_metadata(metadata),
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User'),
+ method='inline writer',
+ context_type=IMPORT_reboot_aio_contexts.WriterContext,
+ ) as writer_context:
+ # Check if we already have performed this mutation!
+ #
+ # We do this _before_ calling 'transactionally()' because
+ # if this call is for a transaction method _and_ we've
+ # already performed the transaction then we don't want to
+ # become a transaction participant (again) we just want to
+ # return the transaction's response.
+ idempotent_mutation = (
+ await servicer._middleware._state_manager.check_for_idempotent_mutation(
+ writer_context
+ )
+ )
+
+ if idempotent_mutation is not None:
+ assert len(idempotent_mutation.response) != 0
+ response = IMPORT_google_protobuf_wrappers_pb2.BytesValue()
+ response.ParseFromString(idempotent_mutation.response)
+ result: UserBaseServicer.InlineWriterCallableResult = IMPORT_pickle.loads(response.value)
+
+ if check_type and not isinstance(result, IMPORT_reboot_aio_workflows._isinstance_type(type_result)):
+ raise TypeError(
+ f"Stored result of type '{type(result).__name__}' from 'writer' "
+ f"is not of expected type '{IMPORT_reboot_aio_workflows._format_type(type_result)}'; have you changed "
+ "the 'type' that you expect after having stored a result?"
+ )
+
+ return result
+
+ async with servicer._middleware._state_manager.transactionally(
+ writer_context,
+ servicer._middleware.tasks_dispatcher,
+ aborted_type=None,
+ ) as transaction:
+ async with servicer._middleware._state_manager.writer(
+ writer_context,
+ servicer.__state_type__,
+ servicer._middleware.tasks_dispatcher,
+ # TODO: Decide if we want to do any kind of authorization for inline
+ # writers otherwise passing `None` here is fine.
+ authorize=None,
+ transaction=transaction,
+ ) as (protobuf_state, state_manager_writer):
+ # Serialize the state so we can see if it changed.
+ serialized_state = protobuf_state.SerializeToString(
+ deterministic=True,
+ )
+
+ typed_state = UserFromProto(protobuf_state)
+
+ result = await writer(state=typed_state)
+
+ UserToProto(typed_state, protobuf_state)
+
+ if check_type and not isinstance(result, IMPORT_reboot_aio_workflows._isinstance_type(type_result)):
+ raise TypeError(
+ f"Result of type '{type(result).__name__}' from 'writer' is "
+ f"not of expected type '{IMPORT_reboot_aio_workflows._format_type(type_result)}'; "
+ "did you specify an incorrect 'type'?"
+ )
+
+ task: IMPORT_typing.Optional[IMPORT_reboot_aio_tasks.TaskEffect] = context.task
+
+ assert task is not None, (
+ "Should always have a task when running a `workflow`"
+ )
+
+ method_name = f"User.{task.method_name} inline writer"
+
+ if idempotency.alias is not None:
+ method_name += " with idempotency alias '" + idempotency.alias + "'"
+ elif idempotency.key is not None:
+ method_name += " with idempotency key=" + str(idempotency.key)
+
+ servicer._middleware.maybe_raise_effect_validation_retry(
+ logger=logger,
+ idempotency_manager=context,
+ method_name=method_name,
+ validating_effects=validating_effects,
+ context=context,
+ checkpoint=checkpoint,
+ )
+
+ # We don't pass the context to the
+ # writer, so we don't expect there to
+ # be any scheduled tasks!
+ assert len(context._tasks) == 0
+
+ effects = IMPORT_reboot_aio_state_managers.Effects(
+ state=(
+ # Pass `None` if the state hasn't changed!
+ protobuf_state if serialized_state != protobuf_state.SerializeToString(
+ deterministic=True,
+ )
+ else None
+ ),
+ response=IMPORT_google_protobuf_wrappers_pb2.BytesValue(
+ value=IMPORT_pickle.dumps(result)
+ ),
+ )
+
+ await state_manager_writer.complete(effects)
+
+ return result
+
+ def per_workflow(self, alias: IMPORT_typing.Optional[str] = None):
+ return UserBaseServicer.WorkflowState._Idempotently(
+ servicer=self._servicer,
+ alias=alias,
+ how=IMPORT_reboot_aio_workflows.PER_WORKFLOW,
+ )
+
+ def per_iteration(self, alias: IMPORT_typing.Optional[str] = None):
+ return UserBaseServicer.WorkflowState._Idempotently(
+ servicer=self._servicer,
+ alias=alias,
+ how=IMPORT_reboot_aio_workflows.PER_ITERATION,
+ )
+
+ class _Always:
+ """Helper class for providing better types for `write` that don't
+ require passing `type` or `check_type`."""
+
+ def __init__(
+ self,
+ *,
+ servicer: UserBaseServicer,
+ ):
+ self._servicer = servicer
+
+ async def read(
+ self, context: IMPORT_reboot_aio_contexts.WorkflowContext
+ ) -> User.State:
+ return await UserBaseServicer.WorkflowState._Idempotently(
+ servicer=self._servicer,
+ alias=None,
+ how=IMPORT_reboot_aio_workflows.ALWAYS,
+ ).read(context)
+
+ async def write(
+ self,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ writer: UserBaseServicer.InlineWriterCallable[UserBaseServicer.InlineWriterCallableResult],
+ __options__: IMPORT_reboot_aio_call.Options = IMPORT_reboot_aio_call.Options(),
+ ) -> UserBaseServicer.InlineWriterCallableResult:
+ return await UserBaseServicer.WorkflowState._Idempotently(
+ servicer=self._servicer,
+ alias=None,
+ how=IMPORT_reboot_aio_workflows.ALWAYS,
+ )._write(
+ context,
+ writer,
+ __options__,
+ type_result=type(None),
+ check_type=False,
+ )
+
+ def always(self):
+ return UserBaseServicer.WorkflowState._Always(
+ servicer=self._servicer,
+ )
+
+ # For 'reboot.ping.UserMethods.CreateCounter'.
+ @IMPORT_abc_abstractmethod
+ async def _CreateCounter(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext,
+ state: User.State,
+ request: reboot.ping.ping_api_pb2.UserCreateCounterRequest,
+ ) -> reboot.ping.ping_api_pb2.UserCreateCounterResponse:
+ raise NotImplementedError
+
+ # For 'reboot.ping.UserMethods.ListCounters'.
+ @IMPORT_abc_abstractmethod
+ async def _ListCounters(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: User.State,
+ request: google.protobuf.empty_pb2.Empty,
+ ) -> reboot.ping.ping_api_pb2.UserListCountersResponse:
+ raise NotImplementedError
+
+ # For 'reboot.ping.UserMethods.Whoami'.
+ @IMPORT_abc_abstractmethod
+ async def _Whoami(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: User.State,
+ request: google.protobuf.empty_pb2.Empty,
+ ) -> reboot.ping.ping_api_pb2.UserWhoamiResponse:
+ raise NotImplementedError
+
+ # For 'reboot.ping.UserMethods.Create'.
+ @IMPORT_abc_abstractmethod
+ async def _Create(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext,
+ state: User.State,
+ request: google.protobuf.empty_pb2.Empty,
+ ) -> google.protobuf.empty_pb2.Empty:
+ raise NotImplementedError
+
+
+
+class UserSingletonServicer(UserBaseServicer):
+
+ @property
+ def state(self):
+ return UserBaseServicer.WorkflowState(
+ servicer=self
+ )
+
+ # For 'reboot.ping.UserMethods.CreateCounter'.
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that new code that
+ # doesn't implement it continues to work.
+ # TODO: make it abstractmethod when renaming is done.
+ async def CreateCounter(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext,
+ state: User.State,
+ request: User.CreateCounterRequest,
+ ) -> User.CreateCounterResponse:
+ # During the migration from 'PascalCase' to 'snake_case' method
+ # naming in Python servicers, we call the 'snake_case' version
+ # by default, so new names will do the correct thing making the
+ # code to be backwards compatible for some time and if a servicer
+ # overrides the 'PascalCase' version - it will override that
+ # method and will just work.
+ return await self.create_counter(
+ context,
+ state,
+ request,
+ )
+
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that existing code that
+ # doesn't implement it continues to work.
+ async def create_counter(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext,
+ state: User.State,
+ request: User.CreateCounterRequest,
+ ) -> User.CreateCounterResponse:
+ raise NotImplementedError
+
+ # For 'reboot.ping.UserMethods.ListCounters'.
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that new code that
+ # doesn't implement it continues to work.
+ # TODO: make it abstractmethod when renaming is done.
+ async def ListCounters(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: User.State,
+ ) -> User.ListCountersResponse:
+ # During the migration from 'PascalCase' to 'snake_case' method
+ # naming in Python servicers, we call the 'snake_case' version
+ # by default, so new names will do the correct thing making the
+ # code to be backwards compatible for some time and if a servicer
+ # overrides the 'PascalCase' version - it will override that
+ # method and will just work.
+ return await self.list_counters(
+ context,
+ state,
+ )
+
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that existing code that
+ # doesn't implement it continues to work.
+ # TODO: make it abstractmethod when renaming is done.
+ async def list_counters(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: User.State,
+ ) -> User.ListCountersResponse:
+ raise NotImplementedError
+
+ # For 'reboot.ping.UserMethods.Whoami'.
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that new code that
+ # doesn't implement it continues to work.
+ # TODO: make it abstractmethod when renaming is done.
+ async def Whoami(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: User.State,
+ ) -> User.WhoamiResponse:
+ # During the migration from 'PascalCase' to 'snake_case' method
+ # naming in Python servicers, we call the 'snake_case' version
+ # by default, so new names will do the correct thing making the
+ # code to be backwards compatible for some time and if a servicer
+ # overrides the 'PascalCase' version - it will override that
+ # method and will just work.
+ return await self.whoami(
+ context,
+ state,
+ )
+
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that existing code that
+ # doesn't implement it continues to work.
+ # TODO: make it abstractmethod when renaming is done.
+ async def whoami(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: User.State,
+ ) -> User.WhoamiResponse:
+ raise NotImplementedError
+
+ # For 'reboot.ping.UserMethods.Create'.
+ # Concrete no-op default for `create`.
+ # Override in your Servicer to run custom initialization
+ # on new User instances.
+ async def Create(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext,
+ state: User.State,
+ ) -> None:
+ pass
+
+ async def create(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext,
+ state: reboot.ping.ping_api_pb2.User,
+ ) -> None:
+ pass
+
+
+ # For 'reboot.ping.UserMethods.CreateCounter'.
+ async def _CreateCounter(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext,
+ state: User.State,
+ request: reboot.ping.ping_api_pb2.UserCreateCounterRequest,
+ ) -> reboot.ping.ping_api_pb2.UserCreateCounterResponse:
+ # Wrap the call to the developer's method in a `span` so that it
+ # is traced using its fully-qualified Python name.
+ with IMPORT_reboot_aio_tracing.span(
+ state_name=f"reboot.ping.User('{context.state_id}')",
+ span_name=f"{IMPORT_reboot_aio_tracing.qualified_type_name(self)}.CreateCounter()",
+ level=IMPORT_reboot_aio_tracing.TraceLevel.CUSTOMER,
+ python_specific=True,
+ ):
+ typed_request = UserCreateCounterRequestFromProto(request)
+ return UserCreateCounterResponseToProto(
+ await self.CreateCounter(
+ context,
+ state,
+ typed_request
+ )
+ )
+
+
+ # For 'reboot.ping.UserMethods.ListCounters'.
+ async def _ListCounters(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: User.State,
+ request: google.protobuf.empty_pb2.Empty,
+ ) -> reboot.ping.ping_api_pb2.UserListCountersResponse:
+ # Wrap the call to the developer's method in a `span` so that it
+ # is traced using its fully-qualified Python name.
+ with IMPORT_reboot_aio_tracing.span(
+ state_name=f"reboot.ping.User('{context.state_id}')",
+ span_name=f"{IMPORT_reboot_aio_tracing.qualified_type_name(self)}.ListCounters()",
+ level=IMPORT_reboot_aio_tracing.TraceLevel.CUSTOMER,
+ python_specific=True,
+ ):
+ response = (
+ self.ListCounters(
+ context,
+ state,
+ )
+ )
+ return UserListCountersResponseToProto(await response)
+
+ # For 'reboot.ping.UserMethods.Whoami'.
+ async def _Whoami(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: User.State,
+ request: google.protobuf.empty_pb2.Empty,
+ ) -> reboot.ping.ping_api_pb2.UserWhoamiResponse:
+ # Wrap the call to the developer's method in a `span` so that it
+ # is traced using its fully-qualified Python name.
+ with IMPORT_reboot_aio_tracing.span(
+ state_name=f"reboot.ping.User('{context.state_id}')",
+ span_name=f"{IMPORT_reboot_aio_tracing.qualified_type_name(self)}.Whoami()",
+ level=IMPORT_reboot_aio_tracing.TraceLevel.CUSTOMER,
+ python_specific=True,
+ ):
+ response = (
+ self.Whoami(
+ context,
+ state,
+ )
+ )
+ return UserWhoamiResponseToProto(await response)
+
+ # For 'reboot.ping.UserMethods.Create'.
+ async def _Create(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext,
+ state: User.State,
+ request: google.protobuf.empty_pb2.Empty,
+ ) -> google.protobuf.empty_pb2.Empty:
+ # Wrap the call to the developer's method in a `span` so that it
+ # is traced using its fully-qualified Python name.
+ with IMPORT_reboot_aio_tracing.span(
+ state_name=f"reboot.ping.User('{context.state_id}')",
+ span_name=f"{IMPORT_reboot_aio_tracing.qualified_type_name(self)}.Create()",
+ level=IMPORT_reboot_aio_tracing.TraceLevel.CUSTOMER,
+ python_specific=True,
+ ):
+ return UserCreateResponseToProto(
+ await self.Create(
+ context,
+ state,
+ )
+ )
+
+
+
+
+class UserServicer(UserBaseServicer):
+
+ _state: IMPORT_contextvars.ContextVar[
+ IMPORT_typing.Optional[User.State]
+ ] = IMPORT_contextvars.ContextVar(
+ 'Provides access to state for each call, i.e., there may be '
+ 'multiple readers executing concurrently but each might have '
+ 'a different `state`',
+ default=None,
+ )
+
+ # An instance of the derived class for each state.
+ _instances: dict[str, UserServicer] = {}
+
+ def _instance(self, state_id: str):
+ instances = UserServicer._instances
+ instance = instances.get(state_id)
+ if instance is None:
+ instance = self.__class__()
+ instance._middleware = self._middleware
+ instances[state_id] = instance
+ return instance
+
+ @property
+ def state(self) -> User.State:
+ state = UserServicer._state.get()
+ if state is None:
+ raise RuntimeError(
+ "`state` property is only relevant within a `Servicer` method"
+ )
+ return state
+
+ @state.setter
+ def state(self, new_state: User.State):
+ state = UserServicer._state.get()
+ if state is None:
+ raise RuntimeError(
+ "`state` property is only relevant within a `Servicer` method"
+ )
+ for field_name, field_value in new_state.model_dump().items():
+ setattr(state, field_name, field_value)
+
+ # For 'reboot.ping.UserMethods.CreateCounter'.
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that new code that
+ # doesn't implement it continues to work.
+ # TODO: make it abstractmethod when renaming is done.
+ async def CreateCounter(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext,
+ request: User.CreateCounterRequest,
+ ) -> User.CreateCounterResponse:
+ # During the migration from 'PascalCase' to 'snake_case' method
+ # naming in Python servicers, we call the 'snake_case' version
+ # by default, so new names will do the correct thing making the
+ # code to be backwards compatible for some time and if a servicer
+ # overrides the 'PascalCase' version - it will override that
+ # method and will just work.
+ return await self.create_counter(
+ context,
+ request,
+ )
+
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that existing code that
+ # doesn't implement it continues to work.
+ async def create_counter(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext,
+ request: User.CreateCounterRequest,
+ ) -> User.CreateCounterResponse:
+ raise NotImplementedError
+
+ # For 'reboot.ping.UserMethods.ListCounters'.
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that new code that
+ # doesn't implement it continues to work.
+ # TODO: make it abstractmethod when renaming is done.
+ async def ListCounters(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ ) -> User.ListCountersResponse:
+ # During the migration from 'PascalCase' to 'snake_case' method
+ # naming in Python servicers, we call the 'snake_case' version
+ # by default, so new names will do the correct thing making the
+ # code to be backwards compatible for some time and if a servicer
+ # overrides the 'PascalCase' version - it will override that
+ # method and will just work.
+ return await self.list_counters(
+ context,
+ )
+
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that existing code that
+ # doesn't implement it continues to work.
+ async def list_counters(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ ) -> User.ListCountersResponse:
+ raise NotImplementedError
+
+ # For 'reboot.ping.UserMethods.Whoami'.
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that new code that
+ # doesn't implement it continues to work.
+ # TODO: make it abstractmethod when renaming is done.
+ async def Whoami(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ ) -> User.WhoamiResponse:
+ # During the migration from 'PascalCase' to 'snake_case' method
+ # naming in Python servicers, we call the 'snake_case' version
+ # by default, so new names will do the correct thing making the
+ # code to be backwards compatible for some time and if a servicer
+ # overrides the 'PascalCase' version - it will override that
+ # method and will just work.
+ return await self.whoami(
+ context,
+ )
+
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that existing code that
+ # doesn't implement it continues to work.
+ async def whoami(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ ) -> User.WhoamiResponse:
+ raise NotImplementedError
+
+ # For 'reboot.ping.UserMethods.Create'.
+ # Concrete no-op default for `create`. The
+ # auto-constructed `create` is always a
+ # `Transaction` (see `api.py`), so it takes a `TransactionContext`.
+ # Override in your Servicer to run custom initialization
+ # on new User instances.
+ async def Create(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext,
+ ) -> None:
+ return await self.create(
+ context,
+ )
+
+ async def create(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext,
+ ) -> None:
+ pass
+
+
+ # For 'reboot.ping.UserMethods.CreateCounter'.
+ async def _CreateCounter(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext,
+ state: User.State,
+ request: reboot.ping.ping_api_pb2.UserCreateCounterRequest,
+ ) -> reboot.ping.ping_api_pb2.UserCreateCounterResponse:
+ # We should have an asyncio task and thus context per request,
+ # let's confirm this assumption by making sure that
+ # `_state is None`.
+ assert UserServicer._state.get() is None
+ UserServicer._state.set(state)
+ try:
+ # Wrap the call to the developer's method in a `span` so that it
+ # is traced using its fully-qualified Python name.
+ instance = self._instance(context.state_id)
+ with IMPORT_reboot_aio_tracing.span(
+ state_name=f"reboot.ping.User('{context.state_id}')",
+ span_name=f"{IMPORT_reboot_aio_tracing.qualified_type_name(instance)}.CreateCounter()",
+ level=IMPORT_reboot_aio_tracing.TraceLevel.CUSTOMER,
+ python_specific=True,
+ ):
+ typed_request = UserCreateCounterRequestFromProto(request)
+
+ return UserCreateCounterResponseToProto(
+ await instance.CreateCounter(
+ context,
+ typed_request,
+ )
+ )
+ finally:
+ UserServicer._state.set(None)
+
+ # For 'reboot.ping.UserMethods.ListCounters'.
+ async def _ListCounters(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: User.State,
+ request: google.protobuf.empty_pb2.Empty,
+ ) -> reboot.ping.ping_api_pb2.UserListCountersResponse:
+ # We should have an asyncio task and thus context per request,
+ # let's confirm this assumption by making sure that
+ # `_state is None`.
+ assert UserServicer._state.get() is None
+ UserServicer._state.set(state)
+ try:
+ # Wrap the call to the developer's method in a `span` so that it
+ # is traced using its fully-qualified Python name.
+ instance = self._instance(context.state_id)
+ with IMPORT_reboot_aio_tracing.span(
+ state_name=f"reboot.ping.User('{context.state_id}')",
+ span_name=f"{IMPORT_reboot_aio_tracing.qualified_type_name(instance)}.ListCounters()",
+ level=IMPORT_reboot_aio_tracing.TraceLevel.CUSTOMER,
+ python_specific=True,
+ ):
+
+ return UserListCountersResponseToProto(
+ await instance.ListCounters(
+ context,
+ )
+ )
+ finally:
+ UserServicer._state.set(None)
+
+ # For 'reboot.ping.UserMethods.Whoami'.
+ async def _Whoami(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: User.State,
+ request: google.protobuf.empty_pb2.Empty,
+ ) -> reboot.ping.ping_api_pb2.UserWhoamiResponse:
+ # We should have an asyncio task and thus context per request,
+ # let's confirm this assumption by making sure that
+ # `_state is None`.
+ assert UserServicer._state.get() is None
+ UserServicer._state.set(state)
+ try:
+ # Wrap the call to the developer's method in a `span` so that it
+ # is traced using its fully-qualified Python name.
+ instance = self._instance(context.state_id)
+ with IMPORT_reboot_aio_tracing.span(
+ state_name=f"reboot.ping.User('{context.state_id}')",
+ span_name=f"{IMPORT_reboot_aio_tracing.qualified_type_name(instance)}.Whoami()",
+ level=IMPORT_reboot_aio_tracing.TraceLevel.CUSTOMER,
+ python_specific=True,
+ ):
+
+ return UserWhoamiResponseToProto(
+ await instance.Whoami(
+ context,
+ )
+ )
+ finally:
+ UserServicer._state.set(None)
+
+ # For 'reboot.ping.UserMethods.Create'.
+ async def _Create(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext,
+ state: User.State,
+ request: google.protobuf.empty_pb2.Empty,
+ ) -> google.protobuf.empty_pb2.Empty:
+ # We should have an asyncio task and thus context per request,
+ # let's confirm this assumption by making sure that
+ # `_state is None`.
+ assert UserServicer._state.get() is None
+ UserServicer._state.set(state)
+ try:
+ # Wrap the call to the developer's method in a `span` so that it
+ # is traced using its fully-qualified Python name.
+ instance = self._instance(context.state_id)
+ with IMPORT_reboot_aio_tracing.span(
+ state_name=f"reboot.ping.User('{context.state_id}')",
+ span_name=f"{IMPORT_reboot_aio_tracing.qualified_type_name(instance)}.Create()",
+ level=IMPORT_reboot_aio_tracing.TraceLevel.CUSTOMER,
+ python_specific=True,
+ ):
+
+ return UserCreateResponseToProto(
+ await instance.Create(
+ context,
+ )
+ )
+ finally:
+ UserServicer._state.set(None)
+
+
+class CounterBaseServicer(IMPORT_reboot_aio_servicers.Servicer):
+ Authorizer: IMPORT_typing.TypeAlias = CounterAuthorizer
+ # Node-backed servicers attach a JS bridge object at runtime. We declare it
+ # on the base class so mypy accepts accesses when a servicer is typed as
+ # `CounterBaseServicer` (e.g. in classmethod workflows).
+ _js_servicer_reference: IMPORT_typing.Any
+
+ __servicer__: IMPORT_contextvars.ContextVar[IMPORT_typing.Optional[CounterBaseServicer]] = IMPORT_contextvars.ContextVar(
+ 'Provides access to a servicer in the current asyncio context. '
+ 'We need that to be able to do inline writes and reads inside '
+ 'a workflow',
+ default=None,
+ )
+
+ __service_names__ = [
+ IMPORT_reboot_aio_types.ServiceName('reboot.ping.CounterMethods'),
+ ]
+ __state_type_name__ = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter')
+ __state_type__ = reboot.ping.ping_api_pb2.Counter
+ __file_descriptor__ = reboot.ping.ping_api_pb2.DESCRIPTOR
+
+ def __init__(self):
+ super().__init__()
+ # NOTE: need to hold on to the middleware so we can do inline
+ # writes (see 'self.write(...)').
+ #
+ # Because '_middleware' is not really private this does mean
+ # users may do possibly dangerous things, but this is no more
+ # likely given they could have already overridden
+ # 'create_middleware()'.
+ self._middleware: IMPORT_typing.Optional[CounterServicerMiddleware] = None
+
+ def create_middleware(
+ self,
+ *,
+ application_id: IMPORT_reboot_aio_types.ApplicationId,
+ server_id: IMPORT_reboot_aio_types.ServerId,
+ state_manager: IMPORT_reboot_aio_state_managers.StateManager,
+ placement_client: IMPORT_reboot_aio_placement.PlacementClient,
+ channel_manager: IMPORT_reboot_aio_internals_channel_manager._ChannelManager,
+ tasks_cache: IMPORT_reboot_aio_internals_tasks_cache.TasksCache,
+ token_verifier: IMPORT_typing.Optional[IMPORT_reboot_aio_auth_token_verifiers.TokenVerifier],
+ effect_validation: IMPORT_reboot_aio_contexts.EffectValidation,
+ ready: IMPORT_asyncio.Event,
+ ) -> CounterServicerMiddleware:
+ self._middleware = CounterServicerMiddleware(
+ servicer=self,
+ application_id=application_id,
+ server_id=server_id,
+ state_manager=state_manager,
+ placement_client=placement_client,
+ channel_manager=channel_manager,
+ tasks_cache=tasks_cache,
+ token_verifier=token_verifier,
+ effect_validation=effect_validation,
+ ready=ready,
+ )
+ return self._middleware
+
+ def authorizer(self) -> IMPORT_typing.Optional[IMPORT_reboot_aio_auth_authorizers.Authorizer | IMPORT_reboot_aio_auth_authorizers.AuthorizerRule]:
+ return None
+
+ def token_verifier(self) -> IMPORT_typing.Optional[IMPORT_reboot_aio_auth_token_verifiers.TokenVerifier]:
+ return None
+
+ _is_auto_construct = False
+
+
+ @staticmethod
+ def _mcp_tool_names() -> list[str]:
+ """Return the MCP tool names this servicer registers."""
+ return [
+ "counter_increment",
+ "counter_value",
+ "counter_show_clicker",
+ ]
+
+ @staticmethod
+ def _add_mcp(
+ mcp: IMPORT_mcp_server_fastmcp.FastMCP,
+ auto_construct_state_type_full_names: list[IMPORT_reboot_aio_types.StateTypeName],
+ ) -> None:
+ """Register MCP tools and resources for Counter."""
+
+ # Tool for 'reboot.ping.CounterMethods.Increment'.
+ @mcp.tool(
+ name="counter_increment",
+ title="Increment",
+ description="Increment the counter.",
+ )
+ async def counter_increment_tool(
+ ctx: IMPORT_mcp_server_fastmcp.Context,
+ counter_id: str,
+ ):
+ reboot_context = IMPORT_reboot_mcp_context.get_reboot_context(ctx)
+ ref = Counter.ref(counter_id)
+ return await ref.increment(reboot_context)
+
+
+ # Tool for 'reboot.ping.CounterMethods.Value'.
+ @mcp.tool(
+ name="counter_value",
+ title="Value",
+ description="Get the current counter value.",
+ )
+ async def counter_value_tool(
+ ctx: IMPORT_mcp_server_fastmcp.Context,
+ counter_id: str,
+ ):
+ reboot_context = IMPORT_reboot_mcp_context.get_reboot_context(ctx)
+ ref = Counter.ref(counter_id)
+ return await ref.value(reboot_context)
+
+
+ # Map UI method names to web app paths.
+ _ui_app_paths: dict[str, str] = {
+ "show_clicker": "web/ui/clicker",
+ }
+
+ # Optional artifact path overrides (only populated
+ # when `artifact_path` is explicitly set).
+ _ui_artifact_paths: dict[str, str] = {
+ }
+
+ # Cache-bust tokens: content hash of each UI's built
+ # `index.html`, or a per-process random fallback if the
+ # artifact is missing. Embedded in the tool's
+ # `_meta.ui.resourceUri` below and in the `@mcp.resource`
+ # URI template so hosts that aggressively cache MCP
+ # resource reads (e.g., ChatGPT) see a fresh URI after
+ # each rebuild. Computed once at servicer setup since
+ # `--config=dist` restarts the process on deploy. The
+ # helper falls back to a per-process token (and logs)
+ # rather than raising if the project root can't be
+ # located — deploys with unusual filesystem layouts
+ # still start cleanly.
+ _ui_cache_busts: dict[str, str] = (
+ IMPORT_reboot_mcp_ui.compute_ui_cache_busts(
+ __file__,
+ _ui_app_paths,
+ _ui_artifact_paths,
+ )
+ )
+
+ # Register each UI tool's recompute inputs in the
+ # server-local registry. The patched `FastMCP.list_tools`
+ # in `reboot.mcp.factories` reads this on every
+ # `tools/list` call and rewrites `_meta.ui.resourceUri`
+ # with a freshly-hashed token so clients that re-list on
+ # new sessions (Claude, MCPJam) pick up dist-file changes
+ # without a server restart. Kept server-local — never
+ # rides on the wire in `_meta`.
+ IMPORT_reboot_mcp_ui.register_ui_tool_for_cache_bust(
+ tool_name="counter_show_clicker",
+ caller_file=__file__,
+ ui_path=_ui_app_paths["show_clicker"],
+ ui_name="show_clicker",
+ artifact_path=_ui_artifact_paths.get("show_clicker"),
+ uri_prefix=(
+ "ui://counter/"
+ "show_clicker/"
+ ),
+ )
+
+ # UI tools — allow AI to trigger loading of UIs.
+ @mcp.tool(
+ name="counter_show_clicker",
+ title="Show Counter Clicker",
+ description="Interactive clicker for the Counter.",
+ meta={
+ "ui": {
+ "resourceUri": (
+ "ui://counter/show_clicker/"
+ + _ui_cache_busts["show_clicker"]
+ ),
+ },
+ },
+ )
+ async def counter_show_clicker_tool(
+ ctx: IMPORT_mcp_server_fastmcp.Context,
+ counter_id: str,
+ *,
+ request: Counter.ShowClickerProps,
+ ) -> dict:
+ _ids: dict[IMPORT_reboot_aio_types.StateTypeName, str | None] = {
+ IMPORT_reboot_aio_types.StateTypeName("reboot.ping.Counter"): counter_id,
+ }
+ # Include all auto-construct IDs.
+ for _auto_construct_full_name in auto_construct_state_type_full_names:
+ _ids[_auto_construct_full_name] = (
+ IMPORT_reboot_mcp_context.get_mcp_user_id(ctx))
+ # Echo the validated request under `request` so
+ # `McpConnector.ontoolresult` carries it across to the
+ # React side without colliding with framework-reserved
+ # keys (`ids`, `bearer_token`). `McpConnector` injects
+ # these fields as props onto the single UI component via
+ # `React.cloneElement` (reading them from
+ # `toolData.request`), so customers write a plain
+ # ` ` and don't read `useMcpToolData()?.request`
+ # themselves. The payload is camelCased to match the
+ # protobuf-es generated TypeScript field names —
+ # Pydantic's default `model_dump` returns snake_case, but
+ # the customer's generated `...Props` interface (the type
+ # parameter of their App's `FC<...>`) is camelCase, so a
+ # field named `primary_color` here must land as
+ # `primaryColor` in the prop.
+ result: dict = {
+ "ids": _ids,
+ "request": IMPORT_reboot_mcp_ui.camelize_request_payload(
+ request.model_dump(mode="json")
+ ),
+ }
+
+ # Invariant: the MCP server has a bearer token for this tool
+ # call because opening a UI requires an authenticated
+ # session. This is the only place where the token can be
+ # passed to the iframe, since the iframe has no other access
+ # to the MCP session credentials. The token is read by
+ # `McpConnector` from `ontoolresult` and forwarded to
+ # `RebootClientProvider` for all Reboot calls inside the
+ # app. This tool is also invoked by `McpConnector` to
+ # refresh an expired token, so the field must always be
+ # present when a token is available.
+ reboot_context = IMPORT_reboot_mcp_context.get_reboot_context(ctx)
+ if reboot_context is not None and reboot_context.bearer_token is not None:
+ result["bearer_token"] = reboot_context.bearer_token
+ return result
+
+ def _get_reboot_url_from_request(
+ ctx: IMPORT_mcp_server_fastmcp.Context,
+ ) -> str:
+ """Extract Reboot URL from request headers."""
+ request = ctx.request_context.request
+ if request is None:
+ raise RuntimeError(
+ "No HTTP request in MCP context"
+ )
+ host = (
+ request.headers.get("x-forwarded-host")
+ or request.headers.get("host")
+ )
+ if not host:
+ raise RuntimeError(
+ "No host header in request"
+ )
+ scheme = (
+ request.headers.get("x-forwarded-proto")
+ or ("https" if request.url.scheme == "https"
+ else "http")
+ )
+ return f"{scheme}://{host}"
+
+ @mcp.resource(
+ "ui://counter/{ui}/{cache_bust}",
+ name="counter-ui",
+ title="Counter UI",
+ description="Counter UIs.",
+ mime_type="text/html;profile=mcp-app",
+ )
+ def counter_ui_resource(
+ ctx: IMPORT_mcp_server_fastmcp.Context,
+ ui: str,
+ cache_bust: str,
+ ) -> str:
+ # `cache_bust` comes from the URI's trailing
+ # segment: the content-hash token that `list_tools`
+ # wrote into the tool's `_meta.ui.resourceUri`. We
+ # pass it to `ui_html` so the per-process HTML
+ # cache keys on it — same token = cache hit,
+ # changed token (because the dist file changed) =
+ # cache miss = fresh read.
+ reboot_url = _get_reboot_url_from_request(ctx)
+ if ui not in _ui_app_paths:
+ raise ValueError(f"Unknown UI: {ui}")
+ return IMPORT_reboot_mcp_ui.ui_html(
+ _ui_app_paths[ui],
+ reboot_url,
+ ui_name=ui,
+ artifact_path=_ui_artifact_paths.get(ui),
+ cache_bust=cache_bust,
+ )
+
+ pass # End of _add_mcp.
+
+
+ def ref(
+ self,
+ *,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ) -> Counter.WeakReference[Counter.WeakReference._WriterSchedule]:
+ context = IMPORT_reboot_aio_contexts.Context.get()
+
+ if context is None:
+ raise RuntimeError(
+ 'Missing asyncio context variable `context`; '
+ 'are you using this class without Reboot?'
+ )
+
+ return Counter.WeakReference(
+ # TODO(https://github.com/reboot-dev/mono/issues/3226): add support for calling other applications.
+ # For now this always stays within the application that creates the context.
+ application_id=None,
+ state_id=context._state_ref.id,
+ schedule_type=Counter.WeakReference._WriterSchedule,
+ # If the user didn't specify a bearer token we may still end up using the app-internal bearer token,
+ # but that's decided at the time of the call.
+ bearer_token=bearer_token,
+ servicer=self,
+ )
+
+ class Effects(IMPORT_reboot_aio_state_managers.Effects):
+ def __init__(
+ self,
+ *,
+ state: reboot.ping.ping_api_pb2.Counter,
+ response: IMPORT_typing.Optional[IMPORT_google_protobuf_message.Message] = None,
+ tasks: IMPORT_typing.Optional[list[IMPORT_reboot_aio_tasks.TaskEffect]] = None,
+ _colocated_upserts: IMPORT_typing.Optional[list[tuple[str, IMPORT_typing.Optional[bytes]]]] = None,
+ ):
+ IMPORT_reboot_aio_types.assert_type(state, [reboot.ping.ping_api_pb2.Counter])
+
+ super().__init__(state=state, response=response, tasks=tasks, _colocated_upserts=_colocated_upserts)
+
+ # For 'reboot.ping.CounterMethods.Create'.
+ class CreateEffects(Effects):
+ def __init__(
+ self,
+ *,
+ state: reboot.ping.ping_api_pb2.Counter,
+ response: google.protobuf.empty_pb2.Empty,
+ tasks: IMPORT_typing.Optional[list[IMPORT_reboot_aio_tasks.TaskEffect]] = None,
+ _colocated_upserts: IMPORT_typing.Optional[list[tuple[str, IMPORT_typing.Optional[bytes]]]] = None,
+ ):
+ IMPORT_reboot_aio_types.assert_type(state, [reboot.ping.ping_api_pb2.Counter])
+ IMPORT_reboot_aio_types.assert_type(response, [google.protobuf.empty_pb2.Empty])
+
+ super().__init__(state=state, response=response, tasks=tasks, _colocated_upserts=_colocated_upserts)
+
+
+ # For 'reboot.ping.CounterMethods.Increment'.
+ class IncrementEffects(Effects):
+ def __init__(
+ self,
+ *,
+ state: reboot.ping.ping_api_pb2.Counter,
+ response: reboot.ping.ping_api_pb2.CounterIncrementResponse,
+ tasks: IMPORT_typing.Optional[list[IMPORT_reboot_aio_tasks.TaskEffect]] = None,
+ _colocated_upserts: IMPORT_typing.Optional[list[tuple[str, IMPORT_typing.Optional[bytes]]]] = None,
+ ):
+ IMPORT_reboot_aio_types.assert_type(state, [reboot.ping.ping_api_pb2.Counter])
+ IMPORT_reboot_aio_types.assert_type(response, [reboot.ping.ping_api_pb2.CounterIncrementResponse])
+
+ super().__init__(state=state, response=response, tasks=tasks, _colocated_upserts=_colocated_upserts)
+
+
+
+
+
+ InlineWriterCallableResult = IMPORT_typing.TypeVar('InlineWriterCallableResult', covariant=True)
+
+ class InlineWriterCallable(IMPORT_typing.Protocol[InlineWriterCallableResult]):
+ async def __call__(
+ self,
+ state: reboot.ping.ping_api_pb2.Counter
+ ) -> CounterBaseServicer.InlineWriterCallableResult:
+ ...
+
+ class WorkflowState:
+
+ def __init__(
+ self,
+ servicer,
+ ):
+ self._servicer = servicer
+
+ async def read(
+ self, context: IMPORT_reboot_aio_contexts.WorkflowContext
+ ) -> Counter.State:
+ """Read the current state within a workflow."""
+ return await (
+ self.always() if context.within_until()
+ else (
+ self.per_iteration() if context.within_loop()
+ else self.per_workflow()
+ )
+ ).read(context)
+
+ async def write(
+ self,
+ idempotency_alias: str,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ writer: CounterBaseServicer.InlineWriterCallable[CounterBaseServicer.InlineWriterCallableResult],
+ __options__: IMPORT_reboot_aio_call.Options = IMPORT_reboot_aio_call.Options(),
+ *,
+ type: IMPORT_reboot_aio_workflows.Type | IMPORT_reboot_aio_workflows._Unset = IMPORT_reboot_aio_workflows._UNSET,
+ ) -> CounterBaseServicer.InlineWriterCallableResult:
+ """Perform an "inline write" within a workflow.
+
+ `type=` is optional: when omitted, `writer`'s return
+ annotation is used (falling back to `type(None)` if
+ there is none).
+ """
+ type_t, _ = IMPORT_reboot_aio_workflows._resolve_callable_return_type(
+ writer, type,
+ )
+ return await (
+ self.per_iteration(idempotency_alias) if context.within_loop()
+ else self.per_workflow(idempotency_alias)
+ ).write(
+ context, writer, __options__, type=type_t
+ )
+
+ class _Idempotently:
+
+ def __init__(
+ self,
+ *,
+ servicer: CounterBaseServicer,
+ alias: IMPORT_typing.Optional[str],
+ how: IMPORT_reboot_aio_workflows.How,
+ ):
+ self._servicer = servicer
+ self._alias = alias
+ self._how = how
+
+ async def read(
+ self, context: IMPORT_reboot_aio_contexts.WorkflowContext
+ ) -> Counter.State:
+ """Read the current state within a workflow."""
+ return await self._read(
+ self._servicer,
+ context.idempotency(
+ how=IMPORT_reboot_aio_idempotency.ALWAYS,
+ ) if self._how == IMPORT_reboot_aio_workflows.ALWAYS else context.idempotency(
+ alias=self._alias,
+ how=self._how,
+ ),
+ context,
+ )
+
+ @staticmethod
+ async def _read(
+ servicer: CounterBaseServicer,
+ idempotency: IMPORT_reboot_aio_idempotency.Idempotency,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ ) -> Counter.State:
+ """Read the current state within a workflow."""
+ IMPORT_reboot_aio_types.assert_type(context, [IMPORT_reboot_aio_contexts.WorkflowContext])
+
+ if servicer._middleware is None:
+ raise RuntimeError(
+ 'Reboot middleware was not created; '
+ 'are you using this class without Reboot?'
+ )
+
+ async def read() -> reboot.ping.ping_api_pb2.Counter:
+ assert servicer._middleware is not None
+ return await servicer._middleware._state_manager.read(
+ context, servicer.__state_type__
+ )
+
+ if idempotency.always:
+ return CounterFromProto(await read())
+
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter')
+
+ # Use the idempotency manager to make sure that this
+ # reader is being called following the rules.
+ with context.idempotently(
+ state_type_name=state_type_name,
+ state_ref=context._state_ref,
+ # Not calling a method so `service_name`,
+ # `method`, `request`, etc are irrelevant.
+ service_name=None,
+ method=None,
+ mutation=False,
+ request=None,
+ metadata=None,
+ idempotency=idempotency,
+ # Only need to pass `aborted_type` for mutations.
+ aborted_type=None,
+ ) as idempotency_key:
+ assert idempotency_key is not None
+ protobuf_state = await IMPORT_reboot_aio_workflows.at_least_once(
+ (
+ # TODO: for easier debugging include the
+ # original alias (or generated alias in
+ # the case of `.per_iteration()` w/o an
+ # alias) instead of just
+ # `idempotency_key`.
+ f"inline reader of '{ state_type_name }' ({str(idempotency_key)})",
+ # NOTE: we want this to be `PER_WORKFLOW`
+ # because any per iteration concerns
+ # should have already been taken care of
+ # by caller using `.per_iteration()`.
+ IMPORT_reboot_aio_workflows.PER_WORKFLOW
+ ),
+ context,
+ read,
+ type=reboot.ping.ping_api_pb2.Counter,
+ )
+
+ return CounterFromProto(protobuf_state)
+
+ async def write(
+ self,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ writer: CounterBaseServicer.InlineWriterCallable[CounterBaseServicer.InlineWriterCallableResult],
+ __options__: IMPORT_reboot_aio_call.Options = IMPORT_reboot_aio_call.Options(),
+ *,
+ type: IMPORT_reboot_aio_workflows.Type | IMPORT_reboot_aio_workflows._Unset = IMPORT_reboot_aio_workflows._UNSET,
+ check_type: bool = True,
+ ) -> CounterBaseServicer.InlineWriterCallableResult:
+ # `type=` is optional: when omitted, `writer`'s
+ # return annotation is used (falling back to
+ # `type(None)` when there is none).
+ type_t, _ = IMPORT_reboot_aio_workflows._resolve_callable_return_type(
+ writer, type,
+ )
+ return await self._write(
+ context,
+ writer,
+ __options__,
+ type_result=type_t,
+ check_type=check_type,
+ )
+
+ async def _write(
+ self,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ writer: CounterBaseServicer.InlineWriterCallable[CounterBaseServicer.InlineWriterCallableResult],
+ __options__: IMPORT_reboot_aio_call.Options = IMPORT_reboot_aio_call.Options(),
+ *,
+ type_result: IMPORT_reboot_aio_workflows.TypeT,
+ check_type: bool,
+ ) -> CounterBaseServicer.InlineWriterCallableResult:
+ unidempotently = self._how == IMPORT_reboot_aio_workflows.ALWAYS
+ idempotency = (
+ context.idempotency(
+ how=IMPORT_reboot_aio_idempotency.ALWAYS,
+ ) if unidempotently else context.idempotency(
+ alias=self._alias,
+ how=self._how,
+ )
+ )
+
+ return await self._write_validating_effects(
+ self._servicer,
+ idempotency,
+ context,
+ writer,
+ __options__,
+ type_result=type_result,
+ check_type=check_type,
+ unidempotently=unidempotently,
+ checkpoint=context.checkpoint(),
+ )
+
+ @staticmethod
+ @IMPORT_reboot_aio_internals_middleware.maybe_run_function_twice_to_validate_effects
+ async def _write_validating_effects(
+ validating_effects: bool,
+ servicer: CounterBaseServicer,
+ idempotency: IMPORT_reboot_aio_idempotency.Idempotency,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ writer: CounterBaseServicer.InlineWriterCallable[CounterBaseServicer.InlineWriterCallableResult],
+ __options__: IMPORT_reboot_aio_call.Options = IMPORT_reboot_aio_call.Options(),
+ *,
+ type_result: IMPORT_reboot_aio_workflows.TypeT,
+ check_type: bool,
+ unidempotently: bool,
+ checkpoint: IMPORT_reboot_aio_idempotency.Checkpoint,
+ ) -> CounterBaseServicer.InlineWriterCallableResult:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+
+ IMPORT_reboot_aio_types.assert_type(context, [IMPORT_reboot_aio_contexts.WorkflowContext])
+
+ if servicer._middleware is None:
+ raise RuntimeError(
+ 'Reboot middleware was not created; '
+ 'are you using this class without Reboot?'
+ )
+
+ metadata: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+
+ if __options__ is not None:
+ if __options__.metadata is not None:
+ metadata = __options__.metadata
+
+ if metadata is None:
+ metadata = ()
+
+ headers = IMPORT_reboot_aio_headers.Headers(
+ application_id=context.application_id,
+ state_ref=context._state_ref,
+ caller_id=IMPORT_reboot_aio_caller_id.CallerID(
+ application_id=context.application_id,
+ ),
+ workflow_id=context.workflow_id,
+ # Only set `workflow_iteration` for
+ # `per_iteration` calls so that the
+ # mutation is stored/recovered with
+ # iteration scoping.
+ workflow_iteration=(
+ context.workflow_iteration
+ if idempotency.per_iteration else None
+ ),
+ )
+
+ metadata += headers.to_grpc_metadata()
+
+ idempotency_key: IMPORT_typing.Optional[IMPORT_uuid.UUID]
+ with context.idempotently(
+ state_type_name=IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ state_ref=context._state_ref,
+ service_name=None, # Indicates an inline writer.
+ method=None, # Indicates an inline writer.
+ mutation=True,
+ request=None, # Indicates an inline writer.
+ metadata=metadata,
+ idempotency=idempotency,
+ aborted_type=None, # Indicates an inline writer.
+ ) as idempotency_key:
+
+ if any(t[0] == IMPORT_reboot_aio_headers.IDEMPOTENCY_KEY_HEADER for t in metadata):
+ raise ValueError(
+ f"Do not set '{IMPORT_reboot_aio_headers.IDEMPOTENCY_KEY_HEADER}' metadata yourself"
+ )
+
+ if idempotency_key is not None:
+ metadata += (
+ (IMPORT_reboot_aio_headers.IDEMPOTENCY_KEY_HEADER, str(idempotency_key)),
+ )
+
+ with servicer._middleware.use_context(
+ headers=IMPORT_reboot_aio_headers.Headers.from_grpc_metadata(metadata),
+ state_type_name = IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ method='inline writer',
+ context_type=IMPORT_reboot_aio_contexts.WriterContext,
+ ) as writer_context:
+ # Check if we already have performed this mutation!
+ #
+ # We do this _before_ calling 'transactionally()' because
+ # if this call is for a transaction method _and_ we've
+ # already performed the transaction then we don't want to
+ # become a transaction participant (again) we just want to
+ # return the transaction's response.
+ idempotent_mutation = (
+ await servicer._middleware._state_manager.check_for_idempotent_mutation(
+ writer_context
+ )
+ )
+
+ if idempotent_mutation is not None:
+ assert len(idempotent_mutation.response) != 0
+ response = IMPORT_google_protobuf_wrappers_pb2.BytesValue()
+ response.ParseFromString(idempotent_mutation.response)
+ result: CounterBaseServicer.InlineWriterCallableResult = IMPORT_pickle.loads(response.value)
+
+ if check_type and not isinstance(result, IMPORT_reboot_aio_workflows._isinstance_type(type_result)):
+ raise TypeError(
+ f"Stored result of type '{type(result).__name__}' from 'writer' "
+ f"is not of expected type '{IMPORT_reboot_aio_workflows._format_type(type_result)}'; have you changed "
+ "the 'type' that you expect after having stored a result?"
+ )
+
+ return result
+
+ async with servicer._middleware._state_manager.transactionally(
+ writer_context,
+ servicer._middleware.tasks_dispatcher,
+ aborted_type=None,
+ ) as transaction:
+ async with servicer._middleware._state_manager.writer(
+ writer_context,
+ servicer.__state_type__,
+ servicer._middleware.tasks_dispatcher,
+ # TODO: Decide if we want to do any kind of authorization for inline
+ # writers otherwise passing `None` here is fine.
+ authorize=None,
+ transaction=transaction,
+ ) as (protobuf_state, state_manager_writer):
+ # Serialize the state so we can see if it changed.
+ serialized_state = protobuf_state.SerializeToString(
+ deterministic=True,
+ )
+
+ typed_state = CounterFromProto(protobuf_state)
+
+ result = await writer(state=typed_state)
+
+ CounterToProto(typed_state, protobuf_state)
+
+ if check_type and not isinstance(result, IMPORT_reboot_aio_workflows._isinstance_type(type_result)):
+ raise TypeError(
+ f"Result of type '{type(result).__name__}' from 'writer' is "
+ f"not of expected type '{IMPORT_reboot_aio_workflows._format_type(type_result)}'; "
+ "did you specify an incorrect 'type'?"
+ )
+
+ task: IMPORT_typing.Optional[IMPORT_reboot_aio_tasks.TaskEffect] = context.task
+
+ assert task is not None, (
+ "Should always have a task when running a `workflow`"
+ )
+
+ method_name = f"Counter.{task.method_name} inline writer"
+
+ if idempotency.alias is not None:
+ method_name += " with idempotency alias '" + idempotency.alias + "'"
+ elif idempotency.key is not None:
+ method_name += " with idempotency key=" + str(idempotency.key)
+
+ servicer._middleware.maybe_raise_effect_validation_retry(
+ logger=logger,
+ idempotency_manager=context,
+ method_name=method_name,
+ validating_effects=validating_effects,
+ context=context,
+ checkpoint=checkpoint,
+ )
+
+ # We don't pass the context to the
+ # writer, so we don't expect there to
+ # be any scheduled tasks!
+ assert len(context._tasks) == 0
+
+ effects = IMPORT_reboot_aio_state_managers.Effects(
+ state=(
+ # Pass `None` if the state hasn't changed!
+ protobuf_state if serialized_state != protobuf_state.SerializeToString(
+ deterministic=True,
+ )
+ else None
+ ),
+ response=IMPORT_google_protobuf_wrappers_pb2.BytesValue(
+ value=IMPORT_pickle.dumps(result)
+ ),
+ )
+
+ await state_manager_writer.complete(effects)
+
+ return result
+
+ def per_workflow(self, alias: IMPORT_typing.Optional[str] = None):
+ return CounterBaseServicer.WorkflowState._Idempotently(
+ servicer=self._servicer,
+ alias=alias,
+ how=IMPORT_reboot_aio_workflows.PER_WORKFLOW,
+ )
+
+ def per_iteration(self, alias: IMPORT_typing.Optional[str] = None):
+ return CounterBaseServicer.WorkflowState._Idempotently(
+ servicer=self._servicer,
+ alias=alias,
+ how=IMPORT_reboot_aio_workflows.PER_ITERATION,
+ )
+
+ class _Always:
+ """Helper class for providing better types for `write` that don't
+ require passing `type` or `check_type`."""
+
+ def __init__(
+ self,
+ *,
+ servicer: CounterBaseServicer,
+ ):
+ self._servicer = servicer
+
+ async def read(
+ self, context: IMPORT_reboot_aio_contexts.WorkflowContext
+ ) -> Counter.State:
+ return await CounterBaseServicer.WorkflowState._Idempotently(
+ servicer=self._servicer,
+ alias=None,
+ how=IMPORT_reboot_aio_workflows.ALWAYS,
+ ).read(context)
+
+ async def write(
+ self,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ writer: CounterBaseServicer.InlineWriterCallable[CounterBaseServicer.InlineWriterCallableResult],
+ __options__: IMPORT_reboot_aio_call.Options = IMPORT_reboot_aio_call.Options(),
+ ) -> CounterBaseServicer.InlineWriterCallableResult:
+ return await CounterBaseServicer.WorkflowState._Idempotently(
+ servicer=self._servicer,
+ alias=None,
+ how=IMPORT_reboot_aio_workflows.ALWAYS,
+ )._write(
+ context,
+ writer,
+ __options__,
+ type_result=type(None),
+ check_type=False,
+ )
+
+ def always(self):
+ return CounterBaseServicer.WorkflowState._Always(
+ servicer=self._servicer,
+ )
+
+ # For 'reboot.ping.CounterMethods.Create'.
+ @IMPORT_abc_abstractmethod
+ async def _Create(
+ self,
+ context: IMPORT_reboot_aio_contexts.WriterContext,
+ state: Counter.State,
+ request: reboot.ping.ping_api_pb2.CounterCreateRequest,
+ ) -> google.protobuf.empty_pb2.Empty:
+ raise NotImplementedError
+
+ # For 'reboot.ping.CounterMethods.Increment'.
+ @IMPORT_abc_abstractmethod
+ async def _Increment(
+ self,
+ context: IMPORT_reboot_aio_contexts.WriterContext,
+ state: Counter.State,
+ request: google.protobuf.empty_pb2.Empty,
+ ) -> reboot.ping.ping_api_pb2.CounterIncrementResponse:
+ raise NotImplementedError
+
+ # For 'reboot.ping.CounterMethods.Value'.
+ @IMPORT_abc_abstractmethod
+ async def _Value(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: Counter.State,
+ request: google.protobuf.empty_pb2.Empty,
+ ) -> reboot.ping.ping_api_pb2.CounterValueResponse:
+ raise NotImplementedError
+
+ # For 'reboot.ping.CounterMethods.Description'.
+ @IMPORT_abc_abstractmethod
+ async def _Description(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: Counter.State,
+ request: google.protobuf.empty_pb2.Empty,
+ ) -> reboot.ping.ping_api_pb2.CounterDescriptionResponse:
+ raise NotImplementedError
+
+
+
+class CounterSingletonServicer(CounterBaseServicer):
+
+ @property
+ def state(self):
+ return CounterBaseServicer.WorkflowState(
+ servicer=self
+ )
+
+ # For 'reboot.ping.CounterMethods.Create'.
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that new code that
+ # doesn't implement it continues to work.
+ # TODO: make it abstractmethod when renaming is done.
+ async def Create(
+ self,
+ context: IMPORT_reboot_aio_contexts.WriterContext,
+ state: Counter.State,
+ request: Counter.CreateRequest,
+ ) -> None:
+ # During the migration from 'PascalCase' to 'snake_case' method
+ # naming in Python servicers, we call the 'snake_case' version
+ # by default, so new names will do the correct thing making the
+ # code to be backwards compatible for some time and if a servicer
+ # overrides the 'PascalCase' version - it will override that
+ # method and will just work.
+ return await self.create(
+ context,
+ state,
+ request,
+ )
+
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that existing code that
+ # doesn't implement it continues to work.
+ async def create(
+ self,
+ context: IMPORT_reboot_aio_contexts.WriterContext,
+ state: reboot.ping.ping_api_pb2.Counter,
+ request: Counter.CreateRequest,
+ ) -> None:
+ raise NotImplementedError
+
+ # For 'reboot.ping.CounterMethods.Increment'.
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that new code that
+ # doesn't implement it continues to work.
+ # TODO: make it abstractmethod when renaming is done.
+ async def Increment(
+ self,
+ context: IMPORT_reboot_aio_contexts.WriterContext,
+ state: Counter.State,
+ ) -> Counter.IncrementResponse:
+ # During the migration from 'PascalCase' to 'snake_case' method
+ # naming in Python servicers, we call the 'snake_case' version
+ # by default, so new names will do the correct thing making the
+ # code to be backwards compatible for some time and if a servicer
+ # overrides the 'PascalCase' version - it will override that
+ # method and will just work.
+ return await self.increment(
+ context,
+ state,
+ )
+
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that existing code that
+ # doesn't implement it continues to work.
+ async def increment(
+ self,
+ context: IMPORT_reboot_aio_contexts.WriterContext,
+ state: reboot.ping.ping_api_pb2.Counter,
+ ) -> Counter.IncrementResponse:
+ raise NotImplementedError
+
+ # For 'reboot.ping.CounterMethods.Value'.
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that new code that
+ # doesn't implement it continues to work.
+ # TODO: make it abstractmethod when renaming is done.
+ async def Value(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: Counter.State,
+ ) -> Counter.ValueResponse:
+ # During the migration from 'PascalCase' to 'snake_case' method
+ # naming in Python servicers, we call the 'snake_case' version
+ # by default, so new names will do the correct thing making the
+ # code to be backwards compatible for some time and if a servicer
+ # overrides the 'PascalCase' version - it will override that
+ # method and will just work.
+ return await self.value(
+ context,
+ state,
+ )
+
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that existing code that
+ # doesn't implement it continues to work.
+ # TODO: make it abstractmethod when renaming is done.
+ async def value(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: Counter.State,
+ ) -> Counter.ValueResponse:
+ raise NotImplementedError
+
+ # For 'reboot.ping.CounterMethods.Description'.
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that new code that
+ # doesn't implement it continues to work.
+ # TODO: make it abstractmethod when renaming is done.
+ async def Description(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: Counter.State,
+ ) -> Counter.DescriptionResponse:
+ # During the migration from 'PascalCase' to 'snake_case' method
+ # naming in Python servicers, we call the 'snake_case' version
+ # by default, so new names will do the correct thing making the
+ # code to be backwards compatible for some time and if a servicer
+ # overrides the 'PascalCase' version - it will override that
+ # method and will just work.
+ return await self.description(
+ context,
+ state,
+ )
+
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that existing code that
+ # doesn't implement it continues to work.
+ # TODO: make it abstractmethod when renaming is done.
+ async def description(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: Counter.State,
+ ) -> Counter.DescriptionResponse:
+ raise NotImplementedError
+
+
+ # For 'reboot.ping.CounterMethods.Create'.
+ async def _Create(
+ self,
+ context: IMPORT_reboot_aio_contexts.WriterContext,
+ state: Counter.State,
+ request: reboot.ping.ping_api_pb2.CounterCreateRequest,
+ ) -> google.protobuf.empty_pb2.Empty:
+ # Wrap the call to the developer's method in a `span` so that it
+ # is traced using its fully-qualified Python name.
+ with IMPORT_reboot_aio_tracing.span(
+ state_name=f"reboot.ping.Counter('{context.state_id}')",
+ span_name=f"{IMPORT_reboot_aio_tracing.qualified_type_name(self)}.Create()",
+ level=IMPORT_reboot_aio_tracing.TraceLevel.CUSTOMER,
+ python_specific=True,
+ ):
+ typed_request = CounterCreateRequestFromProto(request)
+ return CounterCreateResponseToProto(
+ await self.Create(
+ context,
+ state,
+ typed_request
+ )
+ )
+
+
+ # For 'reboot.ping.CounterMethods.Increment'.
+ async def _Increment(
+ self,
+ context: IMPORT_reboot_aio_contexts.WriterContext,
+ state: Counter.State,
+ request: google.protobuf.empty_pb2.Empty,
+ ) -> reboot.ping.ping_api_pb2.CounterIncrementResponse:
+ # Wrap the call to the developer's method in a `span` so that it
+ # is traced using its fully-qualified Python name.
+ with IMPORT_reboot_aio_tracing.span(
+ state_name=f"reboot.ping.Counter('{context.state_id}')",
+ span_name=f"{IMPORT_reboot_aio_tracing.qualified_type_name(self)}.Increment()",
+ level=IMPORT_reboot_aio_tracing.TraceLevel.CUSTOMER,
+ python_specific=True,
+ ):
+ return CounterIncrementResponseToProto(
+ await self.Increment(
+ context,
+ state,
+ )
+ )
+
+
+ # For 'reboot.ping.CounterMethods.Value'.
+ async def _Value(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: Counter.State,
+ request: google.protobuf.empty_pb2.Empty,
+ ) -> reboot.ping.ping_api_pb2.CounterValueResponse:
+ # Wrap the call to the developer's method in a `span` so that it
+ # is traced using its fully-qualified Python name.
+ with IMPORT_reboot_aio_tracing.span(
+ state_name=f"reboot.ping.Counter('{context.state_id}')",
+ span_name=f"{IMPORT_reboot_aio_tracing.qualified_type_name(self)}.Value()",
+ level=IMPORT_reboot_aio_tracing.TraceLevel.CUSTOMER,
+ python_specific=True,
+ ):
+ response = (
+ self.Value(
+ context,
+ state,
+ )
+ )
+ return CounterValueResponseToProto(await response)
+
+ # For 'reboot.ping.CounterMethods.Description'.
+ async def _Description(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: Counter.State,
+ request: google.protobuf.empty_pb2.Empty,
+ ) -> reboot.ping.ping_api_pb2.CounterDescriptionResponse:
+ # Wrap the call to the developer's method in a `span` so that it
+ # is traced using its fully-qualified Python name.
+ with IMPORT_reboot_aio_tracing.span(
+ state_name=f"reboot.ping.Counter('{context.state_id}')",
+ span_name=f"{IMPORT_reboot_aio_tracing.qualified_type_name(self)}.Description()",
+ level=IMPORT_reboot_aio_tracing.TraceLevel.CUSTOMER,
+ python_specific=True,
+ ):
+ response = (
+ self.Description(
+ context,
+ state,
+ )
+ )
+ return CounterDescriptionResponseToProto(await response)
+
+
+
+class CounterServicer(CounterBaseServicer):
+
+ _state: IMPORT_contextvars.ContextVar[
+ IMPORT_typing.Optional[Counter.State]
+ ] = IMPORT_contextvars.ContextVar(
+ 'Provides access to state for each call, i.e., there may be '
+ 'multiple readers executing concurrently but each might have '
+ 'a different `state`',
+ default=None,
+ )
+
+ # An instance of the derived class for each state.
+ _instances: dict[str, CounterServicer] = {}
+
+ def _instance(self, state_id: str):
+ instances = CounterServicer._instances
+ instance = instances.get(state_id)
+ if instance is None:
+ instance = self.__class__()
+ instance._middleware = self._middleware
+ instances[state_id] = instance
+ return instance
+
+ @property
+ def state(self) -> Counter.State:
+ state = CounterServicer._state.get()
+ if state is None:
+ raise RuntimeError(
+ "`state` property is only relevant within a `Servicer` method"
+ )
+ return state
+
+ @state.setter
+ def state(self, new_state: Counter.State):
+ state = CounterServicer._state.get()
+ if state is None:
+ raise RuntimeError(
+ "`state` property is only relevant within a `Servicer` method"
+ )
+ for field_name, field_value in new_state.model_dump().items():
+ setattr(state, field_name, field_value)
+
+ # For 'reboot.ping.CounterMethods.Create'.
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that new code that
+ # doesn't implement it continues to work.
+ # TODO: make it abstractmethod when renaming is done.
+ async def Create(
+ self,
+ context: IMPORT_reboot_aio_contexts.WriterContext,
+ request: Counter.CreateRequest,
+ ) -> None:
+ # During the migration from 'PascalCase' to 'snake_case' method
+ # naming in Python servicers, we call the 'snake_case' version
+ # by default, so new names will do the correct thing making the
+ # code to be backwards compatible for some time and if a servicer
+ # overrides the 'PascalCase' version - it will override that
+ # method and will just work.
+ return await self.create(
+ context,
+ request,
+ )
+
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that existing code that
+ # doesn't implement it continues to work.
+ async def create(
+ self,
+ context: IMPORT_reboot_aio_contexts.WriterContext,
+ request: Counter.CreateRequest,
+ ) -> None:
+ raise NotImplementedError
+
+ # For 'reboot.ping.CounterMethods.Increment'.
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that new code that
+ # doesn't implement it continues to work.
+ # TODO: make it abstractmethod when renaming is done.
+ async def Increment(
+ self,
+ context: IMPORT_reboot_aio_contexts.WriterContext,
+ ) -> Counter.IncrementResponse:
+ # During the migration from 'PascalCase' to 'snake_case' method
+ # naming in Python servicers, we call the 'snake_case' version
+ # by default, so new names will do the correct thing making the
+ # code to be backwards compatible for some time and if a servicer
+ # overrides the 'PascalCase' version - it will override that
+ # method and will just work.
+ return await self.increment(
+ context,
+ )
+
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that existing code that
+ # doesn't implement it continues to work.
+ async def increment(
+ self,
+ context: IMPORT_reboot_aio_contexts.WriterContext,
+ ) -> Counter.IncrementResponse:
+ raise NotImplementedError
+
+ # For 'reboot.ping.CounterMethods.Value'.
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that new code that
+ # doesn't implement it continues to work.
+ # TODO: make it abstractmethod when renaming is done.
+ async def Value(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ ) -> Counter.ValueResponse:
+ # During the migration from 'PascalCase' to 'snake_case' method
+ # naming in Python servicers, we call the 'snake_case' version
+ # by default, so new names will do the correct thing making the
+ # code to be backwards compatible for some time and if a servicer
+ # overrides the 'PascalCase' version - it will override that
+ # method and will just work.
+ return await self.value(
+ context,
+ )
+
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that existing code that
+ # doesn't implement it continues to work.
+ async def value(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ ) -> Counter.ValueResponse:
+ raise NotImplementedError
+
+ # For 'reboot.ping.CounterMethods.Description'.
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that new code that
+ # doesn't implement it continues to work.
+ # TODO: make it abstractmethod when renaming is done.
+ async def Description(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ ) -> Counter.DescriptionResponse:
+ # During the migration from 'PascalCase' to 'snake_case' method
+ # naming in Python servicers, we call the 'snake_case' version
+ # by default, so new names will do the correct thing making the
+ # code to be backwards compatible for some time and if a servicer
+ # overrides the 'PascalCase' version - it will override that
+ # method and will just work.
+ return await self.description(
+ context,
+ )
+
+ # To be backwards compatible during the renaming don't make this
+ # method to be 'abstractmethod', so that existing code that
+ # doesn't implement it continues to work.
+ async def description(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ ) -> Counter.DescriptionResponse:
+ raise NotImplementedError
+
+
+ # For 'reboot.ping.CounterMethods.Create'.
+ async def _Create(
+ self,
+ context: IMPORT_reboot_aio_contexts.WriterContext,
+ state: Counter.State,
+ request: reboot.ping.ping_api_pb2.CounterCreateRequest,
+ ) -> google.protobuf.empty_pb2.Empty:
+ # We should have an asyncio task and thus context per request,
+ # let's confirm this assumption by making sure that
+ # `_state is None`.
+ assert CounterServicer._state.get() is None
+ CounterServicer._state.set(state)
+ try:
+ # Wrap the call to the developer's method in a `span` so that it
+ # is traced using its fully-qualified Python name.
+ instance = self._instance(context.state_id)
+ with IMPORT_reboot_aio_tracing.span(
+ state_name=f"reboot.ping.Counter('{context.state_id}')",
+ span_name=f"{IMPORT_reboot_aio_tracing.qualified_type_name(instance)}.Create()",
+ level=IMPORT_reboot_aio_tracing.TraceLevel.CUSTOMER,
+ python_specific=True,
+ ):
+ typed_request = CounterCreateRequestFromProto(request)
+
+ return CounterCreateResponseToProto(
+ await instance.Create(
+ context,
+ typed_request,
+ )
+ )
+ finally:
+ CounterServicer._state.set(None)
+
+ # For 'reboot.ping.CounterMethods.Increment'.
+ async def _Increment(
+ self,
+ context: IMPORT_reboot_aio_contexts.WriterContext,
+ state: Counter.State,
+ request: google.protobuf.empty_pb2.Empty,
+ ) -> reboot.ping.ping_api_pb2.CounterIncrementResponse:
+ # We should have an asyncio task and thus context per request,
+ # let's confirm this assumption by making sure that
+ # `_state is None`.
+ assert CounterServicer._state.get() is None
+ CounterServicer._state.set(state)
+ try:
+ # Wrap the call to the developer's method in a `span` so that it
+ # is traced using its fully-qualified Python name.
+ instance = self._instance(context.state_id)
+ with IMPORT_reboot_aio_tracing.span(
+ state_name=f"reboot.ping.Counter('{context.state_id}')",
+ span_name=f"{IMPORT_reboot_aio_tracing.qualified_type_name(instance)}.Increment()",
+ level=IMPORT_reboot_aio_tracing.TraceLevel.CUSTOMER,
+ python_specific=True,
+ ):
+
+ return CounterIncrementResponseToProto(
+ await instance.Increment(
+ context,
+ )
+ )
+ finally:
+ CounterServicer._state.set(None)
+
+ # For 'reboot.ping.CounterMethods.Value'.
+ async def _Value(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: Counter.State,
+ request: google.protobuf.empty_pb2.Empty,
+ ) -> reboot.ping.ping_api_pb2.CounterValueResponse:
+ # We should have an asyncio task and thus context per request,
+ # let's confirm this assumption by making sure that
+ # `_state is None`.
+ assert CounterServicer._state.get() is None
+ CounterServicer._state.set(state)
+ try:
+ # Wrap the call to the developer's method in a `span` so that it
+ # is traced using its fully-qualified Python name.
+ instance = self._instance(context.state_id)
+ with IMPORT_reboot_aio_tracing.span(
+ state_name=f"reboot.ping.Counter('{context.state_id}')",
+ span_name=f"{IMPORT_reboot_aio_tracing.qualified_type_name(instance)}.Value()",
+ level=IMPORT_reboot_aio_tracing.TraceLevel.CUSTOMER,
+ python_specific=True,
+ ):
+
+ return CounterValueResponseToProto(
+ await instance.Value(
+ context,
+ )
+ )
+ finally:
+ CounterServicer._state.set(None)
+
+ # For 'reboot.ping.CounterMethods.Description'.
+ async def _Description(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext,
+ state: Counter.State,
+ request: google.protobuf.empty_pb2.Empty,
+ ) -> reboot.ping.ping_api_pb2.CounterDescriptionResponse:
+ # We should have an asyncio task and thus context per request,
+ # let's confirm this assumption by making sure that
+ # `_state is None`.
+ assert CounterServicer._state.get() is None
+ CounterServicer._state.set(state)
+ try:
+ # Wrap the call to the developer's method in a `span` so that it
+ # is traced using its fully-qualified Python name.
+ instance = self._instance(context.state_id)
+ with IMPORT_reboot_aio_tracing.span(
+ state_name=f"reboot.ping.Counter('{context.state_id}')",
+ span_name=f"{IMPORT_reboot_aio_tracing.qualified_type_name(instance)}.Description()",
+ level=IMPORT_reboot_aio_tracing.TraceLevel.CUSTOMER,
+ python_specific=True,
+ ):
+
+ return CounterDescriptionResponseToProto(
+ await instance.Description(
+ context,
+ )
+ )
+ finally:
+ CounterServicer._state.set(None)
+
+
+
+############################ Clients ############################
+# The main developer-facing entrypoints for any Reboot type. Relevant to both
+# clients and servicers (who use it to find the right servicer base types, as well
+# as often being clients themselves).
+
+# Attach an explicit time time zone to "naive" `datetime` objects. A "naive" `datetime` doesn't have a
+# time zone. Such objects are typically interpreted as representing local time, but could be confused
+# for objects representing UTC. This helper function disambiguates by explicitly attaching the local
+# time zone to `datetime` objects that don't already have an explicit time zone. If the `datetime` object
+# is already timezone-aware, we still convert it to our custom `DateTimeWithTimeZone` type.
+def ensure_has_timezone(
+ *,
+ when: IMPORT_typing.Optional[IMPORT_datetime_datetime | IMPORT_datetime_timedelta] = None,
+) -> IMPORT_typing.Optional[IMPORT_reboot_time_DateTimeWithTimeZone | IMPORT_datetime_timedelta]:
+ if isinstance(when, IMPORT_datetime_datetime):
+ return IMPORT_reboot_time_DateTimeWithTimeZone.from_datetime(when)
+ return when
+
+Ping_ScheduleTypeVar = IMPORT_typing.TypeVar('Ping_ScheduleTypeVar', 'Ping.WeakReference._Schedule', 'Ping.WeakReference._WriterSchedule')
+Ping_IdempotentlyScheduleTypeVar = IMPORT_typing.TypeVar('Ping_IdempotentlyScheduleTypeVar', 'Ping.WeakReference._Schedule', 'Ping.WeakReference._WriterSchedule')
+
+Ping_UntilCallableType = IMPORT_typing.TypeVar('Ping_UntilCallableType')
+
+class PingSingleton:
+ Servicer: IMPORT_typing.TypeAlias = PingSingletonServicer
+
+
+class Ping:
+
+
+ Servicer: IMPORT_typing.TypeAlias = PingServicer
+
+ singleton: IMPORT_typing.TypeAlias = PingSingleton
+
+ Effects: IMPORT_typing.TypeAlias = PingBaseServicer.Effects
+
+ Authorizer: IMPORT_typing.TypeAlias = PingAuthorizer
+
+ State: IMPORT_typing.TypeAlias = IMPORT_typing.cast(type, IMPORT_api.Ping.state)
+
+
+ DoPingResponse: IMPORT_typing.TypeAlias = IMPORT_typing.cast(type, IMPORT_api.Ping.methods['do_ping'].response)
+
+ DoPingPeriodicallyRequest: IMPORT_typing.TypeAlias = IMPORT_typing.cast(type, IMPORT_api.Ping.methods['do_ping_periodically'].request)
+ DoPingPeriodicallyResponse: IMPORT_typing.TypeAlias = IMPORT_typing.cast(type, IMPORT_api.Ping.methods['do_ping_periodically'].response)
+
+
+ DescribeResponse: IMPORT_typing.TypeAlias = IMPORT_typing.cast(type, IMPORT_api.Ping.methods['describe'].response)
+
+
+ NumPingsResponse: IMPORT_typing.TypeAlias = IMPORT_typing.cast(type, IMPORT_api.Ping.methods['num_pings'].response)
+
+ __state_type_name__ = IMPORT_reboot_aio_types.StateTypeName("reboot.ping.Ping")
+
+ class DoPingTask:
+ """Represents a scheduled task running for the
+ state. Note that this is not a coroutine because we are trying
+ to convey the semantics that the task is already running (or
+ will soon be).
+ """
+
+ @classmethod
+ def retrieve(
+ cls,
+ context: IMPORT_reboot_aio_contexts.Context | IMPORT_reboot_aio_external.ExternalContext,
+ *,
+ task_id: IMPORT_rbt_v1alpha1.tasks_pb2.TaskId,
+ ):
+ return cls(context, task_id=task_id)
+
+ def __init__(
+ self,
+ context: IMPORT_reboot_aio_contexts.Context | IMPORT_reboot_aio_external.ExternalContext,
+ *,
+ task_id: IMPORT_rbt_v1alpha1.tasks_pb2.TaskId,
+ ) -> None:
+ # Depending on the context type (inside or outside a Reboot application)
+ # we may or may not know the application ID. If we don't know it, then
+ # the `ExternalContext.gateway` will determine it.
+ #
+ # TODO: in the future we expect to support cross-application calls, in
+ # which case the developer may explicitly pass in an application ID
+ # here.
+ self._application_id: IMPORT_typing.Optional[IMPORT_reboot_aio_types.ApplicationId] = None
+ if isinstance(context, IMPORT_reboot_aio_contexts.Context):
+ self._application_id = context.application_id
+ self._channel_manager = context.channel_manager
+ self._task_id = task_id
+
+ @property
+ def task_id(self) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ return self._task_id
+
+ def __await__(self) -> IMPORT_typing.Generator[None, None, Ping.DoPingResponse]:
+ """Awaits for task to finish and returns its response."""
+ async def wait_for_task() -> reboot.ping.ping_api_pb2.PingDoPingResponse:
+ channel = self._channel_manager.get_channel_to_state(
+ IMPORT_reboot_aio_types.StateTypeName(self._task_id.state_type),
+ IMPORT_reboot_aio_types.StateRef(self._task_id.state_ref),
+ )
+
+ stub = IMPORT_rbt_v1alpha1.tasks_pb2_grpc.TasksStub(channel)
+
+ try:
+ call = IMPORT_reboot_aio_stubs.UnaryRetriedCall(
+ call=None, # `RetriedCall` can create the call itself.
+ stub_method=stub.Wait,
+ method_name="Wait",
+ request=IMPORT_rbt_v1alpha1.tasks_pb2.WaitRequest(task_id=self._task_id),
+ metadata=IMPORT_reboot_aio_headers.Headers(
+ state_ref=IMPORT_reboot_aio_types.StateRef(self._task_id.state_ref),
+ application_id=self._application_id,
+ ).to_grpc_metadata(),
+ aborted_type=IMPORT_reboot.aio.aborted.SystemAborted,
+ )
+
+ wait_for_task_response = await call
+ except IMPORT_reboot.aio.aborted.SystemAborted as error:
+ if error.code == IMPORT_grpc.StatusCode.NOT_FOUND:
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.UnknownTask()
+ ) from None
+
+ raise
+ else:
+ response_or_error: IMPORT_typing.Optional[IMPORT_google_protobuf_any_pb2.Any] = None
+ is_error = False
+
+ if wait_for_task_response.response_or_error.WhichOneof("response_or_error") == "response":
+ response_or_error = wait_for_task_response.response_or_error.response
+ else:
+ is_error = True
+ response_or_error = wait_for_task_response.response_or_error.error
+
+ assert response_or_error is not None
+ assert response_or_error.TypeName() != ""
+
+ response = reboot.ping.ping_api_pb2.PingDoPingResponse()
+
+ if (
+ not is_error and response_or_error.TypeName() != response.DESCRIPTOR.full_name
+ ):
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.InvalidArgument(),
+ message=
+ f"task with UUID {IMPORT_uuid.UUID(bytes=self._task_id.task_uuid)} "
+ f"has a response of type '{response_or_error.TypeName()}' "
+ "but expecting type 'reboot.ping.ping_api_pb2.PingDoPingResponse'; "
+ "are you waiting on a task of the correct method?",
+ ) from None
+
+ if is_error:
+ aborted_type = Ping.DoPingAborted
+
+ # In Reboot >= 0.40.2 we expect the error to be a `google.rpc.Status`.
+ if response_or_error.Is(IMPORT_google_rpc_status_pb2.Status.DESCRIPTOR):
+ status = IMPORT_google_rpc_status_pb2.Status()
+ response_or_error.Unpack(status)
+ raise aborted_type.from_status(status)
+
+ # In Reboot < 0.40.2 workflows throwing declared errors behaved poorly;
+ # we don't aim to emulate its behavior. Indicate that we don't know the
+ # reason for the abort.
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ )
+
+ else:
+ response_or_error.Unpack(response)
+ return PingDoPingResponseFromProto(response)
+
+ return wait_for_task().__await__()
+
+
+ class DoPingAborted(IMPORT_reboot.aio.aborted.Aborted):
+
+
+ Error = IMPORT_typing.Union[
+ IMPORT_reboot.aio.aborted.GrpcError,
+ IMPORT_reboot.aio.aborted.RebootError,
+ ]
+
+ METHOD_PROTOBUF_ERROR_TYPES: list[type[IMPORT_google_protobuf_message.Message]] = [
+ ]
+
+ PROTOBUF_ERROR_TYPES: list[type[IMPORT_google_protobuf_message.Message | IMPORT_reboot_api.Model]] = (
+ METHOD_PROTOBUF_ERROR_TYPES +
+ IMPORT_reboot.aio.aborted.GRPC_ERROR_TYPES +
+ IMPORT_reboot.aio.aborted.REBOOT_ERROR_TYPES
+ )
+
+ _error: Error
+
+ MethodProtobufError = IMPORT_typing.Union[
+ IMPORT_reboot.aio.aborted.GrpcError,
+ IMPORT_reboot.aio.aborted.RebootError,
+ ]
+ _method_protobuf_error: MethodProtobufError
+
+ _code: IMPORT_grpc.StatusCode
+ _message: IMPORT_typing.Optional[str]
+
+ def __init__(
+ self,
+ error: IMPORT_reboot.aio.aborted.GrpcError,
+ *,
+ message: IMPORT_typing.Optional[str] = None,
+ # Do not set this value when constructing in order to
+ # raise. This is only used internally when constructing
+ # from aborted calls.
+ error_types: IMPORT_typing.Sequence[type[MethodProtobufError]] = (
+ METHOD_PROTOBUF_ERROR_TYPES + IMPORT_reboot.aio.aborted.GRPC_ERROR_TYPES
+ ),
+ ):
+ super().__init__()
+
+ self._message = None
+
+ if self.is_declared_error(error):
+ # If the error is a declared error, we should always
+ # store both formats for Pydantic and Protobuf for that
+ # error, so users can access the Pydantic version via
+ # `aborted.error` and the Protobuf version will be used
+ # to send the error over the wire.
+ if isinstance(error, IMPORT_reboot.api.Model):
+ # Happens when somebody raises an aborted with a Pydantic
+ # error format, usually in the backend method
+ # implementation.
+
+ self._error = error
+
+ else:
+ # Usually happens on the client side when we are
+ # constructing an aborted from the `AioRpcError`, so
+ # the error will be in Protobuf shape, and we want
+ # to convert it back to Pydantic if possible.
+ self._method_protobuf_error = error
+
+ else:
+ # Non declared error should be well known Reboot or gRPC
+ # error, which always comes in the Protobuf format, however
+ # it might be a case when the server is using new API and
+ # client is old, so server might send a declared error from
+ # the server perspective, but client doesn't know that it
+ # is a declared error now, so we need to gracefully
+ # cover that case.
+ if isinstance(error, IMPORT_reboot.api.Model):
+ # Store both formats so users can access the Pydantic
+ # version via `aborted.error` and the Protobuf version
+ # will be used to send the error over the wire.
+ self._error = IMPORT_rbt_v1alpha1.errors_pb2.Unknown()
+ self._method_protobuf_error = IMPORT_rbt_v1alpha1.errors_pb2.Unknown()
+
+ self._message = (
+ "Received a declared error from the server, "
+ "but the client doesn't know about it; "
+ "please update the client to the latest version\n"
+ ) + (
+ f"Original error: {error.model_dump_json()}\n"
+ ) + message if message is not None else ""
+
+ # Store both formats so users can access the error via
+ # `aborted.error` and the Protobuf version
+ # will be used to send the error over the wire.
+ self._error = error
+ self._method_protobuf_error = error
+
+ IMPORT_reboot_aio_types.assert_type(self._protobuf_error, error_types)
+
+ code = self.grpc_status_code_from_error(self._protobuf_error)
+
+ if code is None:
+ # Must be a Reboot specific or declared method error.
+ code = IMPORT_grpc.StatusCode.ABORTED
+
+ self._code = code
+
+ self._message = self._message if self._message is not None else message
+
+ @property
+ def error(self) -> Error | MethodProtobufError:
+ return self._error
+
+ @property
+ def _protobuf_error(self) -> MethodProtobufError:
+ return self._method_protobuf_error
+
+ @property
+ def code(self) -> IMPORT_grpc.StatusCode:
+ return self._code
+
+ @property
+ def message(self) -> IMPORT_typing.Optional[str]:
+ return self._message
+
+ @classmethod
+ def from_status(cls, status: IMPORT_google_rpc_status_pb2.Status):
+ error = cls.error_from_google_rpc_status_details(
+ status,
+ cls.PROTOBUF_ERROR_TYPES,
+ )
+
+ message = status.message if len(status.message) > 0 else None
+
+ if error is not None:
+ return cls(error, message=message, error_types=cls.PROTOBUF_ERROR_TYPES)
+
+ error = cls.error_from_google_rpc_status_code(status)
+
+ assert error is not None
+
+ # TODO(benh): also consider getting the type names from
+ # `status.details` and including that in `message` to make
+ # debugging easier.
+
+ return cls(error, message=message)
+
+ @classmethod
+ def from_grpc_aio_rpc_error(cls, aio_rpc_error: IMPORT_grpc.aio.AioRpcError):
+ return cls(
+ cls.error_from_grpc_aio_rpc_error(aio_rpc_error),
+ message=aio_rpc_error.details(),
+ )
+
+ @classmethod
+ def is_declared_error(cls, error: IMPORT_google_protobuf_message.Message | IMPORT_reboot.api.Model) -> bool:
+ if isinstance(error, IMPORT_reboot.api.Model):
+ return type(error) in IMPORT_api.Ping.methods['do_ping'].errors
+ return False
+
+ class DoPingPeriodicallyTask:
+ """Represents a scheduled task running for the
+ state. Note that this is not a coroutine because we are trying
+ to convey the semantics that the task is already running (or
+ will soon be).
+ """
+
+ @classmethod
+ def retrieve(
+ cls,
+ context: IMPORT_reboot_aio_contexts.Context | IMPORT_reboot_aio_external.ExternalContext,
+ *,
+ task_id: IMPORT_rbt_v1alpha1.tasks_pb2.TaskId,
+ ):
+ return cls(context, task_id=task_id)
+
+ def __init__(
+ self,
+ context: IMPORT_reboot_aio_contexts.Context | IMPORT_reboot_aio_external.ExternalContext,
+ *,
+ task_id: IMPORT_rbt_v1alpha1.tasks_pb2.TaskId,
+ ) -> None:
+ # Depending on the context type (inside or outside a Reboot application)
+ # we may or may not know the application ID. If we don't know it, then
+ # the `ExternalContext.gateway` will determine it.
+ #
+ # TODO: in the future we expect to support cross-application calls, in
+ # which case the developer may explicitly pass in an application ID
+ # here.
+ self._application_id: IMPORT_typing.Optional[IMPORT_reboot_aio_types.ApplicationId] = None
+ if isinstance(context, IMPORT_reboot_aio_contexts.Context):
+ self._application_id = context.application_id
+ self._channel_manager = context.channel_manager
+ self._task_id = task_id
+
+ @property
+ def task_id(self) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ return self._task_id
+
+ def __await__(self) -> IMPORT_typing.Generator[None, None, Ping.DoPingPeriodicallyResponse]:
+ """Awaits for task to finish and returns its response."""
+ async def wait_for_task() -> reboot.ping.ping_api_pb2.PingDoPingPeriodicallyResponse:
+ channel = self._channel_manager.get_channel_to_state(
+ IMPORT_reboot_aio_types.StateTypeName(self._task_id.state_type),
+ IMPORT_reboot_aio_types.StateRef(self._task_id.state_ref),
+ )
+
+ stub = IMPORT_rbt_v1alpha1.tasks_pb2_grpc.TasksStub(channel)
+
+ try:
+ call = IMPORT_reboot_aio_stubs.UnaryRetriedCall(
+ call=None, # `RetriedCall` can create the call itself.
+ stub_method=stub.Wait,
+ method_name="Wait",
+ request=IMPORT_rbt_v1alpha1.tasks_pb2.WaitRequest(task_id=self._task_id),
+ metadata=IMPORT_reboot_aio_headers.Headers(
+ state_ref=IMPORT_reboot_aio_types.StateRef(self._task_id.state_ref),
+ application_id=self._application_id,
+ ).to_grpc_metadata(),
+ aborted_type=IMPORT_reboot.aio.aborted.SystemAborted,
+ )
+
+ wait_for_task_response = await call
+ except IMPORT_reboot.aio.aborted.SystemAborted as error:
+ if error.code == IMPORT_grpc.StatusCode.NOT_FOUND:
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.UnknownTask()
+ ) from None
+
+ raise
+ else:
+ response_or_error: IMPORT_typing.Optional[IMPORT_google_protobuf_any_pb2.Any] = None
+ is_error = False
+
+ if wait_for_task_response.response_or_error.WhichOneof("response_or_error") == "response":
+ response_or_error = wait_for_task_response.response_or_error.response
+ else:
+ is_error = True
+ response_or_error = wait_for_task_response.response_or_error.error
+
+ assert response_or_error is not None
+ assert response_or_error.TypeName() != ""
+
+ response = reboot.ping.ping_api_pb2.PingDoPingPeriodicallyResponse()
+
+ if (
+ not is_error and response_or_error.TypeName() != response.DESCRIPTOR.full_name
+ ):
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.InvalidArgument(),
+ message=
+ f"task with UUID {IMPORT_uuid.UUID(bytes=self._task_id.task_uuid)} "
+ f"has a response of type '{response_or_error.TypeName()}' "
+ "but expecting type 'reboot.ping.ping_api_pb2.PingDoPingPeriodicallyResponse'; "
+ "are you waiting on a task of the correct method?",
+ ) from None
+
+ if is_error:
+ aborted_type = Ping.DoPingPeriodicallyAborted
+
+ # In Reboot >= 0.40.2 we expect the error to be a `google.rpc.Status`.
+ if response_or_error.Is(IMPORT_google_rpc_status_pb2.Status.DESCRIPTOR):
+ status = IMPORT_google_rpc_status_pb2.Status()
+ response_or_error.Unpack(status)
+ raise aborted_type.from_status(status)
+
+ # In Reboot < 0.40.2 workflows throwing declared errors behaved poorly;
+ # we don't aim to emulate its behavior. Indicate that we don't know the
+ # reason for the abort.
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ )
+
+ else:
+ response_or_error.Unpack(response)
+ return PingDoPingPeriodicallyResponseFromProto(response)
+
+ return wait_for_task().__await__()
+
+
+ class DoPingPeriodicallyAborted(IMPORT_reboot.aio.aborted.Aborted):
+
+
+ Error = IMPORT_typing.Union[
+ IMPORT_reboot.aio.aborted.GrpcError,
+ IMPORT_reboot.aio.aborted.RebootError,
+ ]
+
+ METHOD_PROTOBUF_ERROR_TYPES: list[type[IMPORT_google_protobuf_message.Message]] = [
+ ]
+
+ PROTOBUF_ERROR_TYPES: list[type[IMPORT_google_protobuf_message.Message | IMPORT_reboot_api.Model]] = (
+ METHOD_PROTOBUF_ERROR_TYPES +
+ IMPORT_reboot.aio.aborted.GRPC_ERROR_TYPES +
+ IMPORT_reboot.aio.aborted.REBOOT_ERROR_TYPES
+ )
+
+ _error: Error
+
+ MethodProtobufError = IMPORT_typing.Union[
+ IMPORT_reboot.aio.aborted.GrpcError,
+ IMPORT_reboot.aio.aborted.RebootError,
+ ]
+ _method_protobuf_error: MethodProtobufError
+
+ _code: IMPORT_grpc.StatusCode
+ _message: IMPORT_typing.Optional[str]
+
+ def __init__(
+ self,
+ error: IMPORT_reboot.aio.aborted.GrpcError,
+ *,
+ message: IMPORT_typing.Optional[str] = None,
+ # Do not set this value when constructing in order to
+ # raise. This is only used internally when constructing
+ # from aborted calls.
+ error_types: IMPORT_typing.Sequence[type[MethodProtobufError]] = (
+ METHOD_PROTOBUF_ERROR_TYPES + IMPORT_reboot.aio.aborted.GRPC_ERROR_TYPES
+ ),
+ ):
+ super().__init__()
+
+ self._message = None
+
+ if self.is_declared_error(error):
+ # If the error is a declared error, we should always
+ # store both formats for Pydantic and Protobuf for that
+ # error, so users can access the Pydantic version via
+ # `aborted.error` and the Protobuf version will be used
+ # to send the error over the wire.
+ if isinstance(error, IMPORT_reboot.api.Model):
+ # Happens when somebody raises an aborted with a Pydantic
+ # error format, usually in the backend method
+ # implementation.
+
+ self._error = error
+
+ else:
+ # Usually happens on the client side when we are
+ # constructing an aborted from the `AioRpcError`, so
+ # the error will be in Protobuf shape, and we want
+ # to convert it back to Pydantic if possible.
+ self._method_protobuf_error = error
+
+ else:
+ # Non declared error should be well known Reboot or gRPC
+ # error, which always comes in the Protobuf format, however
+ # it might be a case when the server is using new API and
+ # client is old, so server might send a declared error from
+ # the server perspective, but client doesn't know that it
+ # is a declared error now, so we need to gracefully
+ # cover that case.
+ if isinstance(error, IMPORT_reboot.api.Model):
+ # Store both formats so users can access the Pydantic
+ # version via `aborted.error` and the Protobuf version
+ # will be used to send the error over the wire.
+ self._error = IMPORT_rbt_v1alpha1.errors_pb2.Unknown()
+ self._method_protobuf_error = IMPORT_rbt_v1alpha1.errors_pb2.Unknown()
+
+ self._message = (
+ "Received a declared error from the server, "
+ "but the client doesn't know about it; "
+ "please update the client to the latest version\n"
+ ) + (
+ f"Original error: {error.model_dump_json()}\n"
+ ) + message if message is not None else ""
+
+ # Store both formats so users can access the error via
+ # `aborted.error` and the Protobuf version
+ # will be used to send the error over the wire.
+ self._error = error
+ self._method_protobuf_error = error
+
+ IMPORT_reboot_aio_types.assert_type(self._protobuf_error, error_types)
+
+ code = self.grpc_status_code_from_error(self._protobuf_error)
+
+ if code is None:
+ # Must be a Reboot specific or declared method error.
+ code = IMPORT_grpc.StatusCode.ABORTED
+
+ self._code = code
+
+ self._message = self._message if self._message is not None else message
+
+ @property
+ def error(self) -> Error | MethodProtobufError:
+ return self._error
+
+ @property
+ def _protobuf_error(self) -> MethodProtobufError:
+ return self._method_protobuf_error
+
+ @property
+ def code(self) -> IMPORT_grpc.StatusCode:
+ return self._code
+
+ @property
+ def message(self) -> IMPORT_typing.Optional[str]:
+ return self._message
+
+ @classmethod
+ def from_status(cls, status: IMPORT_google_rpc_status_pb2.Status):
+ error = cls.error_from_google_rpc_status_details(
+ status,
+ cls.PROTOBUF_ERROR_TYPES,
+ )
+
+ message = status.message if len(status.message) > 0 else None
+
+ if error is not None:
+ return cls(error, message=message, error_types=cls.PROTOBUF_ERROR_TYPES)
+
+ error = cls.error_from_google_rpc_status_code(status)
+
+ assert error is not None
+
+ # TODO(benh): also consider getting the type names from
+ # `status.details` and including that in `message` to make
+ # debugging easier.
+
+ return cls(error, message=message)
+
+ @classmethod
+ def from_grpc_aio_rpc_error(cls, aio_rpc_error: IMPORT_grpc.aio.AioRpcError):
+ return cls(
+ cls.error_from_grpc_aio_rpc_error(aio_rpc_error),
+ message=aio_rpc_error.details(),
+ )
+
+ @classmethod
+ def is_declared_error(cls, error: IMPORT_google_protobuf_message.Message | IMPORT_reboot.api.Model) -> bool:
+ if isinstance(error, IMPORT_reboot.api.Model):
+ return type(error) in IMPORT_api.Ping.methods['do_ping_periodically'].errors
+ return False
+
+ class DescribeTask:
+ """Represents a scheduled task running for the
+ state. Note that this is not a coroutine because we are trying
+ to convey the semantics that the task is already running (or
+ will soon be).
+ """
+
+ @classmethod
+ def retrieve(
+ cls,
+ context: IMPORT_reboot_aio_contexts.Context | IMPORT_reboot_aio_external.ExternalContext,
+ *,
+ task_id: IMPORT_rbt_v1alpha1.tasks_pb2.TaskId,
+ ):
+ return cls(context, task_id=task_id)
+
+ def __init__(
+ self,
+ context: IMPORT_reboot_aio_contexts.Context | IMPORT_reboot_aio_external.ExternalContext,
+ *,
+ task_id: IMPORT_rbt_v1alpha1.tasks_pb2.TaskId,
+ ) -> None:
+ # Depending on the context type (inside or outside a Reboot application)
+ # we may or may not know the application ID. If we don't know it, then
+ # the `ExternalContext.gateway` will determine it.
+ #
+ # TODO: in the future we expect to support cross-application calls, in
+ # which case the developer may explicitly pass in an application ID
+ # here.
+ self._application_id: IMPORT_typing.Optional[IMPORT_reboot_aio_types.ApplicationId] = None
+ if isinstance(context, IMPORT_reboot_aio_contexts.Context):
+ self._application_id = context.application_id
+ self._channel_manager = context.channel_manager
+ self._task_id = task_id
+
+ @property
+ def task_id(self) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ return self._task_id
+
+ def __await__(self) -> IMPORT_typing.Generator[None, None, Ping.DescribeResponse]:
+ """Awaits for task to finish and returns its response."""
+ async def wait_for_task() -> reboot.ping.ping_api_pb2.PingDescribeResponse:
+ channel = self._channel_manager.get_channel_to_state(
+ IMPORT_reboot_aio_types.StateTypeName(self._task_id.state_type),
+ IMPORT_reboot_aio_types.StateRef(self._task_id.state_ref),
+ )
+
+ stub = IMPORT_rbt_v1alpha1.tasks_pb2_grpc.TasksStub(channel)
+
+ try:
+ call = IMPORT_reboot_aio_stubs.UnaryRetriedCall(
+ call=None, # `RetriedCall` can create the call itself.
+ stub_method=stub.Wait,
+ method_name="Wait",
+ request=IMPORT_rbt_v1alpha1.tasks_pb2.WaitRequest(task_id=self._task_id),
+ metadata=IMPORT_reboot_aio_headers.Headers(
+ state_ref=IMPORT_reboot_aio_types.StateRef(self._task_id.state_ref),
+ application_id=self._application_id,
+ ).to_grpc_metadata(),
+ aborted_type=IMPORT_reboot.aio.aborted.SystemAborted,
+ )
+
+ wait_for_task_response = await call
+ except IMPORT_reboot.aio.aborted.SystemAborted as error:
+ if error.code == IMPORT_grpc.StatusCode.NOT_FOUND:
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.UnknownTask()
+ ) from None
+
+ raise
+ else:
+ response_or_error: IMPORT_typing.Optional[IMPORT_google_protobuf_any_pb2.Any] = None
+ is_error = False
+
+ if wait_for_task_response.response_or_error.WhichOneof("response_or_error") == "response":
+ response_or_error = wait_for_task_response.response_or_error.response
+ else:
+ is_error = True
+ response_or_error = wait_for_task_response.response_or_error.error
+
+ assert response_or_error is not None
+ assert response_or_error.TypeName() != ""
+
+ response = reboot.ping.ping_api_pb2.PingDescribeResponse()
+
+ if (
+ not is_error and response_or_error.TypeName() != response.DESCRIPTOR.full_name
+ ):
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.InvalidArgument(),
+ message=
+ f"task with UUID {IMPORT_uuid.UUID(bytes=self._task_id.task_uuid)} "
+ f"has a response of type '{response_or_error.TypeName()}' "
+ "but expecting type 'reboot.ping.ping_api_pb2.PingDescribeResponse'; "
+ "are you waiting on a task of the correct method?",
+ ) from None
+
+ if is_error:
+ aborted_type = Ping.DescribeAborted
+
+ # In Reboot >= 0.40.2 we expect the error to be a `google.rpc.Status`.
+ if response_or_error.Is(IMPORT_google_rpc_status_pb2.Status.DESCRIPTOR):
+ status = IMPORT_google_rpc_status_pb2.Status()
+ response_or_error.Unpack(status)
+ raise aborted_type.from_status(status)
+
+ # In Reboot < 0.40.2 workflows throwing declared errors behaved poorly;
+ # we don't aim to emulate its behavior. Indicate that we don't know the
+ # reason for the abort.
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ )
+
+ else:
+ response_or_error.Unpack(response)
+ return PingDescribeResponseFromProto(response)
+
+ return wait_for_task().__await__()
+
+
+ class DescribeAborted(IMPORT_reboot.aio.aborted.Aborted):
+
+
+ Error = IMPORT_typing.Union[
+ IMPORT_reboot.aio.aborted.GrpcError,
+ IMPORT_reboot.aio.aborted.RebootError,
+ ]
+
+ METHOD_PROTOBUF_ERROR_TYPES: list[type[IMPORT_google_protobuf_message.Message]] = [
+ ]
+
+ PROTOBUF_ERROR_TYPES: list[type[IMPORT_google_protobuf_message.Message | IMPORT_reboot_api.Model]] = (
+ METHOD_PROTOBUF_ERROR_TYPES +
+ IMPORT_reboot.aio.aborted.GRPC_ERROR_TYPES +
+ IMPORT_reboot.aio.aborted.REBOOT_ERROR_TYPES
+ )
+
+ _error: Error
+
+ MethodProtobufError = IMPORT_typing.Union[
+ IMPORT_reboot.aio.aborted.GrpcError,
+ IMPORT_reboot.aio.aborted.RebootError,
+ ]
+ _method_protobuf_error: MethodProtobufError
+
+ _code: IMPORT_grpc.StatusCode
+ _message: IMPORT_typing.Optional[str]
+
+ def __init__(
+ self,
+ error: IMPORT_reboot.aio.aborted.GrpcError,
+ *,
+ message: IMPORT_typing.Optional[str] = None,
+ # Do not set this value when constructing in order to
+ # raise. This is only used internally when constructing
+ # from aborted calls.
+ error_types: IMPORT_typing.Sequence[type[MethodProtobufError]] = (
+ METHOD_PROTOBUF_ERROR_TYPES + IMPORT_reboot.aio.aborted.GRPC_ERROR_TYPES
+ ),
+ ):
+ super().__init__()
+
+ self._message = None
+
+ if self.is_declared_error(error):
+ # If the error is a declared error, we should always
+ # store both formats for Pydantic and Protobuf for that
+ # error, so users can access the Pydantic version via
+ # `aborted.error` and the Protobuf version will be used
+ # to send the error over the wire.
+ if isinstance(error, IMPORT_reboot.api.Model):
+ # Happens when somebody raises an aborted with a Pydantic
+ # error format, usually in the backend method
+ # implementation.
+
+ self._error = error
+
+ else:
+ # Usually happens on the client side when we are
+ # constructing an aborted from the `AioRpcError`, so
+ # the error will be in Protobuf shape, and we want
+ # to convert it back to Pydantic if possible.
+ self._method_protobuf_error = error
+
+ else:
+ # Non declared error should be well known Reboot or gRPC
+ # error, which always comes in the Protobuf format, however
+ # it might be a case when the server is using new API and
+ # client is old, so server might send a declared error from
+ # the server perspective, but client doesn't know that it
+ # is a declared error now, so we need to gracefully
+ # cover that case.
+ if isinstance(error, IMPORT_reboot.api.Model):
+ # Store both formats so users can access the Pydantic
+ # version via `aborted.error` and the Protobuf version
+ # will be used to send the error over the wire.
+ self._error = IMPORT_rbt_v1alpha1.errors_pb2.Unknown()
+ self._method_protobuf_error = IMPORT_rbt_v1alpha1.errors_pb2.Unknown()
+
+ self._message = (
+ "Received a declared error from the server, "
+ "but the client doesn't know about it; "
+ "please update the client to the latest version\n"
+ ) + (
+ f"Original error: {error.model_dump_json()}\n"
+ ) + message if message is not None else ""
+
+ # Store both formats so users can access the error via
+ # `aborted.error` and the Protobuf version
+ # will be used to send the error over the wire.
+ self._error = error
+ self._method_protobuf_error = error
+
+ IMPORT_reboot_aio_types.assert_type(self._protobuf_error, error_types)
+
+ code = self.grpc_status_code_from_error(self._protobuf_error)
+
+ if code is None:
+ # Must be a Reboot specific or declared method error.
+ code = IMPORT_grpc.StatusCode.ABORTED
+
+ self._code = code
+
+ self._message = self._message if self._message is not None else message
+
+ @property
+ def error(self) -> Error | MethodProtobufError:
+ return self._error
+
+ @property
+ def _protobuf_error(self) -> MethodProtobufError:
+ return self._method_protobuf_error
+
+ @property
+ def code(self) -> IMPORT_grpc.StatusCode:
+ return self._code
+
+ @property
+ def message(self) -> IMPORT_typing.Optional[str]:
+ return self._message
+
+ @classmethod
+ def from_status(cls, status: IMPORT_google_rpc_status_pb2.Status):
+ error = cls.error_from_google_rpc_status_details(
+ status,
+ cls.PROTOBUF_ERROR_TYPES,
+ )
+
+ message = status.message if len(status.message) > 0 else None
+
+ if error is not None:
+ return cls(error, message=message, error_types=cls.PROTOBUF_ERROR_TYPES)
+
+ error = cls.error_from_google_rpc_status_code(status)
+
+ assert error is not None
+
+ # TODO(benh): also consider getting the type names from
+ # `status.details` and including that in `message` to make
+ # debugging easier.
+
+ return cls(error, message=message)
+
+ @classmethod
+ def from_grpc_aio_rpc_error(cls, aio_rpc_error: IMPORT_grpc.aio.AioRpcError):
+ return cls(
+ cls.error_from_grpc_aio_rpc_error(aio_rpc_error),
+ message=aio_rpc_error.details(),
+ )
+
+ @classmethod
+ def is_declared_error(cls, error: IMPORT_google_protobuf_message.Message | IMPORT_reboot.api.Model) -> bool:
+ if isinstance(error, IMPORT_reboot.api.Model):
+ return type(error) in IMPORT_api.Ping.methods['describe'].errors
+ return False
+
+ class NumPingsTask:
+ """Represents a scheduled task running for the
+ state. Note that this is not a coroutine because we are trying
+ to convey the semantics that the task is already running (or
+ will soon be).
+ """
+
+ @classmethod
+ def retrieve(
+ cls,
+ context: IMPORT_reboot_aio_contexts.Context | IMPORT_reboot_aio_external.ExternalContext,
+ *,
+ task_id: IMPORT_rbt_v1alpha1.tasks_pb2.TaskId,
+ ):
+ return cls(context, task_id=task_id)
+
+ def __init__(
+ self,
+ context: IMPORT_reboot_aio_contexts.Context | IMPORT_reboot_aio_external.ExternalContext,
+ *,
+ task_id: IMPORT_rbt_v1alpha1.tasks_pb2.TaskId,
+ ) -> None:
+ # Depending on the context type (inside or outside a Reboot application)
+ # we may or may not know the application ID. If we don't know it, then
+ # the `ExternalContext.gateway` will determine it.
+ #
+ # TODO: in the future we expect to support cross-application calls, in
+ # which case the developer may explicitly pass in an application ID
+ # here.
+ self._application_id: IMPORT_typing.Optional[IMPORT_reboot_aio_types.ApplicationId] = None
+ if isinstance(context, IMPORT_reboot_aio_contexts.Context):
+ self._application_id = context.application_id
+ self._channel_manager = context.channel_manager
+ self._task_id = task_id
+
+ @property
+ def task_id(self) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ return self._task_id
+
+ def __await__(self) -> IMPORT_typing.Generator[None, None, Ping.NumPingsResponse]:
+ """Awaits for task to finish and returns its response."""
+ async def wait_for_task() -> reboot.ping.ping_api_pb2.PingNumPingsResponse:
+ channel = self._channel_manager.get_channel_to_state(
+ IMPORT_reboot_aio_types.StateTypeName(self._task_id.state_type),
+ IMPORT_reboot_aio_types.StateRef(self._task_id.state_ref),
+ )
+
+ stub = IMPORT_rbt_v1alpha1.tasks_pb2_grpc.TasksStub(channel)
+
+ try:
+ call = IMPORT_reboot_aio_stubs.UnaryRetriedCall(
+ call=None, # `RetriedCall` can create the call itself.
+ stub_method=stub.Wait,
+ method_name="Wait",
+ request=IMPORT_rbt_v1alpha1.tasks_pb2.WaitRequest(task_id=self._task_id),
+ metadata=IMPORT_reboot_aio_headers.Headers(
+ state_ref=IMPORT_reboot_aio_types.StateRef(self._task_id.state_ref),
+ application_id=self._application_id,
+ ).to_grpc_metadata(),
+ aborted_type=IMPORT_reboot.aio.aborted.SystemAborted,
+ )
+
+ wait_for_task_response = await call
+ except IMPORT_reboot.aio.aborted.SystemAborted as error:
+ if error.code == IMPORT_grpc.StatusCode.NOT_FOUND:
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.UnknownTask()
+ ) from None
+
+ raise
+ else:
+ response_or_error: IMPORT_typing.Optional[IMPORT_google_protobuf_any_pb2.Any] = None
+ is_error = False
+
+ if wait_for_task_response.response_or_error.WhichOneof("response_or_error") == "response":
+ response_or_error = wait_for_task_response.response_or_error.response
+ else:
+ is_error = True
+ response_or_error = wait_for_task_response.response_or_error.error
+
+ assert response_or_error is not None
+ assert response_or_error.TypeName() != ""
+
+ response = reboot.ping.ping_api_pb2.PingNumPingsResponse()
+
+ if (
+ not is_error and response_or_error.TypeName() != response.DESCRIPTOR.full_name
+ ):
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.InvalidArgument(),
+ message=
+ f"task with UUID {IMPORT_uuid.UUID(bytes=self._task_id.task_uuid)} "
+ f"has a response of type '{response_or_error.TypeName()}' "
+ "but expecting type 'reboot.ping.ping_api_pb2.PingNumPingsResponse'; "
+ "are you waiting on a task of the correct method?",
+ ) from None
+
+ if is_error:
+ aborted_type = Ping.NumPingsAborted
+
+ # In Reboot >= 0.40.2 we expect the error to be a `google.rpc.Status`.
+ if response_or_error.Is(IMPORT_google_rpc_status_pb2.Status.DESCRIPTOR):
+ status = IMPORT_google_rpc_status_pb2.Status()
+ response_or_error.Unpack(status)
+ raise aborted_type.from_status(status)
+
+ # In Reboot < 0.40.2 workflows throwing declared errors behaved poorly;
+ # we don't aim to emulate its behavior. Indicate that we don't know the
+ # reason for the abort.
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ )
+
+ else:
+ response_or_error.Unpack(response)
+ return PingNumPingsResponseFromProto(response)
+
+ return wait_for_task().__await__()
+
+
+ class NumPingsAborted(IMPORT_reboot.aio.aborted.Aborted):
+
+
+ Error = IMPORT_typing.Union[
+ IMPORT_reboot.aio.aborted.GrpcError,
+ IMPORT_reboot.aio.aborted.RebootError,
+ ]
+
+ METHOD_PROTOBUF_ERROR_TYPES: list[type[IMPORT_google_protobuf_message.Message]] = [
+ ]
+
+ PROTOBUF_ERROR_TYPES: list[type[IMPORT_google_protobuf_message.Message | IMPORT_reboot_api.Model]] = (
+ METHOD_PROTOBUF_ERROR_TYPES +
+ IMPORT_reboot.aio.aborted.GRPC_ERROR_TYPES +
+ IMPORT_reboot.aio.aborted.REBOOT_ERROR_TYPES
+ )
+
+ _error: Error
+
+ MethodProtobufError = IMPORT_typing.Union[
+ IMPORT_reboot.aio.aborted.GrpcError,
+ IMPORT_reboot.aio.aborted.RebootError,
+ ]
+ _method_protobuf_error: MethodProtobufError
+
+ _code: IMPORT_grpc.StatusCode
+ _message: IMPORT_typing.Optional[str]
+
+ def __init__(
+ self,
+ error: IMPORT_reboot.aio.aborted.GrpcError,
+ *,
+ message: IMPORT_typing.Optional[str] = None,
+ # Do not set this value when constructing in order to
+ # raise. This is only used internally when constructing
+ # from aborted calls.
+ error_types: IMPORT_typing.Sequence[type[MethodProtobufError]] = (
+ METHOD_PROTOBUF_ERROR_TYPES + IMPORT_reboot.aio.aborted.GRPC_ERROR_TYPES
+ ),
+ ):
+ super().__init__()
+
+ self._message = None
+
+ if self.is_declared_error(error):
+ # If the error is a declared error, we should always
+ # store both formats for Pydantic and Protobuf for that
+ # error, so users can access the Pydantic version via
+ # `aborted.error` and the Protobuf version will be used
+ # to send the error over the wire.
+ if isinstance(error, IMPORT_reboot.api.Model):
+ # Happens when somebody raises an aborted with a Pydantic
+ # error format, usually in the backend method
+ # implementation.
+
+ self._error = error
+
+ else:
+ # Usually happens on the client side when we are
+ # constructing an aborted from the `AioRpcError`, so
+ # the error will be in Protobuf shape, and we want
+ # to convert it back to Pydantic if possible.
+ self._method_protobuf_error = error
+
+ else:
+ # Non declared error should be well known Reboot or gRPC
+ # error, which always comes in the Protobuf format, however
+ # it might be a case when the server is using new API and
+ # client is old, so server might send a declared error from
+ # the server perspective, but client doesn't know that it
+ # is a declared error now, so we need to gracefully
+ # cover that case.
+ if isinstance(error, IMPORT_reboot.api.Model):
+ # Store both formats so users can access the Pydantic
+ # version via `aborted.error` and the Protobuf version
+ # will be used to send the error over the wire.
+ self._error = IMPORT_rbt_v1alpha1.errors_pb2.Unknown()
+ self._method_protobuf_error = IMPORT_rbt_v1alpha1.errors_pb2.Unknown()
+
+ self._message = (
+ "Received a declared error from the server, "
+ "but the client doesn't know about it; "
+ "please update the client to the latest version\n"
+ ) + (
+ f"Original error: {error.model_dump_json()}\n"
+ ) + message if message is not None else ""
+
+ # Store both formats so users can access the error via
+ # `aborted.error` and the Protobuf version
+ # will be used to send the error over the wire.
+ self._error = error
+ self._method_protobuf_error = error
+
+ IMPORT_reboot_aio_types.assert_type(self._protobuf_error, error_types)
+
+ code = self.grpc_status_code_from_error(self._protobuf_error)
+
+ if code is None:
+ # Must be a Reboot specific or declared method error.
+ code = IMPORT_grpc.StatusCode.ABORTED
+
+ self._code = code
+
+ self._message = self._message if self._message is not None else message
+
+ @property
+ def error(self) -> Error | MethodProtobufError:
+ return self._error
+
+ @property
+ def _protobuf_error(self) -> MethodProtobufError:
+ return self._method_protobuf_error
+
+ @property
+ def code(self) -> IMPORT_grpc.StatusCode:
+ return self._code
+
+ @property
+ def message(self) -> IMPORT_typing.Optional[str]:
+ return self._message
+
+ @classmethod
+ def from_status(cls, status: IMPORT_google_rpc_status_pb2.Status):
+ error = cls.error_from_google_rpc_status_details(
+ status,
+ cls.PROTOBUF_ERROR_TYPES,
+ )
+
+ message = status.message if len(status.message) > 0 else None
+
+ if error is not None:
+ return cls(error, message=message, error_types=cls.PROTOBUF_ERROR_TYPES)
+
+ error = cls.error_from_google_rpc_status_code(status)
+
+ assert error is not None
+
+ # TODO(benh): also consider getting the type names from
+ # `status.details` and including that in `message` to make
+ # debugging easier.
+
+ return cls(error, message=message)
+
+ @classmethod
+ def from_grpc_aio_rpc_error(cls, aio_rpc_error: IMPORT_grpc.aio.AioRpcError):
+ return cls(
+ cls.error_from_grpc_aio_rpc_error(aio_rpc_error),
+ message=aio_rpc_error.details(),
+ )
+
+ @classmethod
+ def is_declared_error(cls, error: IMPORT_google_protobuf_message.Message | IMPORT_reboot.api.Model) -> bool:
+ if isinstance(error, IMPORT_reboot.api.Model):
+ return type(error) in IMPORT_api.Ping.methods['num_pings'].errors
+ return False
+
+
+ class WeakReference(IMPORT_typing.Generic[Ping_ScheduleTypeVar]):
+
+ _schedule_type: type[Ping_ScheduleTypeVar]
+
+ def __init__(
+ self,
+ # When application ID is None, refers to a state within the application given by the context.
+ application_id: IMPORT_typing.Optional[IMPORT_reboot_aio_types.ApplicationId],
+ state_id: IMPORT_reboot_aio_types.StateId,
+ *,
+ schedule_type: type[Ping_ScheduleTypeVar],
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ servicer: IMPORT_typing.Optional[PingBaseServicer] = None,
+ ):
+ self._application_id = application_id
+ self._state_ref = IMPORT_reboot_aio_types.StateRef.from_id(
+ Ping.__state_type_name__,
+ state_id,
+ )
+ self._schedule_type = schedule_type
+ self._idempotency_manager: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.IdempotencyManager] = None
+ self._reader_stub: IMPORT_typing.Optional[PingReaderStub] = None
+ self._writer_stub: IMPORT_typing.Optional[PingWriterStub] = None
+ self._workflow_stub: IMPORT_typing.Optional[PingWorkflowStub] = None
+ self._tasks_stub: IMPORT_typing.Optional[PingTasksStub] = None
+ self._bearer_token = bearer_token
+ self._servicer = servicer
+
+ @property
+ def state_id(self) -> IMPORT_reboot_aio_types.StateId:
+ return self._state_ref.id
+
+ def _reader(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext | IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ ) -> PingReaderStub:
+ if self._reader_stub is None:
+ self._reader_stub = PingReaderStub(
+ context=context,
+ state_ref=self._state_ref,
+ bearer_token=self._bearer_token,
+ )
+ assert self._reader_stub is not None
+ if self._idempotency_manager is None:
+ self._idempotency_manager = context
+ elif self._idempotency_manager != context:
+ raise IMPORT_reboot_aio_call.MixedContextsError(
+ "This `WeakReference` for `Ping` with ID "
+ f"'{self.state_id}' has previously been used by a "
+ "different `Context`. That is not allowed. "
+ "Instead create a new `WeakReference` for every `Context` by calling "
+ f"`Ping.ref('{self.state_id}')`."
+ )
+ return self._reader_stub
+
+ def _writer(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ ) -> PingWriterStub:
+ if self._writer_stub is None:
+ self._writer_stub = PingWriterStub(
+ context=context,
+ state_ref=self._state_ref,
+ bearer_token=self._bearer_token,
+ )
+ assert self._writer_stub is not None
+ if self._idempotency_manager is None:
+ self._idempotency_manager = context
+ elif self._idempotency_manager != context:
+ raise IMPORT_reboot_aio_call.MixedContextsError(
+ "This `WeakReference` for `Ping` with ID "
+ f"'{self.state_id}' has previously been used by a "
+ "different `Context`. That is not allowed. "
+ "Instead create a new `WeakReference` for every `Context` by calling "
+ f"`Ping.ref('{self.state_id}')`."
+ )
+ return self._writer_stub
+
+ def _workflow(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ ) -> PingWorkflowStub:
+ if self._workflow_stub is None:
+ self._workflow_stub = PingWorkflowStub(
+ context=context,
+ state_ref=self._state_ref,
+ bearer_token=self._bearer_token,
+ )
+ assert self._workflow_stub is not None
+ if self._idempotency_manager is None:
+ self._idempotency_manager = context
+ elif self._idempotency_manager != context:
+ raise IMPORT_reboot_aio_call.MixedContextsError(
+ "This `WeakReference` for `Ping` with ID "
+ f"'{self.state_id}' has previously been used by a "
+ "different `Context`. That is not allowed. "
+ "Instead create a new `WeakReference` for every `Context` by calling "
+ f"`Ping.ref('{self.state_id}')`."
+ )
+ return self._workflow_stub
+
+ def _tasks(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ ) -> PingTasksStub:
+ if self._tasks_stub is None:
+ self._tasks_stub = PingTasksStub(
+ context=context,
+ state_ref=self._state_ref,
+ bearer_token=self._bearer_token,
+ )
+ assert self._tasks_stub is not None
+ if self._idempotency_manager is None:
+ self._idempotency_manager = context
+ elif self._idempotency_manager != context:
+ raise IMPORT_reboot_aio_call.MixedContextsError(
+ "This `WeakReference` for `Ping` with ID "
+ f"'{self.state_id}' has previously been used by a "
+ "different `Context`. That is not allowed. "
+ "Instead create a new `WeakReference` for every `Context` by calling "
+ f"`Ping.ref('{self.state_id}')`."
+ )
+ return self._tasks_stub
+
+ class _Reactively:
+
+ def __init__(
+ self,
+ *,
+ application_id: IMPORT_typing.Optional[IMPORT_reboot_aio_types.ApplicationId],
+ state_ref: IMPORT_reboot_aio_types.StateRef,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ):
+ self._application_id = application_id
+ self._state_ref = state_ref
+ self._bearer_token = bearer_token
+
+
+ async def Describe( # type: ignore[misc]
+ # In methods which are dealing with user input, (i.e.,
+ # proto message field names), we should use '__double_underscored__'
+ # variables to avoid any potential name conflicts with the method's
+ # parameters.
+ # The '__self__' parameter is a convention in Python to
+ # indicate that this method is a bound method, so we use
+ # '__this__' instead.
+ __this__,
+ # Explicitly-reactive calls only make sense in the context of either...
+ # (A) an external client, or...
+ # (B) methods that may reasonably run for a long time, which in Reboot means: readers or workflows.
+ __context__: IMPORT_reboot_aio_external.ExternalContext | IMPORT_reboot_aio_contexts.ReaderContext | IMPORT_reboot_aio_contexts.WorkflowContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> IMPORT_typing.AsyncIterator[Ping.DescribeResponse]:
+ IMPORT_reboot_aio_types.assert_type(__context__, [IMPORT_reboot_aio_external.ExternalContext, IMPORT_reboot_aio_contexts.ReaderContext, IMPORT_reboot_aio_contexts.WorkflowContext])
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+ __caller_id__: IMPORT_typing.Optional[IMPORT_reboot_aio_caller_id.CallerID] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ if __bearer_token__ is None:
+ __bearer_token__ = __this__._bearer_token
+ if __bearer_token__ is None and isinstance(__context__, IMPORT_reboot_aio_external.ExternalContext):
+ # Within a Reboot context we do not pass on the caller's bearer token, as that might
+ # have security implications - we cannot simply trust any service we are calling with
+ # the user's credentials. Instead, the developer can rely on the default app-internal
+ # auth, or override that and set an explicit bearer token.
+ #
+ # In the case of `ExternalContext`, however, its `bearer_token` was set specifically
+ # by the developer for the purpose of making these calls. Note that only
+ # `ExternalContext` even has a `bearer_token` field.
+ __bearer_token__ = __context__.bearer_token
+
+ if __metadata__ is None:
+ __metadata__ = ()
+
+ if isinstance(__context__, IMPORT_reboot_aio_contexts.Context):
+ __caller_id__ = IMPORT_reboot_aio_caller_id.CallerID(
+ application_id=__context__.application_id,
+ )
+ if __this__._application_id is None:
+ # Given our context type (inside a Reboot application) we can default to
+ # making the application send traffic to itself.
+ __this__._application_id = __context__.application_id
+ elif isinstance(__context__, IMPORT_reboot_aio_external.ExternalContext):
+ __caller_id__ = __context__.caller_id
+
+ __headers__ = IMPORT_reboot_aio_headers.Headers(
+ bearer_token=__bearer_token__,
+ state_ref=__this__._state_ref,
+ application_id=__this__._application_id,
+ caller_id=__caller_id__,
+ )
+
+ __metadata__ += __headers__.to_grpc_metadata()
+
+ __query_backoff__ = IMPORT_reboot_aio_backoff.Backoff()
+ while True:
+ __call__ = None
+ try:
+ async with __context__.channel_manager.get_channel_to_state(
+ IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping'),
+ __this__._state_ref,
+ ) as __channel__:
+
+ __call__ = IMPORT_rbt_v1alpha1.react_pb2_grpc.ReactStub(
+ __channel__
+ ).Query(
+ IMPORT_rbt_v1alpha1.react_pb2.QueryRequest(
+ method='Describe',
+ request=PingDescribeRequestToProto(
+ ).SerializeToString(),
+ ),
+ metadata=__metadata__,
+ )
+
+ async for __query_response__ in __call__:
+ # Clear the backoff so we don't wait
+ # as long the next time we get
+ # disconnected.
+ __query_backoff__.clear()
+
+ # The backend may have sent us this query
+ # response only to let us know that a new
+ # idempotency key has been recorded; there may
+ # not be a new response. Python callers don't
+ # (currently) care about such an event, so we
+ # simply ignore it.
+ if not __query_response__.HasField("response"):
+ continue
+
+ __response__ = reboot.ping.ping_api_pb2.PingDescribeResponse()
+ __response__.ParseFromString(__query_response__.response)
+ yield PingDescribeResponseFromProto(__response__)
+
+ except IMPORT_grpc.aio.AioRpcError as error:
+ # We expect to get disconnected from the server
+ # from time to time, e.g., when it is being
+ # updated, but we don't want that error to
+ # propagate, we just want to retry.
+ if IMPORT_reboot.aio.aborted.is_grpc_retryable_exception(error):
+ logger.debug(
+ "Reactive read to 'reboot.ping.PingMethods.Describe' "
+ f"failed with a retryable error: '{error}'; "
+ "will retry..."
+ )
+ await __query_backoff__()
+ continue
+ if error.code() == IMPORT_grpc.StatusCode.ABORTED:
+ # Reconstitute the error that the server threw, if it was a declared error.
+ status = await IMPORT_rpc_status_async.from_call(__call__)
+ if status is not None:
+ raise Ping.DescribeAborted.from_status(
+ status
+ ) from None
+ raise Ping.DescribeAborted.from_grpc_aio_rpc_error(
+ error
+ ) from None
+
+ raise
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ describe = Describe
+
+ async def NumPings( # type: ignore[misc]
+ # In methods which are dealing with user input, (i.e.,
+ # proto message field names), we should use '__double_underscored__'
+ # variables to avoid any potential name conflicts with the method's
+ # parameters.
+ # The '__self__' parameter is a convention in Python to
+ # indicate that this method is a bound method, so we use
+ # '__this__' instead.
+ __this__,
+ # Explicitly-reactive calls only make sense in the context of either...
+ # (A) an external client, or...
+ # (B) methods that may reasonably run for a long time, which in Reboot means: readers or workflows.
+ __context__: IMPORT_reboot_aio_external.ExternalContext | IMPORT_reboot_aio_contexts.ReaderContext | IMPORT_reboot_aio_contexts.WorkflowContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> IMPORT_typing.AsyncIterator[Ping.NumPingsResponse]:
+ IMPORT_reboot_aio_types.assert_type(__context__, [IMPORT_reboot_aio_external.ExternalContext, IMPORT_reboot_aio_contexts.ReaderContext, IMPORT_reboot_aio_contexts.WorkflowContext])
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+ __caller_id__: IMPORT_typing.Optional[IMPORT_reboot_aio_caller_id.CallerID] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ if __bearer_token__ is None:
+ __bearer_token__ = __this__._bearer_token
+ if __bearer_token__ is None and isinstance(__context__, IMPORT_reboot_aio_external.ExternalContext):
+ # Within a Reboot context we do not pass on the caller's bearer token, as that might
+ # have security implications - we cannot simply trust any service we are calling with
+ # the user's credentials. Instead, the developer can rely on the default app-internal
+ # auth, or override that and set an explicit bearer token.
+ #
+ # In the case of `ExternalContext`, however, its `bearer_token` was set specifically
+ # by the developer for the purpose of making these calls. Note that only
+ # `ExternalContext` even has a `bearer_token` field.
+ __bearer_token__ = __context__.bearer_token
+
+ if __metadata__ is None:
+ __metadata__ = ()
+
+ if isinstance(__context__, IMPORT_reboot_aio_contexts.Context):
+ __caller_id__ = IMPORT_reboot_aio_caller_id.CallerID(
+ application_id=__context__.application_id,
+ )
+ if __this__._application_id is None:
+ # Given our context type (inside a Reboot application) we can default to
+ # making the application send traffic to itself.
+ __this__._application_id = __context__.application_id
+ elif isinstance(__context__, IMPORT_reboot_aio_external.ExternalContext):
+ __caller_id__ = __context__.caller_id
+
+ __headers__ = IMPORT_reboot_aio_headers.Headers(
+ bearer_token=__bearer_token__,
+ state_ref=__this__._state_ref,
+ application_id=__this__._application_id,
+ caller_id=__caller_id__,
+ )
+
+ __metadata__ += __headers__.to_grpc_metadata()
+
+ __query_backoff__ = IMPORT_reboot_aio_backoff.Backoff()
+ while True:
+ __call__ = None
+ try:
+ async with __context__.channel_manager.get_channel_to_state(
+ IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Ping'),
+ __this__._state_ref,
+ ) as __channel__:
+
+ __call__ = IMPORT_rbt_v1alpha1.react_pb2_grpc.ReactStub(
+ __channel__
+ ).Query(
+ IMPORT_rbt_v1alpha1.react_pb2.QueryRequest(
+ method='NumPings',
+ request=PingNumPingsRequestToProto(
+ ).SerializeToString(),
+ ),
+ metadata=__metadata__,
+ )
+
+ async for __query_response__ in __call__:
+ # Clear the backoff so we don't wait
+ # as long the next time we get
+ # disconnected.
+ __query_backoff__.clear()
+
+ # The backend may have sent us this query
+ # response only to let us know that a new
+ # idempotency key has been recorded; there may
+ # not be a new response. Python callers don't
+ # (currently) care about such an event, so we
+ # simply ignore it.
+ if not __query_response__.HasField("response"):
+ continue
+
+ __response__ = reboot.ping.ping_api_pb2.PingNumPingsResponse()
+ __response__.ParseFromString(__query_response__.response)
+ yield PingNumPingsResponseFromProto(__response__)
+
+ except IMPORT_grpc.aio.AioRpcError as error:
+ # We expect to get disconnected from the server
+ # from time to time, e.g., when it is being
+ # updated, but we don't want that error to
+ # propagate, we just want to retry.
+ if IMPORT_reboot.aio.aborted.is_grpc_retryable_exception(error):
+ logger.debug(
+ "Reactive read to 'reboot.ping.PingMethods.NumPings' "
+ f"failed with a retryable error: '{error}'; "
+ "will retry..."
+ )
+ await __query_backoff__()
+ continue
+ if error.code() == IMPORT_grpc.StatusCode.ABORTED:
+ # Reconstitute the error that the server threw, if it was a declared error.
+ status = await IMPORT_rpc_status_async.from_call(__call__)
+ if status is not None:
+ raise Ping.NumPingsAborted.from_status(
+ status
+ ) from None
+ raise Ping.NumPingsAborted.from_grpc_aio_rpc_error(
+ error
+ ) from None
+
+ raise
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ num_pings = NumPings
+
+ def reactively(self):
+ return Ping.WeakReference._Reactively(
+ application_id=self._application_id,
+ state_ref=self._state_ref,
+ bearer_token=self._bearer_token,
+ )
+
+ class _Idempotently(IMPORT_typing.Generic[Ping_IdempotentlyScheduleTypeVar]):
+
+ _weak_reference: Ping.WeakReference[Ping_IdempotentlyScheduleTypeVar]
+
+ def __init__(
+ self,
+ *,
+ weak_reference: Ping.WeakReference[Ping_IdempotentlyScheduleTypeVar],
+ idempotency: IMPORT_reboot_aio_idempotency.Idempotency,
+ ):
+ self._weak_reference = weak_reference
+ self._idempotency = idempotency
+
+ def schedule(
+ self,
+ *,
+ when: IMPORT_typing.Optional[IMPORT_datetime_datetime | IMPORT_datetime_timedelta] = None,
+ ) -> Ping_IdempotentlyScheduleTypeVar:
+ return self._weak_reference._schedule_type(
+ self._weak_reference._application_id,
+ self._weak_reference._tasks,
+ when=when,
+ idempotency=self._idempotency,
+ )
+
+ def spawn(
+ self,
+ *,
+ when: IMPORT_typing.Optional[IMPORT_datetime_datetime | IMPORT_datetime_timedelta] = None,
+ ) -> Ping.WeakReference._Spawn:
+ return Ping.WeakReference._Spawn(
+ self._weak_reference._application_id,
+ self._weak_reference._tasks,
+ when=when,
+ idempotency=self._idempotency,
+ )
+
+ async def read(
+ self,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ ) -> Ping.State:
+ if self._weak_reference._servicer is None:
+ raise RuntimeError(
+ "`read()` is currently only supported within workflows; "
+ "Please reach out and let us know your use case if this "
+ "is important for you!"
+ )
+
+ return await PingBaseServicer.WorkflowState._Idempotently._read(
+ self._weak_reference._servicer,
+ self._idempotency,
+ context,
+ )
+
+ async def write(
+ self,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ writer: PingBaseServicer.InlineWriterCallable[PingBaseServicer.InlineWriterCallableResult],
+ *,
+ type: IMPORT_reboot_aio_workflows.Type | IMPORT_reboot_aio_workflows._Unset = IMPORT_reboot_aio_workflows._UNSET,
+ ) -> PingBaseServicer.InlineWriterCallableResult:
+ if self._weak_reference._servicer is None:
+ raise RuntimeError(
+ "`write()` is currently only supported within workflows; "
+ "Please reach out and let us know your use case if this "
+ "is important for you!"
+ )
+
+ # `type=` is optional: when omitted, `writer`'s
+ # return annotation is used.
+ type_t, _ = IMPORT_reboot_aio_workflows._resolve_callable_return_type(
+ writer, type,
+ )
+
+ return await PingBaseServicer.WorkflowState._Idempotently._write_validating_effects(
+ self._weak_reference._servicer,
+ self._idempotency,
+ context,
+ writer,
+ type_result=type_t,
+ check_type=not self._idempotency.always,
+ unidempotently=self._idempotency.always,
+ checkpoint=context.checkpoint(),
+ )
+
+
+ async def DoPing( # type: ignore[misc]
+ # In methods which are dealing with user input, (i.e.,
+ # proto message field names), we should use '__double_underscored__'
+ # variables to avoid any potential name conflicts with the method's
+ # parameters.
+ # The '__self__' parameter is a convention in Python to
+ # indicate that this method is a bound method, so we use
+ # '__this__' instead.
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> Ping.DoPingResponse:
+ __options__ = __options__ or IMPORT_reboot_aio_call.Options()
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+
+ return await __this__._weak_reference.DoPing(
+ __context__,
+ __options__,
+ __idempotency__=__this__._idempotency,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ do_ping = DoPing
+ @IMPORT_typing.overload
+ async def DoPingPeriodically(
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __request_or_options__: Ping.DoPingPeriodicallyRequest,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> Ping.DoPingPeriodicallyResponse:
+ ...
+
+ @IMPORT_typing.overload
+ async def DoPingPeriodically(
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __request_or_options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ *,
+ num_pings: int | Unset = UNSET,
+ period_seconds: float | Unset = UNSET,
+ ) -> Ping.DoPingPeriodicallyResponse:
+ ...
+
+ async def DoPingPeriodically( # type: ignore[misc]
+ # In methods which are dealing with user input, (i.e.,
+ # proto message field names), we should use '__double_underscored__'
+ # variables to avoid any potential name conflicts with the method's
+ # parameters.
+ # The '__self__' parameter is a convention in Python to
+ # indicate that this method is a bound method, so we use
+ # '__this__' instead.
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __request_or_options__: IMPORT_typing.Optional[Ping.DoPingPeriodicallyRequest | IMPORT_reboot_aio_call.Options] = None,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ *,
+ num_pings: int | Unset = UNSET,
+ period_seconds: float | Unset = UNSET,
+ ) -> Ping.DoPingPeriodicallyResponse:
+ def error_message_supplement():
+ if any([isinstance(__context__, t) for t in [IMPORT_reboot_aio_contexts.WriterContext, IMPORT_reboot_aio_contexts.TransactionContext]]):
+ return f"'DoPingPeriodically' is a workflow and must be scheduled from a '{type(__context__).__name__}' via `await [...].schedule([...]).DoPingPeriodically(context, [...])`"
+ else:
+ return f"'DoPingPeriodically' is a workflow and can not be called from '{type(__context__).__name__}'"
+
+ IMPORT_reboot_aio_types.assert_type(
+ __context__,
+ [
+ IMPORT_reboot_aio_contexts.WorkflowContext,
+ IMPORT_reboot_aio_external.ExternalContext,
+ ],
+ error_message_supplement=error_message_supplement(),
+ )
+ # UX improvement: check that neither positional argument was accidentally
+ # given a gRPC request type.
+ IMPORT_reboot_aio_types.assert_not_request_type(__context__, request_type=Ping.DoPingPeriodicallyRequest)
+ IMPORT_reboot_aio_types.assert_not_request_type(__options__, request_type=Ping.DoPingPeriodicallyRequest)
+
+ if isinstance(__request_or_options__, Ping.DoPingPeriodicallyRequest):
+ assert __request_or_options__ is not None
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+ __options__ = __options__ or IMPORT_reboot_aio_call.Options()
+
+ assert num_pings is UNSET
+ assert period_seconds is UNSET
+
+ return await (
+ await __this__.spawn().DoPingPeriodically(
+ __context__,
+ __request_or_options__,
+ __options__,
+ )
+ )
+ else:
+ assert __options__ is None
+ assert __request_or_options__ is None or isinstance(__request_or_options__, IMPORT_reboot_aio_call.Options)
+ __options__ = __request_or_options__ or IMPORT_reboot_aio_call.Options()
+
+ return await (
+ await __this__.spawn().DoPingPeriodically(
+ __context__,
+ PingDoPingPeriodicallyRequestFromInputFields(
+ num_pings=num_pings,
+ period_seconds=period_seconds,
+ ),
+ __options__,
+ )
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ do_ping_periodically = DoPingPeriodically
+
+ async def Describe( # type: ignore[misc]
+ # In methods which are dealing with user input, (i.e.,
+ # proto message field names), we should use '__double_underscored__'
+ # variables to avoid any potential name conflicts with the method's
+ # parameters.
+ # The '__self__' parameter is a convention in Python to
+ # indicate that this method is a bound method, so we use
+ # '__this__' instead.
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> Ping.DescribeResponse:
+ __options__ = __options__ or IMPORT_reboot_aio_call.Options()
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+
+ return await __this__._weak_reference.Describe(
+ __context__,
+ __options__,
+ __idempotency__=__this__._idempotency,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ describe = Describe
+
+ async def NumPings( # type: ignore[misc]
+ # In methods which are dealing with user input, (i.e.,
+ # proto message field names), we should use '__double_underscored__'
+ # variables to avoid any potential name conflicts with the method's
+ # parameters.
+ # The '__self__' parameter is a convention in Python to
+ # indicate that this method is a bound method, so we use
+ # '__this__' instead.
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> Ping.NumPingsResponse:
+ __options__ = __options__ or IMPORT_reboot_aio_call.Options()
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+
+ return await __this__._weak_reference.NumPings(
+ __context__,
+ __options__,
+ __idempotency__=__this__._idempotency,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ num_pings = NumPings
+
+ @IMPORT_typing.overload
+ def idempotently(self, alias: IMPORT_typing.Optional[str] = None, *, how: IMPORT_reboot_aio_idempotency.How = IMPORT_reboot_aio_idempotency.PER_WORKFLOW) -> Ping.WeakReference._Idempotently[Ping_ScheduleTypeVar]:
+ ...
+
+ @IMPORT_typing.overload
+ def idempotently(self, *, key: IMPORT_uuid.UUID, how: IMPORT_reboot_aio_idempotency.How = IMPORT_reboot_aio_idempotency.PER_WORKFLOW) -> Ping.WeakReference._Idempotently[Ping_ScheduleTypeVar]:
+ ...
+
+ def idempotently(
+ self,
+ alias: IMPORT_typing.Optional[str] = None,
+ *,
+ key: IMPORT_typing.Optional[IMPORT_uuid.UUID] = None,
+ how: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.How] = None,
+ ) -> Ping.WeakReference._Idempotently[Ping_ScheduleTypeVar]:
+ return Ping.WeakReference._Idempotently(
+ weak_reference=self,
+ idempotency=IMPORT_reboot_aio_contexts.Context.idempotency(
+ alias=alias,
+ key=key,
+ how=how,
+ )
+ )
+
+ def per_workflow(self, alias: IMPORT_typing.Optional[str] = None):
+ return self.idempotently(alias)
+
+ def per_iteration(self, alias: IMPORT_typing.Optional[str] = None):
+ return self.idempotently(
+ alias,
+ how=IMPORT_reboot_aio_idempotency.PER_ITERATION,
+ )
+
+ def always(self):
+ return self.idempotently(
+ how=IMPORT_reboot_aio_idempotency.ALWAYS,
+ )
+
+ class _UntilChangesSatisfies(IMPORT_typing.Generic[Ping_UntilCallableType]):
+
+ _idempotency_alias: str
+ _context: IMPORT_reboot_aio_contexts.WorkflowContext
+ _callable: IMPORT_typing.Callable[[], IMPORT_typing.Awaitable[Ping_UntilCallableType]]
+ _type: type[Ping_UntilCallableType]
+
+ def __init__(
+ self,
+ *,
+ idempotency_alias: str,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ callable: IMPORT_typing.Callable[[], IMPORT_typing.Awaitable[Ping_UntilCallableType]],
+ type: type[Ping_UntilCallableType],
+ ):
+ self._idempotency_alias = idempotency_alias
+ self._context = context
+ self._callable = callable
+ self._type = type
+
+ async def changes(self):
+ return await IMPORT_reboot_aio_workflows.until_changes(
+ self._idempotency_alias,
+ self._context,
+ self._callable,
+ type=self._type,
+ )
+
+ async def satisfies(
+ self,
+ condition: IMPORT_typing.Callable[[Ping_UntilCallableType], bool],
+ ):
+
+ async def converge():
+ response = await self._callable()
+ if condition(response):
+ return response
+ return False
+
+ return await IMPORT_reboot_aio_workflows.until(
+ self._idempotency_alias,
+ self._context,
+ converge,
+ type=self._type,
+ )
+
+ class _Until:
+
+ _weak_reference: Ping.WeakReference
+ _idempotency_alias: str
+
+ def __init__(
+ self,
+ *,
+ weak_reference: Ping.WeakReference,
+ idempotency_alias: str,
+ ):
+ self._weak_reference = weak_reference
+ self._idempotency_alias = idempotency_alias
+
+ def read(
+ self,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ ) -> Ping.WeakReference._UntilChangesSatisfies[Ping.State]:
+ IMPORT_reboot_aio_types.assert_type(
+ context,
+ [IMPORT_reboot_aio_contexts.WorkflowContext],
+ )
+
+ async def callable():
+ return await self._weak_reference.read(context)
+
+ return Ping.WeakReference._UntilChangesSatisfies(
+ idempotency_alias=self._idempotency_alias,
+ context=context,
+ callable=callable,
+ type=Ping.State,
+ )
+
+
+ def Describe( # type: ignore[misc]
+ # In methods which are dealing with user input, (i.e.,
+ # proto message field names), we should use '__double_underscored__'
+ # variables to avoid any potential name conflicts with the method's
+ # parameters.
+ # The '__self__' parameter is a convention in Python to
+ # indicate that this method is a bound method, so we use
+ # '__this__' instead.
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.WorkflowContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> Ping.WeakReference._UntilChangesSatisfies[Ping.DescribeResponse]:
+ IMPORT_reboot_aio_types.assert_type(
+ __context__,
+ [IMPORT_reboot_aio_contexts.WorkflowContext],
+ )
+
+ __options__ = __options__ or IMPORT_reboot_aio_call.Options()
+
+ async def callable():
+ return await __this__._weak_reference.Describe(
+ __context__,
+ __options__,
+ )
+
+ return Ping.WeakReference._UntilChangesSatisfies(
+ idempotency_alias=__this__._idempotency_alias,
+ context=__context__,
+ callable=callable,
+ type=Ping.DescribeResponse,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ describe = Describe
+
+ def NumPings( # type: ignore[misc]
+ # In methods which are dealing with user input, (i.e.,
+ # proto message field names), we should use '__double_underscored__'
+ # variables to avoid any potential name conflicts with the method's
+ # parameters.
+ # The '__self__' parameter is a convention in Python to
+ # indicate that this method is a bound method, so we use
+ # '__this__' instead.
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.WorkflowContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> Ping.WeakReference._UntilChangesSatisfies[Ping.NumPingsResponse]:
+ IMPORT_reboot_aio_types.assert_type(
+ __context__,
+ [IMPORT_reboot_aio_contexts.WorkflowContext],
+ )
+
+ __options__ = __options__ or IMPORT_reboot_aio_call.Options()
+
+ async def callable():
+ return await __this__._weak_reference.NumPings(
+ __context__,
+ __options__,
+ )
+
+ return Ping.WeakReference._UntilChangesSatisfies(
+ idempotency_alias=__this__._idempotency_alias,
+ context=__context__,
+ callable=callable,
+ type=Ping.NumPingsResponse,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ num_pings = NumPings
+
+ def until(self, alias: str):
+ return Ping.WeakReference._Until(
+ weak_reference=self,
+ idempotency_alias=alias,
+ )
+
+ def schedule(
+ self,
+ *,
+ when: IMPORT_typing.Optional[IMPORT_datetime_datetime | IMPORT_datetime_timedelta] = None,
+ ) -> Ping_ScheduleTypeVar:
+ return self._schedule_type(self._application_id, self._tasks, when=when)
+
+ class _Schedule:
+
+ def __init__(
+ self,
+ application_id: IMPORT_typing.Optional[IMPORT_reboot_aio_types.ApplicationId],
+ tasks: IMPORT_typing.Callable[[IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext], PingTasksStub],
+ *,
+ when: IMPORT_typing.Optional[IMPORT_datetime_datetime | IMPORT_datetime_timedelta] = None,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ ) -> None:
+ self._application_id = application_id
+ self._tasks = tasks
+ self._when = ensure_has_timezone(when=when)
+ self._idempotency = idempotency
+
+ # Ping callable tasks:
+
+ async def DoPing( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ IMPORT_reboot_aio_types.assert_type(__context__, [IMPORT_reboot_aio_contexts.TransactionContext])
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ __schedule__: IMPORT_typing.Optional[IMPORT_reboot_time_DateTimeWithTimeZone] = (IMPORT_reboot_time_DateTimeWithTimeZone.now() + __this__._when) if isinstance(
+ __this__._when, IMPORT_datetime_timedelta
+ ) else __this__._when
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = __this__._idempotency
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ # Add scheduling information to the metadata.
+ __metadata__ = (
+ (IMPORT_reboot_aio_headers.TASK_SCHEDULE,
+ __schedule__.isoformat() if __schedule__ else ''),
+ ) + (__metadata__ or tuple())
+
+ __task_id__ = await __this__._tasks(
+ __context__
+ ).DoPing(
+ idempotency=__idempotency__,
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ )
+
+ return __task_id__
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ do_ping = DoPing
+ @IMPORT_typing.overload
+ async def DoPingPeriodically(
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext,
+ __request_or_options__: Ping.DoPingPeriodicallyRequest,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ ...
+
+ @IMPORT_typing.overload
+ async def DoPingPeriodically(
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext,
+ __request_or_options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ *,
+ num_pings: int | Unset = UNSET,
+ period_seconds: float | Unset = UNSET,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ ...
+
+ async def DoPingPeriodically( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext,
+ __request_or_options__: IMPORT_typing.Optional[Ping.DoPingPeriodicallyRequest | IMPORT_reboot_aio_call.Options] = None,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ *,
+ num_pings: int | Unset = UNSET,
+ period_seconds: float | Unset = UNSET,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ IMPORT_reboot_aio_types.assert_type(__context__, [IMPORT_reboot_aio_contexts.TransactionContext])
+ # UX improvement: check that neither positional argument was accidentally
+ # given a gRPC request type.
+ IMPORT_reboot_aio_types.assert_not_request_type(__context__, request_type=Ping.DoPingPeriodicallyRequest)
+ IMPORT_reboot_aio_types.assert_not_request_type(__options__, request_type=Ping.DoPingPeriodicallyRequest)
+
+ __request__: IMPORT_typing.Optional[Ping.DoPingPeriodicallyRequest] = None
+ if isinstance(__request_or_options__, Ping.DoPingPeriodicallyRequest):
+ assert __request_or_options__ is not None
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ assert num_pings is UNSET
+ assert period_seconds is UNSET
+
+ __request__ = __request_or_options__
+ else:
+ assert __options__ is None
+ assert __request_or_options__ is None or isinstance(__request_or_options__, IMPORT_reboot_aio_call.Options)
+ __options__ = __request_or_options__
+
+ __request__ = PingDoPingPeriodicallyRequestFromInputFields(
+ num_pings=num_pings,
+ period_seconds=period_seconds,
+ )
+
+ __schedule__: IMPORT_typing.Optional[IMPORT_reboot_time_DateTimeWithTimeZone] = (IMPORT_reboot_time_DateTimeWithTimeZone.now() + __this__._when) if isinstance(
+ __this__._when, IMPORT_datetime_timedelta
+ ) else __this__._when
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = __this__._idempotency
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ # Add scheduling information to the metadata.
+ __metadata__ = (
+ (IMPORT_reboot_aio_headers.TASK_SCHEDULE,
+ __schedule__.isoformat() if __schedule__ else ''),
+ ) + (__metadata__ or tuple())
+
+ __task_id__ = await __this__._tasks(
+ __context__
+ ).DoPingPeriodically(
+ __request__,
+ idempotency=__idempotency__,
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ )
+
+ return __task_id__
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ do_ping_periodically = DoPingPeriodically
+
+ async def Describe( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ IMPORT_reboot_aio_types.assert_type(__context__, [IMPORT_reboot_aio_contexts.TransactionContext])
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ __schedule__: IMPORT_typing.Optional[IMPORT_reboot_time_DateTimeWithTimeZone] = (IMPORT_reboot_time_DateTimeWithTimeZone.now() + __this__._when) if isinstance(
+ __this__._when, IMPORT_datetime_timedelta
+ ) else __this__._when
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = __this__._idempotency
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ # Add scheduling information to the metadata.
+ __metadata__ = (
+ (IMPORT_reboot_aio_headers.TASK_SCHEDULE,
+ __schedule__.isoformat() if __schedule__ else ''),
+ ) + (__metadata__ or tuple())
+
+ __task_id__ = await __this__._tasks(
+ __context__
+ ).Describe(
+ idempotency=__idempotency__,
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ )
+
+ return __task_id__
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ describe = Describe
+
+ async def NumPings( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ IMPORT_reboot_aio_types.assert_type(__context__, [IMPORT_reboot_aio_contexts.TransactionContext])
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ __schedule__: IMPORT_typing.Optional[IMPORT_reboot_time_DateTimeWithTimeZone] = (IMPORT_reboot_time_DateTimeWithTimeZone.now() + __this__._when) if isinstance(
+ __this__._when, IMPORT_datetime_timedelta
+ ) else __this__._when
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = __this__._idempotency
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ # Add scheduling information to the metadata.
+ __metadata__ = (
+ (IMPORT_reboot_aio_headers.TASK_SCHEDULE,
+ __schedule__.isoformat() if __schedule__ else ''),
+ ) + (__metadata__ or tuple())
+
+ __task_id__ = await __this__._tasks(
+ __context__
+ ).NumPings(
+ idempotency=__idempotency__,
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ )
+
+ return __task_id__
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ num_pings = NumPings
+
+ # A `WriterContext` can not call any methods in `_Schedule` to
+ # prevent a writer from doing a `Foo.ref()` and trying to
+ # schedule. However, we want to allow a writer to schedule
+ # when we are constructing a `WeakReference` from
+ # `self.ref()` so instead we return a `_WriterSchedule` to
+ # provide type safety that allows a `WriterContext` to
+ # schedule (for itself).
+ class _WriterSchedule:
+
+ def __init__(
+ self,
+ application_id: IMPORT_typing.Optional[IMPORT_reboot_aio_types.ApplicationId],
+ tasks: IMPORT_typing.Callable[[IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext], PingTasksStub],
+ *,
+ when: IMPORT_typing.Optional[IMPORT_datetime_datetime | IMPORT_datetime_timedelta] = None,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ ) -> None:
+ self._tasks = tasks
+ self._when = ensure_has_timezone(when=when)
+ self._idempotency = idempotency
+
+ # Ping callable tasks:
+
+ async def DoPing( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ # Only `writer`s and `transaction`s should ``schedule()`, a
+ # `workflow` should `spawn()`.
+ IMPORT_reboot_aio_types.assert_type(__context__, [IMPORT_reboot_aio_contexts.WriterContext, IMPORT_reboot_aio_contexts.TransactionContext])
+
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ if isinstance(__context__, IMPORT_reboot_aio_contexts.WriterContext):
+ return (await PingServicerTasks(
+ context=__context__,
+ state_ref=__context__._state_ref,
+ ).DoPing(
+ schedule=__this__._when,
+ )).task_id
+
+ __schedule__: IMPORT_typing.Optional[IMPORT_reboot_time_DateTimeWithTimeZone] = (IMPORT_reboot_time_DateTimeWithTimeZone.now() + __this__._when) if isinstance(
+ __this__._when, IMPORT_datetime_timedelta
+ ) else __this__._when
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = __this__._idempotency
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ # Add scheduling information to the metadata.
+ __metadata__ = (
+ (IMPORT_reboot_aio_headers.TASK_SCHEDULE,
+ __schedule__.isoformat() if __schedule__ else ''),
+ ) + (__metadata__ or tuple())
+
+ return await __this__._tasks(
+ __context__
+ ).DoPing(
+ idempotency=__idempotency__,
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ do_ping = DoPing
+ @IMPORT_typing.overload
+ async def DoPingPeriodically(
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext,
+ __request_or_options__: Ping.DoPingPeriodicallyRequest,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ ...
+
+ @IMPORT_typing.overload
+ async def DoPingPeriodically(
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext,
+ __request_or_options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ *,
+ num_pings: int | Unset = UNSET,
+ period_seconds: float | Unset = UNSET,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ ...
+
+ async def DoPingPeriodically( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext,
+ __request_or_options__: IMPORT_typing.Optional[Ping.DoPingPeriodicallyRequest | IMPORT_reboot_aio_call.Options] = None,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ *,
+ num_pings: int | Unset = UNSET,
+ period_seconds: float | Unset = UNSET,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ # Only `writer`s and `transaction`s should ``schedule()`, a
+ # `workflow` should `spawn()`.
+ IMPORT_reboot_aio_types.assert_type(__context__, [IMPORT_reboot_aio_contexts.WriterContext, IMPORT_reboot_aio_contexts.TransactionContext])
+
+ # UX improvement: check that neither positional argument was accidentally
+ # given a gRPC request type.
+ IMPORT_reboot_aio_types.assert_not_request_type(__context__, request_type=Ping.DoPingPeriodicallyRequest)
+ IMPORT_reboot_aio_types.assert_not_request_type(__options__, request_type=Ping.DoPingPeriodicallyRequest)
+
+ __request__: IMPORT_typing.Optional[Ping.DoPingPeriodicallyRequest] = None
+ if isinstance(__request_or_options__, Ping.DoPingPeriodicallyRequest):
+ assert __request_or_options__ is not None
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ assert num_pings is UNSET
+ assert period_seconds is UNSET
+
+ __request__ = __request_or_options__
+ else:
+ assert __options__ is None
+ assert __request_or_options__ is None or isinstance(__request_or_options__, IMPORT_reboot_aio_call.Options)
+ __options__ = __request_or_options__
+
+ __request__ = PingDoPingPeriodicallyRequestFromInputFields(
+ num_pings=num_pings,
+ period_seconds=period_seconds,
+ )
+
+ if isinstance(__context__, IMPORT_reboot_aio_contexts.WriterContext):
+ return (await PingServicerTasks(
+ context=__context__,
+ state_ref=__context__._state_ref,
+ ).DoPingPeriodically(
+ __request__,
+ schedule=__this__._when,
+ )).task_id
+
+ __schedule__: IMPORT_typing.Optional[IMPORT_reboot_time_DateTimeWithTimeZone] = (IMPORT_reboot_time_DateTimeWithTimeZone.now() + __this__._when) if isinstance(
+ __this__._when, IMPORT_datetime_timedelta
+ ) else __this__._when
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = __this__._idempotency
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ # Add scheduling information to the metadata.
+ __metadata__ = (
+ (IMPORT_reboot_aio_headers.TASK_SCHEDULE,
+ __schedule__.isoformat() if __schedule__ else ''),
+ ) + (__metadata__ or tuple())
+
+ return await __this__._tasks(
+ __context__
+ ).DoPingPeriodically(
+ __request__,
+ idempotency=__idempotency__,
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ do_ping_periodically = DoPingPeriodically
+
+ async def Describe( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ # Only `writer`s and `transaction`s should ``schedule()`, a
+ # `workflow` should `spawn()`.
+ IMPORT_reboot_aio_types.assert_type(__context__, [IMPORT_reboot_aio_contexts.WriterContext, IMPORT_reboot_aio_contexts.TransactionContext])
+
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ if isinstance(__context__, IMPORT_reboot_aio_contexts.WriterContext):
+ return (await PingServicerTasks(
+ context=__context__,
+ state_ref=__context__._state_ref,
+ ).Describe(
+ schedule=__this__._when,
+ )).task_id
+
+ __schedule__: IMPORT_typing.Optional[IMPORT_reboot_time_DateTimeWithTimeZone] = (IMPORT_reboot_time_DateTimeWithTimeZone.now() + __this__._when) if isinstance(
+ __this__._when, IMPORT_datetime_timedelta
+ ) else __this__._when
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = __this__._idempotency
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ # Add scheduling information to the metadata.
+ __metadata__ = (
+ (IMPORT_reboot_aio_headers.TASK_SCHEDULE,
+ __schedule__.isoformat() if __schedule__ else ''),
+ ) + (__metadata__ or tuple())
+
+ return await __this__._tasks(
+ __context__
+ ).Describe(
+ idempotency=__idempotency__,
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ describe = Describe
+
+ async def NumPings( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ # Only `writer`s and `transaction`s should ``schedule()`, a
+ # `workflow` should `spawn()`.
+ IMPORT_reboot_aio_types.assert_type(__context__, [IMPORT_reboot_aio_contexts.WriterContext, IMPORT_reboot_aio_contexts.TransactionContext])
+
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ if isinstance(__context__, IMPORT_reboot_aio_contexts.WriterContext):
+ return (await PingServicerTasks(
+ context=__context__,
+ state_ref=__context__._state_ref,
+ ).NumPings(
+ schedule=__this__._when,
+ )).task_id
+
+ __schedule__: IMPORT_typing.Optional[IMPORT_reboot_time_DateTimeWithTimeZone] = (IMPORT_reboot_time_DateTimeWithTimeZone.now() + __this__._when) if isinstance(
+ __this__._when, IMPORT_datetime_timedelta
+ ) else __this__._when
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = __this__._idempotency
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ # Add scheduling information to the metadata.
+ __metadata__ = (
+ (IMPORT_reboot_aio_headers.TASK_SCHEDULE,
+ __schedule__.isoformat() if __schedule__ else ''),
+ ) + (__metadata__ or tuple())
+
+ return await __this__._tasks(
+ __context__
+ ).NumPings(
+ idempotency=__idempotency__,
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ num_pings = NumPings
+
+ def spawn(
+ self,
+ *,
+ when: IMPORT_typing.Optional[IMPORT_datetime_datetime | IMPORT_datetime_timedelta] = None,
+ ) -> Ping.WeakReference._Spawn:
+ # Within a `workflow`, all "bare" `spawn()` calls are
+ # syntactic sugar for `per_workflow()`, unless we're
+ # within a control loop, in which case they are syntactic
+ # sugar for `per_iteration()`.
+ context = IMPORT_reboot_aio_contexts.Context.get()
+ if context is not None:
+ if isinstance(context, IMPORT_reboot_aio_contexts.WorkflowContext):
+ return (
+ self.per_iteration() if context.within_loop()
+ else self.per_workflow()
+ ).spawn(when=when)
+ elif isinstance(context, IMPORT_reboot_aio_external.InitializeContext):
+ return self.idempotently().spawn(when=when)
+
+ return Ping.WeakReference._Spawn(
+ self._application_id, self._tasks, when=when
+ )
+
+ class _Spawn:
+
+ def __init__(
+ self,
+ application_id: IMPORT_typing.Optional[IMPORT_reboot_aio_types.ApplicationId],
+ tasks: IMPORT_typing.Callable[[IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext], PingTasksStub],
+ *,
+ when: IMPORT_typing.Optional[IMPORT_datetime_datetime | IMPORT_datetime_timedelta] = None,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ ) -> None:
+ self._application_id = application_id
+ self._tasks = tasks
+ self._when = ensure_has_timezone(when=when)
+ self._idempotency = idempotency
+
+ # Ping callable tasks:
+
+ async def DoPing( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> Ping.DoPingTask:
+ IMPORT_reboot_aio_types.assert_type(__context__, [IMPORT_reboot_aio_contexts.WorkflowContext, IMPORT_reboot_aio_external.ExternalContext])
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+ __schedule__: IMPORT_typing.Optional[IMPORT_reboot_time_DateTimeWithTimeZone] = (IMPORT_reboot_time_DateTimeWithTimeZone.now() + __this__._when) if isinstance(
+ __this__._when, IMPORT_datetime_timedelta
+ ) else __this__._when
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = __this__._idempotency
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ # Add scheduling information to the metadata.
+ __metadata__ = (
+ (IMPORT_reboot_aio_headers.TASK_SCHEDULE,
+ __schedule__.isoformat() if __schedule__ else ''),
+ ) + (__metadata__ or tuple())
+
+ __task_id__ = await __this__._tasks(
+ __context__
+ ).DoPing(
+ idempotency=__idempotency__,
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ )
+
+ return Ping.DoPingTask(
+ __context__,
+ task_id=__task_id__,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ do_ping = DoPing
+ @IMPORT_typing.overload
+ async def DoPingPeriodically(
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __request_or_options__: Ping.DoPingPeriodicallyRequest,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> Ping.DoPingPeriodicallyTask:
+ ...
+
+ @IMPORT_typing.overload
+ async def DoPingPeriodically(
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __request_or_options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ *,
+ num_pings: int | Unset = UNSET,
+ period_seconds: float | Unset = UNSET,
+ ) -> Ping.DoPingPeriodicallyTask:
+ ...
+
+ async def DoPingPeriodically( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __request_or_options__: IMPORT_typing.Optional[Ping.DoPingPeriodicallyRequest | IMPORT_reboot_aio_call.Options] = None,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ *,
+ num_pings: int | Unset = UNSET,
+ period_seconds: float | Unset = UNSET,
+ ) -> Ping.DoPingPeriodicallyTask:
+ IMPORT_reboot_aio_types.assert_type(__context__, [IMPORT_reboot_aio_contexts.WorkflowContext, IMPORT_reboot_aio_external.ExternalContext])
+ # UX improvement: check that neither positional argument was accidentally
+ # given a gRPC request type.
+ IMPORT_reboot_aio_types.assert_not_request_type(__context__, request_type=Ping.DoPingPeriodicallyRequest)
+ IMPORT_reboot_aio_types.assert_not_request_type(__options__, request_type=Ping.DoPingPeriodicallyRequest)
+
+
+ __request__: IMPORT_typing.Optional[Ping.DoPingPeriodicallyRequest] = None
+ if isinstance(__request_or_options__, Ping.DoPingPeriodicallyRequest):
+ assert __request_or_options__ is not None
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ assert num_pings is UNSET
+ assert period_seconds is UNSET
+
+ __request__ = __request_or_options__
+ else:
+ assert __options__ is None
+ assert __request_or_options__ is None or isinstance(__request_or_options__, IMPORT_reboot_aio_call.Options)
+ __options__ = __request_or_options__
+
+ __request__ = PingDoPingPeriodicallyRequestFromInputFields(
+ num_pings=num_pings,
+ period_seconds=period_seconds,
+ )
+ __schedule__: IMPORT_typing.Optional[IMPORT_reboot_time_DateTimeWithTimeZone] = (IMPORT_reboot_time_DateTimeWithTimeZone.now() + __this__._when) if isinstance(
+ __this__._when, IMPORT_datetime_timedelta
+ ) else __this__._when
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = __this__._idempotency
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ # Add scheduling information to the metadata.
+ __metadata__ = (
+ (IMPORT_reboot_aio_headers.TASK_SCHEDULE,
+ __schedule__.isoformat() if __schedule__ else ''),
+ ) + (__metadata__ or tuple())
+
+ __task_id__ = await __this__._tasks(
+ __context__
+ ).DoPingPeriodically(
+ __request__,
+ idempotency=__idempotency__,
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ )
+
+ return Ping.DoPingPeriodicallyTask(
+ __context__,
+ task_id=__task_id__,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ do_ping_periodically = DoPingPeriodically
+
+ async def Describe( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> Ping.DescribeTask:
+ IMPORT_reboot_aio_types.assert_type(__context__, [IMPORT_reboot_aio_contexts.WorkflowContext, IMPORT_reboot_aio_external.ExternalContext])
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+ __schedule__: IMPORT_typing.Optional[IMPORT_reboot_time_DateTimeWithTimeZone] = (IMPORT_reboot_time_DateTimeWithTimeZone.now() + __this__._when) if isinstance(
+ __this__._when, IMPORT_datetime_timedelta
+ ) else __this__._when
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = __this__._idempotency
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ # Add scheduling information to the metadata.
+ __metadata__ = (
+ (IMPORT_reboot_aio_headers.TASK_SCHEDULE,
+ __schedule__.isoformat() if __schedule__ else ''),
+ ) + (__metadata__ or tuple())
+
+ __task_id__ = await __this__._tasks(
+ __context__
+ ).Describe(
+ idempotency=__idempotency__,
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ )
+
+ return Ping.DescribeTask(
+ __context__,
+ task_id=__task_id__,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ describe = Describe
+
+ async def NumPings( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> Ping.NumPingsTask:
+ IMPORT_reboot_aio_types.assert_type(__context__, [IMPORT_reboot_aio_contexts.WorkflowContext, IMPORT_reboot_aio_external.ExternalContext])
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+ __schedule__: IMPORT_typing.Optional[IMPORT_reboot_time_DateTimeWithTimeZone] = (IMPORT_reboot_time_DateTimeWithTimeZone.now() + __this__._when) if isinstance(
+ __this__._when, IMPORT_datetime_timedelta
+ ) else __this__._when
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = __this__._idempotency
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ # Add scheduling information to the metadata.
+ __metadata__ = (
+ (IMPORT_reboot_aio_headers.TASK_SCHEDULE,
+ __schedule__.isoformat() if __schedule__ else ''),
+ ) + (__metadata__ or tuple())
+
+ __task_id__ = await __this__._tasks(
+ __context__
+ ).NumPings(
+ idempotency=__idempotency__,
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ )
+
+ return Ping.NumPingsTask(
+ __context__,
+ task_id=__task_id__,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ num_pings = NumPings
+
+ async def read(
+ self,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ ) -> Ping.State:
+ return await (
+ self.always() if context.within_until()
+ else (
+ self.per_iteration() if context.within_loop()
+ else self.per_workflow()
+ )
+ ).read(context)
+
+ async def write(
+ self,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ writer: PingBaseServicer.InlineWriterCallable[PingBaseServicer.InlineWriterCallableResult],
+ *,
+ type: IMPORT_reboot_aio_workflows.Type | IMPORT_reboot_aio_workflows._Unset = IMPORT_reboot_aio_workflows._UNSET,
+ ) -> PingBaseServicer.InlineWriterCallableResult:
+ """Perform an "inline write" within a workflow.
+
+ `type=` is optional: when omitted, `writer`'s return
+ annotation is used.
+ """
+ # We forward `type=` along to the inner `write` (which
+ # also infers if not passed). Pass through whichever
+ # form the user supplied -- explicit class or the
+ # `_UNSET` sentinel to trigger inference downstream.
+ return await (
+ self.always() if context.within_until()
+ else (
+ self.per_iteration() if context.within_loop()
+ else self.per_workflow()
+ )
+ ).write(
+ context, writer, type=type
+ )
+
+ # Ping specific methods:
+
+ async def DoPing( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ *,
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ ) -> Ping.DoPingResponse:
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ # Within a `workflow`, all "bare" calls are
+ # `per_workflow()` calls, unless we're within a control
+ # loop, in which case they are syntactic sugar for
+ # `per_iteration()`.
+ if __idempotency__ is None:
+ if isinstance(__context__, IMPORT_reboot_aio_contexts.WorkflowContext):
+ return await (
+ __this__.per_iteration() if __context__.within_loop()
+ else __this__.per_workflow()
+ ).DoPing(
+ __context__,
+ __options__ or IMPORT_reboot_aio_call.Options(),
+ )
+ elif isinstance(__context__, IMPORT_reboot_aio_external.InitializeContext):
+ return await __this__.idempotently().DoPing(
+ __context__,
+ __options__ or IMPORT_reboot_aio_call.Options(),
+ )
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ return PingDoPingResponseFromProto(
+ await __this__._workflow(__context__).DoPing(
+ idempotency=__idempotency__,
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ )
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ do_ping = DoPing
+ @IMPORT_typing.overload
+ async def DoPingPeriodically(
+ __this__,
+ __context__: IMPORT_reboot_aio_external.ExternalContext | IMPORT_reboot_aio_contexts.WorkflowContext,
+ __request_or_options__: Ping.DoPingPeriodicallyRequest,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> Ping.DoPingPeriodicallyResponse:
+ ...
+
+ @IMPORT_typing.overload
+ async def DoPingPeriodically(
+ __this__,
+ __context__: IMPORT_reboot_aio_external.ExternalContext | IMPORT_reboot_aio_contexts.WorkflowContext,
+ __request_or_options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ *,
+ num_pings: int | Unset = UNSET,
+ period_seconds: float | Unset = UNSET,
+ ) -> Ping.DoPingPeriodicallyResponse:
+ ...
+
+ async def DoPingPeriodically( # type: ignore[misc]
+ # In methods which are dealing with user input, (i.e.,
+ # proto message field names), we should use '__double_underscored__'
+ # variables to avoid any potential name conflicts with the method's
+ # parameters.
+ # The '__self__' parameter is a convention in Python to
+ # indicate that this method is a bound method, so we use
+ # '__this__' instead.
+ __this__,
+ __context__: IMPORT_reboot_aio_external.ExternalContext | IMPORT_reboot_aio_contexts.WorkflowContext,
+ __request_or_options__: IMPORT_typing.Optional[Ping.DoPingPeriodicallyRequest | IMPORT_reboot_aio_call.Options] = None,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ *,
+ num_pings: int | Unset = UNSET,
+ period_seconds: float | Unset = UNSET,
+ ) -> Ping.DoPingPeriodicallyResponse:
+ def error_message_supplement():
+ if any([isinstance(__context__, t) for t in [IMPORT_reboot_aio_contexts.WriterContext, IMPORT_reboot_aio_contexts.TransactionContext]]):
+ return f"'DoPingPeriodically' is a workflow and must be scheduled from a '{type(__context__).__name__}' via `await [...].schedule([...]).DoPingPeriodically(context, [...])`"
+ else:
+ return f"'DoPingPeriodically' is a workflow and can not be called from a '{type(__context__).__name__}'"
+
+ IMPORT_reboot_aio_types.assert_type(
+ __context__,
+ [IMPORT_reboot_aio_external.ExternalContext, IMPORT_reboot_aio_contexts.WorkflowContext],
+ error_message_supplement=error_message_supplement(),
+ )
+
+ # UX improvement: check that neither positional argument was accidentally
+ # given a gRPC request type.
+ IMPORT_reboot_aio_types.assert_not_request_type(__context__, request_type=Ping.DoPingPeriodicallyRequest)
+ IMPORT_reboot_aio_types.assert_not_request_type(__options__, request_type=Ping.DoPingPeriodicallyRequest)
+
+ if isinstance(__request_or_options__, Ping.DoPingPeriodicallyRequest):
+ assert __request_or_options__ is not None
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+ __options__ = __options__ or IMPORT_reboot_aio_call.Options()
+
+ assert num_pings is UNSET
+ assert period_seconds is UNSET
+
+ return await (
+ await __this__.spawn().DoPingPeriodically(
+ __context__,
+ __request_or_options__,
+ __options__,
+ )
+ )
+ else:
+ assert __options__ is None
+ assert __request_or_options__ is None or isinstance(__request_or_options__, IMPORT_reboot_aio_call.Options)
+ __options__ = __request_or_options__ or IMPORT_reboot_aio_call.Options()
+
+ return await (
+ await __this__.spawn().DoPingPeriodically(
+ __context__,
+ PingDoPingPeriodicallyRequestFromInputFields(
+ num_pings=num_pings,
+ period_seconds=period_seconds,
+ ),
+ __options__,
+ )
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ do_ping_periodically = DoPingPeriodically
+
+ async def Describe( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.ReaderContext | IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ *,
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ ) -> Ping.DescribeResponse:
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ # Within a `workflow`, all "bare" calls are
+ # `per_workflow()` calls, unless we're within a control
+ # loop, in which case they are syntactic sugar for
+ # `per_iteration()`.
+ #
+ # Unless we are "within until" in which case all "bare"
+ # calls are `.always().
+ if __idempotency__ is None:
+ if isinstance(__context__, IMPORT_reboot_aio_contexts.WorkflowContext):
+ return await (
+ __this__.always() if __context__.within_until()
+ else (
+ __this__.per_iteration() if __context__.within_loop()
+ else __this__.per_workflow()
+ )
+ ).Describe(
+ __context__,
+ __options__ or IMPORT_reboot_aio_call.Options(),
+ )
+ elif isinstance(__context__, IMPORT_reboot_aio_external.InitializeContext):
+ return await __this__.idempotently().Describe(
+ __context__,
+ __options__ or IMPORT_reboot_aio_call.Options(),
+ )
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+ return PingDescribeResponseFromProto(
+ await __this__._reader(__context__).Describe(
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ idempotency=__idempotency__,
+ )
+ )
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ describe = Describe
+
+ async def NumPings( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.ReaderContext | IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ *,
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ ) -> Ping.NumPingsResponse:
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ # Within a `workflow`, all "bare" calls are
+ # `per_workflow()` calls, unless we're within a control
+ # loop, in which case they are syntactic sugar for
+ # `per_iteration()`.
+ #
+ # Unless we are "within until" in which case all "bare"
+ # calls are `.always().
+ if __idempotency__ is None:
+ if isinstance(__context__, IMPORT_reboot_aio_contexts.WorkflowContext):
+ return await (
+ __this__.always() if __context__.within_until()
+ else (
+ __this__.per_iteration() if __context__.within_loop()
+ else __this__.per_workflow()
+ )
+ ).NumPings(
+ __context__,
+ __options__ or IMPORT_reboot_aio_call.Options(),
+ )
+ elif isinstance(__context__, IMPORT_reboot_aio_external.InitializeContext):
+ return await __this__.idempotently().NumPings(
+ __context__,
+ __options__ or IMPORT_reboot_aio_call.Options(),
+ )
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+ return PingNumPingsResponseFromProto(
+ await __this__._reader(__context__).NumPings(
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ idempotency=__idempotency__,
+ )
+ )
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ num_pings = NumPings
+
+ class _Forall:
+
+ _ids: IMPORT_typing.Iterable[str]
+
+ def __init__(self, ids: IMPORT_typing.Iterable[str]):
+ self._ids = ids
+
+
+ async def DoPing( # type: ignore[misc]
+ # In methods which are dealing with user input, (i.e.,
+ # proto message field names), we should use '__double_underscored__'
+ # variables to avoid any potential name conflicts with the method's
+ # parameters.
+ # The '__self__' parameter is a convention in Python to
+ # indicate that this method is a bound method, so we use
+ # '__this__' instead.
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> list[Ping.DoPingResponse]:
+ __options__ = __options__ or IMPORT_reboot_aio_call.Options()
+ return await IMPORT_asyncio.gather(
+ *[
+ Ping.ref(
+ id
+ ).DoPing(
+ __context__,
+ __options__,
+ ) for id in __this__._ids
+ ]
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ do_ping = DoPing
+
+ async def Describe( # type: ignore[misc]
+ # In methods which are dealing with user input, (i.e.,
+ # proto message field names), we should use '__double_underscored__'
+ # variables to avoid any potential name conflicts with the method's
+ # parameters.
+ # The '__self__' parameter is a convention in Python to
+ # indicate that this method is a bound method, so we use
+ # '__this__' instead.
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.ReaderContext | IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> list[Ping.DescribeResponse]:
+ __options__ = __options__ or IMPORT_reboot_aio_call.Options()
+ return await IMPORT_asyncio.gather(
+ *[
+ Ping.ref(
+ id
+ ).Describe(
+ __context__,
+ __options__,
+ ) for id in __this__._ids
+ ]
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ describe = Describe
+
+ async def NumPings( # type: ignore[misc]
+ # In methods which are dealing with user input, (i.e.,
+ # proto message field names), we should use '__double_underscored__'
+ # variables to avoid any potential name conflicts with the method's
+ # parameters.
+ # The '__self__' parameter is a convention in Python to
+ # indicate that this method is a bound method, so we use
+ # '__this__' instead.
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.ReaderContext | IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> list[Ping.NumPingsResponse]:
+ __options__ = __options__ or IMPORT_reboot_aio_call.Options()
+ return await IMPORT_asyncio.gather(
+ *[
+ Ping.ref(
+ id
+ ).NumPings(
+ __context__,
+ __options__,
+ ) for id in __this__._ids
+ ]
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ num_pings = NumPings
+
+ @classmethod
+ def forall(cls, ids: IMPORT_typing.Iterable[str]) -> Ping._Forall:
+ return Ping._Forall(ids)
+
+ @classmethod
+ def ref(
+ cls,
+ state_id: IMPORT_typing.Optional[IMPORT_reboot_aio_types.StateId] = None,
+ *,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ) -> Ping.WeakReference[Ping.WeakReference._Schedule] | Ping.WeakReference[Ping.WeakReference._WriterSchedule]:
+ # We support calling `Ping.ref()` with
+ # no `state_id` __only__ inside a workflow to be able to call an
+ # inline writer, inline reader or other method call, since
+ # workflow is a `classmethod` and therefor we can't get a
+ # reference to outselves as `self.ref()`.
+ if state_id is None:
+ context = IMPORT_reboot_aio_contexts.Context.get()
+
+ if context is None:
+ raise RuntimeError(
+ 'Missing asyncio context variable `context`; '
+ 'are you using this class without Reboot?'
+ )
+
+ if not isinstance(context, IMPORT_reboot_aio_contexts.WorkflowContext):
+ raise RuntimeError(
+ '`ref()` called without a `state_id` can only be used within a Workflow.'
+ )
+
+ servicer = PingBaseServicer.__servicer__.get()
+
+ if servicer is None:
+ raise RuntimeError(
+ 'Missing asyncio context variable `servicer`; '
+ 'are you using this class without Reboot?'
+ )
+
+ return Ping.WeakReference(
+ # TODO(https://github.com/reboot-dev/mono/issues/3226): add support for calling other applications.
+ # For now this always stays within the application that creates the context.
+ application_id=None,
+ state_id=context._state_ref.id,
+ schedule_type=Ping.WeakReference._WriterSchedule,
+ # If the user didn't specify a bearer token we may still end up using the app-internal bearer token,
+ # but that's decided at the time of the call.
+ bearer_token=bearer_token,
+ servicer=servicer,
+ )
+
+ return Ping.WeakReference(
+ # TODO(https://github.com/reboot-dev/mono/issues/3226): add support for calling other applications.
+ # For now this always stays within the application that creates the context.
+ application_id=None,
+ state_id=state_id,
+ schedule_type=Ping.WeakReference._Schedule,
+ bearer_token=bearer_token,
+ )
+
+
+ @IMPORT_typing.overload
+ @classmethod
+ def idempotently(cls, alias: IMPORT_typing.Optional[str] = None, *, how: IMPORT_reboot_aio_idempotency.How = IMPORT_reboot_aio_idempotency.PER_WORKFLOW) -> Ping._ConstructIdempotently:
+ ...
+
+ @IMPORT_typing.overload
+ @classmethod
+ def idempotently(cls, *, key: IMPORT_uuid.UUID, how: IMPORT_reboot_aio_idempotency.How = IMPORT_reboot_aio_idempotency.PER_WORKFLOW) -> Ping._ConstructIdempotently:
+ ...
+
+ @classmethod
+ def idempotently(
+ cls,
+ alias: IMPORT_typing.Optional[str] = None,
+ *,
+ key: IMPORT_typing.Optional[IMPORT_uuid.UUID] = None,
+ how: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.How] = None,
+ ) -> Ping._ConstructIdempotently:
+ return Ping._ConstructIdempotently(
+ _idempotency=IMPORT_reboot_aio_contexts.Context.idempotency(
+ alias=alias,
+ key=key,
+ how=how,
+ ),
+ )
+
+ @classmethod
+ def per_workflow(
+ cls,
+ alias: IMPORT_typing.Optional[str] = None,
+ ):
+ return cls.idempotently(alias)
+
+ @classmethod
+ def per_iteration(
+ cls,
+ alias: IMPORT_typing.Optional[str] = None,
+ ):
+ return cls.idempotently(
+ alias,
+ how=IMPORT_reboot_aio_idempotency.PER_ITERATION,
+ )
+
+ @classmethod
+ def always(cls):
+ return cls.idempotently(
+ how=IMPORT_reboot_aio_idempotency.ALWAYS,
+ )
+
+ @IMPORT_dataclasses.dataclass(frozen=True)
+ class _ConstructIdempotently:
+
+ _idempotency: IMPORT_reboot_aio_idempotency.Idempotency
+
+
+Pong_ScheduleTypeVar = IMPORT_typing.TypeVar('Pong_ScheduleTypeVar', 'Pong.WeakReference._Schedule', 'Pong.WeakReference._WriterSchedule')
+Pong_IdempotentlyScheduleTypeVar = IMPORT_typing.TypeVar('Pong_IdempotentlyScheduleTypeVar', 'Pong.WeakReference._Schedule', 'Pong.WeakReference._WriterSchedule')
+
+Pong_UntilCallableType = IMPORT_typing.TypeVar('Pong_UntilCallableType')
+
+class PongSingleton:
+ Servicer: IMPORT_typing.TypeAlias = PongSingletonServicer
+
+
+class Pong:
+
+
+ Servicer: IMPORT_typing.TypeAlias = PongServicer
+
+ singleton: IMPORT_typing.TypeAlias = PongSingleton
+
+ Effects: IMPORT_typing.TypeAlias = PongBaseServicer.Effects
+
+ Authorizer: IMPORT_typing.TypeAlias = PongAuthorizer
+
+ State: IMPORT_typing.TypeAlias = IMPORT_typing.cast(type, IMPORT_api.Pong.state)
+
+
+ DoPongResponse: IMPORT_typing.TypeAlias = IMPORT_typing.cast(type, IMPORT_api.Pong.methods['do_pong'].response)
+
+
+ NumPongsResponse: IMPORT_typing.TypeAlias = IMPORT_typing.cast(type, IMPORT_api.Pong.methods['num_pongs'].response)
+
+ __state_type_name__ = IMPORT_reboot_aio_types.StateTypeName("reboot.ping.Pong")
+
+ class DoPongTask:
+ """Represents a scheduled task running for the
+ state. Note that this is not a coroutine because we are trying
+ to convey the semantics that the task is already running (or
+ will soon be).
+ """
+
+ @classmethod
+ def retrieve(
+ cls,
+ context: IMPORT_reboot_aio_contexts.Context | IMPORT_reboot_aio_external.ExternalContext,
+ *,
+ task_id: IMPORT_rbt_v1alpha1.tasks_pb2.TaskId,
+ ):
+ return cls(context, task_id=task_id)
+
+ def __init__(
+ self,
+ context: IMPORT_reboot_aio_contexts.Context | IMPORT_reboot_aio_external.ExternalContext,
+ *,
+ task_id: IMPORT_rbt_v1alpha1.tasks_pb2.TaskId,
+ ) -> None:
+ # Depending on the context type (inside or outside a Reboot application)
+ # we may or may not know the application ID. If we don't know it, then
+ # the `ExternalContext.gateway` will determine it.
+ #
+ # TODO: in the future we expect to support cross-application calls, in
+ # which case the developer may explicitly pass in an application ID
+ # here.
+ self._application_id: IMPORT_typing.Optional[IMPORT_reboot_aio_types.ApplicationId] = None
+ if isinstance(context, IMPORT_reboot_aio_contexts.Context):
+ self._application_id = context.application_id
+ self._channel_manager = context.channel_manager
+ self._task_id = task_id
+
+ @property
+ def task_id(self) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ return self._task_id
+
+ def __await__(self) -> IMPORT_typing.Generator[None, None, Pong.DoPongResponse]:
+ """Awaits for task to finish and returns its response."""
+ async def wait_for_task() -> reboot.ping.ping_api_pb2.PongDoPongResponse:
+ channel = self._channel_manager.get_channel_to_state(
+ IMPORT_reboot_aio_types.StateTypeName(self._task_id.state_type),
+ IMPORT_reboot_aio_types.StateRef(self._task_id.state_ref),
+ )
+
+ stub = IMPORT_rbt_v1alpha1.tasks_pb2_grpc.TasksStub(channel)
+
+ try:
+ call = IMPORT_reboot_aio_stubs.UnaryRetriedCall(
+ call=None, # `RetriedCall` can create the call itself.
+ stub_method=stub.Wait,
+ method_name="Wait",
+ request=IMPORT_rbt_v1alpha1.tasks_pb2.WaitRequest(task_id=self._task_id),
+ metadata=IMPORT_reboot_aio_headers.Headers(
+ state_ref=IMPORT_reboot_aio_types.StateRef(self._task_id.state_ref),
+ application_id=self._application_id,
+ ).to_grpc_metadata(),
+ aborted_type=IMPORT_reboot.aio.aborted.SystemAborted,
+ )
+
+ wait_for_task_response = await call
+ except IMPORT_reboot.aio.aborted.SystemAborted as error:
+ if error.code == IMPORT_grpc.StatusCode.NOT_FOUND:
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.UnknownTask()
+ ) from None
+
+ raise
+ else:
+ response_or_error: IMPORT_typing.Optional[IMPORT_google_protobuf_any_pb2.Any] = None
+ is_error = False
+
+ if wait_for_task_response.response_or_error.WhichOneof("response_or_error") == "response":
+ response_or_error = wait_for_task_response.response_or_error.response
+ else:
+ is_error = True
+ response_or_error = wait_for_task_response.response_or_error.error
+
+ assert response_or_error is not None
+ assert response_or_error.TypeName() != ""
+
+ response = reboot.ping.ping_api_pb2.PongDoPongResponse()
+
+ if (
+ not is_error and response_or_error.TypeName() != response.DESCRIPTOR.full_name
+ ):
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.InvalidArgument(),
+ message=
+ f"task with UUID {IMPORT_uuid.UUID(bytes=self._task_id.task_uuid)} "
+ f"has a response of type '{response_or_error.TypeName()}' "
+ "but expecting type 'reboot.ping.ping_api_pb2.PongDoPongResponse'; "
+ "are you waiting on a task of the correct method?",
+ ) from None
+
+ if is_error:
+ aborted_type = Pong.DoPongAborted
+
+ # In Reboot >= 0.40.2 we expect the error to be a `google.rpc.Status`.
+ if response_or_error.Is(IMPORT_google_rpc_status_pb2.Status.DESCRIPTOR):
+ status = IMPORT_google_rpc_status_pb2.Status()
+ response_or_error.Unpack(status)
+ raise aborted_type.from_status(status)
+
+ # In Reboot < 0.40.2 workflows throwing declared errors behaved poorly;
+ # we don't aim to emulate its behavior. Indicate that we don't know the
+ # reason for the abort.
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ )
+
+ else:
+ response_or_error.Unpack(response)
+ return PongDoPongResponseFromProto(response)
+
+ return wait_for_task().__await__()
+
+ DoPongEffects: IMPORT_typing.TypeAlias = PongBaseServicer.DoPongEffects
+
+ class DoPongAborted(IMPORT_reboot.aio.aborted.Aborted):
+
+
+ Error = IMPORT_typing.Union[
+ IMPORT_reboot.aio.aborted.GrpcError,
+ IMPORT_reboot.aio.aborted.RebootError,
+ ]
+
+ METHOD_PROTOBUF_ERROR_TYPES: list[type[IMPORT_google_protobuf_message.Message]] = [
+ ]
+
+ PROTOBUF_ERROR_TYPES: list[type[IMPORT_google_protobuf_message.Message | IMPORT_reboot_api.Model]] = (
+ METHOD_PROTOBUF_ERROR_TYPES +
+ IMPORT_reboot.aio.aborted.GRPC_ERROR_TYPES +
+ IMPORT_reboot.aio.aborted.REBOOT_ERROR_TYPES
+ )
+
+ _error: Error
+
+ MethodProtobufError = IMPORT_typing.Union[
+ IMPORT_reboot.aio.aborted.GrpcError,
+ IMPORT_reboot.aio.aborted.RebootError,
+ ]
+ _method_protobuf_error: MethodProtobufError
+
+ _code: IMPORT_grpc.StatusCode
+ _message: IMPORT_typing.Optional[str]
+
+ def __init__(
+ self,
+ error: IMPORT_reboot.aio.aborted.GrpcError,
+ *,
+ message: IMPORT_typing.Optional[str] = None,
+ # Do not set this value when constructing in order to
+ # raise. This is only used internally when constructing
+ # from aborted calls.
+ error_types: IMPORT_typing.Sequence[type[MethodProtobufError]] = (
+ METHOD_PROTOBUF_ERROR_TYPES + IMPORT_reboot.aio.aborted.GRPC_ERROR_TYPES
+ ),
+ ):
+ super().__init__()
+
+ self._message = None
+
+ if self.is_declared_error(error):
+ # If the error is a declared error, we should always
+ # store both formats for Pydantic and Protobuf for that
+ # error, so users can access the Pydantic version via
+ # `aborted.error` and the Protobuf version will be used
+ # to send the error over the wire.
+ if isinstance(error, IMPORT_reboot.api.Model):
+ # Happens when somebody raises an aborted with a Pydantic
+ # error format, usually in the backend method
+ # implementation.
+
+ self._error = error
+
+ else:
+ # Usually happens on the client side when we are
+ # constructing an aborted from the `AioRpcError`, so
+ # the error will be in Protobuf shape, and we want
+ # to convert it back to Pydantic if possible.
+ self._method_protobuf_error = error
+
+ else:
+ # Non declared error should be well known Reboot or gRPC
+ # error, which always comes in the Protobuf format, however
+ # it might be a case when the server is using new API and
+ # client is old, so server might send a declared error from
+ # the server perspective, but client doesn't know that it
+ # is a declared error now, so we need to gracefully
+ # cover that case.
+ if isinstance(error, IMPORT_reboot.api.Model):
+ # Store both formats so users can access the Pydantic
+ # version via `aborted.error` and the Protobuf version
+ # will be used to send the error over the wire.
+ self._error = IMPORT_rbt_v1alpha1.errors_pb2.Unknown()
+ self._method_protobuf_error = IMPORT_rbt_v1alpha1.errors_pb2.Unknown()
+
+ self._message = (
+ "Received a declared error from the server, "
+ "but the client doesn't know about it; "
+ "please update the client to the latest version\n"
+ ) + (
+ f"Original error: {error.model_dump_json()}\n"
+ ) + message if message is not None else ""
+
+ # Store both formats so users can access the error via
+ # `aborted.error` and the Protobuf version
+ # will be used to send the error over the wire.
+ self._error = error
+ self._method_protobuf_error = error
+
+ IMPORT_reboot_aio_types.assert_type(self._protobuf_error, error_types)
+
+ code = self.grpc_status_code_from_error(self._protobuf_error)
+
+ if code is None:
+ # Must be a Reboot specific or declared method error.
+ code = IMPORT_grpc.StatusCode.ABORTED
+
+ self._code = code
+
+ self._message = self._message if self._message is not None else message
+
+ @property
+ def error(self) -> Error | MethodProtobufError:
+ return self._error
+
+ @property
+ def _protobuf_error(self) -> MethodProtobufError:
+ return self._method_protobuf_error
+
+ @property
+ def code(self) -> IMPORT_grpc.StatusCode:
+ return self._code
+
+ @property
+ def message(self) -> IMPORT_typing.Optional[str]:
+ return self._message
+
+ @classmethod
+ def from_status(cls, status: IMPORT_google_rpc_status_pb2.Status):
+ error = cls.error_from_google_rpc_status_details(
+ status,
+ cls.PROTOBUF_ERROR_TYPES,
+ )
+
+ message = status.message if len(status.message) > 0 else None
+
+ if error is not None:
+ return cls(error, message=message, error_types=cls.PROTOBUF_ERROR_TYPES)
+
+ error = cls.error_from_google_rpc_status_code(status)
+
+ assert error is not None
+
+ # TODO(benh): also consider getting the type names from
+ # `status.details` and including that in `message` to make
+ # debugging easier.
+
+ return cls(error, message=message)
+
+ @classmethod
+ def from_grpc_aio_rpc_error(cls, aio_rpc_error: IMPORT_grpc.aio.AioRpcError):
+ return cls(
+ cls.error_from_grpc_aio_rpc_error(aio_rpc_error),
+ message=aio_rpc_error.details(),
+ )
+
+ @classmethod
+ def is_declared_error(cls, error: IMPORT_google_protobuf_message.Message | IMPORT_reboot.api.Model) -> bool:
+ if isinstance(error, IMPORT_reboot.api.Model):
+ return type(error) in IMPORT_api.Pong.methods['do_pong'].errors
+ return False
+
+ class NumPongsTask:
+ """Represents a scheduled task running for the
+ state. Note that this is not a coroutine because we are trying
+ to convey the semantics that the task is already running (or
+ will soon be).
+ """
+
+ @classmethod
+ def retrieve(
+ cls,
+ context: IMPORT_reboot_aio_contexts.Context | IMPORT_reboot_aio_external.ExternalContext,
+ *,
+ task_id: IMPORT_rbt_v1alpha1.tasks_pb2.TaskId,
+ ):
+ return cls(context, task_id=task_id)
+
+ def __init__(
+ self,
+ context: IMPORT_reboot_aio_contexts.Context | IMPORT_reboot_aio_external.ExternalContext,
+ *,
+ task_id: IMPORT_rbt_v1alpha1.tasks_pb2.TaskId,
+ ) -> None:
+ # Depending on the context type (inside or outside a Reboot application)
+ # we may or may not know the application ID. If we don't know it, then
+ # the `ExternalContext.gateway` will determine it.
+ #
+ # TODO: in the future we expect to support cross-application calls, in
+ # which case the developer may explicitly pass in an application ID
+ # here.
+ self._application_id: IMPORT_typing.Optional[IMPORT_reboot_aio_types.ApplicationId] = None
+ if isinstance(context, IMPORT_reboot_aio_contexts.Context):
+ self._application_id = context.application_id
+ self._channel_manager = context.channel_manager
+ self._task_id = task_id
+
+ @property
+ def task_id(self) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ return self._task_id
+
+ def __await__(self) -> IMPORT_typing.Generator[None, None, Pong.NumPongsResponse]:
+ """Awaits for task to finish and returns its response."""
+ async def wait_for_task() -> reboot.ping.ping_api_pb2.PongNumPongsResponse:
+ channel = self._channel_manager.get_channel_to_state(
+ IMPORT_reboot_aio_types.StateTypeName(self._task_id.state_type),
+ IMPORT_reboot_aio_types.StateRef(self._task_id.state_ref),
+ )
+
+ stub = IMPORT_rbt_v1alpha1.tasks_pb2_grpc.TasksStub(channel)
+
+ try:
+ call = IMPORT_reboot_aio_stubs.UnaryRetriedCall(
+ call=None, # `RetriedCall` can create the call itself.
+ stub_method=stub.Wait,
+ method_name="Wait",
+ request=IMPORT_rbt_v1alpha1.tasks_pb2.WaitRequest(task_id=self._task_id),
+ metadata=IMPORT_reboot_aio_headers.Headers(
+ state_ref=IMPORT_reboot_aio_types.StateRef(self._task_id.state_ref),
+ application_id=self._application_id,
+ ).to_grpc_metadata(),
+ aborted_type=IMPORT_reboot.aio.aborted.SystemAborted,
+ )
+
+ wait_for_task_response = await call
+ except IMPORT_reboot.aio.aborted.SystemAborted as error:
+ if error.code == IMPORT_grpc.StatusCode.NOT_FOUND:
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.UnknownTask()
+ ) from None
+
+ raise
+ else:
+ response_or_error: IMPORT_typing.Optional[IMPORT_google_protobuf_any_pb2.Any] = None
+ is_error = False
+
+ if wait_for_task_response.response_or_error.WhichOneof("response_or_error") == "response":
+ response_or_error = wait_for_task_response.response_or_error.response
+ else:
+ is_error = True
+ response_or_error = wait_for_task_response.response_or_error.error
+
+ assert response_or_error is not None
+ assert response_or_error.TypeName() != ""
+
+ response = reboot.ping.ping_api_pb2.PongNumPongsResponse()
+
+ if (
+ not is_error and response_or_error.TypeName() != response.DESCRIPTOR.full_name
+ ):
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.InvalidArgument(),
+ message=
+ f"task with UUID {IMPORT_uuid.UUID(bytes=self._task_id.task_uuid)} "
+ f"has a response of type '{response_or_error.TypeName()}' "
+ "but expecting type 'reboot.ping.ping_api_pb2.PongNumPongsResponse'; "
+ "are you waiting on a task of the correct method?",
+ ) from None
+
+ if is_error:
+ aborted_type = Pong.NumPongsAborted
+
+ # In Reboot >= 0.40.2 we expect the error to be a `google.rpc.Status`.
+ if response_or_error.Is(IMPORT_google_rpc_status_pb2.Status.DESCRIPTOR):
+ status = IMPORT_google_rpc_status_pb2.Status()
+ response_or_error.Unpack(status)
+ raise aborted_type.from_status(status)
+
+ # In Reboot < 0.40.2 workflows throwing declared errors behaved poorly;
+ # we don't aim to emulate its behavior. Indicate that we don't know the
+ # reason for the abort.
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ )
+
+ else:
+ response_or_error.Unpack(response)
+ return PongNumPongsResponseFromProto(response)
+
+ return wait_for_task().__await__()
+
+
+ class NumPongsAborted(IMPORT_reboot.aio.aborted.Aborted):
+
+
+ Error = IMPORT_typing.Union[
+ IMPORT_reboot.aio.aborted.GrpcError,
+ IMPORT_reboot.aio.aborted.RebootError,
+ ]
+
+ METHOD_PROTOBUF_ERROR_TYPES: list[type[IMPORT_google_protobuf_message.Message]] = [
+ ]
+
+ PROTOBUF_ERROR_TYPES: list[type[IMPORT_google_protobuf_message.Message | IMPORT_reboot_api.Model]] = (
+ METHOD_PROTOBUF_ERROR_TYPES +
+ IMPORT_reboot.aio.aborted.GRPC_ERROR_TYPES +
+ IMPORT_reboot.aio.aborted.REBOOT_ERROR_TYPES
+ )
+
+ _error: Error
+
+ MethodProtobufError = IMPORT_typing.Union[
+ IMPORT_reboot.aio.aborted.GrpcError,
+ IMPORT_reboot.aio.aborted.RebootError,
+ ]
+ _method_protobuf_error: MethodProtobufError
+
+ _code: IMPORT_grpc.StatusCode
+ _message: IMPORT_typing.Optional[str]
+
+ def __init__(
+ self,
+ error: IMPORT_reboot.aio.aborted.GrpcError,
+ *,
+ message: IMPORT_typing.Optional[str] = None,
+ # Do not set this value when constructing in order to
+ # raise. This is only used internally when constructing
+ # from aborted calls.
+ error_types: IMPORT_typing.Sequence[type[MethodProtobufError]] = (
+ METHOD_PROTOBUF_ERROR_TYPES + IMPORT_reboot.aio.aborted.GRPC_ERROR_TYPES
+ ),
+ ):
+ super().__init__()
+
+ self._message = None
+
+ if self.is_declared_error(error):
+ # If the error is a declared error, we should always
+ # store both formats for Pydantic and Protobuf for that
+ # error, so users can access the Pydantic version via
+ # `aborted.error` and the Protobuf version will be used
+ # to send the error over the wire.
+ if isinstance(error, IMPORT_reboot.api.Model):
+ # Happens when somebody raises an aborted with a Pydantic
+ # error format, usually in the backend method
+ # implementation.
+
+ self._error = error
+
+ else:
+ # Usually happens on the client side when we are
+ # constructing an aborted from the `AioRpcError`, so
+ # the error will be in Protobuf shape, and we want
+ # to convert it back to Pydantic if possible.
+ self._method_protobuf_error = error
+
+ else:
+ # Non declared error should be well known Reboot or gRPC
+ # error, which always comes in the Protobuf format, however
+ # it might be a case when the server is using new API and
+ # client is old, so server might send a declared error from
+ # the server perspective, but client doesn't know that it
+ # is a declared error now, so we need to gracefully
+ # cover that case.
+ if isinstance(error, IMPORT_reboot.api.Model):
+ # Store both formats so users can access the Pydantic
+ # version via `aborted.error` and the Protobuf version
+ # will be used to send the error over the wire.
+ self._error = IMPORT_rbt_v1alpha1.errors_pb2.Unknown()
+ self._method_protobuf_error = IMPORT_rbt_v1alpha1.errors_pb2.Unknown()
+
+ self._message = (
+ "Received a declared error from the server, "
+ "but the client doesn't know about it; "
+ "please update the client to the latest version\n"
+ ) + (
+ f"Original error: {error.model_dump_json()}\n"
+ ) + message if message is not None else ""
+
+ # Store both formats so users can access the error via
+ # `aborted.error` and the Protobuf version
+ # will be used to send the error over the wire.
+ self._error = error
+ self._method_protobuf_error = error
+
+ IMPORT_reboot_aio_types.assert_type(self._protobuf_error, error_types)
+
+ code = self.grpc_status_code_from_error(self._protobuf_error)
+
+ if code is None:
+ # Must be a Reboot specific or declared method error.
+ code = IMPORT_grpc.StatusCode.ABORTED
+
+ self._code = code
+
+ self._message = self._message if self._message is not None else message
+
+ @property
+ def error(self) -> Error | MethodProtobufError:
+ return self._error
+
+ @property
+ def _protobuf_error(self) -> MethodProtobufError:
+ return self._method_protobuf_error
+
+ @property
+ def code(self) -> IMPORT_grpc.StatusCode:
+ return self._code
+
+ @property
+ def message(self) -> IMPORT_typing.Optional[str]:
+ return self._message
+
+ @classmethod
+ def from_status(cls, status: IMPORT_google_rpc_status_pb2.Status):
+ error = cls.error_from_google_rpc_status_details(
+ status,
+ cls.PROTOBUF_ERROR_TYPES,
+ )
+
+ message = status.message if len(status.message) > 0 else None
+
+ if error is not None:
+ return cls(error, message=message, error_types=cls.PROTOBUF_ERROR_TYPES)
+
+ error = cls.error_from_google_rpc_status_code(status)
+
+ assert error is not None
+
+ # TODO(benh): also consider getting the type names from
+ # `status.details` and including that in `message` to make
+ # debugging easier.
+
+ return cls(error, message=message)
+
+ @classmethod
+ def from_grpc_aio_rpc_error(cls, aio_rpc_error: IMPORT_grpc.aio.AioRpcError):
+ return cls(
+ cls.error_from_grpc_aio_rpc_error(aio_rpc_error),
+ message=aio_rpc_error.details(),
+ )
+
+ @classmethod
+ def is_declared_error(cls, error: IMPORT_google_protobuf_message.Message | IMPORT_reboot.api.Model) -> bool:
+ if isinstance(error, IMPORT_reboot.api.Model):
+ return type(error) in IMPORT_api.Pong.methods['num_pongs'].errors
+ return False
+
+
+ class WeakReference(IMPORT_typing.Generic[Pong_ScheduleTypeVar]):
+
+ _schedule_type: type[Pong_ScheduleTypeVar]
+
+ def __init__(
+ self,
+ # When application ID is None, refers to a state within the application given by the context.
+ application_id: IMPORT_typing.Optional[IMPORT_reboot_aio_types.ApplicationId],
+ state_id: IMPORT_reboot_aio_types.StateId,
+ *,
+ schedule_type: type[Pong_ScheduleTypeVar],
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ servicer: IMPORT_typing.Optional[PongBaseServicer] = None,
+ ):
+ self._application_id = application_id
+ self._state_ref = IMPORT_reboot_aio_types.StateRef.from_id(
+ Pong.__state_type_name__,
+ state_id,
+ )
+ self._schedule_type = schedule_type
+ self._idempotency_manager: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.IdempotencyManager] = None
+ self._reader_stub: IMPORT_typing.Optional[PongReaderStub] = None
+ self._writer_stub: IMPORT_typing.Optional[PongWriterStub] = None
+ self._workflow_stub: IMPORT_typing.Optional[PongWorkflowStub] = None
+ self._tasks_stub: IMPORT_typing.Optional[PongTasksStub] = None
+ self._bearer_token = bearer_token
+ self._servicer = servicer
+
+ @property
+ def state_id(self) -> IMPORT_reboot_aio_types.StateId:
+ return self._state_ref.id
+
+ def _reader(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext | IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ ) -> PongReaderStub:
+ if self._reader_stub is None:
+ self._reader_stub = PongReaderStub(
+ context=context,
+ state_ref=self._state_ref,
+ bearer_token=self._bearer_token,
+ )
+ assert self._reader_stub is not None
+ if self._idempotency_manager is None:
+ self._idempotency_manager = context
+ elif self._idempotency_manager != context:
+ raise IMPORT_reboot_aio_call.MixedContextsError(
+ "This `WeakReference` for `Pong` with ID "
+ f"'{self.state_id}' has previously been used by a "
+ "different `Context`. That is not allowed. "
+ "Instead create a new `WeakReference` for every `Context` by calling "
+ f"`Pong.ref('{self.state_id}')`."
+ )
+ return self._reader_stub
+
+ def _writer(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ ) -> PongWriterStub:
+ if self._writer_stub is None:
+ self._writer_stub = PongWriterStub(
+ context=context,
+ state_ref=self._state_ref,
+ bearer_token=self._bearer_token,
+ )
+ assert self._writer_stub is not None
+ if self._idempotency_manager is None:
+ self._idempotency_manager = context
+ elif self._idempotency_manager != context:
+ raise IMPORT_reboot_aio_call.MixedContextsError(
+ "This `WeakReference` for `Pong` with ID "
+ f"'{self.state_id}' has previously been used by a "
+ "different `Context`. That is not allowed. "
+ "Instead create a new `WeakReference` for every `Context` by calling "
+ f"`Pong.ref('{self.state_id}')`."
+ )
+ return self._writer_stub
+
+ def _workflow(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ ) -> PongWorkflowStub:
+ if self._workflow_stub is None:
+ self._workflow_stub = PongWorkflowStub(
+ context=context,
+ state_ref=self._state_ref,
+ bearer_token=self._bearer_token,
+ )
+ assert self._workflow_stub is not None
+ if self._idempotency_manager is None:
+ self._idempotency_manager = context
+ elif self._idempotency_manager != context:
+ raise IMPORT_reboot_aio_call.MixedContextsError(
+ "This `WeakReference` for `Pong` with ID "
+ f"'{self.state_id}' has previously been used by a "
+ "different `Context`. That is not allowed. "
+ "Instead create a new `WeakReference` for every `Context` by calling "
+ f"`Pong.ref('{self.state_id}')`."
+ )
+ return self._workflow_stub
+
+ def _tasks(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ ) -> PongTasksStub:
+ if self._tasks_stub is None:
+ self._tasks_stub = PongTasksStub(
+ context=context,
+ state_ref=self._state_ref,
+ bearer_token=self._bearer_token,
+ )
+ assert self._tasks_stub is not None
+ if self._idempotency_manager is None:
+ self._idempotency_manager = context
+ elif self._idempotency_manager != context:
+ raise IMPORT_reboot_aio_call.MixedContextsError(
+ "This `WeakReference` for `Pong` with ID "
+ f"'{self.state_id}' has previously been used by a "
+ "different `Context`. That is not allowed. "
+ "Instead create a new `WeakReference` for every `Context` by calling "
+ f"`Pong.ref('{self.state_id}')`."
+ )
+ return self._tasks_stub
+
+ class _Reactively:
+
+ def __init__(
+ self,
+ *,
+ application_id: IMPORT_typing.Optional[IMPORT_reboot_aio_types.ApplicationId],
+ state_ref: IMPORT_reboot_aio_types.StateRef,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ):
+ self._application_id = application_id
+ self._state_ref = state_ref
+ self._bearer_token = bearer_token
+
+
+ async def NumPongs( # type: ignore[misc]
+ # In methods which are dealing with user input, (i.e.,
+ # proto message field names), we should use '__double_underscored__'
+ # variables to avoid any potential name conflicts with the method's
+ # parameters.
+ # The '__self__' parameter is a convention in Python to
+ # indicate that this method is a bound method, so we use
+ # '__this__' instead.
+ __this__,
+ # Explicitly-reactive calls only make sense in the context of either...
+ # (A) an external client, or...
+ # (B) methods that may reasonably run for a long time, which in Reboot means: readers or workflows.
+ __context__: IMPORT_reboot_aio_external.ExternalContext | IMPORT_reboot_aio_contexts.ReaderContext | IMPORT_reboot_aio_contexts.WorkflowContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> IMPORT_typing.AsyncIterator[Pong.NumPongsResponse]:
+ IMPORT_reboot_aio_types.assert_type(__context__, [IMPORT_reboot_aio_external.ExternalContext, IMPORT_reboot_aio_contexts.ReaderContext, IMPORT_reboot_aio_contexts.WorkflowContext])
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+ __caller_id__: IMPORT_typing.Optional[IMPORT_reboot_aio_caller_id.CallerID] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ if __bearer_token__ is None:
+ __bearer_token__ = __this__._bearer_token
+ if __bearer_token__ is None and isinstance(__context__, IMPORT_reboot_aio_external.ExternalContext):
+ # Within a Reboot context we do not pass on the caller's bearer token, as that might
+ # have security implications - we cannot simply trust any service we are calling with
+ # the user's credentials. Instead, the developer can rely on the default app-internal
+ # auth, or override that and set an explicit bearer token.
+ #
+ # In the case of `ExternalContext`, however, its `bearer_token` was set specifically
+ # by the developer for the purpose of making these calls. Note that only
+ # `ExternalContext` even has a `bearer_token` field.
+ __bearer_token__ = __context__.bearer_token
+
+ if __metadata__ is None:
+ __metadata__ = ()
+
+ if isinstance(__context__, IMPORT_reboot_aio_contexts.Context):
+ __caller_id__ = IMPORT_reboot_aio_caller_id.CallerID(
+ application_id=__context__.application_id,
+ )
+ if __this__._application_id is None:
+ # Given our context type (inside a Reboot application) we can default to
+ # making the application send traffic to itself.
+ __this__._application_id = __context__.application_id
+ elif isinstance(__context__, IMPORT_reboot_aio_external.ExternalContext):
+ __caller_id__ = __context__.caller_id
+
+ __headers__ = IMPORT_reboot_aio_headers.Headers(
+ bearer_token=__bearer_token__,
+ state_ref=__this__._state_ref,
+ application_id=__this__._application_id,
+ caller_id=__caller_id__,
+ )
+
+ __metadata__ += __headers__.to_grpc_metadata()
+
+ __query_backoff__ = IMPORT_reboot_aio_backoff.Backoff()
+ while True:
+ __call__ = None
+ try:
+ async with __context__.channel_manager.get_channel_to_state(
+ IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Pong'),
+ __this__._state_ref,
+ ) as __channel__:
+
+ __call__ = IMPORT_rbt_v1alpha1.react_pb2_grpc.ReactStub(
+ __channel__
+ ).Query(
+ IMPORT_rbt_v1alpha1.react_pb2.QueryRequest(
+ method='NumPongs',
+ request=PongNumPongsRequestToProto(
+ ).SerializeToString(),
+ ),
+ metadata=__metadata__,
+ )
+
+ async for __query_response__ in __call__:
+ # Clear the backoff so we don't wait
+ # as long the next time we get
+ # disconnected.
+ __query_backoff__.clear()
+
+ # The backend may have sent us this query
+ # response only to let us know that a new
+ # idempotency key has been recorded; there may
+ # not be a new response. Python callers don't
+ # (currently) care about such an event, so we
+ # simply ignore it.
+ if not __query_response__.HasField("response"):
+ continue
+
+ __response__ = reboot.ping.ping_api_pb2.PongNumPongsResponse()
+ __response__.ParseFromString(__query_response__.response)
+ yield PongNumPongsResponseFromProto(__response__)
+
+ except IMPORT_grpc.aio.AioRpcError as error:
+ # We expect to get disconnected from the server
+ # from time to time, e.g., when it is being
+ # updated, but we don't want that error to
+ # propagate, we just want to retry.
+ if IMPORT_reboot.aio.aborted.is_grpc_retryable_exception(error):
+ logger.debug(
+ "Reactive read to 'reboot.ping.PongMethods.NumPongs' "
+ f"failed with a retryable error: '{error}'; "
+ "will retry..."
+ )
+ await __query_backoff__()
+ continue
+ if error.code() == IMPORT_grpc.StatusCode.ABORTED:
+ # Reconstitute the error that the server threw, if it was a declared error.
+ status = await IMPORT_rpc_status_async.from_call(__call__)
+ if status is not None:
+ raise Pong.NumPongsAborted.from_status(
+ status
+ ) from None
+ raise Pong.NumPongsAborted.from_grpc_aio_rpc_error(
+ error
+ ) from None
+
+ raise
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ num_pongs = NumPongs
+
+ def reactively(self):
+ return Pong.WeakReference._Reactively(
+ application_id=self._application_id,
+ state_ref=self._state_ref,
+ bearer_token=self._bearer_token,
+ )
+
+ class _Idempotently(IMPORT_typing.Generic[Pong_IdempotentlyScheduleTypeVar]):
+
+ _weak_reference: Pong.WeakReference[Pong_IdempotentlyScheduleTypeVar]
+
+ def __init__(
+ self,
+ *,
+ weak_reference: Pong.WeakReference[Pong_IdempotentlyScheduleTypeVar],
+ idempotency: IMPORT_reboot_aio_idempotency.Idempotency,
+ ):
+ self._weak_reference = weak_reference
+ self._idempotency = idempotency
+
+ def schedule(
+ self,
+ *,
+ when: IMPORT_typing.Optional[IMPORT_datetime_datetime | IMPORT_datetime_timedelta] = None,
+ ) -> Pong_IdempotentlyScheduleTypeVar:
+ return self._weak_reference._schedule_type(
+ self._weak_reference._application_id,
+ self._weak_reference._tasks,
+ when=when,
+ idempotency=self._idempotency,
+ )
+
+ def spawn(
+ self,
+ *,
+ when: IMPORT_typing.Optional[IMPORT_datetime_datetime | IMPORT_datetime_timedelta] = None,
+ ) -> Pong.WeakReference._Spawn:
+ return Pong.WeakReference._Spawn(
+ self._weak_reference._application_id,
+ self._weak_reference._tasks,
+ when=when,
+ idempotency=self._idempotency,
+ )
+
+ async def read(
+ self,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ ) -> Pong.State:
+ if self._weak_reference._servicer is None:
+ raise RuntimeError(
+ "`read()` is currently only supported within workflows; "
+ "Please reach out and let us know your use case if this "
+ "is important for you!"
+ )
+
+ return await PongBaseServicer.WorkflowState._Idempotently._read(
+ self._weak_reference._servicer,
+ self._idempotency,
+ context,
+ )
+
+ async def write(
+ self,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ writer: PongBaseServicer.InlineWriterCallable[PongBaseServicer.InlineWriterCallableResult],
+ *,
+ type: IMPORT_reboot_aio_workflows.Type | IMPORT_reboot_aio_workflows._Unset = IMPORT_reboot_aio_workflows._UNSET,
+ ) -> PongBaseServicer.InlineWriterCallableResult:
+ if self._weak_reference._servicer is None:
+ raise RuntimeError(
+ "`write()` is currently only supported within workflows; "
+ "Please reach out and let us know your use case if this "
+ "is important for you!"
+ )
+
+ # `type=` is optional: when omitted, `writer`'s
+ # return annotation is used.
+ type_t, _ = IMPORT_reboot_aio_workflows._resolve_callable_return_type(
+ writer, type,
+ )
+
+ return await PongBaseServicer.WorkflowState._Idempotently._write_validating_effects(
+ self._weak_reference._servicer,
+ self._idempotency,
+ context,
+ writer,
+ type_result=type_t,
+ check_type=not self._idempotency.always,
+ unidempotently=self._idempotency.always,
+ checkpoint=context.checkpoint(),
+ )
+
+
+ async def DoPong( # type: ignore[misc]
+ # In methods which are dealing with user input, (i.e.,
+ # proto message field names), we should use '__double_underscored__'
+ # variables to avoid any potential name conflicts with the method's
+ # parameters.
+ # The '__self__' parameter is a convention in Python to
+ # indicate that this method is a bound method, so we use
+ # '__this__' instead.
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> Pong.DoPongResponse:
+ __options__ = __options__ or IMPORT_reboot_aio_call.Options()
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+
+ return await __this__._weak_reference.DoPong(
+ __context__,
+ __options__,
+ __idempotency__=__this__._idempotency,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ do_pong = DoPong
+
+ async def NumPongs( # type: ignore[misc]
+ # In methods which are dealing with user input, (i.e.,
+ # proto message field names), we should use '__double_underscored__'
+ # variables to avoid any potential name conflicts with the method's
+ # parameters.
+ # The '__self__' parameter is a convention in Python to
+ # indicate that this method is a bound method, so we use
+ # '__this__' instead.
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> Pong.NumPongsResponse:
+ __options__ = __options__ or IMPORT_reboot_aio_call.Options()
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+
+ return await __this__._weak_reference.NumPongs(
+ __context__,
+ __options__,
+ __idempotency__=__this__._idempotency,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ num_pongs = NumPongs
+
+ @IMPORT_typing.overload
+ def idempotently(self, alias: IMPORT_typing.Optional[str] = None, *, how: IMPORT_reboot_aio_idempotency.How = IMPORT_reboot_aio_idempotency.PER_WORKFLOW) -> Pong.WeakReference._Idempotently[Pong_ScheduleTypeVar]:
+ ...
+
+ @IMPORT_typing.overload
+ def idempotently(self, *, key: IMPORT_uuid.UUID, how: IMPORT_reboot_aio_idempotency.How = IMPORT_reboot_aio_idempotency.PER_WORKFLOW) -> Pong.WeakReference._Idempotently[Pong_ScheduleTypeVar]:
+ ...
+
+ def idempotently(
+ self,
+ alias: IMPORT_typing.Optional[str] = None,
+ *,
+ key: IMPORT_typing.Optional[IMPORT_uuid.UUID] = None,
+ how: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.How] = None,
+ ) -> Pong.WeakReference._Idempotently[Pong_ScheduleTypeVar]:
+ return Pong.WeakReference._Idempotently(
+ weak_reference=self,
+ idempotency=IMPORT_reboot_aio_contexts.Context.idempotency(
+ alias=alias,
+ key=key,
+ how=how,
+ )
+ )
+
+ def per_workflow(self, alias: IMPORT_typing.Optional[str] = None):
+ return self.idempotently(alias)
+
+ def per_iteration(self, alias: IMPORT_typing.Optional[str] = None):
+ return self.idempotently(
+ alias,
+ how=IMPORT_reboot_aio_idempotency.PER_ITERATION,
+ )
+
+ def always(self):
+ return self.idempotently(
+ how=IMPORT_reboot_aio_idempotency.ALWAYS,
+ )
+
+ class _UntilChangesSatisfies(IMPORT_typing.Generic[Pong_UntilCallableType]):
+
+ _idempotency_alias: str
+ _context: IMPORT_reboot_aio_contexts.WorkflowContext
+ _callable: IMPORT_typing.Callable[[], IMPORT_typing.Awaitable[Pong_UntilCallableType]]
+ _type: type[Pong_UntilCallableType]
+
+ def __init__(
+ self,
+ *,
+ idempotency_alias: str,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ callable: IMPORT_typing.Callable[[], IMPORT_typing.Awaitable[Pong_UntilCallableType]],
+ type: type[Pong_UntilCallableType],
+ ):
+ self._idempotency_alias = idempotency_alias
+ self._context = context
+ self._callable = callable
+ self._type = type
+
+ async def changes(self):
+ return await IMPORT_reboot_aio_workflows.until_changes(
+ self._idempotency_alias,
+ self._context,
+ self._callable,
+ type=self._type,
+ )
+
+ async def satisfies(
+ self,
+ condition: IMPORT_typing.Callable[[Pong_UntilCallableType], bool],
+ ):
+
+ async def converge():
+ response = await self._callable()
+ if condition(response):
+ return response
+ return False
+
+ return await IMPORT_reboot_aio_workflows.until(
+ self._idempotency_alias,
+ self._context,
+ converge,
+ type=self._type,
+ )
+
+ class _Until:
+
+ _weak_reference: Pong.WeakReference
+ _idempotency_alias: str
+
+ def __init__(
+ self,
+ *,
+ weak_reference: Pong.WeakReference,
+ idempotency_alias: str,
+ ):
+ self._weak_reference = weak_reference
+ self._idempotency_alias = idempotency_alias
+
+ def read(
+ self,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ ) -> Pong.WeakReference._UntilChangesSatisfies[Pong.State]:
+ IMPORT_reboot_aio_types.assert_type(
+ context,
+ [IMPORT_reboot_aio_contexts.WorkflowContext],
+ )
+
+ async def callable():
+ return await self._weak_reference.read(context)
+
+ return Pong.WeakReference._UntilChangesSatisfies(
+ idempotency_alias=self._idempotency_alias,
+ context=context,
+ callable=callable,
+ type=Pong.State,
+ )
+
+
+ def NumPongs( # type: ignore[misc]
+ # In methods which are dealing with user input, (i.e.,
+ # proto message field names), we should use '__double_underscored__'
+ # variables to avoid any potential name conflicts with the method's
+ # parameters.
+ # The '__self__' parameter is a convention in Python to
+ # indicate that this method is a bound method, so we use
+ # '__this__' instead.
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.WorkflowContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> Pong.WeakReference._UntilChangesSatisfies[Pong.NumPongsResponse]:
+ IMPORT_reboot_aio_types.assert_type(
+ __context__,
+ [IMPORT_reboot_aio_contexts.WorkflowContext],
+ )
+
+ __options__ = __options__ or IMPORT_reboot_aio_call.Options()
+
+ async def callable():
+ return await __this__._weak_reference.NumPongs(
+ __context__,
+ __options__,
+ )
+
+ return Pong.WeakReference._UntilChangesSatisfies(
+ idempotency_alias=__this__._idempotency_alias,
+ context=__context__,
+ callable=callable,
+ type=Pong.NumPongsResponse,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ num_pongs = NumPongs
+
+ def until(self, alias: str):
+ return Pong.WeakReference._Until(
+ weak_reference=self,
+ idempotency_alias=alias,
+ )
+
+ def schedule(
+ self,
+ *,
+ when: IMPORT_typing.Optional[IMPORT_datetime_datetime | IMPORT_datetime_timedelta] = None,
+ ) -> Pong_ScheduleTypeVar:
+ return self._schedule_type(self._application_id, self._tasks, when=when)
+
+ class _Schedule:
+
+ def __init__(
+ self,
+ application_id: IMPORT_typing.Optional[IMPORT_reboot_aio_types.ApplicationId],
+ tasks: IMPORT_typing.Callable[[IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext], PongTasksStub],
+ *,
+ when: IMPORT_typing.Optional[IMPORT_datetime_datetime | IMPORT_datetime_timedelta] = None,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ ) -> None:
+ self._application_id = application_id
+ self._tasks = tasks
+ self._when = ensure_has_timezone(when=when)
+ self._idempotency = idempotency
+
+ # Pong callable tasks:
+
+ async def DoPong( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ IMPORT_reboot_aio_types.assert_type(__context__, [IMPORT_reboot_aio_contexts.TransactionContext])
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ __schedule__: IMPORT_typing.Optional[IMPORT_reboot_time_DateTimeWithTimeZone] = (IMPORT_reboot_time_DateTimeWithTimeZone.now() + __this__._when) if isinstance(
+ __this__._when, IMPORT_datetime_timedelta
+ ) else __this__._when
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = __this__._idempotency
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ # Add scheduling information to the metadata.
+ __metadata__ = (
+ (IMPORT_reboot_aio_headers.TASK_SCHEDULE,
+ __schedule__.isoformat() if __schedule__ else ''),
+ ) + (__metadata__ or tuple())
+
+ __task_id__ = await __this__._tasks(
+ __context__
+ ).DoPong(
+ idempotency=__idempotency__,
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ )
+
+ return __task_id__
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ do_pong = DoPong
+
+ async def NumPongs( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ IMPORT_reboot_aio_types.assert_type(__context__, [IMPORT_reboot_aio_contexts.TransactionContext])
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ __schedule__: IMPORT_typing.Optional[IMPORT_reboot_time_DateTimeWithTimeZone] = (IMPORT_reboot_time_DateTimeWithTimeZone.now() + __this__._when) if isinstance(
+ __this__._when, IMPORT_datetime_timedelta
+ ) else __this__._when
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = __this__._idempotency
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ # Add scheduling information to the metadata.
+ __metadata__ = (
+ (IMPORT_reboot_aio_headers.TASK_SCHEDULE,
+ __schedule__.isoformat() if __schedule__ else ''),
+ ) + (__metadata__ or tuple())
+
+ __task_id__ = await __this__._tasks(
+ __context__
+ ).NumPongs(
+ idempotency=__idempotency__,
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ )
+
+ return __task_id__
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ num_pongs = NumPongs
+
+ # A `WriterContext` can not call any methods in `_Schedule` to
+ # prevent a writer from doing a `Foo.ref()` and trying to
+ # schedule. However, we want to allow a writer to schedule
+ # when we are constructing a `WeakReference` from
+ # `self.ref()` so instead we return a `_WriterSchedule` to
+ # provide type safety that allows a `WriterContext` to
+ # schedule (for itself).
+ class _WriterSchedule:
+
+ def __init__(
+ self,
+ application_id: IMPORT_typing.Optional[IMPORT_reboot_aio_types.ApplicationId],
+ tasks: IMPORT_typing.Callable[[IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext], PongTasksStub],
+ *,
+ when: IMPORT_typing.Optional[IMPORT_datetime_datetime | IMPORT_datetime_timedelta] = None,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ ) -> None:
+ self._tasks = tasks
+ self._when = ensure_has_timezone(when=when)
+ self._idempotency = idempotency
+
+ # Pong callable tasks:
+
+ async def DoPong( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ # Only `writer`s and `transaction`s should ``schedule()`, a
+ # `workflow` should `spawn()`.
+ IMPORT_reboot_aio_types.assert_type(__context__, [IMPORT_reboot_aio_contexts.WriterContext, IMPORT_reboot_aio_contexts.TransactionContext])
+
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ if isinstance(__context__, IMPORT_reboot_aio_contexts.WriterContext):
+ return (await PongServicerTasks(
+ context=__context__,
+ state_ref=__context__._state_ref,
+ ).DoPong(
+ schedule=__this__._when,
+ )).task_id
+
+ __schedule__: IMPORT_typing.Optional[IMPORT_reboot_time_DateTimeWithTimeZone] = (IMPORT_reboot_time_DateTimeWithTimeZone.now() + __this__._when) if isinstance(
+ __this__._when, IMPORT_datetime_timedelta
+ ) else __this__._when
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = __this__._idempotency
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ # Add scheduling information to the metadata.
+ __metadata__ = (
+ (IMPORT_reboot_aio_headers.TASK_SCHEDULE,
+ __schedule__.isoformat() if __schedule__ else ''),
+ ) + (__metadata__ or tuple())
+
+ return await __this__._tasks(
+ __context__
+ ).DoPong(
+ idempotency=__idempotency__,
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ do_pong = DoPong
+
+ async def NumPongs( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ # Only `writer`s and `transaction`s should ``schedule()`, a
+ # `workflow` should `spawn()`.
+ IMPORT_reboot_aio_types.assert_type(__context__, [IMPORT_reboot_aio_contexts.WriterContext, IMPORT_reboot_aio_contexts.TransactionContext])
+
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ if isinstance(__context__, IMPORT_reboot_aio_contexts.WriterContext):
+ return (await PongServicerTasks(
+ context=__context__,
+ state_ref=__context__._state_ref,
+ ).NumPongs(
+ schedule=__this__._when,
+ )).task_id
+
+ __schedule__: IMPORT_typing.Optional[IMPORT_reboot_time_DateTimeWithTimeZone] = (IMPORT_reboot_time_DateTimeWithTimeZone.now() + __this__._when) if isinstance(
+ __this__._when, IMPORT_datetime_timedelta
+ ) else __this__._when
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = __this__._idempotency
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ # Add scheduling information to the metadata.
+ __metadata__ = (
+ (IMPORT_reboot_aio_headers.TASK_SCHEDULE,
+ __schedule__.isoformat() if __schedule__ else ''),
+ ) + (__metadata__ or tuple())
+
+ return await __this__._tasks(
+ __context__
+ ).NumPongs(
+ idempotency=__idempotency__,
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ num_pongs = NumPongs
+
+ def spawn(
+ self,
+ *,
+ when: IMPORT_typing.Optional[IMPORT_datetime_datetime | IMPORT_datetime_timedelta] = None,
+ ) -> Pong.WeakReference._Spawn:
+ # Within a `workflow`, all "bare" `spawn()` calls are
+ # syntactic sugar for `per_workflow()`, unless we're
+ # within a control loop, in which case they are syntactic
+ # sugar for `per_iteration()`.
+ context = IMPORT_reboot_aio_contexts.Context.get()
+ if context is not None:
+ if isinstance(context, IMPORT_reboot_aio_contexts.WorkflowContext):
+ return (
+ self.per_iteration() if context.within_loop()
+ else self.per_workflow()
+ ).spawn(when=when)
+ elif isinstance(context, IMPORT_reboot_aio_external.InitializeContext):
+ return self.idempotently().spawn(when=when)
+
+ return Pong.WeakReference._Spawn(
+ self._application_id, self._tasks, when=when
+ )
+
+ class _Spawn:
+
+ def __init__(
+ self,
+ application_id: IMPORT_typing.Optional[IMPORT_reboot_aio_types.ApplicationId],
+ tasks: IMPORT_typing.Callable[[IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext], PongTasksStub],
+ *,
+ when: IMPORT_typing.Optional[IMPORT_datetime_datetime | IMPORT_datetime_timedelta] = None,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ ) -> None:
+ self._application_id = application_id
+ self._tasks = tasks
+ self._when = ensure_has_timezone(when=when)
+ self._idempotency = idempotency
+
+ # Pong callable tasks:
+
+ async def DoPong( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> Pong.DoPongTask:
+ IMPORT_reboot_aio_types.assert_type(__context__, [IMPORT_reboot_aio_contexts.WorkflowContext, IMPORT_reboot_aio_external.ExternalContext])
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+ __schedule__: IMPORT_typing.Optional[IMPORT_reboot_time_DateTimeWithTimeZone] = (IMPORT_reboot_time_DateTimeWithTimeZone.now() + __this__._when) if isinstance(
+ __this__._when, IMPORT_datetime_timedelta
+ ) else __this__._when
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = __this__._idempotency
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ # Add scheduling information to the metadata.
+ __metadata__ = (
+ (IMPORT_reboot_aio_headers.TASK_SCHEDULE,
+ __schedule__.isoformat() if __schedule__ else ''),
+ ) + (__metadata__ or tuple())
+
+ __task_id__ = await __this__._tasks(
+ __context__
+ ).DoPong(
+ idempotency=__idempotency__,
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ )
+
+ return Pong.DoPongTask(
+ __context__,
+ task_id=__task_id__,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ do_pong = DoPong
+
+ async def NumPongs( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> Pong.NumPongsTask:
+ IMPORT_reboot_aio_types.assert_type(__context__, [IMPORT_reboot_aio_contexts.WorkflowContext, IMPORT_reboot_aio_external.ExternalContext])
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+ __schedule__: IMPORT_typing.Optional[IMPORT_reboot_time_DateTimeWithTimeZone] = (IMPORT_reboot_time_DateTimeWithTimeZone.now() + __this__._when) if isinstance(
+ __this__._when, IMPORT_datetime_timedelta
+ ) else __this__._when
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = __this__._idempotency
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ # Add scheduling information to the metadata.
+ __metadata__ = (
+ (IMPORT_reboot_aio_headers.TASK_SCHEDULE,
+ __schedule__.isoformat() if __schedule__ else ''),
+ ) + (__metadata__ or tuple())
+
+ __task_id__ = await __this__._tasks(
+ __context__
+ ).NumPongs(
+ idempotency=__idempotency__,
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ )
+
+ return Pong.NumPongsTask(
+ __context__,
+ task_id=__task_id__,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ num_pongs = NumPongs
+
+ async def read(
+ self,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ ) -> Pong.State:
+ return await (
+ self.always() if context.within_until()
+ else (
+ self.per_iteration() if context.within_loop()
+ else self.per_workflow()
+ )
+ ).read(context)
+
+ async def write(
+ self,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ writer: PongBaseServicer.InlineWriterCallable[PongBaseServicer.InlineWriterCallableResult],
+ *,
+ type: IMPORT_reboot_aio_workflows.Type | IMPORT_reboot_aio_workflows._Unset = IMPORT_reboot_aio_workflows._UNSET,
+ ) -> PongBaseServicer.InlineWriterCallableResult:
+ """Perform an "inline write" within a workflow.
+
+ `type=` is optional: when omitted, `writer`'s return
+ annotation is used.
+ """
+ # We forward `type=` along to the inner `write` (which
+ # also infers if not passed). Pass through whichever
+ # form the user supplied -- explicit class or the
+ # `_UNSET` sentinel to trigger inference downstream.
+ return await (
+ self.always() if context.within_until()
+ else (
+ self.per_iteration() if context.within_loop()
+ else self.per_workflow()
+ )
+ ).write(
+ context, writer, type=type
+ )
+
+ # Pong specific methods:
+
+ async def DoPong( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ *,
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ ) -> Pong.DoPongResponse:
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ # Within a `workflow`, all "bare" calls are
+ # `per_workflow()` calls, unless we're within a control
+ # loop, in which case they are syntactic sugar for
+ # `per_iteration()`.
+ if __idempotency__ is None:
+ if isinstance(__context__, IMPORT_reboot_aio_contexts.WorkflowContext):
+ return await (
+ __this__.per_iteration() if __context__.within_loop()
+ else __this__.per_workflow()
+ ).DoPong(
+ __context__,
+ __options__ or IMPORT_reboot_aio_call.Options(),
+ )
+ elif isinstance(__context__, IMPORT_reboot_aio_external.InitializeContext):
+ return await __this__.idempotently().DoPong(
+ __context__,
+ __options__ or IMPORT_reboot_aio_call.Options(),
+ )
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ return PongDoPongResponseFromProto(
+ await __this__._writer(__context__).DoPong(
+ idempotency=__idempotency__,
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ )
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ do_pong = DoPong
+
+ async def NumPongs( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.ReaderContext | IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ *,
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ ) -> Pong.NumPongsResponse:
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ # Within a `workflow`, all "bare" calls are
+ # `per_workflow()` calls, unless we're within a control
+ # loop, in which case they are syntactic sugar for
+ # `per_iteration()`.
+ #
+ # Unless we are "within until" in which case all "bare"
+ # calls are `.always().
+ if __idempotency__ is None:
+ if isinstance(__context__, IMPORT_reboot_aio_contexts.WorkflowContext):
+ return await (
+ __this__.always() if __context__.within_until()
+ else (
+ __this__.per_iteration() if __context__.within_loop()
+ else __this__.per_workflow()
+ )
+ ).NumPongs(
+ __context__,
+ __options__ or IMPORT_reboot_aio_call.Options(),
+ )
+ elif isinstance(__context__, IMPORT_reboot_aio_external.InitializeContext):
+ return await __this__.idempotently().NumPongs(
+ __context__,
+ __options__ or IMPORT_reboot_aio_call.Options(),
+ )
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+ return PongNumPongsResponseFromProto(
+ await __this__._reader(__context__).NumPongs(
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ idempotency=__idempotency__,
+ )
+ )
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ num_pongs = NumPongs
+
+ class _Forall:
+
+ _ids: IMPORT_typing.Iterable[str]
+
+ def __init__(self, ids: IMPORT_typing.Iterable[str]):
+ self._ids = ids
+
+
+ async def DoPong( # type: ignore[misc]
+ # In methods which are dealing with user input, (i.e.,
+ # proto message field names), we should use '__double_underscored__'
+ # variables to avoid any potential name conflicts with the method's
+ # parameters.
+ # The '__self__' parameter is a convention in Python to
+ # indicate that this method is a bound method, so we use
+ # '__this__' instead.
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> list[Pong.DoPongResponse]:
+ __options__ = __options__ or IMPORT_reboot_aio_call.Options()
+ return await IMPORT_asyncio.gather(
+ *[
+ Pong.ref(
+ id
+ ).DoPong(
+ __context__,
+ __options__,
+ ) for id in __this__._ids
+ ]
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ do_pong = DoPong
+
+ async def NumPongs( # type: ignore[misc]
+ # In methods which are dealing with user input, (i.e.,
+ # proto message field names), we should use '__double_underscored__'
+ # variables to avoid any potential name conflicts with the method's
+ # parameters.
+ # The '__self__' parameter is a convention in Python to
+ # indicate that this method is a bound method, so we use
+ # '__this__' instead.
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.ReaderContext | IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> list[Pong.NumPongsResponse]:
+ __options__ = __options__ or IMPORT_reboot_aio_call.Options()
+ return await IMPORT_asyncio.gather(
+ *[
+ Pong.ref(
+ id
+ ).NumPongs(
+ __context__,
+ __options__,
+ ) for id in __this__._ids
+ ]
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ num_pongs = NumPongs
+
+ @classmethod
+ def forall(cls, ids: IMPORT_typing.Iterable[str]) -> Pong._Forall:
+ return Pong._Forall(ids)
+
+ @classmethod
+ def ref(
+ cls,
+ state_id: IMPORT_typing.Optional[IMPORT_reboot_aio_types.StateId] = None,
+ *,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ) -> Pong.WeakReference[Pong.WeakReference._Schedule] | Pong.WeakReference[Pong.WeakReference._WriterSchedule]:
+ # We support calling `Pong.ref()` with
+ # no `state_id` __only__ inside a workflow to be able to call an
+ # inline writer, inline reader or other method call, since
+ # workflow is a `classmethod` and therefor we can't get a
+ # reference to outselves as `self.ref()`.
+ if state_id is None:
+ context = IMPORT_reboot_aio_contexts.Context.get()
+
+ if context is None:
+ raise RuntimeError(
+ 'Missing asyncio context variable `context`; '
+ 'are you using this class without Reboot?'
+ )
+
+ if not isinstance(context, IMPORT_reboot_aio_contexts.WorkflowContext):
+ raise RuntimeError(
+ '`ref()` called without a `state_id` can only be used within a Workflow.'
+ )
+
+ servicer = PongBaseServicer.__servicer__.get()
+
+ if servicer is None:
+ raise RuntimeError(
+ 'Missing asyncio context variable `servicer`; '
+ 'are you using this class without Reboot?'
+ )
+
+ return Pong.WeakReference(
+ # TODO(https://github.com/reboot-dev/mono/issues/3226): add support for calling other applications.
+ # For now this always stays within the application that creates the context.
+ application_id=None,
+ state_id=context._state_ref.id,
+ schedule_type=Pong.WeakReference._WriterSchedule,
+ # If the user didn't specify a bearer token we may still end up using the app-internal bearer token,
+ # but that's decided at the time of the call.
+ bearer_token=bearer_token,
+ servicer=servicer,
+ )
+
+ return Pong.WeakReference(
+ # TODO(https://github.com/reboot-dev/mono/issues/3226): add support for calling other applications.
+ # For now this always stays within the application that creates the context.
+ application_id=None,
+ state_id=state_id,
+ schedule_type=Pong.WeakReference._Schedule,
+ bearer_token=bearer_token,
+ )
+
+
+ @IMPORT_typing.overload
+ @classmethod
+ def idempotently(cls, alias: IMPORT_typing.Optional[str] = None, *, how: IMPORT_reboot_aio_idempotency.How = IMPORT_reboot_aio_idempotency.PER_WORKFLOW) -> Pong._ConstructIdempotently:
+ ...
+
+ @IMPORT_typing.overload
+ @classmethod
+ def idempotently(cls, *, key: IMPORT_uuid.UUID, how: IMPORT_reboot_aio_idempotency.How = IMPORT_reboot_aio_idempotency.PER_WORKFLOW) -> Pong._ConstructIdempotently:
+ ...
+
+ @classmethod
+ def idempotently(
+ cls,
+ alias: IMPORT_typing.Optional[str] = None,
+ *,
+ key: IMPORT_typing.Optional[IMPORT_uuid.UUID] = None,
+ how: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.How] = None,
+ ) -> Pong._ConstructIdempotently:
+ return Pong._ConstructIdempotently(
+ _idempotency=IMPORT_reboot_aio_contexts.Context.idempotency(
+ alias=alias,
+ key=key,
+ how=how,
+ ),
+ )
+
+ @classmethod
+ def per_workflow(
+ cls,
+ alias: IMPORT_typing.Optional[str] = None,
+ ):
+ return cls.idempotently(alias)
+
+ @classmethod
+ def per_iteration(
+ cls,
+ alias: IMPORT_typing.Optional[str] = None,
+ ):
+ return cls.idempotently(
+ alias,
+ how=IMPORT_reboot_aio_idempotency.PER_ITERATION,
+ )
+
+ @classmethod
+ def always(cls):
+ return cls.idempotently(
+ how=IMPORT_reboot_aio_idempotency.ALWAYS,
+ )
+
+ @IMPORT_dataclasses.dataclass(frozen=True)
+ class _ConstructIdempotently:
+
+ _idempotency: IMPORT_reboot_aio_idempotency.Idempotency
+
+
+User_ScheduleTypeVar = IMPORT_typing.TypeVar('User_ScheduleTypeVar', 'User.WeakReference._Schedule', 'User.WeakReference._WriterSchedule')
+User_IdempotentlyScheduleTypeVar = IMPORT_typing.TypeVar('User_IdempotentlyScheduleTypeVar', 'User.WeakReference._Schedule', 'User.WeakReference._WriterSchedule')
+
+User_UntilCallableType = IMPORT_typing.TypeVar('User_UntilCallableType')
+
+class UserSingleton:
+ Servicer: IMPORT_typing.TypeAlias = UserSingletonServicer
+
+
+class User:
+
+
+ Servicer: IMPORT_typing.TypeAlias = UserServicer
+
+ singleton: IMPORT_typing.TypeAlias = UserSingleton
+
+ Effects: IMPORT_typing.TypeAlias = UserBaseServicer.Effects
+
+ Authorizer: IMPORT_typing.TypeAlias = UserAuthorizer
+
+ State: IMPORT_typing.TypeAlias = IMPORT_typing.cast(type, IMPORT_api.User.state)
+
+ CreateCounterRequest: IMPORT_typing.TypeAlias = IMPORT_typing.cast(type, IMPORT_api.User.methods['create_counter'].request)
+ CreateCounterResponse: IMPORT_typing.TypeAlias = IMPORT_typing.cast(type, IMPORT_api.User.methods['create_counter'].response)
+
+
+ ListCountersResponse: IMPORT_typing.TypeAlias = IMPORT_typing.cast(type, IMPORT_api.User.methods['list_counters'].response)
+
+
+ WhoamiResponse: IMPORT_typing.TypeAlias = IMPORT_typing.cast(type, IMPORT_api.User.methods['whoami'].response)
+
+
+
+
+ __state_type_name__ = IMPORT_reboot_aio_types.StateTypeName("reboot.ping.User")
+
+ class CreateCounterTask:
+ """Represents a scheduled task running for the
+ state. Note that this is not a coroutine because we are trying
+ to convey the semantics that the task is already running (or
+ will soon be).
+ """
+
+ @classmethod
+ def retrieve(
+ cls,
+ context: IMPORT_reboot_aio_contexts.Context | IMPORT_reboot_aio_external.ExternalContext,
+ *,
+ task_id: IMPORT_rbt_v1alpha1.tasks_pb2.TaskId,
+ ):
+ return cls(context, task_id=task_id)
+
+ def __init__(
+ self,
+ context: IMPORT_reboot_aio_contexts.Context | IMPORT_reboot_aio_external.ExternalContext,
+ *,
+ task_id: IMPORT_rbt_v1alpha1.tasks_pb2.TaskId,
+ ) -> None:
+ # Depending on the context type (inside or outside a Reboot application)
+ # we may or may not know the application ID. If we don't know it, then
+ # the `ExternalContext.gateway` will determine it.
+ #
+ # TODO: in the future we expect to support cross-application calls, in
+ # which case the developer may explicitly pass in an application ID
+ # here.
+ self._application_id: IMPORT_typing.Optional[IMPORT_reboot_aio_types.ApplicationId] = None
+ if isinstance(context, IMPORT_reboot_aio_contexts.Context):
+ self._application_id = context.application_id
+ self._channel_manager = context.channel_manager
+ self._task_id = task_id
+
+ @property
+ def task_id(self) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ return self._task_id
+
+ def __await__(self) -> IMPORT_typing.Generator[None, None, User.CreateCounterResponse]:
+ """Awaits for task to finish and returns its response."""
+ async def wait_for_task() -> reboot.ping.ping_api_pb2.UserCreateCounterResponse:
+ channel = self._channel_manager.get_channel_to_state(
+ IMPORT_reboot_aio_types.StateTypeName(self._task_id.state_type),
+ IMPORT_reboot_aio_types.StateRef(self._task_id.state_ref),
+ )
+
+ stub = IMPORT_rbt_v1alpha1.tasks_pb2_grpc.TasksStub(channel)
+
+ try:
+ call = IMPORT_reboot_aio_stubs.UnaryRetriedCall(
+ call=None, # `RetriedCall` can create the call itself.
+ stub_method=stub.Wait,
+ method_name="Wait",
+ request=IMPORT_rbt_v1alpha1.tasks_pb2.WaitRequest(task_id=self._task_id),
+ metadata=IMPORT_reboot_aio_headers.Headers(
+ state_ref=IMPORT_reboot_aio_types.StateRef(self._task_id.state_ref),
+ application_id=self._application_id,
+ ).to_grpc_metadata(),
+ aborted_type=IMPORT_reboot.aio.aborted.SystemAborted,
+ )
+
+ wait_for_task_response = await call
+ except IMPORT_reboot.aio.aborted.SystemAborted as error:
+ if error.code == IMPORT_grpc.StatusCode.NOT_FOUND:
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.UnknownTask()
+ ) from None
+
+ raise
+ else:
+ response_or_error: IMPORT_typing.Optional[IMPORT_google_protobuf_any_pb2.Any] = None
+ is_error = False
+
+ if wait_for_task_response.response_or_error.WhichOneof("response_or_error") == "response":
+ response_or_error = wait_for_task_response.response_or_error.response
+ else:
+ is_error = True
+ response_or_error = wait_for_task_response.response_or_error.error
+
+ assert response_or_error is not None
+ assert response_or_error.TypeName() != ""
+
+ response = reboot.ping.ping_api_pb2.UserCreateCounterResponse()
+
+ if (
+ not is_error and response_or_error.TypeName() != response.DESCRIPTOR.full_name
+ ):
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.InvalidArgument(),
+ message=
+ f"task with UUID {IMPORT_uuid.UUID(bytes=self._task_id.task_uuid)} "
+ f"has a response of type '{response_or_error.TypeName()}' "
+ "but expecting type 'reboot.ping.ping_api_pb2.UserCreateCounterResponse'; "
+ "are you waiting on a task of the correct method?",
+ ) from None
+
+ if is_error:
+ aborted_type = User.CreateCounterAborted
+
+ # In Reboot >= 0.40.2 we expect the error to be a `google.rpc.Status`.
+ if response_or_error.Is(IMPORT_google_rpc_status_pb2.Status.DESCRIPTOR):
+ status = IMPORT_google_rpc_status_pb2.Status()
+ response_or_error.Unpack(status)
+ raise aborted_type.from_status(status)
+
+ # In Reboot < 0.40.2 workflows throwing declared errors behaved poorly;
+ # we don't aim to emulate its behavior. Indicate that we don't know the
+ # reason for the abort.
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ )
+
+ else:
+ response_or_error.Unpack(response)
+ return UserCreateCounterResponseFromProto(response)
+
+ return wait_for_task().__await__()
+
+
+ class CreateCounterAborted(IMPORT_reboot.aio.aborted.Aborted):
+
+
+ Error = IMPORT_typing.Union[
+ IMPORT_reboot.aio.aborted.GrpcError,
+ IMPORT_reboot.aio.aborted.RebootError,
+ ]
+
+ METHOD_PROTOBUF_ERROR_TYPES: list[type[IMPORT_google_protobuf_message.Message]] = [
+ ]
+
+ PROTOBUF_ERROR_TYPES: list[type[IMPORT_google_protobuf_message.Message | IMPORT_reboot_api.Model]] = (
+ METHOD_PROTOBUF_ERROR_TYPES +
+ IMPORT_reboot.aio.aborted.GRPC_ERROR_TYPES +
+ IMPORT_reboot.aio.aborted.REBOOT_ERROR_TYPES
+ )
+
+ _error: Error
+
+ MethodProtobufError = IMPORT_typing.Union[
+ IMPORT_reboot.aio.aborted.GrpcError,
+ IMPORT_reboot.aio.aborted.RebootError,
+ ]
+ _method_protobuf_error: MethodProtobufError
+
+ _code: IMPORT_grpc.StatusCode
+ _message: IMPORT_typing.Optional[str]
+
+ def __init__(
+ self,
+ error: IMPORT_reboot.aio.aborted.GrpcError,
+ *,
+ message: IMPORT_typing.Optional[str] = None,
+ # Do not set this value when constructing in order to
+ # raise. This is only used internally when constructing
+ # from aborted calls.
+ error_types: IMPORT_typing.Sequence[type[MethodProtobufError]] = (
+ METHOD_PROTOBUF_ERROR_TYPES + IMPORT_reboot.aio.aborted.GRPC_ERROR_TYPES
+ ),
+ ):
+ super().__init__()
+
+ self._message = None
+
+ if self.is_declared_error(error):
+ # If the error is a declared error, we should always
+ # store both formats for Pydantic and Protobuf for that
+ # error, so users can access the Pydantic version via
+ # `aborted.error` and the Protobuf version will be used
+ # to send the error over the wire.
+ if isinstance(error, IMPORT_reboot.api.Model):
+ # Happens when somebody raises an aborted with a Pydantic
+ # error format, usually in the backend method
+ # implementation.
+
+ self._error = error
+
+ else:
+ # Usually happens on the client side when we are
+ # constructing an aborted from the `AioRpcError`, so
+ # the error will be in Protobuf shape, and we want
+ # to convert it back to Pydantic if possible.
+ self._method_protobuf_error = error
+
+ else:
+ # Non declared error should be well known Reboot or gRPC
+ # error, which always comes in the Protobuf format, however
+ # it might be a case when the server is using new API and
+ # client is old, so server might send a declared error from
+ # the server perspective, but client doesn't know that it
+ # is a declared error now, so we need to gracefully
+ # cover that case.
+ if isinstance(error, IMPORT_reboot.api.Model):
+ # Store both formats so users can access the Pydantic
+ # version via `aborted.error` and the Protobuf version
+ # will be used to send the error over the wire.
+ self._error = IMPORT_rbt_v1alpha1.errors_pb2.Unknown()
+ self._method_protobuf_error = IMPORT_rbt_v1alpha1.errors_pb2.Unknown()
+
+ self._message = (
+ "Received a declared error from the server, "
+ "but the client doesn't know about it; "
+ "please update the client to the latest version\n"
+ ) + (
+ f"Original error: {error.model_dump_json()}\n"
+ ) + message if message is not None else ""
+
+ # Store both formats so users can access the error via
+ # `aborted.error` and the Protobuf version
+ # will be used to send the error over the wire.
+ self._error = error
+ self._method_protobuf_error = error
+
+ IMPORT_reboot_aio_types.assert_type(self._protobuf_error, error_types)
+
+ code = self.grpc_status_code_from_error(self._protobuf_error)
+
+ if code is None:
+ # Must be a Reboot specific or declared method error.
+ code = IMPORT_grpc.StatusCode.ABORTED
+
+ self._code = code
+
+ self._message = self._message if self._message is not None else message
+
+ @property
+ def error(self) -> Error | MethodProtobufError:
+ return self._error
+
+ @property
+ def _protobuf_error(self) -> MethodProtobufError:
+ return self._method_protobuf_error
+
+ @property
+ def code(self) -> IMPORT_grpc.StatusCode:
+ return self._code
+
+ @property
+ def message(self) -> IMPORT_typing.Optional[str]:
+ return self._message
+
+ @classmethod
+ def from_status(cls, status: IMPORT_google_rpc_status_pb2.Status):
+ error = cls.error_from_google_rpc_status_details(
+ status,
+ cls.PROTOBUF_ERROR_TYPES,
+ )
+
+ message = status.message if len(status.message) > 0 else None
+
+ if error is not None:
+ return cls(error, message=message, error_types=cls.PROTOBUF_ERROR_TYPES)
+
+ error = cls.error_from_google_rpc_status_code(status)
+
+ assert error is not None
+
+ # TODO(benh): also consider getting the type names from
+ # `status.details` and including that in `message` to make
+ # debugging easier.
+
+ return cls(error, message=message)
+
+ @classmethod
+ def from_grpc_aio_rpc_error(cls, aio_rpc_error: IMPORT_grpc.aio.AioRpcError):
+ return cls(
+ cls.error_from_grpc_aio_rpc_error(aio_rpc_error),
+ message=aio_rpc_error.details(),
+ )
+
+ @classmethod
+ def is_declared_error(cls, error: IMPORT_google_protobuf_message.Message | IMPORT_reboot.api.Model) -> bool:
+ if isinstance(error, IMPORT_reboot.api.Model):
+ return type(error) in IMPORT_api.User.methods['create_counter'].errors
+ return False
+
+ class ListCountersTask:
+ """Represents a scheduled task running for the
+ state. Note that this is not a coroutine because we are trying
+ to convey the semantics that the task is already running (or
+ will soon be).
+ """
+
+ @classmethod
+ def retrieve(
+ cls,
+ context: IMPORT_reboot_aio_contexts.Context | IMPORT_reboot_aio_external.ExternalContext,
+ *,
+ task_id: IMPORT_rbt_v1alpha1.tasks_pb2.TaskId,
+ ):
+ return cls(context, task_id=task_id)
+
+ def __init__(
+ self,
+ context: IMPORT_reboot_aio_contexts.Context | IMPORT_reboot_aio_external.ExternalContext,
+ *,
+ task_id: IMPORT_rbt_v1alpha1.tasks_pb2.TaskId,
+ ) -> None:
+ # Depending on the context type (inside or outside a Reboot application)
+ # we may or may not know the application ID. If we don't know it, then
+ # the `ExternalContext.gateway` will determine it.
+ #
+ # TODO: in the future we expect to support cross-application calls, in
+ # which case the developer may explicitly pass in an application ID
+ # here.
+ self._application_id: IMPORT_typing.Optional[IMPORT_reboot_aio_types.ApplicationId] = None
+ if isinstance(context, IMPORT_reboot_aio_contexts.Context):
+ self._application_id = context.application_id
+ self._channel_manager = context.channel_manager
+ self._task_id = task_id
+
+ @property
+ def task_id(self) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ return self._task_id
+
+ def __await__(self) -> IMPORT_typing.Generator[None, None, User.ListCountersResponse]:
+ """Awaits for task to finish and returns its response."""
+ async def wait_for_task() -> reboot.ping.ping_api_pb2.UserListCountersResponse:
+ channel = self._channel_manager.get_channel_to_state(
+ IMPORT_reboot_aio_types.StateTypeName(self._task_id.state_type),
+ IMPORT_reboot_aio_types.StateRef(self._task_id.state_ref),
+ )
+
+ stub = IMPORT_rbt_v1alpha1.tasks_pb2_grpc.TasksStub(channel)
+
+ try:
+ call = IMPORT_reboot_aio_stubs.UnaryRetriedCall(
+ call=None, # `RetriedCall` can create the call itself.
+ stub_method=stub.Wait,
+ method_name="Wait",
+ request=IMPORT_rbt_v1alpha1.tasks_pb2.WaitRequest(task_id=self._task_id),
+ metadata=IMPORT_reboot_aio_headers.Headers(
+ state_ref=IMPORT_reboot_aio_types.StateRef(self._task_id.state_ref),
+ application_id=self._application_id,
+ ).to_grpc_metadata(),
+ aborted_type=IMPORT_reboot.aio.aborted.SystemAborted,
+ )
+
+ wait_for_task_response = await call
+ except IMPORT_reboot.aio.aborted.SystemAborted as error:
+ if error.code == IMPORT_grpc.StatusCode.NOT_FOUND:
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.UnknownTask()
+ ) from None
+
+ raise
+ else:
+ response_or_error: IMPORT_typing.Optional[IMPORT_google_protobuf_any_pb2.Any] = None
+ is_error = False
+
+ if wait_for_task_response.response_or_error.WhichOneof("response_or_error") == "response":
+ response_or_error = wait_for_task_response.response_or_error.response
+ else:
+ is_error = True
+ response_or_error = wait_for_task_response.response_or_error.error
+
+ assert response_or_error is not None
+ assert response_or_error.TypeName() != ""
+
+ response = reboot.ping.ping_api_pb2.UserListCountersResponse()
+
+ if (
+ not is_error and response_or_error.TypeName() != response.DESCRIPTOR.full_name
+ ):
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.InvalidArgument(),
+ message=
+ f"task with UUID {IMPORT_uuid.UUID(bytes=self._task_id.task_uuid)} "
+ f"has a response of type '{response_or_error.TypeName()}' "
+ "but expecting type 'reboot.ping.ping_api_pb2.UserListCountersResponse'; "
+ "are you waiting on a task of the correct method?",
+ ) from None
+
+ if is_error:
+ aborted_type = User.ListCountersAborted
+
+ # In Reboot >= 0.40.2 we expect the error to be a `google.rpc.Status`.
+ if response_or_error.Is(IMPORT_google_rpc_status_pb2.Status.DESCRIPTOR):
+ status = IMPORT_google_rpc_status_pb2.Status()
+ response_or_error.Unpack(status)
+ raise aborted_type.from_status(status)
+
+ # In Reboot < 0.40.2 workflows throwing declared errors behaved poorly;
+ # we don't aim to emulate its behavior. Indicate that we don't know the
+ # reason for the abort.
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ )
+
+ else:
+ response_or_error.Unpack(response)
+ return UserListCountersResponseFromProto(response)
+
+ return wait_for_task().__await__()
+
+
+ class ListCountersAborted(IMPORT_reboot.aio.aborted.Aborted):
+
+
+ Error = IMPORT_typing.Union[
+ IMPORT_reboot.aio.aborted.GrpcError,
+ IMPORT_reboot.aio.aborted.RebootError,
+ ]
+
+ METHOD_PROTOBUF_ERROR_TYPES: list[type[IMPORT_google_protobuf_message.Message]] = [
+ ]
+
+ PROTOBUF_ERROR_TYPES: list[type[IMPORT_google_protobuf_message.Message | IMPORT_reboot_api.Model]] = (
+ METHOD_PROTOBUF_ERROR_TYPES +
+ IMPORT_reboot.aio.aborted.GRPC_ERROR_TYPES +
+ IMPORT_reboot.aio.aborted.REBOOT_ERROR_TYPES
+ )
+
+ _error: Error
+
+ MethodProtobufError = IMPORT_typing.Union[
+ IMPORT_reboot.aio.aborted.GrpcError,
+ IMPORT_reboot.aio.aborted.RebootError,
+ ]
+ _method_protobuf_error: MethodProtobufError
+
+ _code: IMPORT_grpc.StatusCode
+ _message: IMPORT_typing.Optional[str]
+
+ def __init__(
+ self,
+ error: IMPORT_reboot.aio.aborted.GrpcError,
+ *,
+ message: IMPORT_typing.Optional[str] = None,
+ # Do not set this value when constructing in order to
+ # raise. This is only used internally when constructing
+ # from aborted calls.
+ error_types: IMPORT_typing.Sequence[type[MethodProtobufError]] = (
+ METHOD_PROTOBUF_ERROR_TYPES + IMPORT_reboot.aio.aborted.GRPC_ERROR_TYPES
+ ),
+ ):
+ super().__init__()
+
+ self._message = None
+
+ if self.is_declared_error(error):
+ # If the error is a declared error, we should always
+ # store both formats for Pydantic and Protobuf for that
+ # error, so users can access the Pydantic version via
+ # `aborted.error` and the Protobuf version will be used
+ # to send the error over the wire.
+ if isinstance(error, IMPORT_reboot.api.Model):
+ # Happens when somebody raises an aborted with a Pydantic
+ # error format, usually in the backend method
+ # implementation.
+
+ self._error = error
+
+ else:
+ # Usually happens on the client side when we are
+ # constructing an aborted from the `AioRpcError`, so
+ # the error will be in Protobuf shape, and we want
+ # to convert it back to Pydantic if possible.
+ self._method_protobuf_error = error
+
+ else:
+ # Non declared error should be well known Reboot or gRPC
+ # error, which always comes in the Protobuf format, however
+ # it might be a case when the server is using new API and
+ # client is old, so server might send a declared error from
+ # the server perspective, but client doesn't know that it
+ # is a declared error now, so we need to gracefully
+ # cover that case.
+ if isinstance(error, IMPORT_reboot.api.Model):
+ # Store both formats so users can access the Pydantic
+ # version via `aborted.error` and the Protobuf version
+ # will be used to send the error over the wire.
+ self._error = IMPORT_rbt_v1alpha1.errors_pb2.Unknown()
+ self._method_protobuf_error = IMPORT_rbt_v1alpha1.errors_pb2.Unknown()
+
+ self._message = (
+ "Received a declared error from the server, "
+ "but the client doesn't know about it; "
+ "please update the client to the latest version\n"
+ ) + (
+ f"Original error: {error.model_dump_json()}\n"
+ ) + message if message is not None else ""
+
+ # Store both formats so users can access the error via
+ # `aborted.error` and the Protobuf version
+ # will be used to send the error over the wire.
+ self._error = error
+ self._method_protobuf_error = error
+
+ IMPORT_reboot_aio_types.assert_type(self._protobuf_error, error_types)
+
+ code = self.grpc_status_code_from_error(self._protobuf_error)
+
+ if code is None:
+ # Must be a Reboot specific or declared method error.
+ code = IMPORT_grpc.StatusCode.ABORTED
+
+ self._code = code
+
+ self._message = self._message if self._message is not None else message
+
+ @property
+ def error(self) -> Error | MethodProtobufError:
+ return self._error
+
+ @property
+ def _protobuf_error(self) -> MethodProtobufError:
+ return self._method_protobuf_error
+
+ @property
+ def code(self) -> IMPORT_grpc.StatusCode:
+ return self._code
+
+ @property
+ def message(self) -> IMPORT_typing.Optional[str]:
+ return self._message
+
+ @classmethod
+ def from_status(cls, status: IMPORT_google_rpc_status_pb2.Status):
+ error = cls.error_from_google_rpc_status_details(
+ status,
+ cls.PROTOBUF_ERROR_TYPES,
+ )
+
+ message = status.message if len(status.message) > 0 else None
+
+ if error is not None:
+ return cls(error, message=message, error_types=cls.PROTOBUF_ERROR_TYPES)
+
+ error = cls.error_from_google_rpc_status_code(status)
+
+ assert error is not None
+
+ # TODO(benh): also consider getting the type names from
+ # `status.details` and including that in `message` to make
+ # debugging easier.
+
+ return cls(error, message=message)
+
+ @classmethod
+ def from_grpc_aio_rpc_error(cls, aio_rpc_error: IMPORT_grpc.aio.AioRpcError):
+ return cls(
+ cls.error_from_grpc_aio_rpc_error(aio_rpc_error),
+ message=aio_rpc_error.details(),
+ )
+
+ @classmethod
+ def is_declared_error(cls, error: IMPORT_google_protobuf_message.Message | IMPORT_reboot.api.Model) -> bool:
+ if isinstance(error, IMPORT_reboot.api.Model):
+ return type(error) in IMPORT_api.User.methods['list_counters'].errors
+ return False
+
+ class WhoamiTask:
+ """Represents a scheduled task running for the
+ state. Note that this is not a coroutine because we are trying
+ to convey the semantics that the task is already running (or
+ will soon be).
+ """
+
+ @classmethod
+ def retrieve(
+ cls,
+ context: IMPORT_reboot_aio_contexts.Context | IMPORT_reboot_aio_external.ExternalContext,
+ *,
+ task_id: IMPORT_rbt_v1alpha1.tasks_pb2.TaskId,
+ ):
+ return cls(context, task_id=task_id)
+
+ def __init__(
+ self,
+ context: IMPORT_reboot_aio_contexts.Context | IMPORT_reboot_aio_external.ExternalContext,
+ *,
+ task_id: IMPORT_rbt_v1alpha1.tasks_pb2.TaskId,
+ ) -> None:
+ # Depending on the context type (inside or outside a Reboot application)
+ # we may or may not know the application ID. If we don't know it, then
+ # the `ExternalContext.gateway` will determine it.
+ #
+ # TODO: in the future we expect to support cross-application calls, in
+ # which case the developer may explicitly pass in an application ID
+ # here.
+ self._application_id: IMPORT_typing.Optional[IMPORT_reboot_aio_types.ApplicationId] = None
+ if isinstance(context, IMPORT_reboot_aio_contexts.Context):
+ self._application_id = context.application_id
+ self._channel_manager = context.channel_manager
+ self._task_id = task_id
+
+ @property
+ def task_id(self) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ return self._task_id
+
+ def __await__(self) -> IMPORT_typing.Generator[None, None, User.WhoamiResponse]:
+ """Awaits for task to finish and returns its response."""
+ async def wait_for_task() -> reboot.ping.ping_api_pb2.UserWhoamiResponse:
+ channel = self._channel_manager.get_channel_to_state(
+ IMPORT_reboot_aio_types.StateTypeName(self._task_id.state_type),
+ IMPORT_reboot_aio_types.StateRef(self._task_id.state_ref),
+ )
+
+ stub = IMPORT_rbt_v1alpha1.tasks_pb2_grpc.TasksStub(channel)
+
+ try:
+ call = IMPORT_reboot_aio_stubs.UnaryRetriedCall(
+ call=None, # `RetriedCall` can create the call itself.
+ stub_method=stub.Wait,
+ method_name="Wait",
+ request=IMPORT_rbt_v1alpha1.tasks_pb2.WaitRequest(task_id=self._task_id),
+ metadata=IMPORT_reboot_aio_headers.Headers(
+ state_ref=IMPORT_reboot_aio_types.StateRef(self._task_id.state_ref),
+ application_id=self._application_id,
+ ).to_grpc_metadata(),
+ aborted_type=IMPORT_reboot.aio.aborted.SystemAborted,
+ )
+
+ wait_for_task_response = await call
+ except IMPORT_reboot.aio.aborted.SystemAborted as error:
+ if error.code == IMPORT_grpc.StatusCode.NOT_FOUND:
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.UnknownTask()
+ ) from None
+
+ raise
+ else:
+ response_or_error: IMPORT_typing.Optional[IMPORT_google_protobuf_any_pb2.Any] = None
+ is_error = False
+
+ if wait_for_task_response.response_or_error.WhichOneof("response_or_error") == "response":
+ response_or_error = wait_for_task_response.response_or_error.response
+ else:
+ is_error = True
+ response_or_error = wait_for_task_response.response_or_error.error
+
+ assert response_or_error is not None
+ assert response_or_error.TypeName() != ""
+
+ response = reboot.ping.ping_api_pb2.UserWhoamiResponse()
+
+ if (
+ not is_error and response_or_error.TypeName() != response.DESCRIPTOR.full_name
+ ):
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.InvalidArgument(),
+ message=
+ f"task with UUID {IMPORT_uuid.UUID(bytes=self._task_id.task_uuid)} "
+ f"has a response of type '{response_or_error.TypeName()}' "
+ "but expecting type 'reboot.ping.ping_api_pb2.UserWhoamiResponse'; "
+ "are you waiting on a task of the correct method?",
+ ) from None
+
+ if is_error:
+ aborted_type = User.WhoamiAborted
+
+ # In Reboot >= 0.40.2 we expect the error to be a `google.rpc.Status`.
+ if response_or_error.Is(IMPORT_google_rpc_status_pb2.Status.DESCRIPTOR):
+ status = IMPORT_google_rpc_status_pb2.Status()
+ response_or_error.Unpack(status)
+ raise aborted_type.from_status(status)
+
+ # In Reboot < 0.40.2 workflows throwing declared errors behaved poorly;
+ # we don't aim to emulate its behavior. Indicate that we don't know the
+ # reason for the abort.
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ )
+
+ else:
+ response_or_error.Unpack(response)
+ return UserWhoamiResponseFromProto(response)
+
+ return wait_for_task().__await__()
+
+
+ class WhoamiAborted(IMPORT_reboot.aio.aborted.Aborted):
+
+
+ Error = IMPORT_typing.Union[
+ IMPORT_reboot.aio.aborted.GrpcError,
+ IMPORT_reboot.aio.aborted.RebootError,
+ ]
+
+ METHOD_PROTOBUF_ERROR_TYPES: list[type[IMPORT_google_protobuf_message.Message]] = [
+ ]
+
+ PROTOBUF_ERROR_TYPES: list[type[IMPORT_google_protobuf_message.Message | IMPORT_reboot_api.Model]] = (
+ METHOD_PROTOBUF_ERROR_TYPES +
+ IMPORT_reboot.aio.aborted.GRPC_ERROR_TYPES +
+ IMPORT_reboot.aio.aborted.REBOOT_ERROR_TYPES
+ )
+
+ _error: Error
+
+ MethodProtobufError = IMPORT_typing.Union[
+ IMPORT_reboot.aio.aborted.GrpcError,
+ IMPORT_reboot.aio.aborted.RebootError,
+ ]
+ _method_protobuf_error: MethodProtobufError
+
+ _code: IMPORT_grpc.StatusCode
+ _message: IMPORT_typing.Optional[str]
+
+ def __init__(
+ self,
+ error: IMPORT_reboot.aio.aborted.GrpcError,
+ *,
+ message: IMPORT_typing.Optional[str] = None,
+ # Do not set this value when constructing in order to
+ # raise. This is only used internally when constructing
+ # from aborted calls.
+ error_types: IMPORT_typing.Sequence[type[MethodProtobufError]] = (
+ METHOD_PROTOBUF_ERROR_TYPES + IMPORT_reboot.aio.aborted.GRPC_ERROR_TYPES
+ ),
+ ):
+ super().__init__()
+
+ self._message = None
+
+ if self.is_declared_error(error):
+ # If the error is a declared error, we should always
+ # store both formats for Pydantic and Protobuf for that
+ # error, so users can access the Pydantic version via
+ # `aborted.error` and the Protobuf version will be used
+ # to send the error over the wire.
+ if isinstance(error, IMPORT_reboot.api.Model):
+ # Happens when somebody raises an aborted with a Pydantic
+ # error format, usually in the backend method
+ # implementation.
+
+ self._error = error
+
+ else:
+ # Usually happens on the client side when we are
+ # constructing an aborted from the `AioRpcError`, so
+ # the error will be in Protobuf shape, and we want
+ # to convert it back to Pydantic if possible.
+ self._method_protobuf_error = error
+
+ else:
+ # Non declared error should be well known Reboot or gRPC
+ # error, which always comes in the Protobuf format, however
+ # it might be a case when the server is using new API and
+ # client is old, so server might send a declared error from
+ # the server perspective, but client doesn't know that it
+ # is a declared error now, so we need to gracefully
+ # cover that case.
+ if isinstance(error, IMPORT_reboot.api.Model):
+ # Store both formats so users can access the Pydantic
+ # version via `aborted.error` and the Protobuf version
+ # will be used to send the error over the wire.
+ self._error = IMPORT_rbt_v1alpha1.errors_pb2.Unknown()
+ self._method_protobuf_error = IMPORT_rbt_v1alpha1.errors_pb2.Unknown()
+
+ self._message = (
+ "Received a declared error from the server, "
+ "but the client doesn't know about it; "
+ "please update the client to the latest version\n"
+ ) + (
+ f"Original error: {error.model_dump_json()}\n"
+ ) + message if message is not None else ""
+
+ # Store both formats so users can access the error via
+ # `aborted.error` and the Protobuf version
+ # will be used to send the error over the wire.
+ self._error = error
+ self._method_protobuf_error = error
+
+ IMPORT_reboot_aio_types.assert_type(self._protobuf_error, error_types)
+
+ code = self.grpc_status_code_from_error(self._protobuf_error)
+
+ if code is None:
+ # Must be a Reboot specific or declared method error.
+ code = IMPORT_grpc.StatusCode.ABORTED
+
+ self._code = code
+
+ self._message = self._message if self._message is not None else message
+
+ @property
+ def error(self) -> Error | MethodProtobufError:
+ return self._error
+
+ @property
+ def _protobuf_error(self) -> MethodProtobufError:
+ return self._method_protobuf_error
+
+ @property
+ def code(self) -> IMPORT_grpc.StatusCode:
+ return self._code
+
+ @property
+ def message(self) -> IMPORT_typing.Optional[str]:
+ return self._message
+
+ @classmethod
+ def from_status(cls, status: IMPORT_google_rpc_status_pb2.Status):
+ error = cls.error_from_google_rpc_status_details(
+ status,
+ cls.PROTOBUF_ERROR_TYPES,
+ )
+
+ message = status.message if len(status.message) > 0 else None
+
+ if error is not None:
+ return cls(error, message=message, error_types=cls.PROTOBUF_ERROR_TYPES)
+
+ error = cls.error_from_google_rpc_status_code(status)
+
+ assert error is not None
+
+ # TODO(benh): also consider getting the type names from
+ # `status.details` and including that in `message` to make
+ # debugging easier.
+
+ return cls(error, message=message)
+
+ @classmethod
+ def from_grpc_aio_rpc_error(cls, aio_rpc_error: IMPORT_grpc.aio.AioRpcError):
+ return cls(
+ cls.error_from_grpc_aio_rpc_error(aio_rpc_error),
+ message=aio_rpc_error.details(),
+ )
+
+ @classmethod
+ def is_declared_error(cls, error: IMPORT_google_protobuf_message.Message | IMPORT_reboot.api.Model) -> bool:
+ if isinstance(error, IMPORT_reboot.api.Model):
+ return type(error) in IMPORT_api.User.methods['whoami'].errors
+ return False
+
+ class CreateTask:
+ """Represents a scheduled task running for the
+ state. Note that this is not a coroutine because we are trying
+ to convey the semantics that the task is already running (or
+ will soon be).
+ """
+
+ @classmethod
+ def retrieve(
+ cls,
+ context: IMPORT_reboot_aio_contexts.Context | IMPORT_reboot_aio_external.ExternalContext,
+ *,
+ task_id: IMPORT_rbt_v1alpha1.tasks_pb2.TaskId,
+ ):
+ return cls(context, task_id=task_id)
+
+ def __init__(
+ self,
+ context: IMPORT_reboot_aio_contexts.Context | IMPORT_reboot_aio_external.ExternalContext,
+ *,
+ task_id: IMPORT_rbt_v1alpha1.tasks_pb2.TaskId,
+ ) -> None:
+ # Depending on the context type (inside or outside a Reboot application)
+ # we may or may not know the application ID. If we don't know it, then
+ # the `ExternalContext.gateway` will determine it.
+ #
+ # TODO: in the future we expect to support cross-application calls, in
+ # which case the developer may explicitly pass in an application ID
+ # here.
+ self._application_id: IMPORT_typing.Optional[IMPORT_reboot_aio_types.ApplicationId] = None
+ if isinstance(context, IMPORT_reboot_aio_contexts.Context):
+ self._application_id = context.application_id
+ self._channel_manager = context.channel_manager
+ self._task_id = task_id
+
+ @property
+ def task_id(self) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ return self._task_id
+
+ def __await__(self) -> IMPORT_typing.Generator[None, None, None]:
+ """Awaits for task to finish and returns its response."""
+ async def wait_for_task() -> google.protobuf.empty_pb2.Empty:
+ channel = self._channel_manager.get_channel_to_state(
+ IMPORT_reboot_aio_types.StateTypeName(self._task_id.state_type),
+ IMPORT_reboot_aio_types.StateRef(self._task_id.state_ref),
+ )
+
+ stub = IMPORT_rbt_v1alpha1.tasks_pb2_grpc.TasksStub(channel)
+
+ try:
+ call = IMPORT_reboot_aio_stubs.UnaryRetriedCall(
+ call=None, # `RetriedCall` can create the call itself.
+ stub_method=stub.Wait,
+ method_name="Wait",
+ request=IMPORT_rbt_v1alpha1.tasks_pb2.WaitRequest(task_id=self._task_id),
+ metadata=IMPORT_reboot_aio_headers.Headers(
+ state_ref=IMPORT_reboot_aio_types.StateRef(self._task_id.state_ref),
+ application_id=self._application_id,
+ ).to_grpc_metadata(),
+ aborted_type=IMPORT_reboot.aio.aborted.SystemAborted,
+ )
+
+ wait_for_task_response = await call
+ except IMPORT_reboot.aio.aborted.SystemAborted as error:
+ if error.code == IMPORT_grpc.StatusCode.NOT_FOUND:
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.UnknownTask()
+ ) from None
+
+ raise
+ else:
+ response_or_error: IMPORT_typing.Optional[IMPORT_google_protobuf_any_pb2.Any] = None
+ is_error = False
+
+ if wait_for_task_response.response_or_error.WhichOneof("response_or_error") == "response":
+ response_or_error = wait_for_task_response.response_or_error.response
+ else:
+ is_error = True
+ response_or_error = wait_for_task_response.response_or_error.error
+
+ assert response_or_error is not None
+ assert response_or_error.TypeName() != ""
+
+ response = google.protobuf.empty_pb2.Empty()
+
+ if (
+ not is_error and response_or_error.TypeName() != response.DESCRIPTOR.full_name
+ ):
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.InvalidArgument(),
+ message=
+ f"task with UUID {IMPORT_uuid.UUID(bytes=self._task_id.task_uuid)} "
+ f"has a response of type '{response_or_error.TypeName()}' "
+ "but expecting type 'google.protobuf.empty_pb2.Empty'; "
+ "are you waiting on a task of the correct method?",
+ ) from None
+
+ if is_error:
+ aborted_type = User.CreateAborted
+
+ # In Reboot >= 0.40.2 we expect the error to be a `google.rpc.Status`.
+ if response_or_error.Is(IMPORT_google_rpc_status_pb2.Status.DESCRIPTOR):
+ status = IMPORT_google_rpc_status_pb2.Status()
+ response_or_error.Unpack(status)
+ raise aborted_type.from_status(status)
+
+ # In Reboot < 0.40.2 workflows throwing declared errors behaved poorly;
+ # we don't aim to emulate its behavior. Indicate that we don't know the
+ # reason for the abort.
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ )
+
+ else:
+ response_or_error.Unpack(response)
+ return UserCreateResponseFromProto(response)
+
+ return wait_for_task().__await__()
+
+
+ class CreateAborted(IMPORT_reboot.aio.aborted.Aborted):
+
+
+ Error = IMPORT_typing.Union[
+ IMPORT_reboot.aio.aborted.GrpcError,
+ IMPORT_reboot.aio.aborted.RebootError,
+ ]
+
+ METHOD_PROTOBUF_ERROR_TYPES: list[type[IMPORT_google_protobuf_message.Message]] = [
+ ]
+
+ PROTOBUF_ERROR_TYPES: list[type[IMPORT_google_protobuf_message.Message | IMPORT_reboot_api.Model]] = (
+ METHOD_PROTOBUF_ERROR_TYPES +
+ IMPORT_reboot.aio.aborted.GRPC_ERROR_TYPES +
+ IMPORT_reboot.aio.aborted.REBOOT_ERROR_TYPES
+ )
+
+ _error: Error
+
+ MethodProtobufError = IMPORT_typing.Union[
+ IMPORT_reboot.aio.aborted.GrpcError,
+ IMPORT_reboot.aio.aborted.RebootError,
+ ]
+ _method_protobuf_error: MethodProtobufError
+
+ _code: IMPORT_grpc.StatusCode
+ _message: IMPORT_typing.Optional[str]
+
+ def __init__(
+ self,
+ error: IMPORT_reboot.aio.aborted.GrpcError,
+ *,
+ message: IMPORT_typing.Optional[str] = None,
+ # Do not set this value when constructing in order to
+ # raise. This is only used internally when constructing
+ # from aborted calls.
+ error_types: IMPORT_typing.Sequence[type[MethodProtobufError]] = (
+ METHOD_PROTOBUF_ERROR_TYPES + IMPORT_reboot.aio.aborted.GRPC_ERROR_TYPES
+ ),
+ ):
+ super().__init__()
+
+ self._message = None
+
+ if self.is_declared_error(error):
+ # If the error is a declared error, we should always
+ # store both formats for Pydantic and Protobuf for that
+ # error, so users can access the Pydantic version via
+ # `aborted.error` and the Protobuf version will be used
+ # to send the error over the wire.
+ if isinstance(error, IMPORT_reboot.api.Model):
+ # Happens when somebody raises an aborted with a Pydantic
+ # error format, usually in the backend method
+ # implementation.
+
+ self._error = error
+
+ else:
+ # Usually happens on the client side when we are
+ # constructing an aborted from the `AioRpcError`, so
+ # the error will be in Protobuf shape, and we want
+ # to convert it back to Pydantic if possible.
+ self._method_protobuf_error = error
+
+ else:
+ # Non declared error should be well known Reboot or gRPC
+ # error, which always comes in the Protobuf format, however
+ # it might be a case when the server is using new API and
+ # client is old, so server might send a declared error from
+ # the server perspective, but client doesn't know that it
+ # is a declared error now, so we need to gracefully
+ # cover that case.
+ if isinstance(error, IMPORT_reboot.api.Model):
+ # Store both formats so users can access the Pydantic
+ # version via `aborted.error` and the Protobuf version
+ # will be used to send the error over the wire.
+ self._error = IMPORT_rbt_v1alpha1.errors_pb2.Unknown()
+ self._method_protobuf_error = IMPORT_rbt_v1alpha1.errors_pb2.Unknown()
+
+ self._message = (
+ "Received a declared error from the server, "
+ "but the client doesn't know about it; "
+ "please update the client to the latest version\n"
+ ) + (
+ f"Original error: {error.model_dump_json()}\n"
+ ) + message if message is not None else ""
+
+ # Store both formats so users can access the error via
+ # `aborted.error` and the Protobuf version
+ # will be used to send the error over the wire.
+ self._error = error
+ self._method_protobuf_error = error
+
+ IMPORT_reboot_aio_types.assert_type(self._protobuf_error, error_types)
+
+ code = self.grpc_status_code_from_error(self._protobuf_error)
+
+ if code is None:
+ # Must be a Reboot specific or declared method error.
+ code = IMPORT_grpc.StatusCode.ABORTED
+
+ self._code = code
+
+ self._message = self._message if self._message is not None else message
+
+ @property
+ def error(self) -> Error | MethodProtobufError:
+ return self._error
+
+ @property
+ def _protobuf_error(self) -> MethodProtobufError:
+ return self._method_protobuf_error
+
+ @property
+ def code(self) -> IMPORT_grpc.StatusCode:
+ return self._code
+
+ @property
+ def message(self) -> IMPORT_typing.Optional[str]:
+ return self._message
+
+ @classmethod
+ def from_status(cls, status: IMPORT_google_rpc_status_pb2.Status):
+ error = cls.error_from_google_rpc_status_details(
+ status,
+ cls.PROTOBUF_ERROR_TYPES,
+ )
+
+ message = status.message if len(status.message) > 0 else None
+
+ if error is not None:
+ return cls(error, message=message, error_types=cls.PROTOBUF_ERROR_TYPES)
+
+ error = cls.error_from_google_rpc_status_code(status)
+
+ assert error is not None
+
+ # TODO(benh): also consider getting the type names from
+ # `status.details` and including that in `message` to make
+ # debugging easier.
+
+ return cls(error, message=message)
+
+ @classmethod
+ def from_grpc_aio_rpc_error(cls, aio_rpc_error: IMPORT_grpc.aio.AioRpcError):
+ return cls(
+ cls.error_from_grpc_aio_rpc_error(aio_rpc_error),
+ message=aio_rpc_error.details(),
+ )
+
+ @classmethod
+ def is_declared_error(cls, error: IMPORT_google_protobuf_message.Message | IMPORT_reboot.api.Model) -> bool:
+ if isinstance(error, IMPORT_reboot.api.Model):
+ return type(error) in IMPORT_api.User.methods['create'].errors
+ return False
+
+
+ class WeakReference(IMPORT_typing.Generic[User_ScheduleTypeVar]):
+
+ _schedule_type: type[User_ScheduleTypeVar]
+
+ def __init__(
+ self,
+ # When application ID is None, refers to a state within the application given by the context.
+ application_id: IMPORT_typing.Optional[IMPORT_reboot_aio_types.ApplicationId],
+ state_id: IMPORT_reboot_aio_types.StateId,
+ *,
+ schedule_type: type[User_ScheduleTypeVar],
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ servicer: IMPORT_typing.Optional[UserBaseServicer] = None,
+ ):
+ self._application_id = application_id
+ self._state_ref = IMPORT_reboot_aio_types.StateRef.from_id(
+ User.__state_type_name__,
+ state_id,
+ )
+ self._schedule_type = schedule_type
+ self._idempotency_manager: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.IdempotencyManager] = None
+ self._reader_stub: IMPORT_typing.Optional[UserReaderStub] = None
+ self._writer_stub: IMPORT_typing.Optional[UserWriterStub] = None
+ self._workflow_stub: IMPORT_typing.Optional[UserWorkflowStub] = None
+ self._tasks_stub: IMPORT_typing.Optional[UserTasksStub] = None
+ self._bearer_token = bearer_token
+ self._servicer = servicer
+
+ @property
+ def state_id(self) -> IMPORT_reboot_aio_types.StateId:
+ return self._state_ref.id
+
+ def _reader(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext | IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ ) -> UserReaderStub:
+ if self._reader_stub is None:
+ self._reader_stub = UserReaderStub(
+ context=context,
+ state_ref=self._state_ref,
+ bearer_token=self._bearer_token,
+ )
+ assert self._reader_stub is not None
+ if self._idempotency_manager is None:
+ self._idempotency_manager = context
+ elif self._idempotency_manager != context:
+ raise IMPORT_reboot_aio_call.MixedContextsError(
+ "This `WeakReference` for `User` with ID "
+ f"'{self.state_id}' has previously been used by a "
+ "different `Context`. That is not allowed. "
+ "Instead create a new `WeakReference` for every `Context` by calling "
+ f"`User.ref('{self.state_id}')`."
+ )
+ return self._reader_stub
+
+ def _writer(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ ) -> UserWriterStub:
+ if self._writer_stub is None:
+ self._writer_stub = UserWriterStub(
+ context=context,
+ state_ref=self._state_ref,
+ bearer_token=self._bearer_token,
+ )
+ assert self._writer_stub is not None
+ if self._idempotency_manager is None:
+ self._idempotency_manager = context
+ elif self._idempotency_manager != context:
+ raise IMPORT_reboot_aio_call.MixedContextsError(
+ "This `WeakReference` for `User` with ID "
+ f"'{self.state_id}' has previously been used by a "
+ "different `Context`. That is not allowed. "
+ "Instead create a new `WeakReference` for every `Context` by calling "
+ f"`User.ref('{self.state_id}')`."
+ )
+ return self._writer_stub
+
+ def _workflow(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ ) -> UserWorkflowStub:
+ if self._workflow_stub is None:
+ self._workflow_stub = UserWorkflowStub(
+ context=context,
+ state_ref=self._state_ref,
+ bearer_token=self._bearer_token,
+ )
+ assert self._workflow_stub is not None
+ if self._idempotency_manager is None:
+ self._idempotency_manager = context
+ elif self._idempotency_manager != context:
+ raise IMPORT_reboot_aio_call.MixedContextsError(
+ "This `WeakReference` for `User` with ID "
+ f"'{self.state_id}' has previously been used by a "
+ "different `Context`. That is not allowed. "
+ "Instead create a new `WeakReference` for every `Context` by calling "
+ f"`User.ref('{self.state_id}')`."
+ )
+ return self._workflow_stub
+
+ def _tasks(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ ) -> UserTasksStub:
+ if self._tasks_stub is None:
+ self._tasks_stub = UserTasksStub(
+ context=context,
+ state_ref=self._state_ref,
+ bearer_token=self._bearer_token,
+ )
+ assert self._tasks_stub is not None
+ if self._idempotency_manager is None:
+ self._idempotency_manager = context
+ elif self._idempotency_manager != context:
+ raise IMPORT_reboot_aio_call.MixedContextsError(
+ "This `WeakReference` for `User` with ID "
+ f"'{self.state_id}' has previously been used by a "
+ "different `Context`. That is not allowed. "
+ "Instead create a new `WeakReference` for every `Context` by calling "
+ f"`User.ref('{self.state_id}')`."
+ )
+ return self._tasks_stub
+
+ class _Reactively:
+
+ def __init__(
+ self,
+ *,
+ application_id: IMPORT_typing.Optional[IMPORT_reboot_aio_types.ApplicationId],
+ state_ref: IMPORT_reboot_aio_types.StateRef,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ):
+ self._application_id = application_id
+ self._state_ref = state_ref
+ self._bearer_token = bearer_token
+
+
+ async def ListCounters( # type: ignore[misc]
+ # In methods which are dealing with user input, (i.e.,
+ # proto message field names), we should use '__double_underscored__'
+ # variables to avoid any potential name conflicts with the method's
+ # parameters.
+ # The '__self__' parameter is a convention in Python to
+ # indicate that this method is a bound method, so we use
+ # '__this__' instead.
+ __this__,
+ # Explicitly-reactive calls only make sense in the context of either...
+ # (A) an external client, or...
+ # (B) methods that may reasonably run for a long time, which in Reboot means: readers or workflows.
+ __context__: IMPORT_reboot_aio_external.ExternalContext | IMPORT_reboot_aio_contexts.ReaderContext | IMPORT_reboot_aio_contexts.WorkflowContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> IMPORT_typing.AsyncIterator[User.ListCountersResponse]:
+ IMPORT_reboot_aio_types.assert_type(__context__, [IMPORT_reboot_aio_external.ExternalContext, IMPORT_reboot_aio_contexts.ReaderContext, IMPORT_reboot_aio_contexts.WorkflowContext])
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+ __caller_id__: IMPORT_typing.Optional[IMPORT_reboot_aio_caller_id.CallerID] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ if __bearer_token__ is None:
+ __bearer_token__ = __this__._bearer_token
+ if __bearer_token__ is None and isinstance(__context__, IMPORT_reboot_aio_external.ExternalContext):
+ # Within a Reboot context we do not pass on the caller's bearer token, as that might
+ # have security implications - we cannot simply trust any service we are calling with
+ # the user's credentials. Instead, the developer can rely on the default app-internal
+ # auth, or override that and set an explicit bearer token.
+ #
+ # In the case of `ExternalContext`, however, its `bearer_token` was set specifically
+ # by the developer for the purpose of making these calls. Note that only
+ # `ExternalContext` even has a `bearer_token` field.
+ __bearer_token__ = __context__.bearer_token
+
+ if __metadata__ is None:
+ __metadata__ = ()
+
+ if isinstance(__context__, IMPORT_reboot_aio_contexts.Context):
+ __caller_id__ = IMPORT_reboot_aio_caller_id.CallerID(
+ application_id=__context__.application_id,
+ )
+ if __this__._application_id is None:
+ # Given our context type (inside a Reboot application) we can default to
+ # making the application send traffic to itself.
+ __this__._application_id = __context__.application_id
+ elif isinstance(__context__, IMPORT_reboot_aio_external.ExternalContext):
+ __caller_id__ = __context__.caller_id
+
+ __headers__ = IMPORT_reboot_aio_headers.Headers(
+ bearer_token=__bearer_token__,
+ state_ref=__this__._state_ref,
+ application_id=__this__._application_id,
+ caller_id=__caller_id__,
+ )
+
+ __metadata__ += __headers__.to_grpc_metadata()
+
+ __query_backoff__ = IMPORT_reboot_aio_backoff.Backoff()
+ while True:
+ __call__ = None
+ try:
+ async with __context__.channel_manager.get_channel_to_state(
+ IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User'),
+ __this__._state_ref,
+ ) as __channel__:
+
+ __call__ = IMPORT_rbt_v1alpha1.react_pb2_grpc.ReactStub(
+ __channel__
+ ).Query(
+ IMPORT_rbt_v1alpha1.react_pb2.QueryRequest(
+ method='ListCounters',
+ request=UserListCountersRequestToProto(
+ ).SerializeToString(),
+ ),
+ metadata=__metadata__,
+ )
+
+ async for __query_response__ in __call__:
+ # Clear the backoff so we don't wait
+ # as long the next time we get
+ # disconnected.
+ __query_backoff__.clear()
+
+ # The backend may have sent us this query
+ # response only to let us know that a new
+ # idempotency key has been recorded; there may
+ # not be a new response. Python callers don't
+ # (currently) care about such an event, so we
+ # simply ignore it.
+ if not __query_response__.HasField("response"):
+ continue
+
+ __response__ = reboot.ping.ping_api_pb2.UserListCountersResponse()
+ __response__.ParseFromString(__query_response__.response)
+ yield UserListCountersResponseFromProto(__response__)
+
+ except IMPORT_grpc.aio.AioRpcError as error:
+ # We expect to get disconnected from the server
+ # from time to time, e.g., when it is being
+ # updated, but we don't want that error to
+ # propagate, we just want to retry.
+ if IMPORT_reboot.aio.aborted.is_grpc_retryable_exception(error):
+ logger.debug(
+ "Reactive read to 'reboot.ping.UserMethods.ListCounters' "
+ f"failed with a retryable error: '{error}'; "
+ "will retry..."
+ )
+ await __query_backoff__()
+ continue
+ if error.code() == IMPORT_grpc.StatusCode.ABORTED:
+ # Reconstitute the error that the server threw, if it was a declared error.
+ status = await IMPORT_rpc_status_async.from_call(__call__)
+ if status is not None:
+ raise User.ListCountersAborted.from_status(
+ status
+ ) from None
+ raise User.ListCountersAborted.from_grpc_aio_rpc_error(
+ error
+ ) from None
+
+ raise
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ list_counters = ListCounters
+
+ async def Whoami( # type: ignore[misc]
+ # In methods which are dealing with user input, (i.e.,
+ # proto message field names), we should use '__double_underscored__'
+ # variables to avoid any potential name conflicts with the method's
+ # parameters.
+ # The '__self__' parameter is a convention in Python to
+ # indicate that this method is a bound method, so we use
+ # '__this__' instead.
+ __this__,
+ # Explicitly-reactive calls only make sense in the context of either...
+ # (A) an external client, or...
+ # (B) methods that may reasonably run for a long time, which in Reboot means: readers or workflows.
+ __context__: IMPORT_reboot_aio_external.ExternalContext | IMPORT_reboot_aio_contexts.ReaderContext | IMPORT_reboot_aio_contexts.WorkflowContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> IMPORT_typing.AsyncIterator[User.WhoamiResponse]:
+ IMPORT_reboot_aio_types.assert_type(__context__, [IMPORT_reboot_aio_external.ExternalContext, IMPORT_reboot_aio_contexts.ReaderContext, IMPORT_reboot_aio_contexts.WorkflowContext])
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+ __caller_id__: IMPORT_typing.Optional[IMPORT_reboot_aio_caller_id.CallerID] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ if __bearer_token__ is None:
+ __bearer_token__ = __this__._bearer_token
+ if __bearer_token__ is None and isinstance(__context__, IMPORT_reboot_aio_external.ExternalContext):
+ # Within a Reboot context we do not pass on the caller's bearer token, as that might
+ # have security implications - we cannot simply trust any service we are calling with
+ # the user's credentials. Instead, the developer can rely on the default app-internal
+ # auth, or override that and set an explicit bearer token.
+ #
+ # In the case of `ExternalContext`, however, its `bearer_token` was set specifically
+ # by the developer for the purpose of making these calls. Note that only
+ # `ExternalContext` even has a `bearer_token` field.
+ __bearer_token__ = __context__.bearer_token
+
+ if __metadata__ is None:
+ __metadata__ = ()
+
+ if isinstance(__context__, IMPORT_reboot_aio_contexts.Context):
+ __caller_id__ = IMPORT_reboot_aio_caller_id.CallerID(
+ application_id=__context__.application_id,
+ )
+ if __this__._application_id is None:
+ # Given our context type (inside a Reboot application) we can default to
+ # making the application send traffic to itself.
+ __this__._application_id = __context__.application_id
+ elif isinstance(__context__, IMPORT_reboot_aio_external.ExternalContext):
+ __caller_id__ = __context__.caller_id
+
+ __headers__ = IMPORT_reboot_aio_headers.Headers(
+ bearer_token=__bearer_token__,
+ state_ref=__this__._state_ref,
+ application_id=__this__._application_id,
+ caller_id=__caller_id__,
+ )
+
+ __metadata__ += __headers__.to_grpc_metadata()
+
+ __query_backoff__ = IMPORT_reboot_aio_backoff.Backoff()
+ while True:
+ __call__ = None
+ try:
+ async with __context__.channel_manager.get_channel_to_state(
+ IMPORT_reboot_aio_types.StateTypeName('reboot.ping.User'),
+ __this__._state_ref,
+ ) as __channel__:
+
+ __call__ = IMPORT_rbt_v1alpha1.react_pb2_grpc.ReactStub(
+ __channel__
+ ).Query(
+ IMPORT_rbt_v1alpha1.react_pb2.QueryRequest(
+ method='Whoami',
+ request=UserWhoamiRequestToProto(
+ ).SerializeToString(),
+ ),
+ metadata=__metadata__,
+ )
+
+ async for __query_response__ in __call__:
+ # Clear the backoff so we don't wait
+ # as long the next time we get
+ # disconnected.
+ __query_backoff__.clear()
+
+ # The backend may have sent us this query
+ # response only to let us know that a new
+ # idempotency key has been recorded; there may
+ # not be a new response. Python callers don't
+ # (currently) care about such an event, so we
+ # simply ignore it.
+ if not __query_response__.HasField("response"):
+ continue
+
+ __response__ = reboot.ping.ping_api_pb2.UserWhoamiResponse()
+ __response__.ParseFromString(__query_response__.response)
+ yield UserWhoamiResponseFromProto(__response__)
+
+ except IMPORT_grpc.aio.AioRpcError as error:
+ # We expect to get disconnected from the server
+ # from time to time, e.g., when it is being
+ # updated, but we don't want that error to
+ # propagate, we just want to retry.
+ if IMPORT_reboot.aio.aborted.is_grpc_retryable_exception(error):
+ logger.debug(
+ "Reactive read to 'reboot.ping.UserMethods.Whoami' "
+ f"failed with a retryable error: '{error}'; "
+ "will retry..."
+ )
+ await __query_backoff__()
+ continue
+ if error.code() == IMPORT_grpc.StatusCode.ABORTED:
+ # Reconstitute the error that the server threw, if it was a declared error.
+ status = await IMPORT_rpc_status_async.from_call(__call__)
+ if status is not None:
+ raise User.WhoamiAborted.from_status(
+ status
+ ) from None
+ raise User.WhoamiAborted.from_grpc_aio_rpc_error(
+ error
+ ) from None
+
+ raise
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ whoami = Whoami
+
+ def reactively(self):
+ return User.WeakReference._Reactively(
+ application_id=self._application_id,
+ state_ref=self._state_ref,
+ bearer_token=self._bearer_token,
+ )
+
+ class _Idempotently(IMPORT_typing.Generic[User_IdempotentlyScheduleTypeVar]):
+
+ _weak_reference: User.WeakReference[User_IdempotentlyScheduleTypeVar]
+
+ def __init__(
+ self,
+ *,
+ weak_reference: User.WeakReference[User_IdempotentlyScheduleTypeVar],
+ idempotency: IMPORT_reboot_aio_idempotency.Idempotency,
+ ):
+ self._weak_reference = weak_reference
+ self._idempotency = idempotency
+
+ def schedule(
+ self,
+ *,
+ when: IMPORT_typing.Optional[IMPORT_datetime_datetime | IMPORT_datetime_timedelta] = None,
+ ) -> User_IdempotentlyScheduleTypeVar:
+ return self._weak_reference._schedule_type(
+ self._weak_reference._application_id,
+ self._weak_reference._tasks,
+ when=when,
+ idempotency=self._idempotency,
+ )
+
+ def spawn(
+ self,
+ *,
+ when: IMPORT_typing.Optional[IMPORT_datetime_datetime | IMPORT_datetime_timedelta] = None,
+ ) -> User.WeakReference._Spawn:
+ return User.WeakReference._Spawn(
+ self._weak_reference._application_id,
+ self._weak_reference._tasks,
+ when=when,
+ idempotency=self._idempotency,
+ )
+
+ async def read(
+ self,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ ) -> User.State:
+ if self._weak_reference._servicer is None:
+ raise RuntimeError(
+ "`read()` is currently only supported within workflows; "
+ "Please reach out and let us know your use case if this "
+ "is important for you!"
+ )
+
+ return await UserBaseServicer.WorkflowState._Idempotently._read(
+ self._weak_reference._servicer,
+ self._idempotency,
+ context,
+ )
+
+ async def write(
+ self,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ writer: UserBaseServicer.InlineWriterCallable[UserBaseServicer.InlineWriterCallableResult],
+ *,
+ type: IMPORT_reboot_aio_workflows.Type | IMPORT_reboot_aio_workflows._Unset = IMPORT_reboot_aio_workflows._UNSET,
+ ) -> UserBaseServicer.InlineWriterCallableResult:
+ if self._weak_reference._servicer is None:
+ raise RuntimeError(
+ "`write()` is currently only supported within workflows; "
+ "Please reach out and let us know your use case if this "
+ "is important for you!"
+ )
+
+ # `type=` is optional: when omitted, `writer`'s
+ # return annotation is used.
+ type_t, _ = IMPORT_reboot_aio_workflows._resolve_callable_return_type(
+ writer, type,
+ )
+
+ return await UserBaseServicer.WorkflowState._Idempotently._write_validating_effects(
+ self._weak_reference._servicer,
+ self._idempotency,
+ context,
+ writer,
+ type_result=type_t,
+ check_type=not self._idempotency.always,
+ unidempotently=self._idempotency.always,
+ checkpoint=context.checkpoint(),
+ )
+
+ @IMPORT_typing.overload
+ async def CreateCounter(
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __request_or_options__: User.CreateCounterRequest,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> User.CreateCounterResponse:
+ ...
+
+ @IMPORT_typing.overload
+ async def CreateCounter(
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __request_or_options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ *,
+ description: str | Unset = UNSET,
+ ) -> User.CreateCounterResponse:
+ ...
+
+ async def CreateCounter( # type: ignore[misc]
+ # In methods which are dealing with user input, (i.e.,
+ # proto message field names), we should use '__double_underscored__'
+ # variables to avoid any potential name conflicts with the method's
+ # parameters.
+ # The '__self__' parameter is a convention in Python to
+ # indicate that this method is a bound method, so we use
+ # '__this__' instead.
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __request_or_options__: IMPORT_typing.Optional[User.CreateCounterRequest | IMPORT_reboot_aio_call.Options] = None,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ *,
+ description: str | Unset = UNSET,
+ ) -> User.CreateCounterResponse:
+ # UX improvement: check that neither positional argument was accidentally
+ # given a gRPC request type.
+ IMPORT_reboot_aio_types.assert_not_request_type(__context__, request_type=User.CreateCounterRequest)
+ IMPORT_reboot_aio_types.assert_not_request_type(__options__, request_type=User.CreateCounterRequest)
+
+ __request__: IMPORT_typing.Optional[User.CreateCounterRequest] = None
+ if isinstance(__request_or_options__, User.CreateCounterRequest):
+ assert __request_or_options__ is not None
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+ __options__ = __options__ or IMPORT_reboot_aio_call.Options()
+
+ assert description is UNSET
+
+ __request__ = __request_or_options__
+ else:
+ assert __options__ is None
+ assert __request_or_options__ is None or isinstance(__request_or_options__, IMPORT_reboot_aio_call.Options)
+ __options__ = __request_or_options__ or IMPORT_reboot_aio_call.Options()
+
+ __request__ = UserCreateCounterRequestFromInputFields(
+ description=description,
+ )
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+
+ return await __this__._weak_reference.CreateCounter(
+ __context__,
+ __request__,
+ __options__,
+ __idempotency__=__this__._idempotency,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ create_counter = CreateCounter
+
+ async def ListCounters( # type: ignore[misc]
+ # In methods which are dealing with user input, (i.e.,
+ # proto message field names), we should use '__double_underscored__'
+ # variables to avoid any potential name conflicts with the method's
+ # parameters.
+ # The '__self__' parameter is a convention in Python to
+ # indicate that this method is a bound method, so we use
+ # '__this__' instead.
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> User.ListCountersResponse:
+ __options__ = __options__ or IMPORT_reboot_aio_call.Options()
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+
+ return await __this__._weak_reference.ListCounters(
+ __context__,
+ __options__,
+ __idempotency__=__this__._idempotency,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ list_counters = ListCounters
+
+ async def Whoami( # type: ignore[misc]
+ # In methods which are dealing with user input, (i.e.,
+ # proto message field names), we should use '__double_underscored__'
+ # variables to avoid any potential name conflicts with the method's
+ # parameters.
+ # The '__self__' parameter is a convention in Python to
+ # indicate that this method is a bound method, so we use
+ # '__this__' instead.
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> User.WhoamiResponse:
+ __options__ = __options__ or IMPORT_reboot_aio_call.Options()
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+
+ return await __this__._weak_reference.Whoami(
+ __context__,
+ __options__,
+ __idempotency__=__this__._idempotency,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ whoami = Whoami
+
+ @IMPORT_typing.overload
+ def idempotently(self, alias: IMPORT_typing.Optional[str] = None, *, how: IMPORT_reboot_aio_idempotency.How = IMPORT_reboot_aio_idempotency.PER_WORKFLOW) -> User.WeakReference._Idempotently[User_ScheduleTypeVar]:
+ ...
+
+ @IMPORT_typing.overload
+ def idempotently(self, *, key: IMPORT_uuid.UUID, how: IMPORT_reboot_aio_idempotency.How = IMPORT_reboot_aio_idempotency.PER_WORKFLOW) -> User.WeakReference._Idempotently[User_ScheduleTypeVar]:
+ ...
+
+ def idempotently(
+ self,
+ alias: IMPORT_typing.Optional[str] = None,
+ *,
+ key: IMPORT_typing.Optional[IMPORT_uuid.UUID] = None,
+ how: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.How] = None,
+ ) -> User.WeakReference._Idempotently[User_ScheduleTypeVar]:
+ return User.WeakReference._Idempotently(
+ weak_reference=self,
+ idempotency=IMPORT_reboot_aio_contexts.Context.idempotency(
+ alias=alias,
+ key=key,
+ how=how,
+ )
+ )
+
+ def per_workflow(self, alias: IMPORT_typing.Optional[str] = None):
+ return self.idempotently(alias)
+
+ def per_iteration(self, alias: IMPORT_typing.Optional[str] = None):
+ return self.idempotently(
+ alias,
+ how=IMPORT_reboot_aio_idempotency.PER_ITERATION,
+ )
+
+ def always(self):
+ return self.idempotently(
+ how=IMPORT_reboot_aio_idempotency.ALWAYS,
+ )
+
+ class _UntilChangesSatisfies(IMPORT_typing.Generic[User_UntilCallableType]):
+
+ _idempotency_alias: str
+ _context: IMPORT_reboot_aio_contexts.WorkflowContext
+ _callable: IMPORT_typing.Callable[[], IMPORT_typing.Awaitable[User_UntilCallableType]]
+ _type: type[User_UntilCallableType]
+
+ def __init__(
+ self,
+ *,
+ idempotency_alias: str,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ callable: IMPORT_typing.Callable[[], IMPORT_typing.Awaitable[User_UntilCallableType]],
+ type: type[User_UntilCallableType],
+ ):
+ self._idempotency_alias = idempotency_alias
+ self._context = context
+ self._callable = callable
+ self._type = type
+
+ async def changes(self):
+ return await IMPORT_reboot_aio_workflows.until_changes(
+ self._idempotency_alias,
+ self._context,
+ self._callable,
+ type=self._type,
+ )
+
+ async def satisfies(
+ self,
+ condition: IMPORT_typing.Callable[[User_UntilCallableType], bool],
+ ):
+
+ async def converge():
+ response = await self._callable()
+ if condition(response):
+ return response
+ return False
+
+ return await IMPORT_reboot_aio_workflows.until(
+ self._idempotency_alias,
+ self._context,
+ converge,
+ type=self._type,
+ )
+
+ class _Until:
+
+ _weak_reference: User.WeakReference
+ _idempotency_alias: str
+
+ def __init__(
+ self,
+ *,
+ weak_reference: User.WeakReference,
+ idempotency_alias: str,
+ ):
+ self._weak_reference = weak_reference
+ self._idempotency_alias = idempotency_alias
+
+ def read(
+ self,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ ) -> User.WeakReference._UntilChangesSatisfies[User.State]:
+ IMPORT_reboot_aio_types.assert_type(
+ context,
+ [IMPORT_reboot_aio_contexts.WorkflowContext],
+ )
+
+ async def callable():
+ return await self._weak_reference.read(context)
+
+ return User.WeakReference._UntilChangesSatisfies(
+ idempotency_alias=self._idempotency_alias,
+ context=context,
+ callable=callable,
+ type=User.State,
+ )
+
+
+ def ListCounters( # type: ignore[misc]
+ # In methods which are dealing with user input, (i.e.,
+ # proto message field names), we should use '__double_underscored__'
+ # variables to avoid any potential name conflicts with the method's
+ # parameters.
+ # The '__self__' parameter is a convention in Python to
+ # indicate that this method is a bound method, so we use
+ # '__this__' instead.
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.WorkflowContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> User.WeakReference._UntilChangesSatisfies[User.ListCountersResponse]:
+ IMPORT_reboot_aio_types.assert_type(
+ __context__,
+ [IMPORT_reboot_aio_contexts.WorkflowContext],
+ )
+
+ __options__ = __options__ or IMPORT_reboot_aio_call.Options()
+
+ async def callable():
+ return await __this__._weak_reference.ListCounters(
+ __context__,
+ __options__,
+ )
+
+ return User.WeakReference._UntilChangesSatisfies(
+ idempotency_alias=__this__._idempotency_alias,
+ context=__context__,
+ callable=callable,
+ type=User.ListCountersResponse,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ list_counters = ListCounters
+
+ def Whoami( # type: ignore[misc]
+ # In methods which are dealing with user input, (i.e.,
+ # proto message field names), we should use '__double_underscored__'
+ # variables to avoid any potential name conflicts with the method's
+ # parameters.
+ # The '__self__' parameter is a convention in Python to
+ # indicate that this method is a bound method, so we use
+ # '__this__' instead.
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.WorkflowContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> User.WeakReference._UntilChangesSatisfies[User.WhoamiResponse]:
+ IMPORT_reboot_aio_types.assert_type(
+ __context__,
+ [IMPORT_reboot_aio_contexts.WorkflowContext],
+ )
+
+ __options__ = __options__ or IMPORT_reboot_aio_call.Options()
+
+ async def callable():
+ return await __this__._weak_reference.Whoami(
+ __context__,
+ __options__,
+ )
+
+ return User.WeakReference._UntilChangesSatisfies(
+ idempotency_alias=__this__._idempotency_alias,
+ context=__context__,
+ callable=callable,
+ type=User.WhoamiResponse,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ whoami = Whoami
+
+ def until(self, alias: str):
+ return User.WeakReference._Until(
+ weak_reference=self,
+ idempotency_alias=alias,
+ )
+
+ def schedule(
+ self,
+ *,
+ when: IMPORT_typing.Optional[IMPORT_datetime_datetime | IMPORT_datetime_timedelta] = None,
+ ) -> User_ScheduleTypeVar:
+ return self._schedule_type(self._application_id, self._tasks, when=when)
+
+ class _Schedule:
+
+ def __init__(
+ self,
+ application_id: IMPORT_typing.Optional[IMPORT_reboot_aio_types.ApplicationId],
+ tasks: IMPORT_typing.Callable[[IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext], UserTasksStub],
+ *,
+ when: IMPORT_typing.Optional[IMPORT_datetime_datetime | IMPORT_datetime_timedelta] = None,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ ) -> None:
+ self._application_id = application_id
+ self._tasks = tasks
+ self._when = ensure_has_timezone(when=when)
+ self._idempotency = idempotency
+
+ # User callable tasks:
+ @IMPORT_typing.overload
+ async def CreateCounter(
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext,
+ __request_or_options__: User.CreateCounterRequest,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ ...
+
+ @IMPORT_typing.overload
+ async def CreateCounter(
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext,
+ __request_or_options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ *,
+ description: str | Unset = UNSET,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ ...
+
+ async def CreateCounter( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext,
+ __request_or_options__: IMPORT_typing.Optional[User.CreateCounterRequest | IMPORT_reboot_aio_call.Options] = None,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ *,
+ description: str | Unset = UNSET,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ IMPORT_reboot_aio_types.assert_type(__context__, [IMPORT_reboot_aio_contexts.TransactionContext])
+ # UX improvement: check that neither positional argument was accidentally
+ # given a gRPC request type.
+ IMPORT_reboot_aio_types.assert_not_request_type(__context__, request_type=User.CreateCounterRequest)
+ IMPORT_reboot_aio_types.assert_not_request_type(__options__, request_type=User.CreateCounterRequest)
+
+ __request__: IMPORT_typing.Optional[User.CreateCounterRequest] = None
+ if isinstance(__request_or_options__, User.CreateCounterRequest):
+ assert __request_or_options__ is not None
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ assert description is UNSET
+
+ __request__ = __request_or_options__
+ else:
+ assert __options__ is None
+ assert __request_or_options__ is None or isinstance(__request_or_options__, IMPORT_reboot_aio_call.Options)
+ __options__ = __request_or_options__
+
+ __request__ = UserCreateCounterRequestFromInputFields(
+ description=description,
+ )
+
+ __schedule__: IMPORT_typing.Optional[IMPORT_reboot_time_DateTimeWithTimeZone] = (IMPORT_reboot_time_DateTimeWithTimeZone.now() + __this__._when) if isinstance(
+ __this__._when, IMPORT_datetime_timedelta
+ ) else __this__._when
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = __this__._idempotency
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ # Add scheduling information to the metadata.
+ __metadata__ = (
+ (IMPORT_reboot_aio_headers.TASK_SCHEDULE,
+ __schedule__.isoformat() if __schedule__ else ''),
+ ) + (__metadata__ or tuple())
+
+ __task_id__ = await __this__._tasks(
+ __context__
+ ).CreateCounter(
+ __request__,
+ idempotency=__idempotency__,
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ )
+
+ return __task_id__
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ create_counter = CreateCounter
+
+ async def ListCounters( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ IMPORT_reboot_aio_types.assert_type(__context__, [IMPORT_reboot_aio_contexts.TransactionContext])
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ __schedule__: IMPORT_typing.Optional[IMPORT_reboot_time_DateTimeWithTimeZone] = (IMPORT_reboot_time_DateTimeWithTimeZone.now() + __this__._when) if isinstance(
+ __this__._when, IMPORT_datetime_timedelta
+ ) else __this__._when
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = __this__._idempotency
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ # Add scheduling information to the metadata.
+ __metadata__ = (
+ (IMPORT_reboot_aio_headers.TASK_SCHEDULE,
+ __schedule__.isoformat() if __schedule__ else ''),
+ ) + (__metadata__ or tuple())
+
+ __task_id__ = await __this__._tasks(
+ __context__
+ ).ListCounters(
+ idempotency=__idempotency__,
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ )
+
+ return __task_id__
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ list_counters = ListCounters
+
+ async def Whoami( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ IMPORT_reboot_aio_types.assert_type(__context__, [IMPORT_reboot_aio_contexts.TransactionContext])
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ __schedule__: IMPORT_typing.Optional[IMPORT_reboot_time_DateTimeWithTimeZone] = (IMPORT_reboot_time_DateTimeWithTimeZone.now() + __this__._when) if isinstance(
+ __this__._when, IMPORT_datetime_timedelta
+ ) else __this__._when
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = __this__._idempotency
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ # Add scheduling information to the metadata.
+ __metadata__ = (
+ (IMPORT_reboot_aio_headers.TASK_SCHEDULE,
+ __schedule__.isoformat() if __schedule__ else ''),
+ ) + (__metadata__ or tuple())
+
+ __task_id__ = await __this__._tasks(
+ __context__
+ ).Whoami(
+ idempotency=__idempotency__,
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ )
+
+ return __task_id__
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ whoami = Whoami
+
+ # A `WriterContext` can not call any methods in `_Schedule` to
+ # prevent a writer from doing a `Foo.ref()` and trying to
+ # schedule. However, we want to allow a writer to schedule
+ # when we are constructing a `WeakReference` from
+ # `self.ref()` so instead we return a `_WriterSchedule` to
+ # provide type safety that allows a `WriterContext` to
+ # schedule (for itself).
+ class _WriterSchedule:
+
+ def __init__(
+ self,
+ application_id: IMPORT_typing.Optional[IMPORT_reboot_aio_types.ApplicationId],
+ tasks: IMPORT_typing.Callable[[IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext], UserTasksStub],
+ *,
+ when: IMPORT_typing.Optional[IMPORT_datetime_datetime | IMPORT_datetime_timedelta] = None,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ ) -> None:
+ self._tasks = tasks
+ self._when = ensure_has_timezone(when=when)
+ self._idempotency = idempotency
+
+ # User callable tasks:
+ @IMPORT_typing.overload
+ async def CreateCounter(
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext,
+ __request_or_options__: User.CreateCounterRequest,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ ...
+
+ @IMPORT_typing.overload
+ async def CreateCounter(
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext,
+ __request_or_options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ *,
+ description: str | Unset = UNSET,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ ...
+
+ async def CreateCounter( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext,
+ __request_or_options__: IMPORT_typing.Optional[User.CreateCounterRequest | IMPORT_reboot_aio_call.Options] = None,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ *,
+ description: str | Unset = UNSET,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ # Only `writer`s and `transaction`s should ``schedule()`, a
+ # `workflow` should `spawn()`.
+ IMPORT_reboot_aio_types.assert_type(__context__, [IMPORT_reboot_aio_contexts.WriterContext, IMPORT_reboot_aio_contexts.TransactionContext])
+
+ # UX improvement: check that neither positional argument was accidentally
+ # given a gRPC request type.
+ IMPORT_reboot_aio_types.assert_not_request_type(__context__, request_type=User.CreateCounterRequest)
+ IMPORT_reboot_aio_types.assert_not_request_type(__options__, request_type=User.CreateCounterRequest)
+
+ __request__: IMPORT_typing.Optional[User.CreateCounterRequest] = None
+ if isinstance(__request_or_options__, User.CreateCounterRequest):
+ assert __request_or_options__ is not None
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ assert description is UNSET
+
+ __request__ = __request_or_options__
+ else:
+ assert __options__ is None
+ assert __request_or_options__ is None or isinstance(__request_or_options__, IMPORT_reboot_aio_call.Options)
+ __options__ = __request_or_options__
+
+ __request__ = UserCreateCounterRequestFromInputFields(
+ description=description,
+ )
+
+ if isinstance(__context__, IMPORT_reboot_aio_contexts.WriterContext):
+ return (await UserServicerTasks(
+ context=__context__,
+ state_ref=__context__._state_ref,
+ ).CreateCounter(
+ __request__,
+ schedule=__this__._when,
+ )).task_id
+
+ __schedule__: IMPORT_typing.Optional[IMPORT_reboot_time_DateTimeWithTimeZone] = (IMPORT_reboot_time_DateTimeWithTimeZone.now() + __this__._when) if isinstance(
+ __this__._when, IMPORT_datetime_timedelta
+ ) else __this__._when
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = __this__._idempotency
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ # Add scheduling information to the metadata.
+ __metadata__ = (
+ (IMPORT_reboot_aio_headers.TASK_SCHEDULE,
+ __schedule__.isoformat() if __schedule__ else ''),
+ ) + (__metadata__ or tuple())
+
+ return await __this__._tasks(
+ __context__
+ ).CreateCounter(
+ __request__,
+ idempotency=__idempotency__,
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ create_counter = CreateCounter
+
+ async def ListCounters( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ # Only `writer`s and `transaction`s should ``schedule()`, a
+ # `workflow` should `spawn()`.
+ IMPORT_reboot_aio_types.assert_type(__context__, [IMPORT_reboot_aio_contexts.WriterContext, IMPORT_reboot_aio_contexts.TransactionContext])
+
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ if isinstance(__context__, IMPORT_reboot_aio_contexts.WriterContext):
+ return (await UserServicerTasks(
+ context=__context__,
+ state_ref=__context__._state_ref,
+ ).ListCounters(
+ schedule=__this__._when,
+ )).task_id
+
+ __schedule__: IMPORT_typing.Optional[IMPORT_reboot_time_DateTimeWithTimeZone] = (IMPORT_reboot_time_DateTimeWithTimeZone.now() + __this__._when) if isinstance(
+ __this__._when, IMPORT_datetime_timedelta
+ ) else __this__._when
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = __this__._idempotency
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ # Add scheduling information to the metadata.
+ __metadata__ = (
+ (IMPORT_reboot_aio_headers.TASK_SCHEDULE,
+ __schedule__.isoformat() if __schedule__ else ''),
+ ) + (__metadata__ or tuple())
+
+ return await __this__._tasks(
+ __context__
+ ).ListCounters(
+ idempotency=__idempotency__,
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ list_counters = ListCounters
+
+ async def Whoami( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ # Only `writer`s and `transaction`s should ``schedule()`, a
+ # `workflow` should `spawn()`.
+ IMPORT_reboot_aio_types.assert_type(__context__, [IMPORT_reboot_aio_contexts.WriterContext, IMPORT_reboot_aio_contexts.TransactionContext])
+
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ if isinstance(__context__, IMPORT_reboot_aio_contexts.WriterContext):
+ return (await UserServicerTasks(
+ context=__context__,
+ state_ref=__context__._state_ref,
+ ).Whoami(
+ schedule=__this__._when,
+ )).task_id
+
+ __schedule__: IMPORT_typing.Optional[IMPORT_reboot_time_DateTimeWithTimeZone] = (IMPORT_reboot_time_DateTimeWithTimeZone.now() + __this__._when) if isinstance(
+ __this__._when, IMPORT_datetime_timedelta
+ ) else __this__._when
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = __this__._idempotency
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ # Add scheduling information to the metadata.
+ __metadata__ = (
+ (IMPORT_reboot_aio_headers.TASK_SCHEDULE,
+ __schedule__.isoformat() if __schedule__ else ''),
+ ) + (__metadata__ or tuple())
+
+ return await __this__._tasks(
+ __context__
+ ).Whoami(
+ idempotency=__idempotency__,
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ whoami = Whoami
+
+ def spawn(
+ self,
+ *,
+ when: IMPORT_typing.Optional[IMPORT_datetime_datetime | IMPORT_datetime_timedelta] = None,
+ ) -> User.WeakReference._Spawn:
+ # Within a `workflow`, all "bare" `spawn()` calls are
+ # syntactic sugar for `per_workflow()`, unless we're
+ # within a control loop, in which case they are syntactic
+ # sugar for `per_iteration()`.
+ context = IMPORT_reboot_aio_contexts.Context.get()
+ if context is not None:
+ if isinstance(context, IMPORT_reboot_aio_contexts.WorkflowContext):
+ return (
+ self.per_iteration() if context.within_loop()
+ else self.per_workflow()
+ ).spawn(when=when)
+ elif isinstance(context, IMPORT_reboot_aio_external.InitializeContext):
+ return self.idempotently().spawn(when=when)
+
+ return User.WeakReference._Spawn(
+ self._application_id, self._tasks, when=when
+ )
+
+ class _Spawn:
+
+ def __init__(
+ self,
+ application_id: IMPORT_typing.Optional[IMPORT_reboot_aio_types.ApplicationId],
+ tasks: IMPORT_typing.Callable[[IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext], UserTasksStub],
+ *,
+ when: IMPORT_typing.Optional[IMPORT_datetime_datetime | IMPORT_datetime_timedelta] = None,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ ) -> None:
+ self._application_id = application_id
+ self._tasks = tasks
+ self._when = ensure_has_timezone(when=when)
+ self._idempotency = idempotency
+
+ # User callable tasks:
+ @IMPORT_typing.overload
+ async def CreateCounter(
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __request_or_options__: User.CreateCounterRequest,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> User.CreateCounterTask:
+ ...
+
+ @IMPORT_typing.overload
+ async def CreateCounter(
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __request_or_options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ *,
+ description: str | Unset = UNSET,
+ ) -> User.CreateCounterTask:
+ ...
+
+ async def CreateCounter( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __request_or_options__: IMPORT_typing.Optional[User.CreateCounterRequest | IMPORT_reboot_aio_call.Options] = None,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ *,
+ description: str | Unset = UNSET,
+ ) -> User.CreateCounterTask:
+ IMPORT_reboot_aio_types.assert_type(__context__, [IMPORT_reboot_aio_contexts.WorkflowContext, IMPORT_reboot_aio_external.ExternalContext])
+ # UX improvement: check that neither positional argument was accidentally
+ # given a gRPC request type.
+ IMPORT_reboot_aio_types.assert_not_request_type(__context__, request_type=User.CreateCounterRequest)
+ IMPORT_reboot_aio_types.assert_not_request_type(__options__, request_type=User.CreateCounterRequest)
+
+
+ __request__: IMPORT_typing.Optional[User.CreateCounterRequest] = None
+ if isinstance(__request_or_options__, User.CreateCounterRequest):
+ assert __request_or_options__ is not None
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ assert description is UNSET
+
+ __request__ = __request_or_options__
+ else:
+ assert __options__ is None
+ assert __request_or_options__ is None or isinstance(__request_or_options__, IMPORT_reboot_aio_call.Options)
+ __options__ = __request_or_options__
+
+ __request__ = UserCreateCounterRequestFromInputFields(
+ description=description,
+ )
+ __schedule__: IMPORT_typing.Optional[IMPORT_reboot_time_DateTimeWithTimeZone] = (IMPORT_reboot_time_DateTimeWithTimeZone.now() + __this__._when) if isinstance(
+ __this__._when, IMPORT_datetime_timedelta
+ ) else __this__._when
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = __this__._idempotency
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ # Add scheduling information to the metadata.
+ __metadata__ = (
+ (IMPORT_reboot_aio_headers.TASK_SCHEDULE,
+ __schedule__.isoformat() if __schedule__ else ''),
+ ) + (__metadata__ or tuple())
+
+ __task_id__ = await __this__._tasks(
+ __context__
+ ).CreateCounter(
+ __request__,
+ idempotency=__idempotency__,
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ )
+
+ return User.CreateCounterTask(
+ __context__,
+ task_id=__task_id__,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ create_counter = CreateCounter
+
+ async def ListCounters( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> User.ListCountersTask:
+ IMPORT_reboot_aio_types.assert_type(__context__, [IMPORT_reboot_aio_contexts.WorkflowContext, IMPORT_reboot_aio_external.ExternalContext])
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+ __schedule__: IMPORT_typing.Optional[IMPORT_reboot_time_DateTimeWithTimeZone] = (IMPORT_reboot_time_DateTimeWithTimeZone.now() + __this__._when) if isinstance(
+ __this__._when, IMPORT_datetime_timedelta
+ ) else __this__._when
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = __this__._idempotency
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ # Add scheduling information to the metadata.
+ __metadata__ = (
+ (IMPORT_reboot_aio_headers.TASK_SCHEDULE,
+ __schedule__.isoformat() if __schedule__ else ''),
+ ) + (__metadata__ or tuple())
+
+ __task_id__ = await __this__._tasks(
+ __context__
+ ).ListCounters(
+ idempotency=__idempotency__,
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ )
+
+ return User.ListCountersTask(
+ __context__,
+ task_id=__task_id__,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ list_counters = ListCounters
+
+ async def Whoami( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> User.WhoamiTask:
+ IMPORT_reboot_aio_types.assert_type(__context__, [IMPORT_reboot_aio_contexts.WorkflowContext, IMPORT_reboot_aio_external.ExternalContext])
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+ __schedule__: IMPORT_typing.Optional[IMPORT_reboot_time_DateTimeWithTimeZone] = (IMPORT_reboot_time_DateTimeWithTimeZone.now() + __this__._when) if isinstance(
+ __this__._when, IMPORT_datetime_timedelta
+ ) else __this__._when
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = __this__._idempotency
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ # Add scheduling information to the metadata.
+ __metadata__ = (
+ (IMPORT_reboot_aio_headers.TASK_SCHEDULE,
+ __schedule__.isoformat() if __schedule__ else ''),
+ ) + (__metadata__ or tuple())
+
+ __task_id__ = await __this__._tasks(
+ __context__
+ ).Whoami(
+ idempotency=__idempotency__,
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ )
+
+ return User.WhoamiTask(
+ __context__,
+ task_id=__task_id__,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ whoami = Whoami
+
+ async def read(
+ self,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ ) -> User.State:
+ return await (
+ self.always() if context.within_until()
+ else (
+ self.per_iteration() if context.within_loop()
+ else self.per_workflow()
+ )
+ ).read(context)
+
+ async def write(
+ self,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ writer: UserBaseServicer.InlineWriterCallable[UserBaseServicer.InlineWriterCallableResult],
+ *,
+ type: IMPORT_reboot_aio_workflows.Type | IMPORT_reboot_aio_workflows._Unset = IMPORT_reboot_aio_workflows._UNSET,
+ ) -> UserBaseServicer.InlineWriterCallableResult:
+ """Perform an "inline write" within a workflow.
+
+ `type=` is optional: when omitted, `writer`'s return
+ annotation is used.
+ """
+ # We forward `type=` along to the inner `write` (which
+ # also infers if not passed). Pass through whichever
+ # form the user supplied -- explicit class or the
+ # `_UNSET` sentinel to trigger inference downstream.
+ return await (
+ self.always() if context.within_until()
+ else (
+ self.per_iteration() if context.within_loop()
+ else self.per_workflow()
+ )
+ ).write(
+ context, writer, type=type
+ )
+
+ # User specific methods:
+ @IMPORT_typing.overload
+ async def CreateCounter(
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __request_or_options__: User.CreateCounterRequest,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ *,
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ ) -> User.CreateCounterResponse:
+ ...
+
+ @IMPORT_typing.overload
+ async def CreateCounter(
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __request_or_options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ *,
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ description: str | Unset = UNSET,
+ ) -> User.CreateCounterResponse:
+ ...
+
+ async def CreateCounter( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __request_or_options__: IMPORT_typing.Optional[User.CreateCounterRequest | IMPORT_reboot_aio_call.Options] = None,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ *,
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ description: str | Unset = UNSET,
+ ) -> User.CreateCounterResponse:
+ # UX improvement: check that neither positional argument was accidentally
+ # given a gRPC request type.
+ IMPORT_reboot_aio_types.assert_not_request_type(__context__, request_type=User.CreateCounterRequest)
+ IMPORT_reboot_aio_types.assert_not_request_type(__options__, request_type=User.CreateCounterRequest)
+
+ __request__: IMPORT_typing.Optional[User.CreateCounterRequest] = None
+ if isinstance(__request_or_options__, User.CreateCounterRequest):
+ assert __request_or_options__ is not None
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ assert description is UNSET
+
+ __request__ = __request_or_options__
+ else:
+ assert __options__ is None
+ assert __request_or_options__ is None or isinstance(__request_or_options__, IMPORT_reboot_aio_call.Options)
+ __options__ = __request_or_options__
+
+ __request__ = UserCreateCounterRequestFromInputFields(
+ description=description,
+ )
+
+ # Within a `workflow`, all "bare" calls are
+ # `per_workflow()` calls, unless we're within a control
+ # loop, in which case they are syntactic sugar for
+ # `per_iteration()`.
+ if __idempotency__ is None:
+ if isinstance(__context__, IMPORT_reboot_aio_contexts.WorkflowContext):
+ return await (
+ __this__.per_iteration() if __context__.within_loop()
+ else __this__.per_workflow()
+ ).CreateCounter(
+ __context__,
+ __request__,
+ __options__ or IMPORT_reboot_aio_call.Options(),
+ )
+ elif isinstance(__context__, IMPORT_reboot_aio_external.InitializeContext):
+ return await __this__.idempotently().CreateCounter(
+ __context__,
+ __request__,
+ __options__ or IMPORT_reboot_aio_call.Options(),
+ )
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ return UserCreateCounterResponseFromProto(
+ await __this__._workflow(__context__).CreateCounter(
+ __request__,
+ idempotency=__idempotency__,
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ )
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ create_counter = CreateCounter
+
+ async def ListCounters( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.ReaderContext | IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ *,
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ ) -> User.ListCountersResponse:
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ # Within a `workflow`, all "bare" calls are
+ # `per_workflow()` calls, unless we're within a control
+ # loop, in which case they are syntactic sugar for
+ # `per_iteration()`.
+ #
+ # Unless we are "within until" in which case all "bare"
+ # calls are `.always().
+ if __idempotency__ is None:
+ if isinstance(__context__, IMPORT_reboot_aio_contexts.WorkflowContext):
+ return await (
+ __this__.always() if __context__.within_until()
+ else (
+ __this__.per_iteration() if __context__.within_loop()
+ else __this__.per_workflow()
+ )
+ ).ListCounters(
+ __context__,
+ __options__ or IMPORT_reboot_aio_call.Options(),
+ )
+ elif isinstance(__context__, IMPORT_reboot_aio_external.InitializeContext):
+ return await __this__.idempotently().ListCounters(
+ __context__,
+ __options__ or IMPORT_reboot_aio_call.Options(),
+ )
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+ return UserListCountersResponseFromProto(
+ await __this__._reader(__context__).ListCounters(
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ idempotency=__idempotency__,
+ )
+ )
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ list_counters = ListCounters
+
+ async def Whoami( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.ReaderContext | IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ *,
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ ) -> User.WhoamiResponse:
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ # Within a `workflow`, all "bare" calls are
+ # `per_workflow()` calls, unless we're within a control
+ # loop, in which case they are syntactic sugar for
+ # `per_iteration()`.
+ #
+ # Unless we are "within until" in which case all "bare"
+ # calls are `.always().
+ if __idempotency__ is None:
+ if isinstance(__context__, IMPORT_reboot_aio_contexts.WorkflowContext):
+ return await (
+ __this__.always() if __context__.within_until()
+ else (
+ __this__.per_iteration() if __context__.within_loop()
+ else __this__.per_workflow()
+ )
+ ).Whoami(
+ __context__,
+ __options__ or IMPORT_reboot_aio_call.Options(),
+ )
+ elif isinstance(__context__, IMPORT_reboot_aio_external.InitializeContext):
+ return await __this__.idempotently().Whoami(
+ __context__,
+ __options__ or IMPORT_reboot_aio_call.Options(),
+ )
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+ return UserWhoamiResponseFromProto(
+ await __this__._reader(__context__).Whoami(
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ idempotency=__idempotency__,
+ )
+ )
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ whoami = Whoami
+
+ class _Forall:
+
+ _ids: IMPORT_typing.Iterable[str]
+
+ def __init__(self, ids: IMPORT_typing.Iterable[str]):
+ self._ids = ids
+
+ @IMPORT_typing.overload
+ async def CreateCounter(
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __request_or_options__: User.CreateCounterRequest,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> list[User.CreateCounterResponse]:
+ ...
+
+ @IMPORT_typing.overload
+ async def CreateCounter(
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __request_or_options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ *,
+ description: str | Unset = UNSET,
+ ) -> list[User.CreateCounterResponse]:
+ ...
+
+ async def CreateCounter( # type: ignore[misc]
+ # In methods which are dealing with user input, (i.e.,
+ # proto message field names), we should use '__double_underscored__'
+ # variables to avoid any potential name conflicts with the method's
+ # parameters.
+ # The '__self__' parameter is a convention in Python to
+ # indicate that this method is a bound method, so we use
+ # '__this__' instead.
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __request_or_options__: IMPORT_typing.Optional[User.CreateCounterRequest | IMPORT_reboot_aio_call.Options] = None,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ *,
+ description: str | Unset = UNSET,
+ ) -> list[User.CreateCounterResponse]:
+ if isinstance(__request_or_options__, User.CreateCounterRequest):
+ assert __request_or_options__ is not None
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+ __options__ = __options__ or IMPORT_reboot_aio_call.Options()
+
+ assert description is UNSET
+
+ return await IMPORT_asyncio.gather(
+ *[
+ User.ref(
+ id
+ ).CreateCounter(
+ __context__,
+ __request_or_options__,
+ __options__,
+ ) for id in __this__._ids
+ ]
+ )
+ else:
+ assert __options__ is None
+ assert __request_or_options__ is None or isinstance(__request_or_options__, IMPORT_reboot_aio_call.Options)
+ __options__ = __request_or_options__ or IMPORT_reboot_aio_call.Options()
+
+ return await IMPORT_asyncio.gather(
+ *[
+ User.ref(
+ id
+ ).CreateCounter(
+ __context__,
+ UserCreateCounterRequestFromInputFields(
+ description=description,
+ ),
+ __options__,
+ ) for id in __this__._ids
+ ]
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ create_counter = CreateCounter
+
+ async def ListCounters( # type: ignore[misc]
+ # In methods which are dealing with user input, (i.e.,
+ # proto message field names), we should use '__double_underscored__'
+ # variables to avoid any potential name conflicts with the method's
+ # parameters.
+ # The '__self__' parameter is a convention in Python to
+ # indicate that this method is a bound method, so we use
+ # '__this__' instead.
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.ReaderContext | IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> list[User.ListCountersResponse]:
+ __options__ = __options__ or IMPORT_reboot_aio_call.Options()
+ return await IMPORT_asyncio.gather(
+ *[
+ User.ref(
+ id
+ ).ListCounters(
+ __context__,
+ __options__,
+ ) for id in __this__._ids
+ ]
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ list_counters = ListCounters
+
+ async def Whoami( # type: ignore[misc]
+ # In methods which are dealing with user input, (i.e.,
+ # proto message field names), we should use '__double_underscored__'
+ # variables to avoid any potential name conflicts with the method's
+ # parameters.
+ # The '__self__' parameter is a convention in Python to
+ # indicate that this method is a bound method, so we use
+ # '__this__' instead.
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.ReaderContext | IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> list[User.WhoamiResponse]:
+ __options__ = __options__ or IMPORT_reboot_aio_call.Options()
+ return await IMPORT_asyncio.gather(
+ *[
+ User.ref(
+ id
+ ).Whoami(
+ __context__,
+ __options__,
+ ) for id in __this__._ids
+ ]
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ whoami = Whoami
+
+ @classmethod
+ def forall(cls, ids: IMPORT_typing.Iterable[str]) -> User._Forall:
+ return User._Forall(ids)
+
+ @classmethod
+ def ref(
+ cls,
+ state_id: IMPORT_typing.Optional[IMPORT_reboot_aio_types.StateId] = None,
+ *,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ) -> User.WeakReference[User.WeakReference._Schedule] | User.WeakReference[User.WeakReference._WriterSchedule]:
+ # We support calling `User.ref()` with
+ # no `state_id` __only__ inside a workflow to be able to call an
+ # inline writer, inline reader or other method call, since
+ # workflow is a `classmethod` and therefor we can't get a
+ # reference to outselves as `self.ref()`.
+ if state_id is None:
+ context = IMPORT_reboot_aio_contexts.Context.get()
+
+ if context is None:
+ raise RuntimeError(
+ 'Missing asyncio context variable `context`; '
+ 'are you using this class without Reboot?'
+ )
+
+ if not isinstance(context, IMPORT_reboot_aio_contexts.WorkflowContext):
+ raise RuntimeError(
+ '`ref()` called without a `state_id` can only be used within a Workflow.'
+ )
+
+ servicer = UserBaseServicer.__servicer__.get()
+
+ if servicer is None:
+ raise RuntimeError(
+ 'Missing asyncio context variable `servicer`; '
+ 'are you using this class without Reboot?'
+ )
+
+ return User.WeakReference(
+ # TODO(https://github.com/reboot-dev/mono/issues/3226): add support for calling other applications.
+ # For now this always stays within the application that creates the context.
+ application_id=None,
+ state_id=context._state_ref.id,
+ schedule_type=User.WeakReference._WriterSchedule,
+ # If the user didn't specify a bearer token we may still end up using the app-internal bearer token,
+ # but that's decided at the time of the call.
+ bearer_token=bearer_token,
+ servicer=servicer,
+ )
+
+ return User.WeakReference(
+ # TODO(https://github.com/reboot-dev/mono/issues/3226): add support for calling other applications.
+ # For now this always stays within the application that creates the context.
+ application_id=None,
+ state_id=state_id,
+ schedule_type=User.WeakReference._Schedule,
+ bearer_token=bearer_token,
+ )
+
+
+ @classmethod
+ async def Create( # type: ignore[misc]
+ __cls__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __state_id__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.StateId] = None,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ ) -> tuple[User.WeakReference, None]:
+ # Within a `workflow`, all "bare" calls are
+ # `per_workflow()` calls, unless we're within a control
+ # loop, in which case they are syntactic sugar for
+ # `per_iteration()`.
+ #
+ # Unless we are "within until" in which case all "bare"
+ # calls are `.always()`.
+
+ assert __state_id__ is None or isinstance(__state_id__, IMPORT_reboot_aio_types.StateId)
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+ assert __idempotency__ is None or isinstance(__idempotency__, IMPORT_reboot_aio_idempotency.Idempotency)
+
+ if __idempotency__ is None:
+ __args__: tuple[IMPORT_typing.Any, ...] = (
+ __context__,
+ )
+ if __state_id__ is not None:
+ # Don't include `state_id` if it is `None`, so that
+ # we won't break the positional argument deduction.
+ __args__ += (__state_id__,)
+ __args__ += (
+ __options__,
+ )
+ if isinstance(__context__, IMPORT_reboot_aio_contexts.WorkflowContext):
+ return await (
+ __cls__.always() if __context__.within_until()
+ else (
+ __cls__.per_iteration() if __context__.within_loop()
+ else __cls__.per_workflow()
+ )
+ ).Create(
+ *__args__
+ )
+ elif isinstance(__context__, IMPORT_reboot_aio_external.InitializeContext):
+ return await __cls__.idempotently().Create(
+ *__args__
+ )
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ if __state_id__ is None:
+ if __idempotency__ is None:
+ __state_id__ = str(IMPORT_uuid.uuid4())
+ else:
+ __state_id__ = __context__.generate_idempotent_state_id(
+ state_type_name=__cls__.__state_type_name__,
+ service_name=IMPORT_reboot_aio_types.ServiceName('reboot.ping.UserMethods'),
+ method='Create',
+ idempotency=__idempotency__,
+ )
+
+ __reference__ = User.ref(
+ __state_id__, bearer_token=__bearer_token__
+ )
+ __stub__ = __reference__._workflow(__context__)
+ return (
+ __reference__,
+ UserCreateResponseFromProto(
+ await __stub__.Create(
+ idempotency=__idempotency__,
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ )
+ ),
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ create = Create
+
+ @IMPORT_typing.overload
+ @classmethod
+ def idempotently(cls, alias: IMPORT_typing.Optional[str] = None, *, how: IMPORT_reboot_aio_idempotency.How = IMPORT_reboot_aio_idempotency.PER_WORKFLOW) -> User._ConstructIdempotently:
+ ...
+
+ @IMPORT_typing.overload
+ @classmethod
+ def idempotently(cls, *, key: IMPORT_uuid.UUID, how: IMPORT_reboot_aio_idempotency.How = IMPORT_reboot_aio_idempotency.PER_WORKFLOW) -> User._ConstructIdempotently:
+ ...
+
+ @classmethod
+ def idempotently(
+ cls,
+ alias: IMPORT_typing.Optional[str] = None,
+ *,
+ key: IMPORT_typing.Optional[IMPORT_uuid.UUID] = None,
+ how: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.How] = None,
+ ) -> User._ConstructIdempotently:
+ return User._ConstructIdempotently(
+ _idempotency=IMPORT_reboot_aio_contexts.Context.idempotency(
+ alias=alias,
+ key=key,
+ how=how,
+ ),
+ )
+
+ @classmethod
+ def per_workflow(
+ cls,
+ alias: IMPORT_typing.Optional[str] = None,
+ ):
+ return cls.idempotently(alias)
+
+ @classmethod
+ def per_iteration(
+ cls,
+ alias: IMPORT_typing.Optional[str] = None,
+ ):
+ return cls.idempotently(
+ alias,
+ how=IMPORT_reboot_aio_idempotency.PER_ITERATION,
+ )
+
+ @classmethod
+ def always(cls):
+ return cls.idempotently(
+ how=IMPORT_reboot_aio_idempotency.ALWAYS,
+ )
+
+ @IMPORT_dataclasses.dataclass(frozen=True)
+ class _ConstructIdempotently:
+
+ _idempotency: IMPORT_reboot_aio_idempotency.Idempotency
+
+
+ async def Create( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __state_id__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.StateId] = None,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> tuple[User.WeakReference, None]:
+ if __state_id__ is not None and not isinstance(__state_id__, IMPORT_reboot_aio_types.StateId):
+ raise TypeError(f"Expecting second positional argument to be of type 'str', got '{type(__state_id__).__name__}'")
+ if __options__ is not None and not isinstance(__options__, IMPORT_reboot_aio_call.Options):
+ raise TypeError(f"Expecting third positional argument to be of type 'reboot.aio.call.Options', got '{type(__options__).__name__}'")
+ __args__: tuple[IMPORT_typing.Any, ...]
+
+ if __state_id__ is not None:
+ __args__ = (
+ __context__,
+ __state_id__,
+ __options__,
+ __this__._idempotency,
+ )
+ else:
+ __args__ = (
+ __context__,
+ __options__,
+ __this__._idempotency,
+ )
+
+ return await User.Create(
+ *__args__,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ create = Create
+
+Counter_ScheduleTypeVar = IMPORT_typing.TypeVar('Counter_ScheduleTypeVar', 'Counter.WeakReference._Schedule', 'Counter.WeakReference._WriterSchedule')
+Counter_IdempotentlyScheduleTypeVar = IMPORT_typing.TypeVar('Counter_IdempotentlyScheduleTypeVar', 'Counter.WeakReference._Schedule', 'Counter.WeakReference._WriterSchedule')
+
+Counter_UntilCallableType = IMPORT_typing.TypeVar('Counter_UntilCallableType')
+
+class CounterSingleton:
+ Servicer: IMPORT_typing.TypeAlias = CounterSingletonServicer
+
+
+class Counter:
+
+
+ Servicer: IMPORT_typing.TypeAlias = CounterServicer
+
+ singleton: IMPORT_typing.TypeAlias = CounterSingleton
+
+ Effects: IMPORT_typing.TypeAlias = CounterBaseServicer.Effects
+
+ Authorizer: IMPORT_typing.TypeAlias = CounterAuthorizer
+
+ State: IMPORT_typing.TypeAlias = IMPORT_typing.cast(type, IMPORT_api.Counter.state)
+
+ CreateRequest: IMPORT_typing.TypeAlias = IMPORT_typing.cast(type, IMPORT_api.Counter.methods['create'].request)
+
+
+
+ IncrementResponse: IMPORT_typing.TypeAlias = IMPORT_typing.cast(type, IMPORT_api.Counter.methods['increment'].response)
+
+
+ ValueResponse: IMPORT_typing.TypeAlias = IMPORT_typing.cast(type, IMPORT_api.Counter.methods['value'].response)
+
+
+ DescriptionResponse: IMPORT_typing.TypeAlias = IMPORT_typing.cast(type, IMPORT_api.Counter.methods['description'].response)
+
+ ShowClickerProps: IMPORT_typing.TypeAlias = IMPORT_typing.cast(type, IMPORT_api.Counter.methods['show_clicker'].request)
+ __state_type_name__ = IMPORT_reboot_aio_types.StateTypeName("reboot.ping.Counter")
+
+ class CreateTask:
+ """Represents a scheduled task running for the
+ state. Note that this is not a coroutine because we are trying
+ to convey the semantics that the task is already running (or
+ will soon be).
+ """
+
+ @classmethod
+ def retrieve(
+ cls,
+ context: IMPORT_reboot_aio_contexts.Context | IMPORT_reboot_aio_external.ExternalContext,
+ *,
+ task_id: IMPORT_rbt_v1alpha1.tasks_pb2.TaskId,
+ ):
+ return cls(context, task_id=task_id)
+
+ def __init__(
+ self,
+ context: IMPORT_reboot_aio_contexts.Context | IMPORT_reboot_aio_external.ExternalContext,
+ *,
+ task_id: IMPORT_rbt_v1alpha1.tasks_pb2.TaskId,
+ ) -> None:
+ # Depending on the context type (inside or outside a Reboot application)
+ # we may or may not know the application ID. If we don't know it, then
+ # the `ExternalContext.gateway` will determine it.
+ #
+ # TODO: in the future we expect to support cross-application calls, in
+ # which case the developer may explicitly pass in an application ID
+ # here.
+ self._application_id: IMPORT_typing.Optional[IMPORT_reboot_aio_types.ApplicationId] = None
+ if isinstance(context, IMPORT_reboot_aio_contexts.Context):
+ self._application_id = context.application_id
+ self._channel_manager = context.channel_manager
+ self._task_id = task_id
+
+ @property
+ def task_id(self) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ return self._task_id
+
+ def __await__(self) -> IMPORT_typing.Generator[None, None, None]:
+ """Awaits for task to finish and returns its response."""
+ async def wait_for_task() -> google.protobuf.empty_pb2.Empty:
+ channel = self._channel_manager.get_channel_to_state(
+ IMPORT_reboot_aio_types.StateTypeName(self._task_id.state_type),
+ IMPORT_reboot_aio_types.StateRef(self._task_id.state_ref),
+ )
+
+ stub = IMPORT_rbt_v1alpha1.tasks_pb2_grpc.TasksStub(channel)
+
+ try:
+ call = IMPORT_reboot_aio_stubs.UnaryRetriedCall(
+ call=None, # `RetriedCall` can create the call itself.
+ stub_method=stub.Wait,
+ method_name="Wait",
+ request=IMPORT_rbt_v1alpha1.tasks_pb2.WaitRequest(task_id=self._task_id),
+ metadata=IMPORT_reboot_aio_headers.Headers(
+ state_ref=IMPORT_reboot_aio_types.StateRef(self._task_id.state_ref),
+ application_id=self._application_id,
+ ).to_grpc_metadata(),
+ aborted_type=IMPORT_reboot.aio.aborted.SystemAborted,
+ )
+
+ wait_for_task_response = await call
+ except IMPORT_reboot.aio.aborted.SystemAborted as error:
+ if error.code == IMPORT_grpc.StatusCode.NOT_FOUND:
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.UnknownTask()
+ ) from None
+
+ raise
+ else:
+ response_or_error: IMPORT_typing.Optional[IMPORT_google_protobuf_any_pb2.Any] = None
+ is_error = False
+
+ if wait_for_task_response.response_or_error.WhichOneof("response_or_error") == "response":
+ response_or_error = wait_for_task_response.response_or_error.response
+ else:
+ is_error = True
+ response_or_error = wait_for_task_response.response_or_error.error
+
+ assert response_or_error is not None
+ assert response_or_error.TypeName() != ""
+
+ response = google.protobuf.empty_pb2.Empty()
+
+ if (
+ not is_error and response_or_error.TypeName() != response.DESCRIPTOR.full_name
+ ):
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.InvalidArgument(),
+ message=
+ f"task with UUID {IMPORT_uuid.UUID(bytes=self._task_id.task_uuid)} "
+ f"has a response of type '{response_or_error.TypeName()}' "
+ "but expecting type 'google.protobuf.empty_pb2.Empty'; "
+ "are you waiting on a task of the correct method?",
+ ) from None
+
+ if is_error:
+ aborted_type = Counter.CreateAborted
+
+ # In Reboot >= 0.40.2 we expect the error to be a `google.rpc.Status`.
+ if response_or_error.Is(IMPORT_google_rpc_status_pb2.Status.DESCRIPTOR):
+ status = IMPORT_google_rpc_status_pb2.Status()
+ response_or_error.Unpack(status)
+ raise aborted_type.from_status(status)
+
+ # In Reboot < 0.40.2 workflows throwing declared errors behaved poorly;
+ # we don't aim to emulate its behavior. Indicate that we don't know the
+ # reason for the abort.
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ )
+
+ else:
+ response_or_error.Unpack(response)
+ return CounterCreateResponseFromProto(response)
+
+ return wait_for_task().__await__()
+
+ CreateEffects: IMPORT_typing.TypeAlias = CounterBaseServicer.CreateEffects
+
+ class CreateAborted(IMPORT_reboot.aio.aborted.Aborted):
+
+
+ Error = IMPORT_typing.Union[
+ IMPORT_reboot.aio.aborted.GrpcError,
+ IMPORT_reboot.aio.aborted.RebootError,
+ ]
+
+ METHOD_PROTOBUF_ERROR_TYPES: list[type[IMPORT_google_protobuf_message.Message]] = [
+ ]
+
+ PROTOBUF_ERROR_TYPES: list[type[IMPORT_google_protobuf_message.Message | IMPORT_reboot_api.Model]] = (
+ METHOD_PROTOBUF_ERROR_TYPES +
+ IMPORT_reboot.aio.aborted.GRPC_ERROR_TYPES +
+ IMPORT_reboot.aio.aborted.REBOOT_ERROR_TYPES
+ )
+
+ _error: Error
+
+ MethodProtobufError = IMPORT_typing.Union[
+ IMPORT_reboot.aio.aborted.GrpcError,
+ IMPORT_reboot.aio.aborted.RebootError,
+ ]
+ _method_protobuf_error: MethodProtobufError
+
+ _code: IMPORT_grpc.StatusCode
+ _message: IMPORT_typing.Optional[str]
+
+ def __init__(
+ self,
+ error: IMPORT_reboot.aio.aborted.GrpcError,
+ *,
+ message: IMPORT_typing.Optional[str] = None,
+ # Do not set this value when constructing in order to
+ # raise. This is only used internally when constructing
+ # from aborted calls.
+ error_types: IMPORT_typing.Sequence[type[MethodProtobufError]] = (
+ METHOD_PROTOBUF_ERROR_TYPES + IMPORT_reboot.aio.aborted.GRPC_ERROR_TYPES
+ ),
+ ):
+ super().__init__()
+
+ self._message = None
+
+ if self.is_declared_error(error):
+ # If the error is a declared error, we should always
+ # store both formats for Pydantic and Protobuf for that
+ # error, so users can access the Pydantic version via
+ # `aborted.error` and the Protobuf version will be used
+ # to send the error over the wire.
+ if isinstance(error, IMPORT_reboot.api.Model):
+ # Happens when somebody raises an aborted with a Pydantic
+ # error format, usually in the backend method
+ # implementation.
+
+ self._error = error
+
+ else:
+ # Usually happens on the client side when we are
+ # constructing an aborted from the `AioRpcError`, so
+ # the error will be in Protobuf shape, and we want
+ # to convert it back to Pydantic if possible.
+ self._method_protobuf_error = error
+
+ else:
+ # Non declared error should be well known Reboot or gRPC
+ # error, which always comes in the Protobuf format, however
+ # it might be a case when the server is using new API and
+ # client is old, so server might send a declared error from
+ # the server perspective, but client doesn't know that it
+ # is a declared error now, so we need to gracefully
+ # cover that case.
+ if isinstance(error, IMPORT_reboot.api.Model):
+ # Store both formats so users can access the Pydantic
+ # version via `aborted.error` and the Protobuf version
+ # will be used to send the error over the wire.
+ self._error = IMPORT_rbt_v1alpha1.errors_pb2.Unknown()
+ self._method_protobuf_error = IMPORT_rbt_v1alpha1.errors_pb2.Unknown()
+
+ self._message = (
+ "Received a declared error from the server, "
+ "but the client doesn't know about it; "
+ "please update the client to the latest version\n"
+ ) + (
+ f"Original error: {error.model_dump_json()}\n"
+ ) + message if message is not None else ""
+
+ # Store both formats so users can access the error via
+ # `aborted.error` and the Protobuf version
+ # will be used to send the error over the wire.
+ self._error = error
+ self._method_protobuf_error = error
+
+ IMPORT_reboot_aio_types.assert_type(self._protobuf_error, error_types)
+
+ code = self.grpc_status_code_from_error(self._protobuf_error)
+
+ if code is None:
+ # Must be a Reboot specific or declared method error.
+ code = IMPORT_grpc.StatusCode.ABORTED
+
+ self._code = code
+
+ self._message = self._message if self._message is not None else message
+
+ @property
+ def error(self) -> Error | MethodProtobufError:
+ return self._error
+
+ @property
+ def _protobuf_error(self) -> MethodProtobufError:
+ return self._method_protobuf_error
+
+ @property
+ def code(self) -> IMPORT_grpc.StatusCode:
+ return self._code
+
+ @property
+ def message(self) -> IMPORT_typing.Optional[str]:
+ return self._message
+
+ @classmethod
+ def from_status(cls, status: IMPORT_google_rpc_status_pb2.Status):
+ error = cls.error_from_google_rpc_status_details(
+ status,
+ cls.PROTOBUF_ERROR_TYPES,
+ )
+
+ message = status.message if len(status.message) > 0 else None
+
+ if error is not None:
+ return cls(error, message=message, error_types=cls.PROTOBUF_ERROR_TYPES)
+
+ error = cls.error_from_google_rpc_status_code(status)
+
+ assert error is not None
+
+ # TODO(benh): also consider getting the type names from
+ # `status.details` and including that in `message` to make
+ # debugging easier.
+
+ return cls(error, message=message)
+
+ @classmethod
+ def from_grpc_aio_rpc_error(cls, aio_rpc_error: IMPORT_grpc.aio.AioRpcError):
+ return cls(
+ cls.error_from_grpc_aio_rpc_error(aio_rpc_error),
+ message=aio_rpc_error.details(),
+ )
+
+ @classmethod
+ def is_declared_error(cls, error: IMPORT_google_protobuf_message.Message | IMPORT_reboot.api.Model) -> bool:
+ if isinstance(error, IMPORT_reboot.api.Model):
+ return type(error) in IMPORT_api.Counter.methods['create'].errors
+ return False
+
+ class IncrementTask:
+ """Represents a scheduled task running for the
+ state. Note that this is not a coroutine because we are trying
+ to convey the semantics that the task is already running (or
+ will soon be).
+ """
+
+ @classmethod
+ def retrieve(
+ cls,
+ context: IMPORT_reboot_aio_contexts.Context | IMPORT_reboot_aio_external.ExternalContext,
+ *,
+ task_id: IMPORT_rbt_v1alpha1.tasks_pb2.TaskId,
+ ):
+ return cls(context, task_id=task_id)
+
+ def __init__(
+ self,
+ context: IMPORT_reboot_aio_contexts.Context | IMPORT_reboot_aio_external.ExternalContext,
+ *,
+ task_id: IMPORT_rbt_v1alpha1.tasks_pb2.TaskId,
+ ) -> None:
+ # Depending on the context type (inside or outside a Reboot application)
+ # we may or may not know the application ID. If we don't know it, then
+ # the `ExternalContext.gateway` will determine it.
+ #
+ # TODO: in the future we expect to support cross-application calls, in
+ # which case the developer may explicitly pass in an application ID
+ # here.
+ self._application_id: IMPORT_typing.Optional[IMPORT_reboot_aio_types.ApplicationId] = None
+ if isinstance(context, IMPORT_reboot_aio_contexts.Context):
+ self._application_id = context.application_id
+ self._channel_manager = context.channel_manager
+ self._task_id = task_id
+
+ @property
+ def task_id(self) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ return self._task_id
+
+ def __await__(self) -> IMPORT_typing.Generator[None, None, Counter.IncrementResponse]:
+ """Awaits for task to finish and returns its response."""
+ async def wait_for_task() -> reboot.ping.ping_api_pb2.CounterIncrementResponse:
+ channel = self._channel_manager.get_channel_to_state(
+ IMPORT_reboot_aio_types.StateTypeName(self._task_id.state_type),
+ IMPORT_reboot_aio_types.StateRef(self._task_id.state_ref),
+ )
+
+ stub = IMPORT_rbt_v1alpha1.tasks_pb2_grpc.TasksStub(channel)
+
+ try:
+ call = IMPORT_reboot_aio_stubs.UnaryRetriedCall(
+ call=None, # `RetriedCall` can create the call itself.
+ stub_method=stub.Wait,
+ method_name="Wait",
+ request=IMPORT_rbt_v1alpha1.tasks_pb2.WaitRequest(task_id=self._task_id),
+ metadata=IMPORT_reboot_aio_headers.Headers(
+ state_ref=IMPORT_reboot_aio_types.StateRef(self._task_id.state_ref),
+ application_id=self._application_id,
+ ).to_grpc_metadata(),
+ aborted_type=IMPORT_reboot.aio.aborted.SystemAborted,
+ )
+
+ wait_for_task_response = await call
+ except IMPORT_reboot.aio.aborted.SystemAborted as error:
+ if error.code == IMPORT_grpc.StatusCode.NOT_FOUND:
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.UnknownTask()
+ ) from None
+
+ raise
+ else:
+ response_or_error: IMPORT_typing.Optional[IMPORT_google_protobuf_any_pb2.Any] = None
+ is_error = False
+
+ if wait_for_task_response.response_or_error.WhichOneof("response_or_error") == "response":
+ response_or_error = wait_for_task_response.response_or_error.response
+ else:
+ is_error = True
+ response_or_error = wait_for_task_response.response_or_error.error
+
+ assert response_or_error is not None
+ assert response_or_error.TypeName() != ""
+
+ response = reboot.ping.ping_api_pb2.CounterIncrementResponse()
+
+ if (
+ not is_error and response_or_error.TypeName() != response.DESCRIPTOR.full_name
+ ):
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.InvalidArgument(),
+ message=
+ f"task with UUID {IMPORT_uuid.UUID(bytes=self._task_id.task_uuid)} "
+ f"has a response of type '{response_or_error.TypeName()}' "
+ "but expecting type 'reboot.ping.ping_api_pb2.CounterIncrementResponse'; "
+ "are you waiting on a task of the correct method?",
+ ) from None
+
+ if is_error:
+ aborted_type = Counter.IncrementAborted
+
+ # In Reboot >= 0.40.2 we expect the error to be a `google.rpc.Status`.
+ if response_or_error.Is(IMPORT_google_rpc_status_pb2.Status.DESCRIPTOR):
+ status = IMPORT_google_rpc_status_pb2.Status()
+ response_or_error.Unpack(status)
+ raise aborted_type.from_status(status)
+
+ # In Reboot < 0.40.2 workflows throwing declared errors behaved poorly;
+ # we don't aim to emulate its behavior. Indicate that we don't know the
+ # reason for the abort.
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ )
+
+ else:
+ response_or_error.Unpack(response)
+ return CounterIncrementResponseFromProto(response)
+
+ return wait_for_task().__await__()
+
+ IncrementEffects: IMPORT_typing.TypeAlias = CounterBaseServicer.IncrementEffects
+
+ class IncrementAborted(IMPORT_reboot.aio.aborted.Aborted):
+
+
+ Error = IMPORT_typing.Union[
+ IMPORT_reboot.aio.aborted.GrpcError,
+ IMPORT_reboot.aio.aborted.RebootError,
+ ]
+
+ METHOD_PROTOBUF_ERROR_TYPES: list[type[IMPORT_google_protobuf_message.Message]] = [
+ ]
+
+ PROTOBUF_ERROR_TYPES: list[type[IMPORT_google_protobuf_message.Message | IMPORT_reboot_api.Model]] = (
+ METHOD_PROTOBUF_ERROR_TYPES +
+ IMPORT_reboot.aio.aborted.GRPC_ERROR_TYPES +
+ IMPORT_reboot.aio.aborted.REBOOT_ERROR_TYPES
+ )
+
+ _error: Error
+
+ MethodProtobufError = IMPORT_typing.Union[
+ IMPORT_reboot.aio.aborted.GrpcError,
+ IMPORT_reboot.aio.aborted.RebootError,
+ ]
+ _method_protobuf_error: MethodProtobufError
+
+ _code: IMPORT_grpc.StatusCode
+ _message: IMPORT_typing.Optional[str]
+
+ def __init__(
+ self,
+ error: IMPORT_reboot.aio.aborted.GrpcError,
+ *,
+ message: IMPORT_typing.Optional[str] = None,
+ # Do not set this value when constructing in order to
+ # raise. This is only used internally when constructing
+ # from aborted calls.
+ error_types: IMPORT_typing.Sequence[type[MethodProtobufError]] = (
+ METHOD_PROTOBUF_ERROR_TYPES + IMPORT_reboot.aio.aborted.GRPC_ERROR_TYPES
+ ),
+ ):
+ super().__init__()
+
+ self._message = None
+
+ if self.is_declared_error(error):
+ # If the error is a declared error, we should always
+ # store both formats for Pydantic and Protobuf for that
+ # error, so users can access the Pydantic version via
+ # `aborted.error` and the Protobuf version will be used
+ # to send the error over the wire.
+ if isinstance(error, IMPORT_reboot.api.Model):
+ # Happens when somebody raises an aborted with a Pydantic
+ # error format, usually in the backend method
+ # implementation.
+
+ self._error = error
+
+ else:
+ # Usually happens on the client side when we are
+ # constructing an aborted from the `AioRpcError`, so
+ # the error will be in Protobuf shape, and we want
+ # to convert it back to Pydantic if possible.
+ self._method_protobuf_error = error
+
+ else:
+ # Non declared error should be well known Reboot or gRPC
+ # error, which always comes in the Protobuf format, however
+ # it might be a case when the server is using new API and
+ # client is old, so server might send a declared error from
+ # the server perspective, but client doesn't know that it
+ # is a declared error now, so we need to gracefully
+ # cover that case.
+ if isinstance(error, IMPORT_reboot.api.Model):
+ # Store both formats so users can access the Pydantic
+ # version via `aborted.error` and the Protobuf version
+ # will be used to send the error over the wire.
+ self._error = IMPORT_rbt_v1alpha1.errors_pb2.Unknown()
+ self._method_protobuf_error = IMPORT_rbt_v1alpha1.errors_pb2.Unknown()
+
+ self._message = (
+ "Received a declared error from the server, "
+ "but the client doesn't know about it; "
+ "please update the client to the latest version\n"
+ ) + (
+ f"Original error: {error.model_dump_json()}\n"
+ ) + message if message is not None else ""
+
+ # Store both formats so users can access the error via
+ # `aborted.error` and the Protobuf version
+ # will be used to send the error over the wire.
+ self._error = error
+ self._method_protobuf_error = error
+
+ IMPORT_reboot_aio_types.assert_type(self._protobuf_error, error_types)
+
+ code = self.grpc_status_code_from_error(self._protobuf_error)
+
+ if code is None:
+ # Must be a Reboot specific or declared method error.
+ code = IMPORT_grpc.StatusCode.ABORTED
+
+ self._code = code
+
+ self._message = self._message if self._message is not None else message
+
+ @property
+ def error(self) -> Error | MethodProtobufError:
+ return self._error
+
+ @property
+ def _protobuf_error(self) -> MethodProtobufError:
+ return self._method_protobuf_error
+
+ @property
+ def code(self) -> IMPORT_grpc.StatusCode:
+ return self._code
+
+ @property
+ def message(self) -> IMPORT_typing.Optional[str]:
+ return self._message
+
+ @classmethod
+ def from_status(cls, status: IMPORT_google_rpc_status_pb2.Status):
+ error = cls.error_from_google_rpc_status_details(
+ status,
+ cls.PROTOBUF_ERROR_TYPES,
+ )
+
+ message = status.message if len(status.message) > 0 else None
+
+ if error is not None:
+ return cls(error, message=message, error_types=cls.PROTOBUF_ERROR_TYPES)
+
+ error = cls.error_from_google_rpc_status_code(status)
+
+ assert error is not None
+
+ # TODO(benh): also consider getting the type names from
+ # `status.details` and including that in `message` to make
+ # debugging easier.
+
+ return cls(error, message=message)
+
+ @classmethod
+ def from_grpc_aio_rpc_error(cls, aio_rpc_error: IMPORT_grpc.aio.AioRpcError):
+ return cls(
+ cls.error_from_grpc_aio_rpc_error(aio_rpc_error),
+ message=aio_rpc_error.details(),
+ )
+
+ @classmethod
+ def is_declared_error(cls, error: IMPORT_google_protobuf_message.Message | IMPORT_reboot.api.Model) -> bool:
+ if isinstance(error, IMPORT_reboot.api.Model):
+ return type(error) in IMPORT_api.Counter.methods['increment'].errors
+ return False
+
+ class ValueTask:
+ """Represents a scheduled task running for the
+ state. Note that this is not a coroutine because we are trying
+ to convey the semantics that the task is already running (or
+ will soon be).
+ """
+
+ @classmethod
+ def retrieve(
+ cls,
+ context: IMPORT_reboot_aio_contexts.Context | IMPORT_reboot_aio_external.ExternalContext,
+ *,
+ task_id: IMPORT_rbt_v1alpha1.tasks_pb2.TaskId,
+ ):
+ return cls(context, task_id=task_id)
+
+ def __init__(
+ self,
+ context: IMPORT_reboot_aio_contexts.Context | IMPORT_reboot_aio_external.ExternalContext,
+ *,
+ task_id: IMPORT_rbt_v1alpha1.tasks_pb2.TaskId,
+ ) -> None:
+ # Depending on the context type (inside or outside a Reboot application)
+ # we may or may not know the application ID. If we don't know it, then
+ # the `ExternalContext.gateway` will determine it.
+ #
+ # TODO: in the future we expect to support cross-application calls, in
+ # which case the developer may explicitly pass in an application ID
+ # here.
+ self._application_id: IMPORT_typing.Optional[IMPORT_reboot_aio_types.ApplicationId] = None
+ if isinstance(context, IMPORT_reboot_aio_contexts.Context):
+ self._application_id = context.application_id
+ self._channel_manager = context.channel_manager
+ self._task_id = task_id
+
+ @property
+ def task_id(self) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ return self._task_id
+
+ def __await__(self) -> IMPORT_typing.Generator[None, None, Counter.ValueResponse]:
+ """Awaits for task to finish and returns its response."""
+ async def wait_for_task() -> reboot.ping.ping_api_pb2.CounterValueResponse:
+ channel = self._channel_manager.get_channel_to_state(
+ IMPORT_reboot_aio_types.StateTypeName(self._task_id.state_type),
+ IMPORT_reboot_aio_types.StateRef(self._task_id.state_ref),
+ )
+
+ stub = IMPORT_rbt_v1alpha1.tasks_pb2_grpc.TasksStub(channel)
+
+ try:
+ call = IMPORT_reboot_aio_stubs.UnaryRetriedCall(
+ call=None, # `RetriedCall` can create the call itself.
+ stub_method=stub.Wait,
+ method_name="Wait",
+ request=IMPORT_rbt_v1alpha1.tasks_pb2.WaitRequest(task_id=self._task_id),
+ metadata=IMPORT_reboot_aio_headers.Headers(
+ state_ref=IMPORT_reboot_aio_types.StateRef(self._task_id.state_ref),
+ application_id=self._application_id,
+ ).to_grpc_metadata(),
+ aborted_type=IMPORT_reboot.aio.aborted.SystemAborted,
+ )
+
+ wait_for_task_response = await call
+ except IMPORT_reboot.aio.aborted.SystemAborted as error:
+ if error.code == IMPORT_grpc.StatusCode.NOT_FOUND:
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.UnknownTask()
+ ) from None
+
+ raise
+ else:
+ response_or_error: IMPORT_typing.Optional[IMPORT_google_protobuf_any_pb2.Any] = None
+ is_error = False
+
+ if wait_for_task_response.response_or_error.WhichOneof("response_or_error") == "response":
+ response_or_error = wait_for_task_response.response_or_error.response
+ else:
+ is_error = True
+ response_or_error = wait_for_task_response.response_or_error.error
+
+ assert response_or_error is not None
+ assert response_or_error.TypeName() != ""
+
+ response = reboot.ping.ping_api_pb2.CounterValueResponse()
+
+ if (
+ not is_error and response_or_error.TypeName() != response.DESCRIPTOR.full_name
+ ):
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.InvalidArgument(),
+ message=
+ f"task with UUID {IMPORT_uuid.UUID(bytes=self._task_id.task_uuid)} "
+ f"has a response of type '{response_or_error.TypeName()}' "
+ "but expecting type 'reboot.ping.ping_api_pb2.CounterValueResponse'; "
+ "are you waiting on a task of the correct method?",
+ ) from None
+
+ if is_error:
+ aborted_type = Counter.ValueAborted
+
+ # In Reboot >= 0.40.2 we expect the error to be a `google.rpc.Status`.
+ if response_or_error.Is(IMPORT_google_rpc_status_pb2.Status.DESCRIPTOR):
+ status = IMPORT_google_rpc_status_pb2.Status()
+ response_or_error.Unpack(status)
+ raise aborted_type.from_status(status)
+
+ # In Reboot < 0.40.2 workflows throwing declared errors behaved poorly;
+ # we don't aim to emulate its behavior. Indicate that we don't know the
+ # reason for the abort.
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ )
+
+ else:
+ response_or_error.Unpack(response)
+ return CounterValueResponseFromProto(response)
+
+ return wait_for_task().__await__()
+
+
+ class ValueAborted(IMPORT_reboot.aio.aborted.Aborted):
+
+
+ Error = IMPORT_typing.Union[
+ IMPORT_reboot.aio.aborted.GrpcError,
+ IMPORT_reboot.aio.aborted.RebootError,
+ ]
+
+ METHOD_PROTOBUF_ERROR_TYPES: list[type[IMPORT_google_protobuf_message.Message]] = [
+ ]
+
+ PROTOBUF_ERROR_TYPES: list[type[IMPORT_google_protobuf_message.Message | IMPORT_reboot_api.Model]] = (
+ METHOD_PROTOBUF_ERROR_TYPES +
+ IMPORT_reboot.aio.aborted.GRPC_ERROR_TYPES +
+ IMPORT_reboot.aio.aborted.REBOOT_ERROR_TYPES
+ )
+
+ _error: Error
+
+ MethodProtobufError = IMPORT_typing.Union[
+ IMPORT_reboot.aio.aborted.GrpcError,
+ IMPORT_reboot.aio.aborted.RebootError,
+ ]
+ _method_protobuf_error: MethodProtobufError
+
+ _code: IMPORT_grpc.StatusCode
+ _message: IMPORT_typing.Optional[str]
+
+ def __init__(
+ self,
+ error: IMPORT_reboot.aio.aborted.GrpcError,
+ *,
+ message: IMPORT_typing.Optional[str] = None,
+ # Do not set this value when constructing in order to
+ # raise. This is only used internally when constructing
+ # from aborted calls.
+ error_types: IMPORT_typing.Sequence[type[MethodProtobufError]] = (
+ METHOD_PROTOBUF_ERROR_TYPES + IMPORT_reboot.aio.aborted.GRPC_ERROR_TYPES
+ ),
+ ):
+ super().__init__()
+
+ self._message = None
+
+ if self.is_declared_error(error):
+ # If the error is a declared error, we should always
+ # store both formats for Pydantic and Protobuf for that
+ # error, so users can access the Pydantic version via
+ # `aborted.error` and the Protobuf version will be used
+ # to send the error over the wire.
+ if isinstance(error, IMPORT_reboot.api.Model):
+ # Happens when somebody raises an aborted with a Pydantic
+ # error format, usually in the backend method
+ # implementation.
+
+ self._error = error
+
+ else:
+ # Usually happens on the client side when we are
+ # constructing an aborted from the `AioRpcError`, so
+ # the error will be in Protobuf shape, and we want
+ # to convert it back to Pydantic if possible.
+ self._method_protobuf_error = error
+
+ else:
+ # Non declared error should be well known Reboot or gRPC
+ # error, which always comes in the Protobuf format, however
+ # it might be a case when the server is using new API and
+ # client is old, so server might send a declared error from
+ # the server perspective, but client doesn't know that it
+ # is a declared error now, so we need to gracefully
+ # cover that case.
+ if isinstance(error, IMPORT_reboot.api.Model):
+ # Store both formats so users can access the Pydantic
+ # version via `aborted.error` and the Protobuf version
+ # will be used to send the error over the wire.
+ self._error = IMPORT_rbt_v1alpha1.errors_pb2.Unknown()
+ self._method_protobuf_error = IMPORT_rbt_v1alpha1.errors_pb2.Unknown()
+
+ self._message = (
+ "Received a declared error from the server, "
+ "but the client doesn't know about it; "
+ "please update the client to the latest version\n"
+ ) + (
+ f"Original error: {error.model_dump_json()}\n"
+ ) + message if message is not None else ""
+
+ # Store both formats so users can access the error via
+ # `aborted.error` and the Protobuf version
+ # will be used to send the error over the wire.
+ self._error = error
+ self._method_protobuf_error = error
+
+ IMPORT_reboot_aio_types.assert_type(self._protobuf_error, error_types)
+
+ code = self.grpc_status_code_from_error(self._protobuf_error)
+
+ if code is None:
+ # Must be a Reboot specific or declared method error.
+ code = IMPORT_grpc.StatusCode.ABORTED
+
+ self._code = code
+
+ self._message = self._message if self._message is not None else message
+
+ @property
+ def error(self) -> Error | MethodProtobufError:
+ return self._error
+
+ @property
+ def _protobuf_error(self) -> MethodProtobufError:
+ return self._method_protobuf_error
+
+ @property
+ def code(self) -> IMPORT_grpc.StatusCode:
+ return self._code
+
+ @property
+ def message(self) -> IMPORT_typing.Optional[str]:
+ return self._message
+
+ @classmethod
+ def from_status(cls, status: IMPORT_google_rpc_status_pb2.Status):
+ error = cls.error_from_google_rpc_status_details(
+ status,
+ cls.PROTOBUF_ERROR_TYPES,
+ )
+
+ message = status.message if len(status.message) > 0 else None
+
+ if error is not None:
+ return cls(error, message=message, error_types=cls.PROTOBUF_ERROR_TYPES)
+
+ error = cls.error_from_google_rpc_status_code(status)
+
+ assert error is not None
+
+ # TODO(benh): also consider getting the type names from
+ # `status.details` and including that in `message` to make
+ # debugging easier.
+
+ return cls(error, message=message)
+
+ @classmethod
+ def from_grpc_aio_rpc_error(cls, aio_rpc_error: IMPORT_grpc.aio.AioRpcError):
+ return cls(
+ cls.error_from_grpc_aio_rpc_error(aio_rpc_error),
+ message=aio_rpc_error.details(),
+ )
+
+ @classmethod
+ def is_declared_error(cls, error: IMPORT_google_protobuf_message.Message | IMPORT_reboot.api.Model) -> bool:
+ if isinstance(error, IMPORT_reboot.api.Model):
+ return type(error) in IMPORT_api.Counter.methods['value'].errors
+ return False
+
+ class DescriptionTask:
+ """Represents a scheduled task running for the
+ state. Note that this is not a coroutine because we are trying
+ to convey the semantics that the task is already running (or
+ will soon be).
+ """
+
+ @classmethod
+ def retrieve(
+ cls,
+ context: IMPORT_reboot_aio_contexts.Context | IMPORT_reboot_aio_external.ExternalContext,
+ *,
+ task_id: IMPORT_rbt_v1alpha1.tasks_pb2.TaskId,
+ ):
+ return cls(context, task_id=task_id)
+
+ def __init__(
+ self,
+ context: IMPORT_reboot_aio_contexts.Context | IMPORT_reboot_aio_external.ExternalContext,
+ *,
+ task_id: IMPORT_rbt_v1alpha1.tasks_pb2.TaskId,
+ ) -> None:
+ # Depending on the context type (inside or outside a Reboot application)
+ # we may or may not know the application ID. If we don't know it, then
+ # the `ExternalContext.gateway` will determine it.
+ #
+ # TODO: in the future we expect to support cross-application calls, in
+ # which case the developer may explicitly pass in an application ID
+ # here.
+ self._application_id: IMPORT_typing.Optional[IMPORT_reboot_aio_types.ApplicationId] = None
+ if isinstance(context, IMPORT_reboot_aio_contexts.Context):
+ self._application_id = context.application_id
+ self._channel_manager = context.channel_manager
+ self._task_id = task_id
+
+ @property
+ def task_id(self) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ return self._task_id
+
+ def __await__(self) -> IMPORT_typing.Generator[None, None, Counter.DescriptionResponse]:
+ """Awaits for task to finish and returns its response."""
+ async def wait_for_task() -> reboot.ping.ping_api_pb2.CounterDescriptionResponse:
+ channel = self._channel_manager.get_channel_to_state(
+ IMPORT_reboot_aio_types.StateTypeName(self._task_id.state_type),
+ IMPORT_reboot_aio_types.StateRef(self._task_id.state_ref),
+ )
+
+ stub = IMPORT_rbt_v1alpha1.tasks_pb2_grpc.TasksStub(channel)
+
+ try:
+ call = IMPORT_reboot_aio_stubs.UnaryRetriedCall(
+ call=None, # `RetriedCall` can create the call itself.
+ stub_method=stub.Wait,
+ method_name="Wait",
+ request=IMPORT_rbt_v1alpha1.tasks_pb2.WaitRequest(task_id=self._task_id),
+ metadata=IMPORT_reboot_aio_headers.Headers(
+ state_ref=IMPORT_reboot_aio_types.StateRef(self._task_id.state_ref),
+ application_id=self._application_id,
+ ).to_grpc_metadata(),
+ aborted_type=IMPORT_reboot.aio.aborted.SystemAborted,
+ )
+
+ wait_for_task_response = await call
+ except IMPORT_reboot.aio.aborted.SystemAborted as error:
+ if error.code == IMPORT_grpc.StatusCode.NOT_FOUND:
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.UnknownTask()
+ ) from None
+
+ raise
+ else:
+ response_or_error: IMPORT_typing.Optional[IMPORT_google_protobuf_any_pb2.Any] = None
+ is_error = False
+
+ if wait_for_task_response.response_or_error.WhichOneof("response_or_error") == "response":
+ response_or_error = wait_for_task_response.response_or_error.response
+ else:
+ is_error = True
+ response_or_error = wait_for_task_response.response_or_error.error
+
+ assert response_or_error is not None
+ assert response_or_error.TypeName() != ""
+
+ response = reboot.ping.ping_api_pb2.CounterDescriptionResponse()
+
+ if (
+ not is_error and response_or_error.TypeName() != response.DESCRIPTOR.full_name
+ ):
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.InvalidArgument(),
+ message=
+ f"task with UUID {IMPORT_uuid.UUID(bytes=self._task_id.task_uuid)} "
+ f"has a response of type '{response_or_error.TypeName()}' "
+ "but expecting type 'reboot.ping.ping_api_pb2.CounterDescriptionResponse'; "
+ "are you waiting on a task of the correct method?",
+ ) from None
+
+ if is_error:
+ aborted_type = Counter.DescriptionAborted
+
+ # In Reboot >= 0.40.2 we expect the error to be a `google.rpc.Status`.
+ if response_or_error.Is(IMPORT_google_rpc_status_pb2.Status.DESCRIPTOR):
+ status = IMPORT_google_rpc_status_pb2.Status()
+ response_or_error.Unpack(status)
+ raise aborted_type.from_status(status)
+
+ # In Reboot < 0.40.2 workflows throwing declared errors behaved poorly;
+ # we don't aim to emulate its behavior. Indicate that we don't know the
+ # reason for the abort.
+ raise IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ )
+
+ else:
+ response_or_error.Unpack(response)
+ return CounterDescriptionResponseFromProto(response)
+
+ return wait_for_task().__await__()
+
+
+ class DescriptionAborted(IMPORT_reboot.aio.aborted.Aborted):
+
+
+ Error = IMPORT_typing.Union[
+ IMPORT_reboot.aio.aborted.GrpcError,
+ IMPORT_reboot.aio.aborted.RebootError,
+ ]
+
+ METHOD_PROTOBUF_ERROR_TYPES: list[type[IMPORT_google_protobuf_message.Message]] = [
+ ]
+
+ PROTOBUF_ERROR_TYPES: list[type[IMPORT_google_protobuf_message.Message | IMPORT_reboot_api.Model]] = (
+ METHOD_PROTOBUF_ERROR_TYPES +
+ IMPORT_reboot.aio.aborted.GRPC_ERROR_TYPES +
+ IMPORT_reboot.aio.aborted.REBOOT_ERROR_TYPES
+ )
+
+ _error: Error
+
+ MethodProtobufError = IMPORT_typing.Union[
+ IMPORT_reboot.aio.aborted.GrpcError,
+ IMPORT_reboot.aio.aborted.RebootError,
+ ]
+ _method_protobuf_error: MethodProtobufError
+
+ _code: IMPORT_grpc.StatusCode
+ _message: IMPORT_typing.Optional[str]
+
+ def __init__(
+ self,
+ error: IMPORT_reboot.aio.aborted.GrpcError,
+ *,
+ message: IMPORT_typing.Optional[str] = None,
+ # Do not set this value when constructing in order to
+ # raise. This is only used internally when constructing
+ # from aborted calls.
+ error_types: IMPORT_typing.Sequence[type[MethodProtobufError]] = (
+ METHOD_PROTOBUF_ERROR_TYPES + IMPORT_reboot.aio.aborted.GRPC_ERROR_TYPES
+ ),
+ ):
+ super().__init__()
+
+ self._message = None
+
+ if self.is_declared_error(error):
+ # If the error is a declared error, we should always
+ # store both formats for Pydantic and Protobuf for that
+ # error, so users can access the Pydantic version via
+ # `aborted.error` and the Protobuf version will be used
+ # to send the error over the wire.
+ if isinstance(error, IMPORT_reboot.api.Model):
+ # Happens when somebody raises an aborted with a Pydantic
+ # error format, usually in the backend method
+ # implementation.
+
+ self._error = error
+
+ else:
+ # Usually happens on the client side when we are
+ # constructing an aborted from the `AioRpcError`, so
+ # the error will be in Protobuf shape, and we want
+ # to convert it back to Pydantic if possible.
+ self._method_protobuf_error = error
+
+ else:
+ # Non declared error should be well known Reboot or gRPC
+ # error, which always comes in the Protobuf format, however
+ # it might be a case when the server is using new API and
+ # client is old, so server might send a declared error from
+ # the server perspective, but client doesn't know that it
+ # is a declared error now, so we need to gracefully
+ # cover that case.
+ if isinstance(error, IMPORT_reboot.api.Model):
+ # Store both formats so users can access the Pydantic
+ # version via `aborted.error` and the Protobuf version
+ # will be used to send the error over the wire.
+ self._error = IMPORT_rbt_v1alpha1.errors_pb2.Unknown()
+ self._method_protobuf_error = IMPORT_rbt_v1alpha1.errors_pb2.Unknown()
+
+ self._message = (
+ "Received a declared error from the server, "
+ "but the client doesn't know about it; "
+ "please update the client to the latest version\n"
+ ) + (
+ f"Original error: {error.model_dump_json()}\n"
+ ) + message if message is not None else ""
+
+ # Store both formats so users can access the error via
+ # `aborted.error` and the Protobuf version
+ # will be used to send the error over the wire.
+ self._error = error
+ self._method_protobuf_error = error
+
+ IMPORT_reboot_aio_types.assert_type(self._protobuf_error, error_types)
+
+ code = self.grpc_status_code_from_error(self._protobuf_error)
+
+ if code is None:
+ # Must be a Reboot specific or declared method error.
+ code = IMPORT_grpc.StatusCode.ABORTED
+
+ self._code = code
+
+ self._message = self._message if self._message is not None else message
+
+ @property
+ def error(self) -> Error | MethodProtobufError:
+ return self._error
+
+ @property
+ def _protobuf_error(self) -> MethodProtobufError:
+ return self._method_protobuf_error
+
+ @property
+ def code(self) -> IMPORT_grpc.StatusCode:
+ return self._code
+
+ @property
+ def message(self) -> IMPORT_typing.Optional[str]:
+ return self._message
+
+ @classmethod
+ def from_status(cls, status: IMPORT_google_rpc_status_pb2.Status):
+ error = cls.error_from_google_rpc_status_details(
+ status,
+ cls.PROTOBUF_ERROR_TYPES,
+ )
+
+ message = status.message if len(status.message) > 0 else None
+
+ if error is not None:
+ return cls(error, message=message, error_types=cls.PROTOBUF_ERROR_TYPES)
+
+ error = cls.error_from_google_rpc_status_code(status)
+
+ assert error is not None
+
+ # TODO(benh): also consider getting the type names from
+ # `status.details` and including that in `message` to make
+ # debugging easier.
+
+ return cls(error, message=message)
+
+ @classmethod
+ def from_grpc_aio_rpc_error(cls, aio_rpc_error: IMPORT_grpc.aio.AioRpcError):
+ return cls(
+ cls.error_from_grpc_aio_rpc_error(aio_rpc_error),
+ message=aio_rpc_error.details(),
+ )
+
+ @classmethod
+ def is_declared_error(cls, error: IMPORT_google_protobuf_message.Message | IMPORT_reboot.api.Model) -> bool:
+ if isinstance(error, IMPORT_reboot.api.Model):
+ return type(error) in IMPORT_api.Counter.methods['description'].errors
+ return False
+
+
+ class WeakReference(IMPORT_typing.Generic[Counter_ScheduleTypeVar]):
+
+ _schedule_type: type[Counter_ScheduleTypeVar]
+
+ def __init__(
+ self,
+ # When application ID is None, refers to a state within the application given by the context.
+ application_id: IMPORT_typing.Optional[IMPORT_reboot_aio_types.ApplicationId],
+ state_id: IMPORT_reboot_aio_types.StateId,
+ *,
+ schedule_type: type[Counter_ScheduleTypeVar],
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ servicer: IMPORT_typing.Optional[CounterBaseServicer] = None,
+ ):
+ self._application_id = application_id
+ self._state_ref = IMPORT_reboot_aio_types.StateRef.from_id(
+ Counter.__state_type_name__,
+ state_id,
+ )
+ self._schedule_type = schedule_type
+ self._idempotency_manager: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.IdempotencyManager] = None
+ self._reader_stub: IMPORT_typing.Optional[CounterReaderStub] = None
+ self._writer_stub: IMPORT_typing.Optional[CounterWriterStub] = None
+ self._workflow_stub: IMPORT_typing.Optional[CounterWorkflowStub] = None
+ self._tasks_stub: IMPORT_typing.Optional[CounterTasksStub] = None
+ self._bearer_token = bearer_token
+ self._servicer = servicer
+
+ @property
+ def state_id(self) -> IMPORT_reboot_aio_types.StateId:
+ return self._state_ref.id
+
+ def _reader(
+ self,
+ context: IMPORT_reboot_aio_contexts.ReaderContext | IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ ) -> CounterReaderStub:
+ if self._reader_stub is None:
+ self._reader_stub = CounterReaderStub(
+ context=context,
+ state_ref=self._state_ref,
+ bearer_token=self._bearer_token,
+ )
+ assert self._reader_stub is not None
+ if self._idempotency_manager is None:
+ self._idempotency_manager = context
+ elif self._idempotency_manager != context:
+ raise IMPORT_reboot_aio_call.MixedContextsError(
+ "This `WeakReference` for `Counter` with ID "
+ f"'{self.state_id}' has previously been used by a "
+ "different `Context`. That is not allowed. "
+ "Instead create a new `WeakReference` for every `Context` by calling "
+ f"`Counter.ref('{self.state_id}')`."
+ )
+ return self._reader_stub
+
+ def _writer(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ ) -> CounterWriterStub:
+ if self._writer_stub is None:
+ self._writer_stub = CounterWriterStub(
+ context=context,
+ state_ref=self._state_ref,
+ bearer_token=self._bearer_token,
+ )
+ assert self._writer_stub is not None
+ if self._idempotency_manager is None:
+ self._idempotency_manager = context
+ elif self._idempotency_manager != context:
+ raise IMPORT_reboot_aio_call.MixedContextsError(
+ "This `WeakReference` for `Counter` with ID "
+ f"'{self.state_id}' has previously been used by a "
+ "different `Context`. That is not allowed. "
+ "Instead create a new `WeakReference` for every `Context` by calling "
+ f"`Counter.ref('{self.state_id}')`."
+ )
+ return self._writer_stub
+
+ def _workflow(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ ) -> CounterWorkflowStub:
+ if self._workflow_stub is None:
+ self._workflow_stub = CounterWorkflowStub(
+ context=context,
+ state_ref=self._state_ref,
+ bearer_token=self._bearer_token,
+ )
+ assert self._workflow_stub is not None
+ if self._idempotency_manager is None:
+ self._idempotency_manager = context
+ elif self._idempotency_manager != context:
+ raise IMPORT_reboot_aio_call.MixedContextsError(
+ "This `WeakReference` for `Counter` with ID "
+ f"'{self.state_id}' has previously been used by a "
+ "different `Context`. That is not allowed. "
+ "Instead create a new `WeakReference` for every `Context` by calling "
+ f"`Counter.ref('{self.state_id}')`."
+ )
+ return self._workflow_stub
+
+ def _tasks(
+ self,
+ context: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ ) -> CounterTasksStub:
+ if self._tasks_stub is None:
+ self._tasks_stub = CounterTasksStub(
+ context=context,
+ state_ref=self._state_ref,
+ bearer_token=self._bearer_token,
+ )
+ assert self._tasks_stub is not None
+ if self._idempotency_manager is None:
+ self._idempotency_manager = context
+ elif self._idempotency_manager != context:
+ raise IMPORT_reboot_aio_call.MixedContextsError(
+ "This `WeakReference` for `Counter` with ID "
+ f"'{self.state_id}' has previously been used by a "
+ "different `Context`. That is not allowed. "
+ "Instead create a new `WeakReference` for every `Context` by calling "
+ f"`Counter.ref('{self.state_id}')`."
+ )
+ return self._tasks_stub
+
+ class _Reactively:
+
+ def __init__(
+ self,
+ *,
+ application_id: IMPORT_typing.Optional[IMPORT_reboot_aio_types.ApplicationId],
+ state_ref: IMPORT_reboot_aio_types.StateRef,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ):
+ self._application_id = application_id
+ self._state_ref = state_ref
+ self._bearer_token = bearer_token
+
+
+ async def Value( # type: ignore[misc]
+ # In methods which are dealing with user input, (i.e.,
+ # proto message field names), we should use '__double_underscored__'
+ # variables to avoid any potential name conflicts with the method's
+ # parameters.
+ # The '__self__' parameter is a convention in Python to
+ # indicate that this method is a bound method, so we use
+ # '__this__' instead.
+ __this__,
+ # Explicitly-reactive calls only make sense in the context of either...
+ # (A) an external client, or...
+ # (B) methods that may reasonably run for a long time, which in Reboot means: readers or workflows.
+ __context__: IMPORT_reboot_aio_external.ExternalContext | IMPORT_reboot_aio_contexts.ReaderContext | IMPORT_reboot_aio_contexts.WorkflowContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> IMPORT_typing.AsyncIterator[Counter.ValueResponse]:
+ IMPORT_reboot_aio_types.assert_type(__context__, [IMPORT_reboot_aio_external.ExternalContext, IMPORT_reboot_aio_contexts.ReaderContext, IMPORT_reboot_aio_contexts.WorkflowContext])
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+ __caller_id__: IMPORT_typing.Optional[IMPORT_reboot_aio_caller_id.CallerID] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ if __bearer_token__ is None:
+ __bearer_token__ = __this__._bearer_token
+ if __bearer_token__ is None and isinstance(__context__, IMPORT_reboot_aio_external.ExternalContext):
+ # Within a Reboot context we do not pass on the caller's bearer token, as that might
+ # have security implications - we cannot simply trust any service we are calling with
+ # the user's credentials. Instead, the developer can rely on the default app-internal
+ # auth, or override that and set an explicit bearer token.
+ #
+ # In the case of `ExternalContext`, however, its `bearer_token` was set specifically
+ # by the developer for the purpose of making these calls. Note that only
+ # `ExternalContext` even has a `bearer_token` field.
+ __bearer_token__ = __context__.bearer_token
+
+ if __metadata__ is None:
+ __metadata__ = ()
+
+ if isinstance(__context__, IMPORT_reboot_aio_contexts.Context):
+ __caller_id__ = IMPORT_reboot_aio_caller_id.CallerID(
+ application_id=__context__.application_id,
+ )
+ if __this__._application_id is None:
+ # Given our context type (inside a Reboot application) we can default to
+ # making the application send traffic to itself.
+ __this__._application_id = __context__.application_id
+ elif isinstance(__context__, IMPORT_reboot_aio_external.ExternalContext):
+ __caller_id__ = __context__.caller_id
+
+ __headers__ = IMPORT_reboot_aio_headers.Headers(
+ bearer_token=__bearer_token__,
+ state_ref=__this__._state_ref,
+ application_id=__this__._application_id,
+ caller_id=__caller_id__,
+ )
+
+ __metadata__ += __headers__.to_grpc_metadata()
+
+ __query_backoff__ = IMPORT_reboot_aio_backoff.Backoff()
+ while True:
+ __call__ = None
+ try:
+ async with __context__.channel_manager.get_channel_to_state(
+ IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ __this__._state_ref,
+ ) as __channel__:
+
+ __call__ = IMPORT_rbt_v1alpha1.react_pb2_grpc.ReactStub(
+ __channel__
+ ).Query(
+ IMPORT_rbt_v1alpha1.react_pb2.QueryRequest(
+ method='Value',
+ request=CounterValueRequestToProto(
+ ).SerializeToString(),
+ ),
+ metadata=__metadata__,
+ )
+
+ async for __query_response__ in __call__:
+ # Clear the backoff so we don't wait
+ # as long the next time we get
+ # disconnected.
+ __query_backoff__.clear()
+
+ # The backend may have sent us this query
+ # response only to let us know that a new
+ # idempotency key has been recorded; there may
+ # not be a new response. Python callers don't
+ # (currently) care about such an event, so we
+ # simply ignore it.
+ if not __query_response__.HasField("response"):
+ continue
+
+ __response__ = reboot.ping.ping_api_pb2.CounterValueResponse()
+ __response__.ParseFromString(__query_response__.response)
+ yield CounterValueResponseFromProto(__response__)
+
+ except IMPORT_grpc.aio.AioRpcError as error:
+ # We expect to get disconnected from the server
+ # from time to time, e.g., when it is being
+ # updated, but we don't want that error to
+ # propagate, we just want to retry.
+ if IMPORT_reboot.aio.aborted.is_grpc_retryable_exception(error):
+ logger.debug(
+ "Reactive read to 'reboot.ping.CounterMethods.Value' "
+ f"failed with a retryable error: '{error}'; "
+ "will retry..."
+ )
+ await __query_backoff__()
+ continue
+ if error.code() == IMPORT_grpc.StatusCode.ABORTED:
+ # Reconstitute the error that the server threw, if it was a declared error.
+ status = await IMPORT_rpc_status_async.from_call(__call__)
+ if status is not None:
+ raise Counter.ValueAborted.from_status(
+ status
+ ) from None
+ raise Counter.ValueAborted.from_grpc_aio_rpc_error(
+ error
+ ) from None
+
+ raise
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ value = Value
+
+ async def Description( # type: ignore[misc]
+ # In methods which are dealing with user input, (i.e.,
+ # proto message field names), we should use '__double_underscored__'
+ # variables to avoid any potential name conflicts with the method's
+ # parameters.
+ # The '__self__' parameter is a convention in Python to
+ # indicate that this method is a bound method, so we use
+ # '__this__' instead.
+ __this__,
+ # Explicitly-reactive calls only make sense in the context of either...
+ # (A) an external client, or...
+ # (B) methods that may reasonably run for a long time, which in Reboot means: readers or workflows.
+ __context__: IMPORT_reboot_aio_external.ExternalContext | IMPORT_reboot_aio_contexts.ReaderContext | IMPORT_reboot_aio_contexts.WorkflowContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> IMPORT_typing.AsyncIterator[Counter.DescriptionResponse]:
+ IMPORT_reboot_aio_types.assert_type(__context__, [IMPORT_reboot_aio_external.ExternalContext, IMPORT_reboot_aio_contexts.ReaderContext, IMPORT_reboot_aio_contexts.WorkflowContext])
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+ __caller_id__: IMPORT_typing.Optional[IMPORT_reboot_aio_caller_id.CallerID] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ if __bearer_token__ is None:
+ __bearer_token__ = __this__._bearer_token
+ if __bearer_token__ is None and isinstance(__context__, IMPORT_reboot_aio_external.ExternalContext):
+ # Within a Reboot context we do not pass on the caller's bearer token, as that might
+ # have security implications - we cannot simply trust any service we are calling with
+ # the user's credentials. Instead, the developer can rely on the default app-internal
+ # auth, or override that and set an explicit bearer token.
+ #
+ # In the case of `ExternalContext`, however, its `bearer_token` was set specifically
+ # by the developer for the purpose of making these calls. Note that only
+ # `ExternalContext` even has a `bearer_token` field.
+ __bearer_token__ = __context__.bearer_token
+
+ if __metadata__ is None:
+ __metadata__ = ()
+
+ if isinstance(__context__, IMPORT_reboot_aio_contexts.Context):
+ __caller_id__ = IMPORT_reboot_aio_caller_id.CallerID(
+ application_id=__context__.application_id,
+ )
+ if __this__._application_id is None:
+ # Given our context type (inside a Reboot application) we can default to
+ # making the application send traffic to itself.
+ __this__._application_id = __context__.application_id
+ elif isinstance(__context__, IMPORT_reboot_aio_external.ExternalContext):
+ __caller_id__ = __context__.caller_id
+
+ __headers__ = IMPORT_reboot_aio_headers.Headers(
+ bearer_token=__bearer_token__,
+ state_ref=__this__._state_ref,
+ application_id=__this__._application_id,
+ caller_id=__caller_id__,
+ )
+
+ __metadata__ += __headers__.to_grpc_metadata()
+
+ __query_backoff__ = IMPORT_reboot_aio_backoff.Backoff()
+ while True:
+ __call__ = None
+ try:
+ async with __context__.channel_manager.get_channel_to_state(
+ IMPORT_reboot_aio_types.StateTypeName('reboot.ping.Counter'),
+ __this__._state_ref,
+ ) as __channel__:
+
+ __call__ = IMPORT_rbt_v1alpha1.react_pb2_grpc.ReactStub(
+ __channel__
+ ).Query(
+ IMPORT_rbt_v1alpha1.react_pb2.QueryRequest(
+ method='Description',
+ request=CounterDescriptionRequestToProto(
+ ).SerializeToString(),
+ ),
+ metadata=__metadata__,
+ )
+
+ async for __query_response__ in __call__:
+ # Clear the backoff so we don't wait
+ # as long the next time we get
+ # disconnected.
+ __query_backoff__.clear()
+
+ # The backend may have sent us this query
+ # response only to let us know that a new
+ # idempotency key has been recorded; there may
+ # not be a new response. Python callers don't
+ # (currently) care about such an event, so we
+ # simply ignore it.
+ if not __query_response__.HasField("response"):
+ continue
+
+ __response__ = reboot.ping.ping_api_pb2.CounterDescriptionResponse()
+ __response__.ParseFromString(__query_response__.response)
+ yield CounterDescriptionResponseFromProto(__response__)
+
+ except IMPORT_grpc.aio.AioRpcError as error:
+ # We expect to get disconnected from the server
+ # from time to time, e.g., when it is being
+ # updated, but we don't want that error to
+ # propagate, we just want to retry.
+ if IMPORT_reboot.aio.aborted.is_grpc_retryable_exception(error):
+ logger.debug(
+ "Reactive read to 'reboot.ping.CounterMethods.Description' "
+ f"failed with a retryable error: '{error}'; "
+ "will retry..."
+ )
+ await __query_backoff__()
+ continue
+ if error.code() == IMPORT_grpc.StatusCode.ABORTED:
+ # Reconstitute the error that the server threw, if it was a declared error.
+ status = await IMPORT_rpc_status_async.from_call(__call__)
+ if status is not None:
+ raise Counter.DescriptionAborted.from_status(
+ status
+ ) from None
+ raise Counter.DescriptionAborted.from_grpc_aio_rpc_error(
+ error
+ ) from None
+
+ raise
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ description = Description
+
+ def reactively(self):
+ return Counter.WeakReference._Reactively(
+ application_id=self._application_id,
+ state_ref=self._state_ref,
+ bearer_token=self._bearer_token,
+ )
+
+ class _Idempotently(IMPORT_typing.Generic[Counter_IdempotentlyScheduleTypeVar]):
+
+ _weak_reference: Counter.WeakReference[Counter_IdempotentlyScheduleTypeVar]
+
+ def __init__(
+ self,
+ *,
+ weak_reference: Counter.WeakReference[Counter_IdempotentlyScheduleTypeVar],
+ idempotency: IMPORT_reboot_aio_idempotency.Idempotency,
+ ):
+ self._weak_reference = weak_reference
+ self._idempotency = idempotency
+
+ def schedule(
+ self,
+ *,
+ when: IMPORT_typing.Optional[IMPORT_datetime_datetime | IMPORT_datetime_timedelta] = None,
+ ) -> Counter_IdempotentlyScheduleTypeVar:
+ return self._weak_reference._schedule_type(
+ self._weak_reference._application_id,
+ self._weak_reference._tasks,
+ when=when,
+ idempotency=self._idempotency,
+ )
+
+ def spawn(
+ self,
+ *,
+ when: IMPORT_typing.Optional[IMPORT_datetime_datetime | IMPORT_datetime_timedelta] = None,
+ ) -> Counter.WeakReference._Spawn:
+ return Counter.WeakReference._Spawn(
+ self._weak_reference._application_id,
+ self._weak_reference._tasks,
+ when=when,
+ idempotency=self._idempotency,
+ )
+
+ async def read(
+ self,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ ) -> Counter.State:
+ if self._weak_reference._servicer is None:
+ raise RuntimeError(
+ "`read()` is currently only supported within workflows; "
+ "Please reach out and let us know your use case if this "
+ "is important for you!"
+ )
+
+ return await CounterBaseServicer.WorkflowState._Idempotently._read(
+ self._weak_reference._servicer,
+ self._idempotency,
+ context,
+ )
+
+ async def write(
+ self,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ writer: CounterBaseServicer.InlineWriterCallable[CounterBaseServicer.InlineWriterCallableResult],
+ *,
+ type: IMPORT_reboot_aio_workflows.Type | IMPORT_reboot_aio_workflows._Unset = IMPORT_reboot_aio_workflows._UNSET,
+ ) -> CounterBaseServicer.InlineWriterCallableResult:
+ if self._weak_reference._servicer is None:
+ raise RuntimeError(
+ "`write()` is currently only supported within workflows; "
+ "Please reach out and let us know your use case if this "
+ "is important for you!"
+ )
+
+ # `type=` is optional: when omitted, `writer`'s
+ # return annotation is used.
+ type_t, _ = IMPORT_reboot_aio_workflows._resolve_callable_return_type(
+ writer, type,
+ )
+
+ return await CounterBaseServicer.WorkflowState._Idempotently._write_validating_effects(
+ self._weak_reference._servicer,
+ self._idempotency,
+ context,
+ writer,
+ type_result=type_t,
+ check_type=not self._idempotency.always,
+ unidempotently=self._idempotency.always,
+ checkpoint=context.checkpoint(),
+ )
+
+
+ async def Increment( # type: ignore[misc]
+ # In methods which are dealing with user input, (i.e.,
+ # proto message field names), we should use '__double_underscored__'
+ # variables to avoid any potential name conflicts with the method's
+ # parameters.
+ # The '__self__' parameter is a convention in Python to
+ # indicate that this method is a bound method, so we use
+ # '__this__' instead.
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> Counter.IncrementResponse:
+ __options__ = __options__ or IMPORT_reboot_aio_call.Options()
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+
+ return await __this__._weak_reference.Increment(
+ __context__,
+ __options__,
+ __idempotency__=__this__._idempotency,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ increment = Increment
+
+ async def Value( # type: ignore[misc]
+ # In methods which are dealing with user input, (i.e.,
+ # proto message field names), we should use '__double_underscored__'
+ # variables to avoid any potential name conflicts with the method's
+ # parameters.
+ # The '__self__' parameter is a convention in Python to
+ # indicate that this method is a bound method, so we use
+ # '__this__' instead.
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> Counter.ValueResponse:
+ __options__ = __options__ or IMPORT_reboot_aio_call.Options()
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+
+ return await __this__._weak_reference.Value(
+ __context__,
+ __options__,
+ __idempotency__=__this__._idempotency,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ value = Value
+
+ async def Description( # type: ignore[misc]
+ # In methods which are dealing with user input, (i.e.,
+ # proto message field names), we should use '__double_underscored__'
+ # variables to avoid any potential name conflicts with the method's
+ # parameters.
+ # The '__self__' parameter is a convention in Python to
+ # indicate that this method is a bound method, so we use
+ # '__this__' instead.
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> Counter.DescriptionResponse:
+ __options__ = __options__ or IMPORT_reboot_aio_call.Options()
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+
+ return await __this__._weak_reference.Description(
+ __context__,
+ __options__,
+ __idempotency__=__this__._idempotency,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ description = Description
+
+ @IMPORT_typing.overload
+ def idempotently(self, alias: IMPORT_typing.Optional[str] = None, *, how: IMPORT_reboot_aio_idempotency.How = IMPORT_reboot_aio_idempotency.PER_WORKFLOW) -> Counter.WeakReference._Idempotently[Counter_ScheduleTypeVar]:
+ ...
+
+ @IMPORT_typing.overload
+ def idempotently(self, *, key: IMPORT_uuid.UUID, how: IMPORT_reboot_aio_idempotency.How = IMPORT_reboot_aio_idempotency.PER_WORKFLOW) -> Counter.WeakReference._Idempotently[Counter_ScheduleTypeVar]:
+ ...
+
+ def idempotently(
+ self,
+ alias: IMPORT_typing.Optional[str] = None,
+ *,
+ key: IMPORT_typing.Optional[IMPORT_uuid.UUID] = None,
+ how: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.How] = None,
+ ) -> Counter.WeakReference._Idempotently[Counter_ScheduleTypeVar]:
+ return Counter.WeakReference._Idempotently(
+ weak_reference=self,
+ idempotency=IMPORT_reboot_aio_contexts.Context.idempotency(
+ alias=alias,
+ key=key,
+ how=how,
+ )
+ )
+
+ def per_workflow(self, alias: IMPORT_typing.Optional[str] = None):
+ return self.idempotently(alias)
+
+ def per_iteration(self, alias: IMPORT_typing.Optional[str] = None):
+ return self.idempotently(
+ alias,
+ how=IMPORT_reboot_aio_idempotency.PER_ITERATION,
+ )
+
+ def always(self):
+ return self.idempotently(
+ how=IMPORT_reboot_aio_idempotency.ALWAYS,
+ )
+
+ class _UntilChangesSatisfies(IMPORT_typing.Generic[Counter_UntilCallableType]):
+
+ _idempotency_alias: str
+ _context: IMPORT_reboot_aio_contexts.WorkflowContext
+ _callable: IMPORT_typing.Callable[[], IMPORT_typing.Awaitable[Counter_UntilCallableType]]
+ _type: type[Counter_UntilCallableType]
+
+ def __init__(
+ self,
+ *,
+ idempotency_alias: str,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ callable: IMPORT_typing.Callable[[], IMPORT_typing.Awaitable[Counter_UntilCallableType]],
+ type: type[Counter_UntilCallableType],
+ ):
+ self._idempotency_alias = idempotency_alias
+ self._context = context
+ self._callable = callable
+ self._type = type
+
+ async def changes(self):
+ return await IMPORT_reboot_aio_workflows.until_changes(
+ self._idempotency_alias,
+ self._context,
+ self._callable,
+ type=self._type,
+ )
+
+ async def satisfies(
+ self,
+ condition: IMPORT_typing.Callable[[Counter_UntilCallableType], bool],
+ ):
+
+ async def converge():
+ response = await self._callable()
+ if condition(response):
+ return response
+ return False
+
+ return await IMPORT_reboot_aio_workflows.until(
+ self._idempotency_alias,
+ self._context,
+ converge,
+ type=self._type,
+ )
+
+ class _Until:
+
+ _weak_reference: Counter.WeakReference
+ _idempotency_alias: str
+
+ def __init__(
+ self,
+ *,
+ weak_reference: Counter.WeakReference,
+ idempotency_alias: str,
+ ):
+ self._weak_reference = weak_reference
+ self._idempotency_alias = idempotency_alias
+
+ def read(
+ self,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ ) -> Counter.WeakReference._UntilChangesSatisfies[Counter.State]:
+ IMPORT_reboot_aio_types.assert_type(
+ context,
+ [IMPORT_reboot_aio_contexts.WorkflowContext],
+ )
+
+ async def callable():
+ return await self._weak_reference.read(context)
+
+ return Counter.WeakReference._UntilChangesSatisfies(
+ idempotency_alias=self._idempotency_alias,
+ context=context,
+ callable=callable,
+ type=Counter.State,
+ )
+
+
+ def Value( # type: ignore[misc]
+ # In methods which are dealing with user input, (i.e.,
+ # proto message field names), we should use '__double_underscored__'
+ # variables to avoid any potential name conflicts with the method's
+ # parameters.
+ # The '__self__' parameter is a convention in Python to
+ # indicate that this method is a bound method, so we use
+ # '__this__' instead.
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.WorkflowContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> Counter.WeakReference._UntilChangesSatisfies[Counter.ValueResponse]:
+ IMPORT_reboot_aio_types.assert_type(
+ __context__,
+ [IMPORT_reboot_aio_contexts.WorkflowContext],
+ )
+
+ __options__ = __options__ or IMPORT_reboot_aio_call.Options()
+
+ async def callable():
+ return await __this__._weak_reference.Value(
+ __context__,
+ __options__,
+ )
+
+ return Counter.WeakReference._UntilChangesSatisfies(
+ idempotency_alias=__this__._idempotency_alias,
+ context=__context__,
+ callable=callable,
+ type=Counter.ValueResponse,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ value = Value
+
+ def Description( # type: ignore[misc]
+ # In methods which are dealing with user input, (i.e.,
+ # proto message field names), we should use '__double_underscored__'
+ # variables to avoid any potential name conflicts with the method's
+ # parameters.
+ # The '__self__' parameter is a convention in Python to
+ # indicate that this method is a bound method, so we use
+ # '__this__' instead.
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.WorkflowContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> Counter.WeakReference._UntilChangesSatisfies[Counter.DescriptionResponse]:
+ IMPORT_reboot_aio_types.assert_type(
+ __context__,
+ [IMPORT_reboot_aio_contexts.WorkflowContext],
+ )
+
+ __options__ = __options__ or IMPORT_reboot_aio_call.Options()
+
+ async def callable():
+ return await __this__._weak_reference.Description(
+ __context__,
+ __options__,
+ )
+
+ return Counter.WeakReference._UntilChangesSatisfies(
+ idempotency_alias=__this__._idempotency_alias,
+ context=__context__,
+ callable=callable,
+ type=Counter.DescriptionResponse,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ description = Description
+
+ def until(self, alias: str):
+ return Counter.WeakReference._Until(
+ weak_reference=self,
+ idempotency_alias=alias,
+ )
+
+ def schedule(
+ self,
+ *,
+ when: IMPORT_typing.Optional[IMPORT_datetime_datetime | IMPORT_datetime_timedelta] = None,
+ ) -> Counter_ScheduleTypeVar:
+ return self._schedule_type(self._application_id, self._tasks, when=when)
+
+ class _Schedule:
+
+ def __init__(
+ self,
+ application_id: IMPORT_typing.Optional[IMPORT_reboot_aio_types.ApplicationId],
+ tasks: IMPORT_typing.Callable[[IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext], CounterTasksStub],
+ *,
+ when: IMPORT_typing.Optional[IMPORT_datetime_datetime | IMPORT_datetime_timedelta] = None,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ ) -> None:
+ self._application_id = application_id
+ self._tasks = tasks
+ self._when = ensure_has_timezone(when=when)
+ self._idempotency = idempotency
+
+ # Counter callable tasks:
+
+ async def Increment( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ IMPORT_reboot_aio_types.assert_type(__context__, [IMPORT_reboot_aio_contexts.TransactionContext])
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ __schedule__: IMPORT_typing.Optional[IMPORT_reboot_time_DateTimeWithTimeZone] = (IMPORT_reboot_time_DateTimeWithTimeZone.now() + __this__._when) if isinstance(
+ __this__._when, IMPORT_datetime_timedelta
+ ) else __this__._when
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = __this__._idempotency
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ # Add scheduling information to the metadata.
+ __metadata__ = (
+ (IMPORT_reboot_aio_headers.TASK_SCHEDULE,
+ __schedule__.isoformat() if __schedule__ else ''),
+ ) + (__metadata__ or tuple())
+
+ __task_id__ = await __this__._tasks(
+ __context__
+ ).Increment(
+ idempotency=__idempotency__,
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ )
+
+ return __task_id__
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ increment = Increment
+
+ async def Value( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ IMPORT_reboot_aio_types.assert_type(__context__, [IMPORT_reboot_aio_contexts.TransactionContext])
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ __schedule__: IMPORT_typing.Optional[IMPORT_reboot_time_DateTimeWithTimeZone] = (IMPORT_reboot_time_DateTimeWithTimeZone.now() + __this__._when) if isinstance(
+ __this__._when, IMPORT_datetime_timedelta
+ ) else __this__._when
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = __this__._idempotency
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ # Add scheduling information to the metadata.
+ __metadata__ = (
+ (IMPORT_reboot_aio_headers.TASK_SCHEDULE,
+ __schedule__.isoformat() if __schedule__ else ''),
+ ) + (__metadata__ or tuple())
+
+ __task_id__ = await __this__._tasks(
+ __context__
+ ).Value(
+ idempotency=__idempotency__,
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ )
+
+ return __task_id__
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ value = Value
+
+ async def Description( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ IMPORT_reboot_aio_types.assert_type(__context__, [IMPORT_reboot_aio_contexts.TransactionContext])
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ __schedule__: IMPORT_typing.Optional[IMPORT_reboot_time_DateTimeWithTimeZone] = (IMPORT_reboot_time_DateTimeWithTimeZone.now() + __this__._when) if isinstance(
+ __this__._when, IMPORT_datetime_timedelta
+ ) else __this__._when
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = __this__._idempotency
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ # Add scheduling information to the metadata.
+ __metadata__ = (
+ (IMPORT_reboot_aio_headers.TASK_SCHEDULE,
+ __schedule__.isoformat() if __schedule__ else ''),
+ ) + (__metadata__ or tuple())
+
+ __task_id__ = await __this__._tasks(
+ __context__
+ ).Description(
+ idempotency=__idempotency__,
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ )
+
+ return __task_id__
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ description = Description
+
+ # A `WriterContext` can not call any methods in `_Schedule` to
+ # prevent a writer from doing a `Foo.ref()` and trying to
+ # schedule. However, we want to allow a writer to schedule
+ # when we are constructing a `WeakReference` from
+ # `self.ref()` so instead we return a `_WriterSchedule` to
+ # provide type safety that allows a `WriterContext` to
+ # schedule (for itself).
+ class _WriterSchedule:
+
+ def __init__(
+ self,
+ application_id: IMPORT_typing.Optional[IMPORT_reboot_aio_types.ApplicationId],
+ tasks: IMPORT_typing.Callable[[IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext], CounterTasksStub],
+ *,
+ when: IMPORT_typing.Optional[IMPORT_datetime_datetime | IMPORT_datetime_timedelta] = None,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ ) -> None:
+ self._tasks = tasks
+ self._when = ensure_has_timezone(when=when)
+ self._idempotency = idempotency
+
+ # Counter callable tasks:
+
+ async def Increment( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ # Only `writer`s and `transaction`s should ``schedule()`, a
+ # `workflow` should `spawn()`.
+ IMPORT_reboot_aio_types.assert_type(__context__, [IMPORT_reboot_aio_contexts.WriterContext, IMPORT_reboot_aio_contexts.TransactionContext])
+
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ if isinstance(__context__, IMPORT_reboot_aio_contexts.WriterContext):
+ return (await CounterServicerTasks(
+ context=__context__,
+ state_ref=__context__._state_ref,
+ ).Increment(
+ schedule=__this__._when,
+ )).task_id
+
+ __schedule__: IMPORT_typing.Optional[IMPORT_reboot_time_DateTimeWithTimeZone] = (IMPORT_reboot_time_DateTimeWithTimeZone.now() + __this__._when) if isinstance(
+ __this__._when, IMPORT_datetime_timedelta
+ ) else __this__._when
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = __this__._idempotency
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ # Add scheduling information to the metadata.
+ __metadata__ = (
+ (IMPORT_reboot_aio_headers.TASK_SCHEDULE,
+ __schedule__.isoformat() if __schedule__ else ''),
+ ) + (__metadata__ or tuple())
+
+ return await __this__._tasks(
+ __context__
+ ).Increment(
+ idempotency=__idempotency__,
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ increment = Increment
+
+ async def Value( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ # Only `writer`s and `transaction`s should ``schedule()`, a
+ # `workflow` should `spawn()`.
+ IMPORT_reboot_aio_types.assert_type(__context__, [IMPORT_reboot_aio_contexts.WriterContext, IMPORT_reboot_aio_contexts.TransactionContext])
+
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ if isinstance(__context__, IMPORT_reboot_aio_contexts.WriterContext):
+ return (await CounterServicerTasks(
+ context=__context__,
+ state_ref=__context__._state_ref,
+ ).Value(
+ schedule=__this__._when,
+ )).task_id
+
+ __schedule__: IMPORT_typing.Optional[IMPORT_reboot_time_DateTimeWithTimeZone] = (IMPORT_reboot_time_DateTimeWithTimeZone.now() + __this__._when) if isinstance(
+ __this__._when, IMPORT_datetime_timedelta
+ ) else __this__._when
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = __this__._idempotency
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ # Add scheduling information to the metadata.
+ __metadata__ = (
+ (IMPORT_reboot_aio_headers.TASK_SCHEDULE,
+ __schedule__.isoformat() if __schedule__ else ''),
+ ) + (__metadata__ or tuple())
+
+ return await __this__._tasks(
+ __context__
+ ).Value(
+ idempotency=__idempotency__,
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ value = Value
+
+ async def Description( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> IMPORT_rbt_v1alpha1.tasks_pb2.TaskId:
+ # Only `writer`s and `transaction`s should ``schedule()`, a
+ # `workflow` should `spawn()`.
+ IMPORT_reboot_aio_types.assert_type(__context__, [IMPORT_reboot_aio_contexts.WriterContext, IMPORT_reboot_aio_contexts.TransactionContext])
+
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ if isinstance(__context__, IMPORT_reboot_aio_contexts.WriterContext):
+ return (await CounterServicerTasks(
+ context=__context__,
+ state_ref=__context__._state_ref,
+ ).Description(
+ schedule=__this__._when,
+ )).task_id
+
+ __schedule__: IMPORT_typing.Optional[IMPORT_reboot_time_DateTimeWithTimeZone] = (IMPORT_reboot_time_DateTimeWithTimeZone.now() + __this__._when) if isinstance(
+ __this__._when, IMPORT_datetime_timedelta
+ ) else __this__._when
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = __this__._idempotency
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ # Add scheduling information to the metadata.
+ __metadata__ = (
+ (IMPORT_reboot_aio_headers.TASK_SCHEDULE,
+ __schedule__.isoformat() if __schedule__ else ''),
+ ) + (__metadata__ or tuple())
+
+ return await __this__._tasks(
+ __context__
+ ).Description(
+ idempotency=__idempotency__,
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ description = Description
+
+ def spawn(
+ self,
+ *,
+ when: IMPORT_typing.Optional[IMPORT_datetime_datetime | IMPORT_datetime_timedelta] = None,
+ ) -> Counter.WeakReference._Spawn:
+ # Within a `workflow`, all "bare" `spawn()` calls are
+ # syntactic sugar for `per_workflow()`, unless we're
+ # within a control loop, in which case they are syntactic
+ # sugar for `per_iteration()`.
+ context = IMPORT_reboot_aio_contexts.Context.get()
+ if context is not None:
+ if isinstance(context, IMPORT_reboot_aio_contexts.WorkflowContext):
+ return (
+ self.per_iteration() if context.within_loop()
+ else self.per_workflow()
+ ).spawn(when=when)
+ elif isinstance(context, IMPORT_reboot_aio_external.InitializeContext):
+ return self.idempotently().spawn(when=when)
+
+ return Counter.WeakReference._Spawn(
+ self._application_id, self._tasks, when=when
+ )
+
+ class _Spawn:
+
+ def __init__(
+ self,
+ application_id: IMPORT_typing.Optional[IMPORT_reboot_aio_types.ApplicationId],
+ tasks: IMPORT_typing.Callable[[IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext], CounterTasksStub],
+ *,
+ when: IMPORT_typing.Optional[IMPORT_datetime_datetime | IMPORT_datetime_timedelta] = None,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ ) -> None:
+ self._application_id = application_id
+ self._tasks = tasks
+ self._when = ensure_has_timezone(when=when)
+ self._idempotency = idempotency
+
+ # Counter callable tasks:
+
+ async def Increment( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> Counter.IncrementTask:
+ IMPORT_reboot_aio_types.assert_type(__context__, [IMPORT_reboot_aio_contexts.WorkflowContext, IMPORT_reboot_aio_external.ExternalContext])
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+ __schedule__: IMPORT_typing.Optional[IMPORT_reboot_time_DateTimeWithTimeZone] = (IMPORT_reboot_time_DateTimeWithTimeZone.now() + __this__._when) if isinstance(
+ __this__._when, IMPORT_datetime_timedelta
+ ) else __this__._when
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = __this__._idempotency
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ # Add scheduling information to the metadata.
+ __metadata__ = (
+ (IMPORT_reboot_aio_headers.TASK_SCHEDULE,
+ __schedule__.isoformat() if __schedule__ else ''),
+ ) + (__metadata__ or tuple())
+
+ __task_id__ = await __this__._tasks(
+ __context__
+ ).Increment(
+ idempotency=__idempotency__,
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ )
+
+ return Counter.IncrementTask(
+ __context__,
+ task_id=__task_id__,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ increment = Increment
+
+ async def Value( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> Counter.ValueTask:
+ IMPORT_reboot_aio_types.assert_type(__context__, [IMPORT_reboot_aio_contexts.WorkflowContext, IMPORT_reboot_aio_external.ExternalContext])
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+ __schedule__: IMPORT_typing.Optional[IMPORT_reboot_time_DateTimeWithTimeZone] = (IMPORT_reboot_time_DateTimeWithTimeZone.now() + __this__._when) if isinstance(
+ __this__._when, IMPORT_datetime_timedelta
+ ) else __this__._when
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = __this__._idempotency
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ # Add scheduling information to the metadata.
+ __metadata__ = (
+ (IMPORT_reboot_aio_headers.TASK_SCHEDULE,
+ __schedule__.isoformat() if __schedule__ else ''),
+ ) + (__metadata__ or tuple())
+
+ __task_id__ = await __this__._tasks(
+ __context__
+ ).Value(
+ idempotency=__idempotency__,
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ )
+
+ return Counter.ValueTask(
+ __context__,
+ task_id=__task_id__,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ value = Value
+
+ async def Description( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> Counter.DescriptionTask:
+ IMPORT_reboot_aio_types.assert_type(__context__, [IMPORT_reboot_aio_contexts.WorkflowContext, IMPORT_reboot_aio_external.ExternalContext])
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+ __schedule__: IMPORT_typing.Optional[IMPORT_reboot_time_DateTimeWithTimeZone] = (IMPORT_reboot_time_DateTimeWithTimeZone.now() + __this__._when) if isinstance(
+ __this__._when, IMPORT_datetime_timedelta
+ ) else __this__._when
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = __this__._idempotency
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ # Add scheduling information to the metadata.
+ __metadata__ = (
+ (IMPORT_reboot_aio_headers.TASK_SCHEDULE,
+ __schedule__.isoformat() if __schedule__ else ''),
+ ) + (__metadata__ or tuple())
+
+ __task_id__ = await __this__._tasks(
+ __context__
+ ).Description(
+ idempotency=__idempotency__,
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ )
+
+ return Counter.DescriptionTask(
+ __context__,
+ task_id=__task_id__,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ description = Description
+
+ async def read(
+ self,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ ) -> Counter.State:
+ return await (
+ self.always() if context.within_until()
+ else (
+ self.per_iteration() if context.within_loop()
+ else self.per_workflow()
+ )
+ ).read(context)
+
+ async def write(
+ self,
+ context: IMPORT_reboot_aio_contexts.WorkflowContext,
+ writer: CounterBaseServicer.InlineWriterCallable[CounterBaseServicer.InlineWriterCallableResult],
+ *,
+ type: IMPORT_reboot_aio_workflows.Type | IMPORT_reboot_aio_workflows._Unset = IMPORT_reboot_aio_workflows._UNSET,
+ ) -> CounterBaseServicer.InlineWriterCallableResult:
+ """Perform an "inline write" within a workflow.
+
+ `type=` is optional: when omitted, `writer`'s return
+ annotation is used.
+ """
+ # We forward `type=` along to the inner `write` (which
+ # also infers if not passed). Pass through whichever
+ # form the user supplied -- explicit class or the
+ # `_UNSET` sentinel to trigger inference downstream.
+ return await (
+ self.always() if context.within_until()
+ else (
+ self.per_iteration() if context.within_loop()
+ else self.per_workflow()
+ )
+ ).write(
+ context, writer, type=type
+ )
+
+ # Counter specific methods:
+
+ async def Increment( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ *,
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ ) -> Counter.IncrementResponse:
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ # Within a `workflow`, all "bare" calls are
+ # `per_workflow()` calls, unless we're within a control
+ # loop, in which case they are syntactic sugar for
+ # `per_iteration()`.
+ if __idempotency__ is None:
+ if isinstance(__context__, IMPORT_reboot_aio_contexts.WorkflowContext):
+ return await (
+ __this__.per_iteration() if __context__.within_loop()
+ else __this__.per_workflow()
+ ).Increment(
+ __context__,
+ __options__ or IMPORT_reboot_aio_call.Options(),
+ )
+ elif isinstance(__context__, IMPORT_reboot_aio_external.InitializeContext):
+ return await __this__.idempotently().Increment(
+ __context__,
+ __options__ or IMPORT_reboot_aio_call.Options(),
+ )
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ return CounterIncrementResponseFromProto(
+ await __this__._writer(__context__).Increment(
+ idempotency=__idempotency__,
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ )
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ increment = Increment
+
+ async def Value( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.ReaderContext | IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ *,
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ ) -> Counter.ValueResponse:
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ # Within a `workflow`, all "bare" calls are
+ # `per_workflow()` calls, unless we're within a control
+ # loop, in which case they are syntactic sugar for
+ # `per_iteration()`.
+ #
+ # Unless we are "within until" in which case all "bare"
+ # calls are `.always().
+ if __idempotency__ is None:
+ if isinstance(__context__, IMPORT_reboot_aio_contexts.WorkflowContext):
+ return await (
+ __this__.always() if __context__.within_until()
+ else (
+ __this__.per_iteration() if __context__.within_loop()
+ else __this__.per_workflow()
+ )
+ ).Value(
+ __context__,
+ __options__ or IMPORT_reboot_aio_call.Options(),
+ )
+ elif isinstance(__context__, IMPORT_reboot_aio_external.InitializeContext):
+ return await __this__.idempotently().Value(
+ __context__,
+ __options__ or IMPORT_reboot_aio_call.Options(),
+ )
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+ return CounterValueResponseFromProto(
+ await __this__._reader(__context__).Value(
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ idempotency=__idempotency__,
+ )
+ )
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ value = Value
+
+ async def Description( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.ReaderContext | IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ *,
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ ) -> Counter.DescriptionResponse:
+ assert __options__ is None or isinstance(__options__, IMPORT_reboot_aio_call.Options)
+
+ # Within a `workflow`, all "bare" calls are
+ # `per_workflow()` calls, unless we're within a control
+ # loop, in which case they are syntactic sugar for
+ # `per_iteration()`.
+ #
+ # Unless we are "within until" in which case all "bare"
+ # calls are `.always().
+ if __idempotency__ is None:
+ if isinstance(__context__, IMPORT_reboot_aio_contexts.WorkflowContext):
+ return await (
+ __this__.always() if __context__.within_until()
+ else (
+ __this__.per_iteration() if __context__.within_loop()
+ else __this__.per_workflow()
+ )
+ ).Description(
+ __context__,
+ __options__ or IMPORT_reboot_aio_call.Options(),
+ )
+ elif isinstance(__context__, IMPORT_reboot_aio_external.InitializeContext):
+ return await __this__.idempotently().Description(
+ __context__,
+ __options__ or IMPORT_reboot_aio_call.Options(),
+ )
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+ return CounterDescriptionResponseFromProto(
+ await __this__._reader(__context__).Description(
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ idempotency=__idempotency__,
+ )
+ )
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ description = Description
+
+ class _Forall:
+
+ _ids: IMPORT_typing.Iterable[str]
+
+ def __init__(self, ids: IMPORT_typing.Iterable[str]):
+ self._ids = ids
+
+
+ async def Increment( # type: ignore[misc]
+ # In methods which are dealing with user input, (i.e.,
+ # proto message field names), we should use '__double_underscored__'
+ # variables to avoid any potential name conflicts with the method's
+ # parameters.
+ # The '__self__' parameter is a convention in Python to
+ # indicate that this method is a bound method, so we use
+ # '__this__' instead.
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> list[Counter.IncrementResponse]:
+ __options__ = __options__ or IMPORT_reboot_aio_call.Options()
+ return await IMPORT_asyncio.gather(
+ *[
+ Counter.ref(
+ id
+ ).Increment(
+ __context__,
+ __options__,
+ ) for id in __this__._ids
+ ]
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ increment = Increment
+
+ async def Value( # type: ignore[misc]
+ # In methods which are dealing with user input, (i.e.,
+ # proto message field names), we should use '__double_underscored__'
+ # variables to avoid any potential name conflicts with the method's
+ # parameters.
+ # The '__self__' parameter is a convention in Python to
+ # indicate that this method is a bound method, so we use
+ # '__this__' instead.
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.ReaderContext | IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> list[Counter.ValueResponse]:
+ __options__ = __options__ or IMPORT_reboot_aio_call.Options()
+ return await IMPORT_asyncio.gather(
+ *[
+ Counter.ref(
+ id
+ ).Value(
+ __context__,
+ __options__,
+ ) for id in __this__._ids
+ ]
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ value = Value
+
+ async def Description( # type: ignore[misc]
+ # In methods which are dealing with user input, (i.e.,
+ # proto message field names), we should use '__double_underscored__'
+ # variables to avoid any potential name conflicts with the method's
+ # parameters.
+ # The '__self__' parameter is a convention in Python to
+ # indicate that this method is a bound method, so we use
+ # '__this__' instead.
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.ReaderContext | IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> list[Counter.DescriptionResponse]:
+ __options__ = __options__ or IMPORT_reboot_aio_call.Options()
+ return await IMPORT_asyncio.gather(
+ *[
+ Counter.ref(
+ id
+ ).Description(
+ __context__,
+ __options__,
+ ) for id in __this__._ids
+ ]
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ description = Description
+
+ @classmethod
+ def forall(cls, ids: IMPORT_typing.Iterable[str]) -> Counter._Forall:
+ return Counter._Forall(ids)
+
+ @classmethod
+ def ref(
+ cls,
+ state_id: IMPORT_typing.Optional[IMPORT_reboot_aio_types.StateId] = None,
+ *,
+ bearer_token: IMPORT_typing.Optional[str] = None,
+ ) -> Counter.WeakReference[Counter.WeakReference._Schedule] | Counter.WeakReference[Counter.WeakReference._WriterSchedule]:
+ # We support calling `Counter.ref()` with
+ # no `state_id` __only__ inside a workflow to be able to call an
+ # inline writer, inline reader or other method call, since
+ # workflow is a `classmethod` and therefor we can't get a
+ # reference to outselves as `self.ref()`.
+ if state_id is None:
+ context = IMPORT_reboot_aio_contexts.Context.get()
+
+ if context is None:
+ raise RuntimeError(
+ 'Missing asyncio context variable `context`; '
+ 'are you using this class without Reboot?'
+ )
+
+ if not isinstance(context, IMPORT_reboot_aio_contexts.WorkflowContext):
+ raise RuntimeError(
+ '`ref()` called without a `state_id` can only be used within a Workflow.'
+ )
+
+ servicer = CounterBaseServicer.__servicer__.get()
+
+ if servicer is None:
+ raise RuntimeError(
+ 'Missing asyncio context variable `servicer`; '
+ 'are you using this class without Reboot?'
+ )
+
+ return Counter.WeakReference(
+ # TODO(https://github.com/reboot-dev/mono/issues/3226): add support for calling other applications.
+ # For now this always stays within the application that creates the context.
+ application_id=None,
+ state_id=context._state_ref.id,
+ schedule_type=Counter.WeakReference._WriterSchedule,
+ # If the user didn't specify a bearer token we may still end up using the app-internal bearer token,
+ # but that's decided at the time of the call.
+ bearer_token=bearer_token,
+ servicer=servicer,
+ )
+
+ return Counter.WeakReference(
+ # TODO(https://github.com/reboot-dev/mono/issues/3226): add support for calling other applications.
+ # For now this always stays within the application that creates the context.
+ application_id=None,
+ state_id=state_id,
+ schedule_type=Counter.WeakReference._Schedule,
+ bearer_token=bearer_token,
+ )
+
+ @IMPORT_typing.overload
+ @classmethod
+ async def Create(
+ __cls__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __state_id_or_request_or_options__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.StateId | Counter.CreateRequest] = None,
+ __request_or_options_or_idempotency__: IMPORT_typing.Optional[Counter.CreateRequest | IMPORT_reboot_aio_call.Options] = None,
+ __options_or_idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options | IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ ) -> tuple[Counter.WeakReference, None]:
+ ...
+
+ @IMPORT_typing.overload
+ @classmethod
+ async def Create(
+ __cls__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __state_id_or_request_or_options__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.StateId | IMPORT_reboot_aio_call.Options] = None,
+ __request_or_options_or_idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options | IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ __options_or_idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ *,
+ description: str | Unset = UNSET,
+ ) -> tuple[Counter.WeakReference, None]:
+ ...
+
+ @classmethod
+ async def Create( # type: ignore[misc]
+ __cls__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __state_id_or_request_or_options__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.StateId | Counter.CreateRequest | IMPORT_reboot_aio_call.Options] = None,
+ __request_or_options_or_idempotency__: IMPORT_typing.Optional[Counter.CreateRequest | IMPORT_reboot_aio_call.Options | IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ __options_or_idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options | IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ __idempotency__: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None,
+ *,
+ description: str | Unset = UNSET,
+ ) -> tuple[Counter.WeakReference, None]:
+ # Within a `workflow`, all "bare" calls are
+ # `per_workflow()` calls, unless we're within a control
+ # loop, in which case they are syntactic sugar for
+ # `per_iteration()`.
+ #
+ # Unless we are "within until" in which case all "bare"
+ # calls are `.always()`.
+
+ __request__: IMPORT_typing.Optional[Counter.CreateRequest] = None
+ __state_id__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.StateId] = None
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None
+
+ if isinstance(__state_id_or_request_or_options__, IMPORT_reboot_aio_types.StateId):
+ __state_id__ = __state_id_or_request_or_options__
+ if isinstance(__request_or_options_or_idempotency__, Counter.CreateRequest):
+ __request__ = __request_or_options_or_idempotency__
+ assert __options_or_idempotency__ is None or isinstance(__options_or_idempotency__, IMPORT_reboot_aio_call.Options)
+ __options__ = __options_or_idempotency__
+ assert __idempotency__ is None or isinstance(__idempotency__, IMPORT_reboot_aio_idempotency.Idempotency)
+
+ assert description is UNSET
+ else:
+ assert __request_or_options_or_idempotency__ is None or isinstance(__request_or_options_or_idempotency__, IMPORT_reboot_aio_call.Options)
+ __options__ = __request_or_options_or_idempotency__
+ assert __options_or_idempotency__ is None or isinstance(__options_or_idempotency__, IMPORT_reboot_aio_idempotency.Idempotency)
+ __idempotency__ = __options_or_idempotency__
+
+ __request__ = CounterCreateRequestFromInputFields(
+ description=description,
+ )
+ elif isinstance(__state_id_or_request_or_options__, Counter.CreateRequest):
+ __request__ = __state_id_or_request_or_options__
+ assert __request_or_options_or_idempotency__ is None or isinstance(__request_or_options_or_idempotency__, IMPORT_reboot_aio_call.Options)
+ __options__ = __request_or_options_or_idempotency__
+ assert __options_or_idempotency__ is None or isinstance(__options_or_idempotency__, IMPORT_reboot_aio_idempotency.Idempotency)
+ __idempotency__ = __options_or_idempotency__
+ else:
+ assert (
+ __state_id_or_request_or_options__ is None
+ or isinstance(__state_id_or_request_or_options__, IMPORT_reboot_aio_call.Options)
+ ), "Invalid argument type. Did you pass:\n" \
+ " - A 'state_id' that is not of type 'reboot.aio.types.StateId'?\n" \
+ " - 'options' that are not of type 'reboot.aio.call.Options'?"
+ __options__ = __state_id_or_request_or_options__
+ assert __request_or_options_or_idempotency__ is None or isinstance(__request_or_options_or_idempotency__, IMPORT_reboot_aio_idempotency.Idempotency)
+ __idempotency__ = __request_or_options_or_idempotency__
+
+ __request__ = CounterCreateRequestFromInputFields(
+ description=description,
+ )
+
+ if __idempotency__ is None:
+ __args__: tuple[IMPORT_typing.Any, ...] = (
+ __context__,
+ )
+ if __state_id__ is not None:
+ # Don't include `state_id` if it is `None`, so that
+ # we won't break the positional argument deduction.
+ __args__ += (__state_id__,)
+ __args__ += (
+ __request__,
+ __options__,
+ )
+ if isinstance(__context__, IMPORT_reboot_aio_contexts.WorkflowContext):
+ return await (
+ __cls__.always() if __context__.within_until()
+ else (
+ __cls__.per_iteration() if __context__.within_loop()
+ else __cls__.per_workflow()
+ )
+ ).Create(
+ *__args__
+ )
+ elif isinstance(__context__, IMPORT_reboot_aio_external.InitializeContext):
+ return await __cls__.idempotently().Create(
+ *__args__
+ )
+
+ __metadata__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.GrpcMetadata] = None
+ __bearer_token__: IMPORT_typing.Optional[str] = None
+
+ if __options__ is not None:
+ IMPORT_reboot_aio_types.assert_type(__options__, [IMPORT_reboot_aio_call.Options])
+ if __options__.metadata is not None:
+ __metadata__ = __options__.metadata
+ if __options__.bearer_token is not None:
+ __bearer_token__ = __options__.bearer_token
+
+ if __state_id__ is None:
+ if __idempotency__ is None:
+ __state_id__ = str(IMPORT_uuid.uuid4())
+ else:
+ __state_id__ = __context__.generate_idempotent_state_id(
+ state_type_name=__cls__.__state_type_name__,
+ service_name=IMPORT_reboot_aio_types.ServiceName('reboot.ping.CounterMethods'),
+ method='Create',
+ idempotency=__idempotency__,
+ )
+
+ __reference__ = Counter.ref(
+ __state_id__, bearer_token=__bearer_token__
+ )
+ __stub__ = __reference__._writer(__context__)
+ return (
+ __reference__,
+ CounterCreateResponseFromProto(
+ await __stub__.Create(
+ __request__,
+ idempotency=__idempotency__,
+ metadata=__metadata__,
+ bearer_token=__bearer_token__,
+ )
+ ),
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ create = Create
+
+ @IMPORT_typing.overload
+ @classmethod
+ def idempotently(cls, alias: IMPORT_typing.Optional[str] = None, *, how: IMPORT_reboot_aio_idempotency.How = IMPORT_reboot_aio_idempotency.PER_WORKFLOW) -> Counter._ConstructIdempotently:
+ ...
+
+ @IMPORT_typing.overload
+ @classmethod
+ def idempotently(cls, *, key: IMPORT_uuid.UUID, how: IMPORT_reboot_aio_idempotency.How = IMPORT_reboot_aio_idempotency.PER_WORKFLOW) -> Counter._ConstructIdempotently:
+ ...
+
+ @classmethod
+ def idempotently(
+ cls,
+ alias: IMPORT_typing.Optional[str] = None,
+ *,
+ key: IMPORT_typing.Optional[IMPORT_uuid.UUID] = None,
+ how: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.How] = None,
+ ) -> Counter._ConstructIdempotently:
+ return Counter._ConstructIdempotently(
+ _idempotency=IMPORT_reboot_aio_contexts.Context.idempotency(
+ alias=alias,
+ key=key,
+ how=how,
+ ),
+ )
+
+ @classmethod
+ def per_workflow(
+ cls,
+ alias: IMPORT_typing.Optional[str] = None,
+ ):
+ return cls.idempotently(alias)
+
+ @classmethod
+ def per_iteration(
+ cls,
+ alias: IMPORT_typing.Optional[str] = None,
+ ):
+ return cls.idempotently(
+ alias,
+ how=IMPORT_reboot_aio_idempotency.PER_ITERATION,
+ )
+
+ @classmethod
+ def always(cls):
+ return cls.idempotently(
+ how=IMPORT_reboot_aio_idempotency.ALWAYS,
+ )
+
+ @IMPORT_dataclasses.dataclass(frozen=True)
+ class _ConstructIdempotently:
+
+ _idempotency: IMPORT_reboot_aio_idempotency.Idempotency
+
+ @IMPORT_typing.overload
+ async def Create(
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __state_id_or_request_or_options__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.StateId | Counter.CreateRequest] = None,
+ __request_or_options__: IMPORT_typing.Optional[Counter.CreateRequest | IMPORT_reboot_aio_call.Options] = None,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ ) -> tuple[Counter.WeakReference, None]:
+ ...
+
+ @IMPORT_typing.overload
+ async def Create(
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __state_id_or_request_or_options__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.StateId | IMPORT_reboot_aio_call.Options] = None,
+ __request_or_options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ *,
+ description: str | Unset = UNSET,
+ ) -> tuple[Counter.WeakReference, None]:
+ ...
+
+ async def Create( # type: ignore[misc]
+ __this__,
+ __context__: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ __state_id_or_request_or_options__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.StateId | Counter.CreateRequest | IMPORT_reboot_aio_call.Options] = None,
+ __request_or_options__: IMPORT_typing.Optional[Counter.CreateRequest | IMPORT_reboot_aio_call.Options] = None,
+ __options__: IMPORT_typing.Optional[IMPORT_reboot_aio_call.Options] = None,
+ *,
+ description: str | Unset = UNSET,
+ ) -> tuple[Counter.WeakReference, None]:
+ # UX improvement: check that neither positional argument was accidentally
+ # given a gRPC request type.
+ IMPORT_reboot_aio_types.assert_not_request_type(__context__, request_type=Counter.CreateRequest)
+ IMPORT_reboot_aio_types.assert_not_request_type(__options__, request_type=Counter.CreateRequest)
+
+ __state_id__: IMPORT_typing.Optional[IMPORT_reboot_aio_types.StateId] = None
+ __request__: IMPORT_typing.Optional[Counter.CreateRequest] = None
+
+ if isinstance(__state_id_or_request_or_options__, IMPORT_reboot_aio_types.StateId):
+ __state_id__ = __state_id_or_request_or_options__
+ if isinstance(__request_or_options__, Counter.CreateRequest):
+ __request__ = __request_or_options__
+ if __options__ is not None and not isinstance(__options__, IMPORT_reboot_aio_call.Options):
+ raise TypeError(f"Expecting fourth positional argument to be of type 'reboot.aio.call.Options', got '{type(__request_or_options__).__name__}'")
+
+ assert description is UNSET
+ else:
+ if __request_or_options__ is not None and not isinstance(__request_or_options__, IMPORT_reboot_aio_call.Options):
+ raise TypeError(f"Expecting third positional argument to be of type 'reboot.aio.call.Options', got '{type(__request_or_options__).__name__}'")
+
+ __options__ = __request_or_options__
+
+ __request__ = CounterCreateRequestFromInputFields(
+ description=description,
+ )
+ elif isinstance(__state_id_or_request_or_options__, Counter.CreateRequest):
+ __request__ = __state_id_or_request_or_options__
+ if __request_or_options__ is not None and not isinstance(__request_or_options__, IMPORT_reboot_aio_call.Options):
+ raise TypeError(f"Expecting third positional argument to be of type 'reboot.aio.call.Options', got '{type(__request_or_options__).__name__}'")
+
+ __options__ = __request_or_options__
+ else:
+ if __state_id_or_request_or_options__ is not None and not isinstance(__state_id_or_request_or_options__, IMPORT_reboot_aio_call.Options):
+ raise TypeError(f"Expecting second positional argument to either be of type 'str' or 'reboot.aio.call.Options', got '{type(__state_id_or_request_or_options__).__name__}'")
+
+ __options__ = __state_id_or_request_or_options__
+
+ __request__ = CounterCreateRequestFromInputFields(
+ description=description,
+ )
+ __args__: tuple[IMPORT_typing.Any, ...]
+
+ if __state_id__ is not None:
+ __args__ = (
+ __context__,
+ __state_id__,
+ __request__,
+ __options__,
+ __this__._idempotency,
+ )
+ else:
+ __args__ = (
+ __context__,
+ __request__,
+ __options__,
+ __this__._idempotency,
+ )
+
+ return await Counter.Create(
+ *__args__,
+ )
+
+ # Keep the original functions on the client, so old code will
+ # continue to work, but use the new 'snake_case' method in
+ # the new code.
+ create = Create
+
+
+
+############################ Reference Node adapters ############################
+# Used by Node.js WeakReference implementations to access Python code and
+# vice-versa. Relevant to clients.
+
+class PingWeakReferenceNodeAdaptor(Ping.WeakReference[Ping.WeakReference._Schedule]):
+
+ async def _call( # type: ignore[override]
+ self,
+ *,
+ callable: IMPORT_typing.Callable[[IMPORT_google_protobuf_message.Message], IMPORT_typing.Awaitable],
+ aborted_type: type[IMPORT_reboot.aio.aborted.Aborted],
+ request_type: type[IMPORT_google_protobuf_message.Message],
+ json_request: str,
+ ) -> str:
+ request = request_type()
+
+ try:
+ IMPORT_google_protobuf_json_format.Parse(json_request, request)
+ response = await callable(request)
+ except IMPORT_google_protobuf_json_format.ParseError as parse_error:
+ aborted_error = IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ message=f"{parse_error}; "
+ "This is usually caused by a deeply nested protobuf message, which is not supported by protobuf.\n"
+ "See the limits here: https://protobuf.dev/programming-guides/proto-limits/"
+ )
+
+ return IMPORT_json.dumps(
+ {
+ 'status': IMPORT_google_protobuf_json_format.MessageToDict(
+ aborted_error.to_status()
+ )
+ }
+ )
+ except BaseException as exception:
+ if isinstance(exception, aborted_type):
+ return IMPORT_json.dumps(
+ {
+ 'status': IMPORT_google_protobuf_json_format.MessageToDict(
+ exception.to_status()
+ )
+ }
+ )
+ raise
+ else:
+ return IMPORT_json.dumps(
+ {
+ 'response': IMPORT_google_protobuf_json_format.MessageToDict(
+ response
+ )
+ }
+ )
+
+ async def _schedule( # type: ignore[override]
+ self,
+ *,
+ method: str,
+ context: IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ schedule: IMPORT_reboot_time_DateTimeWithTimeZone,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency],
+ request_type: type[IMPORT_google_protobuf_message.Message],
+ json_request: str,
+ ) -> str:
+ request = request_type()
+
+ IMPORT_google_protobuf_json_format.Parse(json_request, request)
+
+ if isinstance(context, IMPORT_reboot_aio_contexts.WriterContext):
+ task = await getattr(
+ PingServicerTasks(
+ context=context,
+ state_ref=context._state_ref,
+ ),
+ method,
+ )(request, schedule=schedule)
+
+ return IMPORT_json.dumps(
+ {
+ 'taskId': IMPORT_google_protobuf_json_format.MessageToDict(
+ task.task_id
+ )
+ }
+ )
+
+ # Add scheduling information to the metadata.
+ metadata: IMPORT_reboot_aio_types.GrpcMetadata = (
+ (IMPORT_reboot_aio_headers.TASK_SCHEDULE, schedule.isoformat()),
+ )
+
+ task_id = await getattr(super()._tasks(context), method)(
+ request,
+ idempotency=idempotency,
+ metadata=metadata,
+ )
+
+ return IMPORT_json.dumps(
+ {
+ 'taskId': IMPORT_google_protobuf_json_format.MessageToDict(task_id)
+ }
+ )
+
+ async def _reader( # type: ignore[override]
+ self,
+ method: str,
+ context: IMPORT_reboot_aio_contexts.ReaderContext | IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ request_type: type[IMPORT_google_protobuf_message.Message],
+ json_request: str,
+ json_options: str,
+ ) -> str:
+ options = IMPORT_json.loads(json_options)
+
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None
+
+ if 'idempotency' in options:
+ idempotency = IMPORT_reboot_aio_contexts.Context.idempotency(
+ alias=options['idempotency'].get('alias'),
+ key=options['idempotency'].get('key'),
+ how=(
+ IMPORT_reboot_aio_idempotency.ALWAYS
+ if options['idempotency'].get('always', False)
+ else IMPORT_reboot_aio_idempotency.PER_ITERATION
+ if options['idempotency'].get('perIteration')
+ else None
+ ),
+ )
+
+ method_handle = IMPORT_functools.partial(
+ getattr(super()._reader(context), method),
+ bearer_token=options.get("bearerToken"),
+ idempotency=idempotency,
+ )
+ return await self._call(
+ callable=method_handle,
+ aborted_type=getattr(
+ Ping, method + 'Aborted'
+ ),
+ request_type=request_type,
+ json_request=json_request,
+ )
+
+ async def _writer( # type: ignore[override]
+ self,
+ method: str,
+ context: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ request_type: type[IMPORT_google_protobuf_message.Message],
+ json_request: str,
+ json_options: str,
+ ) -> str:
+ options = IMPORT_json.loads(json_options)
+
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None
+
+ if 'idempotency' in options:
+ idempotency = IMPORT_reboot_aio_contexts.Context.idempotency(
+ alias=options['idempotency'].get('alias'),
+ key=options['idempotency'].get('key'),
+ how=(
+ IMPORT_reboot_aio_idempotency.ALWAYS
+ if options['idempotency'].get('always', False)
+ else IMPORT_reboot_aio_idempotency.PER_ITERATION
+ if options['idempotency'].get('perIteration')
+ else None
+ ),
+ )
+
+ if 'schedule' in options:
+ when = IMPORT_google_protobuf_timestamp_pb2.Timestamp()
+ when.FromJsonString(options['schedule']['when'])
+ return await self._schedule(
+ method=method,
+ context=context,
+ schedule=IMPORT_reboot_time_DateTimeWithTimeZone.from_protobuf_timestamp(when),
+ idempotency=idempotency,
+ request_type=request_type,
+ json_request=json_request,
+ )
+
+ method_handle = IMPORT_functools.partial(
+ getattr(super()._writer(context), method),
+ idempotency=idempotency,
+ bearer_token=options.get("bearerToken"),
+ )
+ return await self._call(
+ callable=method_handle,
+ aborted_type=getattr(
+ Ping, method + 'Aborted'
+ ),
+ request_type=request_type,
+ json_request=json_request,
+ )
+
+ async def _transaction( # type: ignore[override]
+ self,
+ method: str,
+ context: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ request_type: type[IMPORT_google_protobuf_message.Message],
+ json_request: str,
+ json_options: str,
+ ) -> str:
+ options = IMPORT_json.loads(json_options)
+
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None
+
+ if 'idempotency' in options:
+ idempotency = IMPORT_reboot_aio_contexts.Context.idempotency(
+ alias=options['idempotency'].get('alias'),
+ key=options['idempotency'].get('key'),
+ how=(
+ IMPORT_reboot_aio_idempotency.ALWAYS
+ if options['idempotency'].get('always', False)
+ else IMPORT_reboot_aio_idempotency.PER_ITERATION
+ if options['idempotency'].get('perIteration')
+ else None
+ ),
+ )
+
+ if 'schedule' in options:
+ when = IMPORT_google_protobuf_timestamp_pb2.Timestamp()
+ when.FromJsonString(options['schedule']['when'])
+ return await self._schedule(
+ method=method,
+ context=context,
+ schedule=IMPORT_reboot_time_DateTimeWithTimeZone.from_protobuf_timestamp(when),
+ idempotency=idempotency,
+ request_type=request_type,
+ json_request=json_request,
+ )
+
+ method_handle = IMPORT_functools.partial(
+ getattr(super()._workflow(context), method),
+ idempotency=idempotency,
+ bearer_token=options.get("bearerToken"),
+ )
+ return await self._call(
+ callable=method_handle,
+ aborted_type=getattr(
+ Ping, method + 'Aborted'
+ ),
+ request_type=request_type,
+ json_request=json_request,
+ )
+
+ async def _workflow( # type: ignore[override]
+ self,
+ method: str,
+ context: IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ request_type: type[IMPORT_google_protobuf_message.Message],
+ json_request: str,
+ json_options: str,
+ ) -> str:
+ options = IMPORT_json.loads(json_options)
+
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None
+
+ if 'idempotency' in options:
+ idempotency = IMPORT_reboot_aio_contexts.Context.idempotency(
+ alias=options['idempotency'].get('alias'),
+ key=options['idempotency'].get('key'),
+ how=(
+ IMPORT_reboot_aio_idempotency.ALWAYS
+ if options['idempotency'].get('always', False)
+ else IMPORT_reboot_aio_idempotency.PER_ITERATION
+ if options['idempotency'].get('perIteration')
+ else None
+ ),
+ )
+
+ assert 'schedule' in options
+
+ when = IMPORT_google_protobuf_timestamp_pb2.Timestamp()
+ when.FromJsonString(options['schedule']['when'])
+
+ return await self._schedule(
+ method=method,
+ context=context,
+ schedule=IMPORT_reboot_time_DateTimeWithTimeZone.from_protobuf_timestamp(when),
+ idempotency=idempotency,
+ request_type=request_type,
+ json_request=json_request,
+ )
+
+class PongWeakReferenceNodeAdaptor(Pong.WeakReference[Pong.WeakReference._Schedule]):
+
+ async def _call( # type: ignore[override]
+ self,
+ *,
+ callable: IMPORT_typing.Callable[[IMPORT_google_protobuf_message.Message], IMPORT_typing.Awaitable],
+ aborted_type: type[IMPORT_reboot.aio.aborted.Aborted],
+ request_type: type[IMPORT_google_protobuf_message.Message],
+ json_request: str,
+ ) -> str:
+ request = request_type()
+
+ try:
+ IMPORT_google_protobuf_json_format.Parse(json_request, request)
+ response = await callable(request)
+ except IMPORT_google_protobuf_json_format.ParseError as parse_error:
+ aborted_error = IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ message=f"{parse_error}; "
+ "This is usually caused by a deeply nested protobuf message, which is not supported by protobuf.\n"
+ "See the limits here: https://protobuf.dev/programming-guides/proto-limits/"
+ )
+
+ return IMPORT_json.dumps(
+ {
+ 'status': IMPORT_google_protobuf_json_format.MessageToDict(
+ aborted_error.to_status()
+ )
+ }
+ )
+ except BaseException as exception:
+ if isinstance(exception, aborted_type):
+ return IMPORT_json.dumps(
+ {
+ 'status': IMPORT_google_protobuf_json_format.MessageToDict(
+ exception.to_status()
+ )
+ }
+ )
+ raise
+ else:
+ return IMPORT_json.dumps(
+ {
+ 'response': IMPORT_google_protobuf_json_format.MessageToDict(
+ response
+ )
+ }
+ )
+
+ async def _schedule( # type: ignore[override]
+ self,
+ *,
+ method: str,
+ context: IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ schedule: IMPORT_reboot_time_DateTimeWithTimeZone,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency],
+ request_type: type[IMPORT_google_protobuf_message.Message],
+ json_request: str,
+ ) -> str:
+ request = request_type()
+
+ IMPORT_google_protobuf_json_format.Parse(json_request, request)
+
+ if isinstance(context, IMPORT_reboot_aio_contexts.WriterContext):
+ task = await getattr(
+ PongServicerTasks(
+ context=context,
+ state_ref=context._state_ref,
+ ),
+ method,
+ )(request, schedule=schedule)
+
+ return IMPORT_json.dumps(
+ {
+ 'taskId': IMPORT_google_protobuf_json_format.MessageToDict(
+ task.task_id
+ )
+ }
+ )
+
+ # Add scheduling information to the metadata.
+ metadata: IMPORT_reboot_aio_types.GrpcMetadata = (
+ (IMPORT_reboot_aio_headers.TASK_SCHEDULE, schedule.isoformat()),
+ )
+
+ task_id = await getattr(super()._tasks(context), method)(
+ request,
+ idempotency=idempotency,
+ metadata=metadata,
+ )
+
+ return IMPORT_json.dumps(
+ {
+ 'taskId': IMPORT_google_protobuf_json_format.MessageToDict(task_id)
+ }
+ )
+
+ async def _reader( # type: ignore[override]
+ self,
+ method: str,
+ context: IMPORT_reboot_aio_contexts.ReaderContext | IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ request_type: type[IMPORT_google_protobuf_message.Message],
+ json_request: str,
+ json_options: str,
+ ) -> str:
+ options = IMPORT_json.loads(json_options)
+
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None
+
+ if 'idempotency' in options:
+ idempotency = IMPORT_reboot_aio_contexts.Context.idempotency(
+ alias=options['idempotency'].get('alias'),
+ key=options['idempotency'].get('key'),
+ how=(
+ IMPORT_reboot_aio_idempotency.ALWAYS
+ if options['idempotency'].get('always', False)
+ else IMPORT_reboot_aio_idempotency.PER_ITERATION
+ if options['idempotency'].get('perIteration')
+ else None
+ ),
+ )
+
+ method_handle = IMPORT_functools.partial(
+ getattr(super()._reader(context), method),
+ bearer_token=options.get("bearerToken"),
+ idempotency=idempotency,
+ )
+ return await self._call(
+ callable=method_handle,
+ aborted_type=getattr(
+ Pong, method + 'Aborted'
+ ),
+ request_type=request_type,
+ json_request=json_request,
+ )
+
+ async def _writer( # type: ignore[override]
+ self,
+ method: str,
+ context: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ request_type: type[IMPORT_google_protobuf_message.Message],
+ json_request: str,
+ json_options: str,
+ ) -> str:
+ options = IMPORT_json.loads(json_options)
+
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None
+
+ if 'idempotency' in options:
+ idempotency = IMPORT_reboot_aio_contexts.Context.idempotency(
+ alias=options['idempotency'].get('alias'),
+ key=options['idempotency'].get('key'),
+ how=(
+ IMPORT_reboot_aio_idempotency.ALWAYS
+ if options['idempotency'].get('always', False)
+ else IMPORT_reboot_aio_idempotency.PER_ITERATION
+ if options['idempotency'].get('perIteration')
+ else None
+ ),
+ )
+
+ if 'schedule' in options:
+ when = IMPORT_google_protobuf_timestamp_pb2.Timestamp()
+ when.FromJsonString(options['schedule']['when'])
+ return await self._schedule(
+ method=method,
+ context=context,
+ schedule=IMPORT_reboot_time_DateTimeWithTimeZone.from_protobuf_timestamp(when),
+ idempotency=idempotency,
+ request_type=request_type,
+ json_request=json_request,
+ )
+
+ method_handle = IMPORT_functools.partial(
+ getattr(super()._writer(context), method),
+ idempotency=idempotency,
+ bearer_token=options.get("bearerToken"),
+ )
+ return await self._call(
+ callable=method_handle,
+ aborted_type=getattr(
+ Pong, method + 'Aborted'
+ ),
+ request_type=request_type,
+ json_request=json_request,
+ )
+
+ async def _transaction( # type: ignore[override]
+ self,
+ method: str,
+ context: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ request_type: type[IMPORT_google_protobuf_message.Message],
+ json_request: str,
+ json_options: str,
+ ) -> str:
+ options = IMPORT_json.loads(json_options)
+
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None
+
+ if 'idempotency' in options:
+ idempotency = IMPORT_reboot_aio_contexts.Context.idempotency(
+ alias=options['idempotency'].get('alias'),
+ key=options['idempotency'].get('key'),
+ how=(
+ IMPORT_reboot_aio_idempotency.ALWAYS
+ if options['idempotency'].get('always', False)
+ else IMPORT_reboot_aio_idempotency.PER_ITERATION
+ if options['idempotency'].get('perIteration')
+ else None
+ ),
+ )
+
+ if 'schedule' in options:
+ when = IMPORT_google_protobuf_timestamp_pb2.Timestamp()
+ when.FromJsonString(options['schedule']['when'])
+ return await self._schedule(
+ method=method,
+ context=context,
+ schedule=IMPORT_reboot_time_DateTimeWithTimeZone.from_protobuf_timestamp(when),
+ idempotency=idempotency,
+ request_type=request_type,
+ json_request=json_request,
+ )
+
+ method_handle = IMPORT_functools.partial(
+ getattr(super()._workflow(context), method),
+ idempotency=idempotency,
+ bearer_token=options.get("bearerToken"),
+ )
+ return await self._call(
+ callable=method_handle,
+ aborted_type=getattr(
+ Pong, method + 'Aborted'
+ ),
+ request_type=request_type,
+ json_request=json_request,
+ )
+
+ async def _workflow( # type: ignore[override]
+ self,
+ method: str,
+ context: IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ request_type: type[IMPORT_google_protobuf_message.Message],
+ json_request: str,
+ json_options: str,
+ ) -> str:
+ options = IMPORT_json.loads(json_options)
+
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None
+
+ if 'idempotency' in options:
+ idempotency = IMPORT_reboot_aio_contexts.Context.idempotency(
+ alias=options['idempotency'].get('alias'),
+ key=options['idempotency'].get('key'),
+ how=(
+ IMPORT_reboot_aio_idempotency.ALWAYS
+ if options['idempotency'].get('always', False)
+ else IMPORT_reboot_aio_idempotency.PER_ITERATION
+ if options['idempotency'].get('perIteration')
+ else None
+ ),
+ )
+
+ assert 'schedule' in options
+
+ when = IMPORT_google_protobuf_timestamp_pb2.Timestamp()
+ when.FromJsonString(options['schedule']['when'])
+
+ return await self._schedule(
+ method=method,
+ context=context,
+ schedule=IMPORT_reboot_time_DateTimeWithTimeZone.from_protobuf_timestamp(when),
+ idempotency=idempotency,
+ request_type=request_type,
+ json_request=json_request,
+ )
+
+class UserWeakReferenceNodeAdaptor(User.WeakReference[User.WeakReference._Schedule]):
+
+ async def _call( # type: ignore[override]
+ self,
+ *,
+ callable: IMPORT_typing.Callable[[IMPORT_google_protobuf_message.Message], IMPORT_typing.Awaitable],
+ aborted_type: type[IMPORT_reboot.aio.aborted.Aborted],
+ request_type: type[IMPORT_google_protobuf_message.Message],
+ json_request: str,
+ ) -> str:
+ request = request_type()
+
+ try:
+ IMPORT_google_protobuf_json_format.Parse(json_request, request)
+ response = await callable(request)
+ except IMPORT_google_protobuf_json_format.ParseError as parse_error:
+ aborted_error = IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ message=f"{parse_error}; "
+ "This is usually caused by a deeply nested protobuf message, which is not supported by protobuf.\n"
+ "See the limits here: https://protobuf.dev/programming-guides/proto-limits/"
+ )
+
+ return IMPORT_json.dumps(
+ {
+ 'status': IMPORT_google_protobuf_json_format.MessageToDict(
+ aborted_error.to_status()
+ )
+ }
+ )
+ except BaseException as exception:
+ if isinstance(exception, aborted_type):
+ return IMPORT_json.dumps(
+ {
+ 'status': IMPORT_google_protobuf_json_format.MessageToDict(
+ exception.to_status()
+ )
+ }
+ )
+ raise
+ else:
+ return IMPORT_json.dumps(
+ {
+ 'response': IMPORT_google_protobuf_json_format.MessageToDict(
+ response
+ )
+ }
+ )
+
+ async def _schedule( # type: ignore[override]
+ self,
+ *,
+ method: str,
+ context: IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ schedule: IMPORT_reboot_time_DateTimeWithTimeZone,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency],
+ request_type: type[IMPORT_google_protobuf_message.Message],
+ json_request: str,
+ ) -> str:
+ request = request_type()
+
+ IMPORT_google_protobuf_json_format.Parse(json_request, request)
+
+ if isinstance(context, IMPORT_reboot_aio_contexts.WriterContext):
+ task = await getattr(
+ UserServicerTasks(
+ context=context,
+ state_ref=context._state_ref,
+ ),
+ method,
+ )(request, schedule=schedule)
+
+ return IMPORT_json.dumps(
+ {
+ 'taskId': IMPORT_google_protobuf_json_format.MessageToDict(
+ task.task_id
+ )
+ }
+ )
+
+ # Add scheduling information to the metadata.
+ metadata: IMPORT_reboot_aio_types.GrpcMetadata = (
+ (IMPORT_reboot_aio_headers.TASK_SCHEDULE, schedule.isoformat()),
+ )
+
+ task_id = await getattr(super()._tasks(context), method)(
+ request,
+ idempotency=idempotency,
+ metadata=metadata,
+ )
+
+ return IMPORT_json.dumps(
+ {
+ 'taskId': IMPORT_google_protobuf_json_format.MessageToDict(task_id)
+ }
+ )
+
+ async def _reader( # type: ignore[override]
+ self,
+ method: str,
+ context: IMPORT_reboot_aio_contexts.ReaderContext | IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ request_type: type[IMPORT_google_protobuf_message.Message],
+ json_request: str,
+ json_options: str,
+ ) -> str:
+ options = IMPORT_json.loads(json_options)
+
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None
+
+ if 'idempotency' in options:
+ idempotency = IMPORT_reboot_aio_contexts.Context.idempotency(
+ alias=options['idempotency'].get('alias'),
+ key=options['idempotency'].get('key'),
+ how=(
+ IMPORT_reboot_aio_idempotency.ALWAYS
+ if options['idempotency'].get('always', False)
+ else IMPORT_reboot_aio_idempotency.PER_ITERATION
+ if options['idempotency'].get('perIteration')
+ else None
+ ),
+ )
+
+ method_handle = IMPORT_functools.partial(
+ getattr(super()._reader(context), method),
+ bearer_token=options.get("bearerToken"),
+ idempotency=idempotency,
+ )
+ return await self._call(
+ callable=method_handle,
+ aborted_type=getattr(
+ User, method + 'Aborted'
+ ),
+ request_type=request_type,
+ json_request=json_request,
+ )
+
+ async def _writer( # type: ignore[override]
+ self,
+ method: str,
+ context: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ request_type: type[IMPORT_google_protobuf_message.Message],
+ json_request: str,
+ json_options: str,
+ ) -> str:
+ options = IMPORT_json.loads(json_options)
+
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None
+
+ if 'idempotency' in options:
+ idempotency = IMPORT_reboot_aio_contexts.Context.idempotency(
+ alias=options['idempotency'].get('alias'),
+ key=options['idempotency'].get('key'),
+ how=(
+ IMPORT_reboot_aio_idempotency.ALWAYS
+ if options['idempotency'].get('always', False)
+ else IMPORT_reboot_aio_idempotency.PER_ITERATION
+ if options['idempotency'].get('perIteration')
+ else None
+ ),
+ )
+
+ if 'schedule' in options:
+ when = IMPORT_google_protobuf_timestamp_pb2.Timestamp()
+ when.FromJsonString(options['schedule']['when'])
+ return await self._schedule(
+ method=method,
+ context=context,
+ schedule=IMPORT_reboot_time_DateTimeWithTimeZone.from_protobuf_timestamp(when),
+ idempotency=idempotency,
+ request_type=request_type,
+ json_request=json_request,
+ )
+
+ method_handle = IMPORT_functools.partial(
+ getattr(super()._writer(context), method),
+ idempotency=idempotency,
+ bearer_token=options.get("bearerToken"),
+ )
+ return await self._call(
+ callable=method_handle,
+ aborted_type=getattr(
+ User, method + 'Aborted'
+ ),
+ request_type=request_type,
+ json_request=json_request,
+ )
+
+ async def _transaction( # type: ignore[override]
+ self,
+ method: str,
+ context: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ request_type: type[IMPORT_google_protobuf_message.Message],
+ json_request: str,
+ json_options: str,
+ ) -> str:
+ options = IMPORT_json.loads(json_options)
+
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None
+
+ if 'idempotency' in options:
+ idempotency = IMPORT_reboot_aio_contexts.Context.idempotency(
+ alias=options['idempotency'].get('alias'),
+ key=options['idempotency'].get('key'),
+ how=(
+ IMPORT_reboot_aio_idempotency.ALWAYS
+ if options['idempotency'].get('always', False)
+ else IMPORT_reboot_aio_idempotency.PER_ITERATION
+ if options['idempotency'].get('perIteration')
+ else None
+ ),
+ )
+
+ if 'schedule' in options:
+ when = IMPORT_google_protobuf_timestamp_pb2.Timestamp()
+ when.FromJsonString(options['schedule']['when'])
+ return await self._schedule(
+ method=method,
+ context=context,
+ schedule=IMPORT_reboot_time_DateTimeWithTimeZone.from_protobuf_timestamp(when),
+ idempotency=idempotency,
+ request_type=request_type,
+ json_request=json_request,
+ )
+
+ method_handle = IMPORT_functools.partial(
+ getattr(super()._workflow(context), method),
+ idempotency=idempotency,
+ bearer_token=options.get("bearerToken"),
+ )
+ return await self._call(
+ callable=method_handle,
+ aborted_type=getattr(
+ User, method + 'Aborted'
+ ),
+ request_type=request_type,
+ json_request=json_request,
+ )
+
+ async def _workflow( # type: ignore[override]
+ self,
+ method: str,
+ context: IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ request_type: type[IMPORT_google_protobuf_message.Message],
+ json_request: str,
+ json_options: str,
+ ) -> str:
+ options = IMPORT_json.loads(json_options)
+
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None
+
+ if 'idempotency' in options:
+ idempotency = IMPORT_reboot_aio_contexts.Context.idempotency(
+ alias=options['idempotency'].get('alias'),
+ key=options['idempotency'].get('key'),
+ how=(
+ IMPORT_reboot_aio_idempotency.ALWAYS
+ if options['idempotency'].get('always', False)
+ else IMPORT_reboot_aio_idempotency.PER_ITERATION
+ if options['idempotency'].get('perIteration')
+ else None
+ ),
+ )
+
+ assert 'schedule' in options
+
+ when = IMPORT_google_protobuf_timestamp_pb2.Timestamp()
+ when.FromJsonString(options['schedule']['when'])
+
+ return await self._schedule(
+ method=method,
+ context=context,
+ schedule=IMPORT_reboot_time_DateTimeWithTimeZone.from_protobuf_timestamp(when),
+ idempotency=idempotency,
+ request_type=request_type,
+ json_request=json_request,
+ )
+
+class CounterWeakReferenceNodeAdaptor(Counter.WeakReference[Counter.WeakReference._Schedule]):
+
+ async def _call( # type: ignore[override]
+ self,
+ *,
+ callable: IMPORT_typing.Callable[[IMPORT_google_protobuf_message.Message], IMPORT_typing.Awaitable],
+ aborted_type: type[IMPORT_reboot.aio.aborted.Aborted],
+ request_type: type[IMPORT_google_protobuf_message.Message],
+ json_request: str,
+ ) -> str:
+ request = request_type()
+
+ try:
+ IMPORT_google_protobuf_json_format.Parse(json_request, request)
+ response = await callable(request)
+ except IMPORT_google_protobuf_json_format.ParseError as parse_error:
+ aborted_error = IMPORT_reboot.aio.aborted.SystemAborted(
+ IMPORT_rbt_v1alpha1.errors_pb2.Unknown(),
+ message=f"{parse_error}; "
+ "This is usually caused by a deeply nested protobuf message, which is not supported by protobuf.\n"
+ "See the limits here: https://protobuf.dev/programming-guides/proto-limits/"
+ )
+
+ return IMPORT_json.dumps(
+ {
+ 'status': IMPORT_google_protobuf_json_format.MessageToDict(
+ aborted_error.to_status()
+ )
+ }
+ )
+ except BaseException as exception:
+ if isinstance(exception, aborted_type):
+ return IMPORT_json.dumps(
+ {
+ 'status': IMPORT_google_protobuf_json_format.MessageToDict(
+ exception.to_status()
+ )
+ }
+ )
+ raise
+ else:
+ return IMPORT_json.dumps(
+ {
+ 'response': IMPORT_google_protobuf_json_format.MessageToDict(
+ response
+ )
+ }
+ )
+
+ async def _schedule( # type: ignore[override]
+ self,
+ *,
+ method: str,
+ context: IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ schedule: IMPORT_reboot_time_DateTimeWithTimeZone,
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency],
+ request_type: type[IMPORT_google_protobuf_message.Message],
+ json_request: str,
+ ) -> str:
+ request = request_type()
+
+ IMPORT_google_protobuf_json_format.Parse(json_request, request)
+
+ if isinstance(context, IMPORT_reboot_aio_contexts.WriterContext):
+ task = await getattr(
+ CounterServicerTasks(
+ context=context,
+ state_ref=context._state_ref,
+ ),
+ method,
+ )(request, schedule=schedule)
+
+ return IMPORT_json.dumps(
+ {
+ 'taskId': IMPORT_google_protobuf_json_format.MessageToDict(
+ task.task_id
+ )
+ }
+ )
+
+ # Add scheduling information to the metadata.
+ metadata: IMPORT_reboot_aio_types.GrpcMetadata = (
+ (IMPORT_reboot_aio_headers.TASK_SCHEDULE, schedule.isoformat()),
+ )
+
+ task_id = await getattr(super()._tasks(context), method)(
+ request,
+ idempotency=idempotency,
+ metadata=metadata,
+ )
+
+ return IMPORT_json.dumps(
+ {
+ 'taskId': IMPORT_google_protobuf_json_format.MessageToDict(task_id)
+ }
+ )
+
+ async def _reader( # type: ignore[override]
+ self,
+ method: str,
+ context: IMPORT_reboot_aio_contexts.ReaderContext | IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ request_type: type[IMPORT_google_protobuf_message.Message],
+ json_request: str,
+ json_options: str,
+ ) -> str:
+ options = IMPORT_json.loads(json_options)
+
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None
+
+ if 'idempotency' in options:
+ idempotency = IMPORT_reboot_aio_contexts.Context.idempotency(
+ alias=options['idempotency'].get('alias'),
+ key=options['idempotency'].get('key'),
+ how=(
+ IMPORT_reboot_aio_idempotency.ALWAYS
+ if options['idempotency'].get('always', False)
+ else IMPORT_reboot_aio_idempotency.PER_ITERATION
+ if options['idempotency'].get('perIteration')
+ else None
+ ),
+ )
+
+ method_handle = IMPORT_functools.partial(
+ getattr(super()._reader(context), method),
+ bearer_token=options.get("bearerToken"),
+ idempotency=idempotency,
+ )
+ return await self._call(
+ callable=method_handle,
+ aborted_type=getattr(
+ Counter, method + 'Aborted'
+ ),
+ request_type=request_type,
+ json_request=json_request,
+ )
+
+ async def _writer( # type: ignore[override]
+ self,
+ method: str,
+ context: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ request_type: type[IMPORT_google_protobuf_message.Message],
+ json_request: str,
+ json_options: str,
+ ) -> str:
+ options = IMPORT_json.loads(json_options)
+
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None
+
+ if 'idempotency' in options:
+ idempotency = IMPORT_reboot_aio_contexts.Context.idempotency(
+ alias=options['idempotency'].get('alias'),
+ key=options['idempotency'].get('key'),
+ how=(
+ IMPORT_reboot_aio_idempotency.ALWAYS
+ if options['idempotency'].get('always', False)
+ else IMPORT_reboot_aio_idempotency.PER_ITERATION
+ if options['idempotency'].get('perIteration')
+ else None
+ ),
+ )
+
+ if 'schedule' in options:
+ when = IMPORT_google_protobuf_timestamp_pb2.Timestamp()
+ when.FromJsonString(options['schedule']['when'])
+ return await self._schedule(
+ method=method,
+ context=context,
+ schedule=IMPORT_reboot_time_DateTimeWithTimeZone.from_protobuf_timestamp(when),
+ idempotency=idempotency,
+ request_type=request_type,
+ json_request=json_request,
+ )
+
+ method_handle = IMPORT_functools.partial(
+ getattr(super()._writer(context), method),
+ idempotency=idempotency,
+ bearer_token=options.get("bearerToken"),
+ )
+ return await self._call(
+ callable=method_handle,
+ aborted_type=getattr(
+ Counter, method + 'Aborted'
+ ),
+ request_type=request_type,
+ json_request=json_request,
+ )
+
+ async def _transaction( # type: ignore[override]
+ self,
+ method: str,
+ context: IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ request_type: type[IMPORT_google_protobuf_message.Message],
+ json_request: str,
+ json_options: str,
+ ) -> str:
+ options = IMPORT_json.loads(json_options)
+
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None
+
+ if 'idempotency' in options:
+ idempotency = IMPORT_reboot_aio_contexts.Context.idempotency(
+ alias=options['idempotency'].get('alias'),
+ key=options['idempotency'].get('key'),
+ how=(
+ IMPORT_reboot_aio_idempotency.ALWAYS
+ if options['idempotency'].get('always', False)
+ else IMPORT_reboot_aio_idempotency.PER_ITERATION
+ if options['idempotency'].get('perIteration')
+ else None
+ ),
+ )
+
+ if 'schedule' in options:
+ when = IMPORT_google_protobuf_timestamp_pb2.Timestamp()
+ when.FromJsonString(options['schedule']['when'])
+ return await self._schedule(
+ method=method,
+ context=context,
+ schedule=IMPORT_reboot_time_DateTimeWithTimeZone.from_protobuf_timestamp(when),
+ idempotency=idempotency,
+ request_type=request_type,
+ json_request=json_request,
+ )
+
+ method_handle = IMPORT_functools.partial(
+ getattr(super()._workflow(context), method),
+ idempotency=idempotency,
+ bearer_token=options.get("bearerToken"),
+ )
+ return await self._call(
+ callable=method_handle,
+ aborted_type=getattr(
+ Counter, method + 'Aborted'
+ ),
+ request_type=request_type,
+ json_request=json_request,
+ )
+
+ async def _workflow( # type: ignore[override]
+ self,
+ method: str,
+ context: IMPORT_reboot_aio_contexts.WriterContext | IMPORT_reboot_aio_contexts.TransactionContext | IMPORT_reboot_aio_contexts.WorkflowContext | IMPORT_reboot_aio_external.ExternalContext,
+ request_type: type[IMPORT_google_protobuf_message.Message],
+ json_request: str,
+ json_options: str,
+ ) -> str:
+ options = IMPORT_json.loads(json_options)
+
+ idempotency: IMPORT_typing.Optional[IMPORT_reboot_aio_idempotency.Idempotency] = None
+
+ if 'idempotency' in options:
+ idempotency = IMPORT_reboot_aio_contexts.Context.idempotency(
+ alias=options['idempotency'].get('alias'),
+ key=options['idempotency'].get('key'),
+ how=(
+ IMPORT_reboot_aio_idempotency.ALWAYS
+ if options['idempotency'].get('always', False)
+ else IMPORT_reboot_aio_idempotency.PER_ITERATION
+ if options['idempotency'].get('perIteration')
+ else None
+ ),
+ )
+
+ assert 'schedule' in options
+
+ when = IMPORT_google_protobuf_timestamp_pb2.Timestamp()
+ when.FromJsonString(options['schedule']['when'])
+
+ return await self._schedule(
+ method=method,
+ context=context,
+ schedule=IMPORT_reboot_time_DateTimeWithTimeZone.from_protobuf_timestamp(when),
+ idempotency=idempotency,
+ request_type=request_type,
+ json_request=json_request,
+ )
+
+
+# yapf: enable
diff --git a/tests/reboot/plugin/hooks/auto_approve_test.py b/tests/reboot/plugin/hooks/auto_approve_test.py
index 12335ed8..72ea0bc3 100644
--- a/tests/reboot/plugin/hooks/auto_approve_test.py
+++ b/tests/reboot/plugin/hooks/auto_approve_test.py
@@ -662,8 +662,8 @@ def decision_from_stdout(stdout: str) -> Decision:
),
(
"dev: npx @mcpjam/inspector",
- "npx @mcpjam/inspector@2.9.3 --config mcp_servers.json "
- "--server my-app",
+ "npx @mcpjam/inspector@2.18.1 --url http://localhost:9991/mcp "
+ "--oauth",
Where.PROJECT,
Decision.APPROVE,
),
diff --git a/tests/reboot/pydantic/auto_construct_user/BUILD.bazel b/tests/reboot/pydantic/auto_construct_user/BUILD.bazel
new file mode 100644
index 00000000..ca7c5e46
--- /dev/null
+++ b/tests/reboot/pydantic/auto_construct_user/BUILD.bazel
@@ -0,0 +1,38 @@
+load("@rules_python//python:defs.bzl", "py_library", "py_test")
+load("//reboot:pydantic_to_proto.bzl", "py_reboot_library_from_pydantic")
+
+py_library(
+ name = "servicer_api_py",
+ srcs = ["servicer_api.py"],
+ deps = [
+ "//reboot:api_py",
+ ],
+)
+
+py_reboot_library_from_pydantic(
+ name = "servicer_py_reboot",
+ py_deps = [
+ ":servicer_api_py",
+ ],
+ pydantic = ":servicer_api.py",
+)
+
+py_library(
+ name = "servicer_py",
+ srcs = ["servicer.py"],
+ deps = [
+ ":servicer_api_py",
+ ":servicer_py_reboot",
+ ],
+)
+
+py_test(
+ name = "test_py",
+ srcs = ["test.py"],
+ main = "test.py",
+ deps = [
+ ":servicer_py",
+ "//reboot/aio:applications_py",
+ "//reboot/aio:tests_py",
+ ],
+)
diff --git a/tests/reboot/pydantic/auto_construct_user/servicer.py b/tests/reboot/pydantic/auto_construct_user/servicer.py
new file mode 100644
index 00000000..4e9a4be1
--- /dev/null
+++ b/tests/reboot/pydantic/auto_construct_user/servicer.py
@@ -0,0 +1,36 @@
+from reboot.aio.auth.authorizers import allow
+from reboot.aio.contexts import (
+ ReaderContext,
+ TransactionContext,
+ WriterContext,
+)
+from tests.reboot.pydantic.auto_construct_user.servicer_api_rbt import (
+ Profile,
+ User,
+)
+
+
+class UserServicer(User.Servicer):
+
+ async def create(self, context: TransactionContext) -> None:
+ """Override the auto-constructed `create` to also construct a
+ `Profile` for this user; a `Transaction` can call other state
+ machines during construction."""
+ profile_id = f"profile-{context.state_id}"
+ await Profile.create(context, profile_id)
+ self.state.profile_id = profile_id
+
+ async def get(self, context: ReaderContext) -> User.GetResponse:
+ return User.GetResponse(profile_id=self.state.profile_id)
+
+
+class ProfileServicer(Profile.Servicer):
+
+ def authorizer(self):
+ return allow()
+
+ async def create(self, context: WriterContext) -> None:
+ self.state.created = True
+
+ async def get(self, context: ReaderContext) -> Profile.GetResponse:
+ return Profile.GetResponse(created=self.state.created)
diff --git a/tests/reboot/pydantic/auto_construct_user/servicer_api.py b/tests/reboot/pydantic/auto_construct_user/servicer_api.py
new file mode 100644
index 00000000..0b16a970
--- /dev/null
+++ b/tests/reboot/pydantic/auto_construct_user/servicer_api.py
@@ -0,0 +1,53 @@
+from reboot.api import API, Field, Methods, Model, Reader, Type, Writer
+
+
+# A `Profile` is a separate state machine that the `User` servicer
+# constructs as a side effect of its own construction. It exists to
+# prove that the auto-constructed `User.create` can call other state
+# machines, which is possible because `create` is a `Transaction`.
+class ProfileState(Model):
+ created: bool = Field(tag=1, default=False)
+
+
+class ProfileGetResponse(Model):
+ created: bool = Field(tag=1)
+
+
+# The `User` state is auto-constructed `PER_USER_ID`, so the framework
+# injects a reserved, overridable `create` factory onto it.
+class UserState(Model):
+ profile_id: str = Field(tag=1, default="")
+
+
+class UserGetResponse(Model):
+ profile_id: str = Field(tag=1)
+
+
+api = API(
+ User=Type(
+ state=UserState,
+ methods=Methods(
+ get=Reader(
+ request=None,
+ response=UserGetResponse,
+ mcp=None,
+ ),
+ ),
+ ),
+ Profile=Type(
+ state=ProfileState,
+ methods=Methods(
+ create=Writer(
+ request=None,
+ response=None,
+ factory=True,
+ mcp=None,
+ ),
+ get=Reader(
+ request=None,
+ response=ProfileGetResponse,
+ mcp=None,
+ ),
+ ),
+ ),
+)
diff --git a/tests/reboot/pydantic/auto_construct_user/test.py b/tests/reboot/pydantic/auto_construct_user/test.py
new file mode 100644
index 00000000..05c02d3a
--- /dev/null
+++ b/tests/reboot/pydantic/auto_construct_user/test.py
@@ -0,0 +1,78 @@
+import unittest
+from reboot.aio.applications import Application
+from reboot.aio.auth.oauth_providers import Anonymous
+from reboot.aio.tests import OAuthProviderForTest, Reboot
+from tests.reboot.pydantic.auto_construct_user.servicer import (
+ ProfileServicer,
+ UserServicer,
+)
+from tests.reboot.pydantic.auto_construct_user.servicer_api_rbt import (
+ Profile,
+ User,
+)
+
+_USER_ID = "test-user"
+
+
+class AutoConstructUserTest(unittest.IsolatedAsyncioTestCase):
+ """The auto-constructed `User.create` is a `Transaction`, so an
+ overriding servicer can construct other state machines as part of
+ user creation."""
+
+ async def asyncSetUp(self) -> None:
+ self.rbt = Reboot()
+ await self.rbt.start()
+ await self.rbt.up(
+ Application(
+ servicers=[UserServicer, ProfileServicer],
+ oauth=OAuthProviderForTest(Anonymous()),
+ )
+ )
+ # An authenticated context whose user-id matches the `User`
+ # state-id, which is what the framework requires to reach an
+ # auto-constructed `User`.
+ self.context = self.rbt.create_external_context(
+ name=f"test-{self.id()}",
+ bearer_token=self.rbt.make_valid_oauth_access_token(
+ user_id=_USER_ID,
+ ),
+ )
+
+ async def asyncTearDown(self) -> None:
+ await self.rbt.stop()
+
+ async def test_create_transaction_constructs_related_state(
+ self,
+ ) -> None:
+ await UserServicer._auto_construct(self.context, state_id=_USER_ID)
+
+ # The override recorded the `Profile` it created on the `User`.
+ user_response = await User.ref(_USER_ID).get(self.context)
+ self.assertEqual(
+ user_response.profile_id,
+ f"profile-{_USER_ID}",
+ )
+
+ # The `Profile` was really constructed by the transaction, not
+ # just referenced: reading it back shows the state its own
+ # `create` wrote.
+ profile = Profile.ref(user_response.profile_id)
+ profile_response = await profile.get(self.context)
+ self.assertTrue(profile_response.created)
+
+ async def test_auto_construct_is_idempotent(self) -> None:
+ # Auto-construction may be triggered repeatedly (e.g. at the
+ # start of every session); doing so must be a no-op rather
+ # than re-running the constructing transaction.
+ await UserServicer._auto_construct(self.context, state_id=_USER_ID)
+ await UserServicer._auto_construct(self.context, state_id=_USER_ID)
+
+ user_response = await User.ref(_USER_ID).get(self.context)
+ self.assertEqual(
+ user_response.profile_id,
+ f"profile-{_USER_ID}",
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/reboot/routing/xds_server_test.py b/tests/reboot/routing/xds_server_test.py
index e99bea1b..94fb54a2 100644
--- a/tests/reboot/routing/xds_server_test.py
+++ b/tests/reboot/routing/xds_server_test.py
@@ -10,10 +10,23 @@
TYPE_CLUSTER,
TYPE_LISTENER,
AggregatedDiscoveryServiceServicer,
+ ConfigRejected,
+ ServerError,
)
from reboot.settings import ENVOY_VERSION
+def _envoy_node() -> base_pb2.Node:
+ """A `Node` advertising our pinned Envoy version, so the servicer
+ doesn't log a version-mismatch error."""
+ node = base_pb2.Node()
+ major, minor, patch = ENVOY_VERSION.split(".")
+ node.user_agent_build_version.version.major_number = int(major)
+ node.user_agent_build_version.version.minor_number = int(minor)
+ node.user_agent_build_version.version.patch = int(patch)
+ return node
+
+
def _cluster_from_any(cluster_any: any_pb2.Any) -> cluster_pb2.Cluster:
cluster = cluster_pb2.Cluster()
cluster_any.Unpack(cluster)
@@ -48,21 +61,10 @@ async def asyncTearDown(self):
await self._grpc_server.wait_for_termination()
async def test_basic(self):
- # Discovery requests should carry version info to prevent error logs
- # from the xDS server.
- node = base_pb2.Node()
- envoy_version_parts = ENVOY_VERSION.split(".")
- node.user_agent_build_version.version.major_number = int(
- envoy_version_parts[0]
- )
- node.user_agent_build_version.version.minor_number = int(
- envoy_version_parts[1]
- )
- node.user_agent_build_version.version.patch = int(
- envoy_version_parts[2]
- )
+ node = _envoy_node()
- # A client begins by asking for the clusters.
+ # A client asks for the clusters first; the server never
+ # withholds clusters, so it answers immediately.
call = self._client.StreamAggregatedResources()
await call.write(
discovery_pb2.DiscoveryRequest(node=node, type_url=TYPE_CLUSTER)
@@ -70,9 +72,8 @@ async def test_basic(self):
response = await call.read()
self.assertEqual(response.type_url, TYPE_CLUSTER)
- # The client will immediately circle back and ask for the next version
- # of the clusters, but there will be no response until the server has a
- # new version to give.
+ # The client circles back to listen for the next version of the
+ # clusters; there is no response until the config changes.
await call.write(
discovery_pb2.DiscoveryRequest(
node=node,
@@ -81,17 +82,19 @@ async def test_basic(self):
)
)
response_task = asyncio.create_task(call.read())
+ await asyncio.sleep(0.1)
self.assertFalse(response_task.done())
- # The client will then ask for the listeners.
+ # The client then asks for the listeners and gets them: the
+ # clusters they reference are already known to it.
await call.write(
discovery_pb2.DiscoveryRequest(node=node, type_url=TYPE_LISTENER)
)
response = await response_task
self.assertEqual(response.type_url, TYPE_LISTENER)
- # Like with the clusters, the client will immediately go listen for
- # changes on the listeners, but won't hear anything (yet).
+ # Likewise, the client listens for the next version of the
+ # listeners and hears nothing until the config changes.
await call.write(
discovery_pb2.DiscoveryRequest(
node=node,
@@ -100,6 +103,7 @@ async def test_basic(self):
)
)
response_task = asyncio.create_task(call.read())
+ await asyncio.sleep(0.1)
self.assertFalse(response_task.done())
# When we make a change to the config, the client will be informed.
@@ -169,6 +173,192 @@ async def test_basic(self):
# The `set_config` call still completes.
await set_config_task
+ async def test_rejection_fails_in_flight_set_config(self):
+ # A connected Envoy that NACKs a config update makes the
+ # in-flight `set_config` fail, rather than blocking forever
+ # waiting for an acknowledgement that will never come. This
+ # matters for `rbt dev run`, where there may never be a
+ # subsequent `set_config` to surface the rejection.
+ node = _envoy_node()
+ call = self._client.StreamAggregatedResources()
+
+ # Bring the fake Envoy up to date with the server's initial
+ # (empty) config, so it counts as a connected, up-to-date client.
+ await call.write(
+ discovery_pb2.DiscoveryRequest(node=node, type_url=TYPE_CLUSTER)
+ )
+ await call.write(
+ discovery_pb2.DiscoveryRequest(node=node, type_url=TYPE_LISTENER)
+ )
+ cluster_response = await call.read()
+ listener_response = await call.read()
+ await call.write(
+ discovery_pb2.DiscoveryRequest(
+ node=node,
+ type_url=TYPE_CLUSTER,
+ version_info=cluster_response.version_info,
+ )
+ )
+ await call.write(
+ discovery_pb2.DiscoveryRequest(
+ node=node,
+ type_url=TYPE_LISTENER,
+ version_info=listener_response.version_info,
+ )
+ )
+
+ # Start a config update; it blocks until every connected Envoy
+ # has acknowledged the new version.
+ set_config_task = asyncio.create_task(
+ self._servicer.set_config(
+ clusters=[cluster_pb2.Cluster(name="some_cluster")],
+ listeners=[listener_pb2.Listener(name="some_listener")],
+ )
+ )
+
+ # The fake Envoy receives the new config and rejects it. Setting
+ # `error_detail` marks the request as a NACK rather than an ACK;
+ # its contents don't matter.
+ await call.read()
+ await call.read()
+ nack = discovery_pb2.DiscoveryRequest(
+ node=node,
+ type_url=TYPE_LISTENER,
+ version_info=listener_response.version_info,
+ )
+ nack.error_detail.message = "this test says no"
+ await call.write(nack)
+
+ # The in-flight `set_config` fails with the rejection.
+ with self.assertRaises(ConfigRejected) as context:
+ await set_config_task
+ self.assertIn("this test says no", context.exception.envoy_message)
+
+ async def test_unsupported_envoy_version_is_fatal(self):
+ # An Envoy older than the version we require is also fatal, so a
+ # version mismatch crashes the process rather than only logging.
+ node = base_pb2.Node()
+ node.user_agent_build_version.version.major_number = 1
+ node.user_agent_build_version.version.minor_number = 0
+ node.user_agent_build_version.version.patch = 0
+
+ call = self._client.StreamAggregatedResources()
+ await call.write(
+ discovery_pb2.DiscoveryRequest(node=node, type_url=TYPE_CLUSTER)
+ )
+
+ fatal_error = await self._wait_for_fatal_error()
+ self.assertNotIsInstance(fatal_error, ConfigRejected)
+ self.assertIn("is less than the minimum required", str(fatal_error))
+
+ async def test_listener_requested_first_is_served_after_cluster(self):
+ # Envoy requests CDS and LDS independently and may ask for the
+ # listeners first, but it rejects a listener that routes to a
+ # cluster it doesn't have yet. The server must therefore hold
+ # back every response until both have been requested, then send
+ # clusters before listeners regardless of request order.
+ node = _envoy_node()
+ call = self._client.StreamAggregatedResources()
+
+ # The client asks for the listeners first. The server must NOT
+ # answer yet: it hasn't been asked for the clusters.
+ await call.write(
+ discovery_pb2.DiscoveryRequest(node=node, type_url=TYPE_LISTENER)
+ )
+ response_task = asyncio.create_task(call.read())
+ await asyncio.sleep(0.5)
+ self.assertFalse(response_task.done())
+
+ # Once the client also asks for the clusters, the server answers
+ # both, with the clusters first even though the listeners were
+ # requested first.
+ await call.write(
+ discovery_pb2.DiscoveryRequest(node=node, type_url=TYPE_CLUSTER)
+ )
+ first = await response_task
+ self.assertEqual(first.type_url, TYPE_CLUSTER)
+ second = await call.read()
+ self.assertEqual(second.type_url, TYPE_LISTENER)
+
+ async def test_listener_not_served_ahead_of_new_clusters(self):
+ # A config update can add a cluster that a new listener routes
+ # to. Envoy rejects a listener whose cluster it doesn't have yet
+ # ("unknown cluster ..."), so the new listener must not be served
+ # until the client has been sent the new clusters — even when the
+ # client happens to re-subscribe to listeners before clusters.
+ node = _envoy_node()
+ call = self._client.StreamAggregatedResources()
+
+ # Bring the client up to date at the initial version.
+ await call.write(
+ discovery_pb2.DiscoveryRequest(node=node, type_url=TYPE_CLUSTER)
+ )
+ cluster_response = await call.read()
+ await call.write(
+ discovery_pb2.DiscoveryRequest(node=node, type_url=TYPE_LISTENER)
+ )
+ listener_response = await call.read()
+
+ # A config update adds a cluster (and a listener routing to it).
+ set_config_task = asyncio.create_task(
+ self._servicer.set_config(
+ clusters=[cluster_pb2.Cluster(name="new_cluster")],
+ listeners=[listener_pb2.Listener(name="new_listener")],
+ )
+ )
+
+ # The client re-subscribes to listeners first (acking the old
+ # version). The server must NOT send the new listener yet: it
+ # hasn't sent this client the new clusters.
+ await call.write(
+ discovery_pb2.DiscoveryRequest(
+ node=node,
+ type_url=TYPE_LISTENER,
+ version_info=listener_response.version_info,
+ )
+ )
+ response_task = asyncio.create_task(call.read())
+ await asyncio.sleep(0.5)
+ self.assertFalse(response_task.done())
+
+ # Once the client re-subscribes to clusters, the server sends the
+ # new clusters first, then releases the new listener.
+ await call.write(
+ discovery_pb2.DiscoveryRequest(
+ node=node,
+ type_url=TYPE_CLUSTER,
+ version_info=cluster_response.version_info,
+ )
+ )
+ first = await response_task
+ self.assertEqual(first.type_url, TYPE_CLUSTER)
+ second = await call.read()
+ self.assertEqual(second.type_url, TYPE_LISTENER)
+
+ # Acknowledge both so the in-flight `set_config` can complete.
+ await call.write(
+ discovery_pb2.DiscoveryRequest(
+ node=node,
+ type_url=TYPE_CLUSTER,
+ version_info=first.version_info,
+ )
+ )
+ await call.write(
+ discovery_pb2.DiscoveryRequest(
+ node=node,
+ type_url=TYPE_LISTENER,
+ version_info=second.version_info,
+ )
+ )
+ await set_config_task
+
+ async def _wait_for_fatal_error(self) -> ServerError:
+ # No timeout here: a hang is caught by Bazel's test timeout, and
+ # an explicit timeout would only add flakes on slow machines.
+ while self._servicer.fatal_error is None:
+ await asyncio.sleep(0.01)
+ return self._servicer.fatal_error
+
if __name__ == '__main__':
unittest.main()
diff --git a/tests/reboot/state_manager_tests.py b/tests/reboot/state_manager_tests.py
index 1ff9b83d..fb7459a8 100644
--- a/tests/reboot/state_manager_tests.py
+++ b/tests/reboot/state_manager_tests.py
@@ -12,6 +12,7 @@
from rbt.v1alpha1.errors_pb2 import (
StateAlreadyConstructed,
StateNotConstructed,
+ TransactionShouldRetryWithoutBackoff,
Unavailable,
)
from reboot.aio.aborted import SystemAborted
@@ -689,9 +690,10 @@ async def test_uuid7_transaction_id(self):
# And the transaction should not be stored.
self.assertFalse(transaction._stored)
- async def test_stale_uuid7_transaction_id_raises_unavailable(self):
+ async def test_stale_uuid7_transaction_id_retries_without_backoff(self):
"""Test that a UUIDv7 with a timestamp older than the recovery
- timestamp raises UNAVAILABLE.
+ timestamp aborts with `TransactionShouldRetryWithoutBackoff`
+ (which is retried like UNAVAILABLE).
"""
# Server recovered at time 5000.
self.state_manager._recovery_timestamp_ms = 5000
@@ -713,7 +715,11 @@ async def test_stale_uuid7_transaction_id_raises_unavailable(self):
):
pass
- self.assertEqual(type(aborted.exception.error), Unavailable)
+ self.assertEqual(
+ type(aborted.exception.error),
+ TransactionShouldRetryWithoutBackoff,
+ )
+ # It is retried like UNAVAILABLE.
self.assertEqual(aborted.exception.code, grpc.StatusCode.UNAVAILABLE)
assert aborted.exception.message is not None
self.assertIn("retry required", aborted.exception.message)