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
|
+
]
|
trigora_client/client.py
ADDED
|
@@ -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 @@
|
|
|
1
|
+
trigora_client
|