Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions .github/workflows/sdk-readme.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
name: SDK README Guard

on:
push:
branches: [main]
paths:
- 'sdk/**/README.md'
- 'sdk/README.md'
- 'sdk/packages/rust/iii/examples/readme_hello.rs'
- 'sdk/packages/rust/iii/src/**'
- 'sdk/packages/python/iii/src/**'
- 'sdk/fixtures/readme-guard/**'
- 'scripts/readme-guard/**'
- '.github/workflows/sdk-readme.yml'
pull_request:
branches: [main]
paths:
- 'sdk/**/README.md'
- 'sdk/README.md'
- 'sdk/packages/rust/iii/examples/readme_hello.rs'
- 'sdk/packages/rust/iii/src/**'
- 'sdk/packages/python/iii/src/**'
- 'sdk/fixtures/readme-guard/**'
- 'scripts/readme-guard/**'
- '.github/workflows/sdk-readme.yml'
Comment on lines +6 to +25
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Path filters omit Node/root SDK sources that the guard probes.

The final guard step probes sdk/packages/node/iii/README.md and sdk/README.md, but the triggers do not include sdk/packages/node/iii/src/** or anything that would catch Node API-surface churn. If a Node public API renames, the Node README could regress without this workflow running. Either narrow the guard targets (see sibling comment) or expand the path filters to match what you're actually checking.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/sdk-readme.yml around lines 6 - 25, Update the path
filters in .github/workflows/sdk-readme.yml so the workflow actually triggers
when Node SDK sources change: add the Node package paths (for example include
'sdk/packages/node/iii/src/**' and 'sdk/packages/node/iii/README.md' or broader
'sdk/packages/node/**') to both the push and pull_request path lists so the
guard step that probes sdk/packages/node/iii/README.md and sdk/README.md runs
when Node API-source files are modified.

workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

# This workflow runs in warn-only mode so it can ship alongside the first round
# of README fixes without blocking merges. After the follow-up PR flips
# `continue-on-error` to `false`, any new README drift will fail CI.
jobs:
rust-readme:
name: Rust — cargo check --example readme_hello
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v4

- uses: dtolnay/rust-toolchain@stable

- uses: Swatinem/rust-cache@v2

- name: Compile the canonical Rust Hello World
run: cargo check -p iii-sdk --example readme_hello

python-readme:
name: Python — attribute-probe against real `iii.III`
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Install guard deps + editable SDK
run: |
python -m pip install --upgrade pip
python -m pip install markdown-it-py
python -m pip install -e sdk/packages/python/iii

- name: Verify golden-good fixture passes
run: python scripts/readme-guard/check_python_readme.py sdk/fixtures/readme-guard/golden-good-python.md

- name: Verify golden-bad fixture fails (meta-test)
# The guard *must* exit non-zero on this fixture. If it exits 0, the
# guard is silently broken and would let real bugs ship.
run: |
set +e
python scripts/readme-guard/check_python_readme.py sdk/fixtures/readme-guard/golden-bad-python.md
rc=$?
set -e
if [ "$rc" = "0" ]; then
echo "::error::Guard passed the golden-bad fixture — it is silently broken."
exit 1
fi
echo "Guard correctly rejected golden-bad (exit $rc)."

- name: Run guard against real READMEs
run: |
python scripts/readme-guard/check_python_readme.py \
sdk/packages/python/iii/README.md \
sdk/packages/node/iii/README.md \
sdk/README.md
Comment on lines +84 to +89
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Probing the Node README with the Python attribute probe is a no-op.

check_python_readme.py parses Python fenced blocks and checks iii.<attr>(...) against iii.iii.III. The Node README contains TypeScript/JS code blocks, so this step will almost certainly find nothing and exit 0 regardless of drift — it provides false confidence. Either drop sdk/packages/node/iii/README.md from this invocation, or add a separate Node guard (e.g., a TS type-check of the Node Hello World) and run that against the Node README.

📝 Minimal fix
       - name: Run guard against real READMEs
         run: |
           python scripts/readme-guard/check_python_readme.py \
             sdk/packages/python/iii/README.md \
-            sdk/packages/node/iii/README.md \
             sdk/README.md
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Run guard against real READMEs
run: |
python scripts/readme-guard/check_python_readme.py \
sdk/packages/python/iii/README.md \
sdk/packages/node/iii/README.md \
sdk/README.md
- name: Run guard against real READMEs
run: |
python scripts/readme-guard/check_python_readme.py \
sdk/packages/python/iii/README.md \
sdk/README.md
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/sdk-readme.yml around lines 84 - 89, The workflow step
invoking check_python_readme.py is incorrectly being run against the Node README
(sdk/packages/node/iii/README.md), which is a no-op because
check_python_readme.py parses Python fenced blocks; remove
sdk/packages/node/iii/README.md from the arguments to the check_python_readme.py
invocation OR add a separate Node-specific guard: create/run a Node/TS checker
(e.g., check_node_readme.js or a tsc/ts-node type-check command) against
sdk/packages/node/iii/README.md and update the workflow step to call that
checker instead of passing the Node README into check_python_readme.py.

135 changes: 135 additions & 0 deletions scripts/readme-guard/check_python_readme.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
#!/usr/bin/env python3
"""Verify Python README code blocks use only real `III` attributes.

Catches the class of bug where a README teaches `iii.connect()` while the
actual class exposes only `connect_async()`. Compile-only checks (py_compile,
mypy without type stubs) pass such code because `iii.connect()` is valid
Python syntax — it AttributeErrors only at runtime.

Usage:
python check_python_readme.py README.md [README.md ...]

Exit code 0 = all snippets reference real attributes, 1 = at least one
snippet calls a method that does not exist on `iii.III`.

The script uses `markdown-it-py` to parse code fences correctly (no regex)
and `ast` + `inspect` to resolve attribute references against the real
class.
"""

from __future__ import annotations

import ast
import inspect
import sys
from pathlib import Path

try:
from markdown_it import MarkdownIt
except ImportError: # pragma: no cover - bootstrap guidance
sys.stderr.write(
"check_python_readme: markdown-it-py is required. "
"Install with `pip install markdown-it-py`.\n"
)
sys.exit(2)


def extract_python_fences(md_text: str) -> list[tuple[int, str]]:
"""Return (line_number, source) for each ```python fenced block."""
md = MarkdownIt("commonmark")
tokens = md.parse(md_text)
fences: list[tuple[int, str]] = []
for tok in tokens:
if tok.type != "fence":
continue
info = (tok.info or "").strip().lower()
# Accept `python`, `py`, or info strings that start with `python`
# (e.g., `python3`, `python,ignore`).
if info == "python" or info == "py" or info.startswith("python"):
line = (tok.map[0] + 1) if tok.map else 0
fences.append((line, tok.content))
return fences


def iii_class_attrs() -> set[str]:
"""Return the set of public attribute names available on `iii.III`."""
from iii.iii import III # noqa: E402 - runtime import

return {name for name, _ in inspect.getmembers(III) if not name.startswith("_")}


def attr_calls_on_iii_var(source: str) -> list[tuple[int, str]]:
"""Find method calls of the form `iii.<name>(...)` in a snippet.

Returns a list of (lineno, attr_name). Heuristic: we treat any bare
identifier named ``iii`` as an `III` instance. The Hello World pattern
`iii = register_worker(...)` makes this reliable.
"""
try:
tree = ast.parse(source)
except SyntaxError:
# Syntax errors are a separate class of problem; `py_compile` already
# catches them. Skip attribute checking when parse fails.
return []
Comment on lines +68 to +73
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether syntax errors are suppressed here and whether another workflow step explicitly compiles README fences.
rg -n -C4 'except SyntaxError|return \[\]' scripts/readme-guard/check_python_readme.py
rg -n -C3 'py_compile|compileall|check_python_readme' --glob '*.yml' --glob '*.yaml'

Repository: iii-hq/iii

Length of output: 493


🏁 Script executed:

#!/bin/bash
# Search for py_compile and compileall usage throughout the codebase
echo "=== Searching for py_compile usage ==="
rg -r 'py_compile' --no-heading

echo ""
echo "=== Searching for compileall usage ==="
rg -r 'compileall' --no-heading

echo ""
echo "=== Searching for check_python_readme invocation in all files ==="
rg -r 'check_python_readme' --no-heading

Repository: iii-hq/iii

Length of output: 1407


🏁 Script executed:

#!/bin/bash
# View the full context of the check_file function and lines 93-94
echo "=== Full check_python_readme.py file ==="
wc -l scripts/readme-guard/check_python_readme.py
echo ""
cat -n scripts/readme-guard/check_python_readme.py

Repository: iii-hq/iii

Length of output: 5648


🏁 Script executed:

#!/bin/bash
# Check for workflow files and their content
echo "=== Workflow files ==="
fd -e yml -e yaml | head -20

echo ""
echo "=== Check for any CI/workflow references to readme or syntax ==="
rg -l 'readme|syntax' --glob '*.yml' --glob '*.yaml' --glob '.github'

Repository: iii-hq/iii

Length of output: 912


🏁 Script executed:

#!/bin/bash
cat -n .github/workflows/sdk-readme.yml

Repository: iii-hq/iii

Length of output: 3619


🏁 Script executed:

#!/bin/bash
cat -n sdk/fixtures/readme-guard/golden-bad-python.md

Repository: iii-hq/iii

Length of output: 655


Fail closed on Python syntax errors in README fences.

Right now a malformed Python snippet returns [] instead of reporting an error, allowing this guard to exit successfully when it should fail. The comment claiming py_compile already catches syntax errors is incorrect—the workflow contains no such compile step.

Lines 93–94 in check_file also need wrapping to catch and report SyntaxError from attr_calls_on_iii_var instead of silently skipping syntax-invalid blocks.

Proposed fix
-    try:
-        tree = ast.parse(source)
-    except SyntaxError:
-        # Syntax errors are a separate class of problem; `py_compile` already
-        # catches them. Skip attribute checking when parse fails.
-        return []
+    tree = ast.parse(source)
@@
     md_text = path.read_text()
     for block_line, source in extract_python_fences(md_text):
-        for call_lineno, attr in attr_calls_on_iii_var(source):
+        try:
+            calls = attr_calls_on_iii_var(source)
+        except SyntaxError as exc:
+            errors.append(
+                f"{path}:{block_line + (exc.lineno or 1) - 1}: "
+                f"Python README snippet has invalid syntax: {exc.msg}"
+            )
+            continue
+
+        for call_lineno, attr in calls:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/readme-guard/check_python_readme.py` around lines 68 - 73, The parser
currently swallows SyntaxError by returning [] after ast.parse(source) fails;
change this so a SyntaxError in README Python fences is treated as a test
failure — raise or append a descriptive error (include the snippet context)
instead of returning an empty list in the try/except around ast.parse in the
function that parses fences; additionally, wrap the calls to
attr_calls_on_iii_var inside check_file (the block around the lines currently
invoking attr_calls_on_iii_var) in a try/except that catches SyntaxError and
reports it (rather than skipping), so malformed Python snippets are reported as
errors with clear context.


calls: list[tuple[int, str]] = []
for node in ast.walk(tree):
if not isinstance(node, ast.Call):
continue
fn = node.func
if not isinstance(fn, ast.Attribute):
continue
if not isinstance(fn.value, ast.Name):
continue
if fn.value.id != "iii":
continue
calls.append((node.lineno, fn.attr))
return calls


def check_file(path: Path, allowed_attrs: set[str]) -> list[str]:
errors: list[str] = []
md_text = path.read_text()
for block_line, source in extract_python_fences(md_text):
for call_lineno, attr in attr_calls_on_iii_var(source):
if attr in allowed_attrs:
continue
# Allow the dynamic `attr` sentinel names we know about:
# e.g., handler closures aren't on `iii`.
errors.append(
f"{path}:{block_line + call_lineno - 1}: "
f"iii.{attr}() is called but `iii.III` has no public "
f"attribute named `{attr}`. "
f"Did the README outlive a refactor?"
)
return errors


def main() -> int:
if len(sys.argv) < 2:
sys.stderr.write(f"usage: {sys.argv[0]} README.md [README.md ...]\n")
return 2

try:
allowed = iii_class_attrs()
except Exception as exc: # pragma: no cover - import failure path
sys.stderr.write(
f"check_python_readme: failed to import `iii.III` to probe its "
f"attributes: {exc}\n"
"Make sure the iii-sdk package is installed (editable mode is "
"fine) before running this check.\n"
)
return 2

all_errors: list[str] = []
for arg in sys.argv[1:]:
all_errors.extend(check_file(Path(arg), allowed))

for msg in all_errors:
sys.stderr.write(msg + "\n")

return 0 if not all_errors else 1


if __name__ == "__main__":
sys.exit(main())
69 changes: 44 additions & 25 deletions sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ These are iii official SDKs for Node, Python, and Rust. See the [engine README](
| [`iii-sdk`](https://pypi.org/project/iii-sdk/) | Python | `pip install iii-sdk` | [README](./packages/python/iii/README.md) |
| [`iii-sdk`](https://crates.io/crates/iii-sdk) | Rust | Add to `Cargo.toml` | [README](./packages/rust/iii/README.md) |

## Prerequisites

All three SDKs connect to a running iii engine over WebSocket. Before running
any snippet below:

1. Install the engine: `curl -fsSL https://install.iii.dev/iii/main/install.sh | sh`
2. Start the engine: `iii --config config.yaml` (default URL: `ws://localhost:49134`)

See the [quickstart](https://iii.dev/docs/quickstart) to scaffold a full project.

## Hello World

### Node.js
Expand Down Expand Up @@ -47,58 +57,67 @@ iii = register_worker("ws://localhost:49134")
def greet(data):
return {"message": f"Hello, {data['name']}!"}

iii.register_function({"id": "greet"}, greet)
iii.register_function("greet", greet)

iii.register_trigger({
"type": "http",
"function_id": "greet",
"config": {"api_path": "/greet", "http_method": "POST"}
"config": {"api_path": "/greet", "http_method": "POST"},
})

result = iii.trigger({"function_id": "greet", "payload": {"name": "world"}})
```

### Rust

```rust
use iii_sdk::{register_worker, InitOptions, TriggerRequest, RegisterFunctionMessage, RegisterTriggerInput};
use serde_json::json;
```rust,no_run
use iii_sdk::builtin_triggers::{HttpMethod, HttpTriggerConfig};
use iii_sdk::{IIITrigger, InitOptions, RegisterFunction, TriggerRequest, register_worker};
use serde_json::{Value, json};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let iii = register_worker("ws://127.0.0.1:49134", InitOptions::default())?;
let iii = register_worker("ws://localhost:49134", InitOptions::default());

iii.register_function(RegisterFunctionMessage::with_id("greet".into()), |input| async move {
iii.register_function(RegisterFunction::new_async("greet", |input: Value| async move {
let name = input.get("name").and_then(|v| v.as_str()).unwrap_or("world");
Ok(json!({ "message": format!("Hello, {name}!") }))
});
Ok::<Value, iii_sdk::IIIError>(json!({ "message": format!("Hello, {name}!") }))
}));

iii.register_trigger(RegisterTriggerInput { trigger_type: "http".into(), function_id: "greet".into(), config: json!({
"api_path": "/greet",
"http_method": "POST"
}) })?;
iii.register_trigger(
IIITrigger::Http(HttpTriggerConfig::new("/greet").method(HttpMethod::Post))
.for_function("greet"),
)?;

let result: serde_json::Value = iii
let result: Value = iii
.trigger(TriggerRequest::new("greet", json!({ "name": "world" })))
.await?;

iii.shutdown();
Ok(())
}
```

## API

| Operation | Node.js | Python | Rust | Description |
| ------------------------ | ---------------------------------------------------- | ------------------------------------------- | -------------------------------------------- | ------------------------------------------------------ |
| Initialize | `registerWorker(url)` | `register_worker(url, options?)` | `register_worker(url, options)` | Create an SDK instance and auto-connect |
| Register function | `iii.registerFunction(id, handler, options?)` | `iii.register_function(id, handler)` | `iii.register_function(id, \|input\| ...)` | Register a function that can be invoked by name |
| Register trigger | `iii.registerTrigger({ type, function_id, config })` | `iii.register_trigger({"type": ..., "function_id": ..., "config": ...})` | `iii.register_trigger(type, fn_id, config)?` | Bind a trigger (HTTP, cron, queue, etc.) to a function |
| Invoke (await) | `await iii.trigger({ function_id, payload })` | `await iii.trigger({"function_id": id, "payload": data})` | `iii.trigger(TriggerRequest::new(id, data)).await?` | Invoke a function and wait for the result |
| Invoke (fire-and-forget) | `iii.trigger({ function_id, payload, action: TriggerAction.Void() })` | Same | Same | Invoke without waiting |
The Rust snippet is kept in sync with `sdk/packages/rust/iii/examples/readme_hello.rs`
and verified by CI.

`registerWorker()` / `register_worker()` creates an SDK instance and auto-connects to the engine. It handles WebSocket communication, automatic reconnection, and OpenTelemetry instrumentation. All three SDKs expose the same API surface — register functions and triggers, then invoke them.
## API

> `call`, `callVoid`, `triggerVoid` (and Python/Rust equivalents) have been removed. Use `trigger()` for all invocations. For fire-and-forget, use `trigger({ function_id, payload, action: TriggerAction.Void() })`.
| Operation | Node.js | Python | Rust | Description |
| ------------------------ | ------------------------------------------------------------- | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
| Initialize | `registerWorker(url)` | `register_worker(url, options?)` | `register_worker(url, options)` | Create an SDK instance and auto-connect |
| Register function | `iii.registerFunction(id, handler, options?)` | `iii.register_function(id, handler)` | `iii.register_function(RegisterFunction::new_async(id, handler))` | Register a function that can be invoked by name |
| Register trigger | `iii.registerTrigger({ type, function_id, config })` | `iii.register_trigger({"type": ..., "function_id": ..., "config": ...})` | `iii.register_trigger(IIITrigger::Http(...).for_function(id))?` | Bind a trigger (HTTP, cron, queue, etc.) to a function |
| Invoke (await) | `await iii.trigger({ function_id, payload })` | `await iii.trigger({"function_id": id, "payload": data})` | `iii.trigger(TriggerRequest::new(id, payload)).await?` | Invoke a function and wait for the result |
| Invoke (fire-and-forget) | `iii.trigger({ function_id, payload, action: TriggerAction.Void() })` | `iii.trigger({"function_id": id, "payload": data, "action": TriggerAction.Void()})` | `iii.trigger(TriggerRequest::new(id, payload).with_action(TriggerAction::Void)).await?` | Invoke without waiting |
Comment on lines +111 to +112
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm whether Python `III.trigger` is synchronous and locate README guidance that still says to await it.
rg -n -C3 '^\s*(async\s+)?def\s+trigger\s*\(' --glob 'iii.py'
rg -n -C2 'await\s+iii\.trigger|Node/Python|TriggerAction\.Void\(\)' --glob 'README.md'

Repository: iii-hq/iii

Length of output: 7795


Fix Python trigger() invocation and removed-method note in cross-SDK API table

Line 111 incorrectly shows await iii.trigger(...) for Python, but Python's trigger() is synchronous. Additionally, line 120's removed-method note uses JS object syntax for "Node/Python" when Python requires dict syntax.

Proposed fix
-| Invoke (await)           | `await iii.trigger({ function_id, payload })`                 | `await iii.trigger({"function_id": id, "payload": data})`                        | `iii.trigger(TriggerRequest::new(id, payload)).await?`                                         | Invoke a function and wait for the result              |
+| Invoke (wait for result) | `await iii.trigger({ function_id, payload })`                 | `iii.trigger({"function_id": id, "payload": data})`                              | `iii.trigger(TriggerRequest::new(id, payload)).await?`                                         | Invoke a function and wait for the result              |
@@
-> `call`, `callVoid`, `triggerVoid` (and Python/Rust equivalents) have been removed. Use `trigger()` for all invocations. For fire-and-forget, use `trigger({ function_id, payload, action: TriggerAction.Void() })` (Node/Python) or `TriggerRequest::new(id, payload).with_action(TriggerAction::Void)` (Rust).
+> `call`, `callVoid`, `triggerVoid` (and Python/Rust equivalents) have been removed. Use `trigger()` for all invocations. For fire-and-forget, use `iii.trigger({ function_id, payload, action: TriggerAction.Void() })` in Node, `iii.trigger({"function_id": id, "payload": data, "action": TriggerAction.Void()})` in Python, or `TriggerRequest::new(id, payload).with_action(TriggerAction::Void)` in Rust.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/README.md` around lines 111 - 112, Update the cross-SDK API table to
reflect that Python's trigger() is synchronous (remove the leading "await" in
the Python column for the Invoke rows) and change any "Node/Python" example that
currently uses JS object literal syntax to use Python dict syntax instead (e.g.,
use {"function_id": ..., "payload": ...} style for Python rather than JS object
notation); specifically edit the entries referencing iii.trigger(),
TriggerAction.Void(), and TriggerRequest examples so Python examples omit await
and use Python dicts where Node/Python is indicated.


`registerWorker()` / `register_worker()` creates an SDK instance and auto-connects
to the engine. It handles WebSocket communication, automatic reconnection, and
OpenTelemetry instrumentation. Rust's `register_worker` returns `III` directly
(not `Result`); Node/Python mirror this. All three SDKs expose the same concept
surface — register functions and triggers, then invoke them.

> `call`, `callVoid`, `triggerVoid` (and Python/Rust equivalents) have been removed. Use `trigger()` for all invocations. For fire-and-forget, use `trigger({ function_id, payload, action: TriggerAction.Void() })` (Node/Python) or `TriggerRequest::new(id, payload).with_action(TriggerAction::Void)` (Rust).

For language-specific details (modules, streams, OpenTelemetry), see the per-SDK READMEs linked in the table above.

Expand Down
17 changes: 17 additions & 0 deletions sdk/fixtures/readme-guard/golden-bad-python.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Golden bad (Python)

This fixture MUST make the guard exit 1 — it contains the exact bug the
guard was built to catch: `iii.connect()` is valid Python syntax but the
`iii.III` class does not expose a public `connect` method.

```python
from iii import register_worker

iii = register_worker("ws://localhost:49134")

def greet(data):
return {"message": f"Hello, {data['name']}!"}

iii.register_function("greet", greet)
iii.connect() # <-- intentional bug: no such method
```
23 changes: 23 additions & 0 deletions sdk/fixtures/readme-guard/golden-good-python.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Golden good (Python)

This fixture exists so CI can prove the guard is *working* — not silently
letting everything pass. The guard must exit 0 on this file.

```python
from iii import register_worker

iii = register_worker("ws://localhost:49134")

def greet(data):
return {"message": f"Hello, {data['name']}!"}

iii.register_function("greet", greet)

iii.register_trigger({
"type": "http",
"function_id": "greet",
"config": {"api_path": "/greet", "http_method": "POST"},
})

result = iii.trigger({"function_id": "greet", "payload": {"name": "world"}})
```
Loading
Loading