Python SDK¶
curupira-sdk (import curupira_sdk) turns plain Python functions into Curupira
triggers. It is stdlib-only by default; Pydantic support is optional. The
GitHub connector is written with it.
Install¶
Declare curupira-sdk as a dependency of your package and expose a console script, so
uvx can run it directly.
A minimal trigger¶
from dataclasses import dataclass
from curupira_sdk import Curupira
app = Curupira(name="acme-issues", version="0.1.0")
@dataclass
class Issue:
number: int
title: str
url: str
@app.trigger # (1)!
async def github_issues(
repo: str, # (2)!
labels: list[str] | None = None,
state: str = "open",
) -> Issue | None:
"""Return the next issue, or None when nothing was found."""
return None # (3)!
def main() -> None:
app.run() # (4)!
if __name__ == "__main__":
main()
- The local trigger name is the function name in kebab-case:
github-issues. Use@app.trigger(alias="issues")to choose another name. - Parameters are the automation's settings. Required, optional, and default values come from the signature and type hints.
- Return one object, or
Nonewhen nothing was found. - Speaks the protocol on stdin and stdout. Log to stderr, never to stdout.
Use it from settings¶
Register the package under an alias, then put the trigger's parameters next to the core automation fields:
[plugins.acme]
type = "uvx"
args = ["--from", "acme-issues==0.1.0", "acme-issues"]
[automations.resolve-ready-issues]
trigger_type = "acme/github-issues"
repository = "api" # core: the checkout alias
repo = "acme/api" # trigger parameter
labels = ["bug"]
state = "open"
prompt = """
Resolve issue ${number}: ${title}
${url}
"""
trigger_type, repository, prompt, profile, and checkout are core fields. Do not
use them as parameter names; use repo or another name for forge ids. The prompt can use
exactly the fields your trigger returns: number, title, and url here, because
Issue declares them.
Rules¶
- Return one object or
None.Nonemeans nothing was found. Lists, tuples, sets, and iterators are rejected, including a hand-built list of pairs. - An empty mapping is an item with no fields, not "nothing found".
- Sync
defandasync defshare the same decorator. - The host does not pass
previeworlimitinto the function. - Mappings, dataclasses, and TypedDicts work out of the box. Pydantic v2 models need
curupira-sdk[pydantic]. - The SDK does not add
id,title,url, or any other field. What you return is what the prompt gets. - The host owns polling, concurrency, Git, worktrees, and coding agents. The SDK never schedules or retries work.
How a return value becomes fields¶
The SDK flattens the returned object into { "key", "value" } string pairs before it
reaches the host. Top-level keys keep their declaration or insertion order.
| Python value | Field value |
|---|---|
str |
the same string, without extra quotes |
bool |
true or false |
int |
decimal text (1 and 1.0 stay distinct) |
float |
the same text json.dumps would emit |
None |
null (a field value, not "nothing found") |
| nested mapping, list, tuple, dataclass, or model | compact JSON (sort_keys=True, no extra spaces) |
These raise SerializationError, which reaches the host as a JSON-RPC error, not as
"nothing found":
- a top-level list, tuple, set, generator, or other collection
- a top-level string, number, or boolean
- a key that is not a string (keys are not coerced with
str) - an empty key, or a key containing
}or${ - a duplicate key
- a non-finite float,
bytes,set,datetime, or any other value that cannot be serialized
describe may list prompt_fields taken from the declared return type. That list is a
hint for editors and docs. It does not make any key required.
Ship it¶
- Add a
curupira-extension.tomlmanifest and support--curupira-print-manifestsocurupira plugins listcan describe the package. See Manifest. - Test with
curupira validateandcurupira run --dry-runagainst a local path:args = ["--from", "/path/to/your/package", "acme-issues"].
Examples in the repository:
packages/curupira-plugins-py/src/curupira_plugins/__main__.py: the GitHub connector, a complete SDK example.packages/curupira-cli/tests/fixtures/python-ext/: a minimal protocol fixture without the SDK.