trigora-client 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ from .client import (
4
+ DEFAULT_CLOUD_API_URL,
5
+ DEFAULT_RUNTIME_URL,
6
+ Client,
7
+ ExecutionHandle,
8
+ TrigoraError,
9
+ start,
10
+ )
11
+
12
+ __all__ = [
13
+ "DEFAULT_CLOUD_API_URL",
14
+ "DEFAULT_RUNTIME_URL",
15
+ "Client",
16
+ "ExecutionHandle",
17
+ "TrigoraError",
18
+ "start",
19
+ ]
@@ -0,0 +1,225 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import time
6
+ import urllib.error
7
+ import urllib.parse
8
+ import urllib.request
9
+ from typing import Any
10
+
11
+ DEFAULT_RUNTIME_URL = "http://127.0.0.1:3477"
12
+ DEFAULT_CLOUD_API_URL = "https://api.trigora.dev"
13
+ _PROJECT_HEADER = "X-Trigora-Project-Id"
14
+
15
+
16
+ class TrigoraError(RuntimeError):
17
+ def __init__(self, message: str, *, status: int = 0, code: str | None = None) -> None:
18
+ super().__init__(message)
19
+ self.status = status
20
+ self.code = code
21
+
22
+
23
+ def _present(value: str | None) -> str | None:
24
+ if value is None:
25
+ return None
26
+ trimmed = value.strip()
27
+ return trimmed or None
28
+
29
+
30
+ def _env_url(name: str, fallback: str) -> str:
31
+ return (_present(os.environ.get(name)) or fallback).rstrip("/")
32
+
33
+
34
+ def _resolve(url: str | None, token: str | None, remote: bool) -> tuple[str, str | None, bool]:
35
+ explicit_token = _present(token)
36
+ if url:
37
+ return url.rstrip("/"), explicit_token, False
38
+ if remote:
39
+ resolved = explicit_token or _present(os.environ.get("TRIGORA_TOKEN"))
40
+ if not resolved:
41
+ raise TrigoraError("TRIGORA_TOKEN is not set.")
42
+ return _env_url("TRIGORA_API_BASE_URL", DEFAULT_CLOUD_API_URL), resolved, True
43
+ return _env_url("TRIGORA_RUNTIME_URL", DEFAULT_RUNTIME_URL), explicit_token, False
44
+
45
+
46
+ class Client:
47
+ def __init__(
48
+ self,
49
+ *,
50
+ url: str | None = None,
51
+ remote: bool = False,
52
+ token: str | None = None,
53
+ project_id: str | None = None,
54
+ ) -> None:
55
+ resolved_url, resolved_token, cloud = _resolve(url, token, remote)
56
+ self.url = resolved_url
57
+ self.token = resolved_token
58
+ self.cloud = cloud
59
+ self.project_id = project_id
60
+ self.projects = Projects(self)
61
+ self.programs = Programs(self)
62
+ self.executions = Executions(self)
63
+
64
+ def whoami(self) -> dict[str, Any]:
65
+ return self._request("GET", "/v1/whoami")
66
+
67
+ def deploy(self, body: dict[str, Any]) -> dict[str, Any]:
68
+ return self._request("POST", "/v1/programs/deploy", body)
69
+
70
+ def _request(
71
+ self,
72
+ method: str,
73
+ path: str,
74
+ payload: dict[str, Any] | None = None,
75
+ query: dict[str, str] | None = None,
76
+ ) -> Any:
77
+ target = f"{self.url}{path}"
78
+ if query:
79
+ target = f"{target}?{urllib.parse.urlencode(query)}"
80
+ data = None if payload is None else json.dumps(payload).encode("utf-8")
81
+ headers = {"Accept": "application/json"}
82
+ if payload is not None:
83
+ headers["Content-Type"] = "application/json"
84
+ if self.token:
85
+ headers["Authorization"] = f"Bearer {self.token}"
86
+ if self.project_id:
87
+ headers[_PROJECT_HEADER] = self.project_id
88
+ request = urllib.request.Request(target, data=data, method=method, headers=headers)
89
+ try:
90
+ with urllib.request.urlopen(request) as response:
91
+ body = response.read().decode("utf-8")
92
+ return json.loads(body) if body else None
93
+ except urllib.error.HTTPError as error:
94
+ raw = error.read().decode("utf-8")
95
+ message = raw or str(error)
96
+ code = None
97
+ try:
98
+ parsed = json.loads(raw)
99
+ message = parsed.get("error", {}).get("message") or message
100
+ code = parsed.get("error", {}).get("code")
101
+ except json.JSONDecodeError:
102
+ pass
103
+ raise TrigoraError(message, status=error.code or 0, code=code) from error
104
+ except urllib.error.URLError as error:
105
+ if self.cloud:
106
+ message = f"Could not reach Trigora Cloud at {target}. {error.reason}"
107
+ else:
108
+ message = (
109
+ f"Could not reach the local Trigora runtime at {target}. "
110
+ f"Is `trigora dev` running? {error.reason}"
111
+ )
112
+ raise TrigoraError(message, status=0) from error
113
+
114
+
115
+ class Projects:
116
+ def __init__(self, client: Client) -> None:
117
+ self._client = client
118
+
119
+ def list(self) -> dict[str, Any]:
120
+ return self._client._request("GET", "/v1/projects")
121
+
122
+ def create(self, body: dict[str, Any]) -> dict[str, Any]:
123
+ return self._client._request("POST", "/v1/projects", body)
124
+
125
+
126
+ class Programs:
127
+ def __init__(self, client: Client) -> None:
128
+ self._client = client
129
+
130
+ def list(self, *, limit: int | None = None, cursor: str | None = None) -> dict[str, Any]:
131
+ return self._client._request("GET", "/v1/programs", query=_page(limit, cursor))
132
+
133
+ def get(self, program_id: str) -> dict[str, Any]:
134
+ return self._client._request("GET", f"/v1/programs/{urllib.parse.quote(program_id)}")
135
+
136
+ def versions(
137
+ self, program_id: str, *, limit: int | None = None, cursor: str | None = None
138
+ ) -> dict[str, Any]:
139
+ return self._client._request(
140
+ "GET",
141
+ f"/v1/programs/{urllib.parse.quote(program_id)}/versions",
142
+ query=_page(limit, cursor),
143
+ )
144
+
145
+
146
+ class Executions:
147
+ def __init__(self, client: Client) -> None:
148
+ self._client = client
149
+
150
+ def start(self, program_id: str, input: Any | None = None) -> ExecutionHandle:
151
+ body = self._client._request(
152
+ "POST",
153
+ "/v1/executions",
154
+ {"programId": program_id, "input": {} if input is None else input},
155
+ )
156
+ return ExecutionHandle(self._client, body["execution"]["id"])
157
+
158
+ def get(self, execution_id: str) -> dict[str, Any]:
159
+ body = self._client._request("GET", f"/v1/executions/{urllib.parse.quote(execution_id)}")
160
+ return body["execution"]
161
+
162
+ def list(self, *, limit: int | None = None, cursor: str | None = None) -> dict[str, Any]:
163
+ return self._client._request("GET", "/v1/executions", query=_page(limit, cursor))
164
+
165
+ def send(self, execution_id: str, name: str, payload: Any = None) -> dict[str, Any]:
166
+ return self._client._request(
167
+ "POST",
168
+ f"/v1/executions/{urllib.parse.quote(execution_id)}/events",
169
+ {"name": name, "payload": {} if payload is None else payload},
170
+ )
171
+
172
+ def cancel(self, execution_id: str) -> dict[str, Any]:
173
+ return self._client._request(
174
+ "POST",
175
+ f"/v1/executions/{urllib.parse.quote(execution_id)}/cancel",
176
+ {},
177
+ )
178
+
179
+ def result(self, execution_id: str, *, poll_seconds: float = 0.05) -> Any:
180
+ return ExecutionHandle(self._client, execution_id).result(poll_seconds=poll_seconds)
181
+
182
+
183
+ class ExecutionHandle:
184
+ def __init__(self, client: Client, execution_id: str) -> None:
185
+ self._client = client
186
+ self.id = execution_id
187
+
188
+ def send(self, name: str, payload: Any = None) -> None:
189
+ self._client.executions.send(self.id, name, payload)
190
+
191
+ def cancel(self) -> None:
192
+ self._client.executions.cancel(self.id)
193
+
194
+ def result(self, *, poll_seconds: float = 0.05) -> Any:
195
+ while True:
196
+ execution = self._client.executions.get(self.id)
197
+ status = execution["status"]
198
+ if status == "completed":
199
+ return execution.get("result")
200
+ if status == "failed":
201
+ error = execution.get("error") or {}
202
+ raise TrigoraError(error.get("message") or "Execution failed.")
203
+ if status == "cancelled":
204
+ raise TrigoraError(f'Execution "{self.id}" was cancelled.', status=409)
205
+ time.sleep(poll_seconds)
206
+
207
+
208
+ def start(
209
+ program_id: str,
210
+ input: Any | None = None,
211
+ *,
212
+ url: str | None = None,
213
+ remote: bool = False,
214
+ token: str | None = None,
215
+ ) -> ExecutionHandle:
216
+ return Client(url=url, remote=remote, token=token).executions.start(program_id, input)
217
+
218
+
219
+ def _page(limit: int | None, cursor: str | None) -> dict[str, str] | None:
220
+ query: dict[str, str] = {}
221
+ if limit is not None:
222
+ query["limit"] = str(limit)
223
+ if cursor:
224
+ query["cursor"] = cursor
225
+ return query or None
@@ -0,0 +1,117 @@
1
+ Metadata-Version: 2.4
2
+ Name: trigora-client
3
+ Version: 1.0.0
4
+ Summary: Python client for Trigora Cloud and the local runtime
5
+ Author: Trigora
6
+ License: MIT
7
+ Project-URL: Homepage, https://trigora.dev
8
+ Project-URL: Repository, https://github.com/trigora-dev/trigora-python
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown
13
+
14
+ # trigora-client
15
+
16
+ Python client for starting and controlling Trigora durable executions.
17
+
18
+ Use the same API locally with `trigora dev` or remotely with Trigora Cloud.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ pip install trigora-client
24
+ ```
25
+
26
+ ## Quick example
27
+
28
+ ```python
29
+ from trigora_client import Client
30
+
31
+ client = Client()
32
+
33
+ run = client.executions.start("approval", {"request": "example"})
34
+ run.send("approved", "ok")
35
+ result = run.result()
36
+
37
+ print(result)
38
+ ```
39
+
40
+ ## Local and Cloud
41
+
42
+ By default, the client connects to the local runtime started by `trigora dev`.
43
+
44
+ ```python
45
+ client = Client()
46
+ ```
47
+
48
+ To connect to Trigora Cloud:
49
+
50
+ ```python
51
+ client = Client(remote=True)
52
+ ```
53
+
54
+ Cloud requires a token. You can pass one directly or set `TRIGORA_TOKEN`.
55
+
56
+ ```python
57
+ import os
58
+
59
+ client = Client(remote=True, token=os.environ["TRIGORA_TOKEN"])
60
+ ```
61
+
62
+ You can also connect to a custom endpoint:
63
+
64
+ ```python
65
+ client = Client(url="http://127.0.0.1:3477")
66
+ ```
67
+
68
+ Endpoint defaults:
69
+
70
+ - Local runtime: `TRIGORA_RUNTIME_URL` or `http://127.0.0.1:3477`
71
+ - Trigora Cloud: `TRIGORA_API_BASE_URL` or `https://api.trigora.dev`
72
+
73
+ An explicit `url` overrides the default endpoint. An explicit `token` is sent with requests to that endpoint.
74
+
75
+ ## API
76
+
77
+ Workspace and project operations:
78
+
79
+ - `whoami()`
80
+ - `projects.list()`
81
+ - `projects.create()`
82
+
83
+ Programs:
84
+
85
+ - `deploy()`
86
+ - `programs.list()`
87
+ - `programs.get()`
88
+ - `programs.versions()`
89
+
90
+ Executions:
91
+
92
+ - `executions.start(program, input)`
93
+ - `executions.list()`
94
+ - `executions.get(id)`
95
+
96
+ Execution handle:
97
+
98
+ - `run.id`
99
+ - `run.send(event, payload)`
100
+ - `run.cancel()`
101
+ - `run.result()`
102
+
103
+ `programs.list`, `programs.versions`, and `executions.list` accept optional `limit` and `cursor` arguments.
104
+
105
+ `program` is a program id string.
106
+
107
+ `event` is an event name string.
108
+
109
+ ## Learn more
110
+
111
+ - [Trigora documentation](https://trigora.dev/docs)
112
+ - [Client documentation](https://trigora.dev/docs/client)
113
+ - [Trigora Cloud](https://cloud.trigora.dev)
114
+
115
+ ## License
116
+
117
+ MIT © 2026 Trigora, Inc.
@@ -0,0 +1,6 @@
1
+ trigora_client/__init__.py,sha256=KtlJYGzLx6s1imJbybwoncY5FJshQhx8Fc3cELyS0zI,315
2
+ trigora_client/client.py,sha256=QU0_lM8oMobG5lbCv-d4olMHd5pV_MsFAIwcKL8MmPQ,8000
3
+ trigora_client-1.0.0.dist-info/METADATA,sha256=zBrWBCReTr8jPzyj2jbRKNClv7ApptR8jW5HQrZGWhg,2370
4
+ trigora_client-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
5
+ trigora_client-1.0.0.dist-info/top_level.txt,sha256=sy8v7bmk0gRttoIXirS_9rmvwPgfNbXs9DcBFrvNKsI,15
6
+ trigora_client-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ trigora_client