esoul 0.1.0__tar.gz
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.
- esoul-0.1.0/.gitignore +24 -0
- esoul-0.1.0/CHANGELOG.md +57 -0
- esoul-0.1.0/LICENSE +21 -0
- esoul-0.1.0/PKG-INFO +203 -0
- esoul-0.1.0/README.md +145 -0
- esoul-0.1.0/pyproject.toml +117 -0
- esoul-0.1.0/src/esoul/__init__.py +79 -0
- esoul-0.1.0/src/esoul/_async_client.py +134 -0
- esoul-0.1.0/src/esoul/_auth.py +289 -0
- esoul-0.1.0/src/esoul/_client.py +201 -0
- esoul-0.1.0/src/esoul/_transport.py +637 -0
- esoul-0.1.0/src/esoul/_version.py +8 -0
- esoul-0.1.0/src/esoul/exceptions.py +341 -0
- esoul-0.1.0/src/esoul/py.typed +0 -0
- esoul-0.1.0/src/esoul/resources/__init__.py +50 -0
- esoul-0.1.0/src/esoul/resources/agents.py +483 -0
- esoul-0.1.0/src/esoul/resources/dispatch.py +340 -0
- esoul-0.1.0/src/esoul/resources/drive.py +334 -0
- esoul-0.1.0/src/esoul/resources/questions.py +428 -0
- esoul-0.1.0/src/esoul/resources/workspaces.py +454 -0
esoul-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Build artifacts
|
|
2
|
+
dist/
|
|
3
|
+
build/
|
|
4
|
+
*.egg-info/
|
|
5
|
+
*.egg
|
|
6
|
+
|
|
7
|
+
# Python
|
|
8
|
+
__pycache__/
|
|
9
|
+
*.py[cod]
|
|
10
|
+
*$py.class
|
|
11
|
+
.mypy_cache/
|
|
12
|
+
.pytest_cache/
|
|
13
|
+
.ruff_cache/
|
|
14
|
+
.coverage
|
|
15
|
+
htmlcov/
|
|
16
|
+
|
|
17
|
+
# Virtual envs
|
|
18
|
+
.venv/
|
|
19
|
+
venv/
|
|
20
|
+
env/
|
|
21
|
+
|
|
22
|
+
# IDE
|
|
23
|
+
.idea/
|
|
24
|
+
.vscode/
|
esoul-0.1.0/CHANGELOG.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file. The
|
|
4
|
+
format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
|
|
5
|
+
this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] — 2026-05-17
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **`client.agents`** — SDK-driven agent_builder invocation. Primary
|
|
13
|
+
surface `agents.invoke(workspace_id, agent, input=..., images=...)`
|
|
14
|
+
where `agent` is a nodeId (uuid) or instanceName (case-insensitive).
|
|
15
|
+
Convenience layer `agents.invoke_pin(pinned_agent_id, ...)` for
|
|
16
|
+
cross-workspace pins on the caller's own account. Polling handle
|
|
17
|
+
with `wait(timeout, on_question, on_approval)` (exponential backoff
|
|
18
|
+
2s → 10s, callbacks dedup'd by questionId / summary). `agents.get`,
|
|
19
|
+
`agents.list`, `handle.cancel`.
|
|
20
|
+
- **`client.questions`** — workspace HIL queue. Programs can ask
|
|
21
|
+
questions via `questions.ask(workspace_id, "...", default_on_timeout=)`
|
|
22
|
+
which blocks until a human answers via the workspace's header drawer.
|
|
23
|
+
Also `ask_async`, `wait_for_answer`, `list`, `get`, `answer`, `cancel`.
|
|
24
|
+
- Async parity for both resources: `AsyncAgentsResource`,
|
|
25
|
+
`AsyncInvocationHandle`, `AsyncQuestionsResource`.
|
|
26
|
+
- New typed dataclasses: `InvocationStatus`, `InvocationResult`,
|
|
27
|
+
`Question`, `Answer`.
|
|
28
|
+
- New typed exceptions: `InvocationTimeout`, `InvocationError`,
|
|
29
|
+
`QuestionTimeout`, `QuestionAlreadyResolved`. Mapped from server
|
|
30
|
+
error codes `agent_not_found`, `agent_ambiguous`, `agent_invalid_type`,
|
|
31
|
+
`invocation_*`, `pinned_agent_*`, `question_*`.
|
|
32
|
+
- Initial sync `Esoul` and async `AsyncEsoul` clients.
|
|
33
|
+
- Credential auto-detection: explicit kwarg → `ESOUL_TOKEN` env →
|
|
34
|
+
`/var/run/esoul/token` (sandbox) → `~/.config/esoul/credentials` (PAT).
|
|
35
|
+
- Low-level dispatch surface: `dispatch_event`, `dispatch_batch`,
|
|
36
|
+
`read_state`, `describe`, `refresh_token`.
|
|
37
|
+
- Drive proxy resource: `drive.list_folder`, `drive.read_file`,
|
|
38
|
+
`drive.upload_file`.
|
|
39
|
+
- Typed exception hierarchy mapped from server `error.code`:
|
|
40
|
+
`AuthError`, `WorkspaceAccessDenied`, `AppNotFoundError`,
|
|
41
|
+
`EventNotRegistered`, `IdempotencyConflict`, `RateLimitError`,
|
|
42
|
+
`DriveNotConnected`, `APIError`.
|
|
43
|
+
- Auto-generated `Idempotency-Key` (UUID v4 per call), reused across
|
|
44
|
+
retries; caller can override via `idempotency_key=` kwarg.
|
|
45
|
+
- Retry-on-5xx + network-error policy with exponential backoff + jitter.
|
|
46
|
+
4xx errors don't retry. 429 respects `Retry-After`.
|
|
47
|
+
- Background token-refresh for sandbox JWTs (60s before expiry). PAT
|
|
48
|
+
tokens don't refresh.
|
|
49
|
+
- `py.typed` marker for PEP 561 / mypy compatibility.
|
|
50
|
+
|
|
51
|
+
### Not yet shipped
|
|
52
|
+
- Per-app typed resources (`spreadsheet`, `notes`, etc.) — landing as
|
|
53
|
+
codegen pipeline ships, one app at a time.
|
|
54
|
+
- Batch context manager (`with client.batch():`) for client-side id
|
|
55
|
+
pre-generation in multi-event flows.
|
|
56
|
+
- Per-event return shapes (depends on server adding `returnShape` to
|
|
57
|
+
event definitions).
|
esoul-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ExternalSoul
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
esoul-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: esoul
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the ExternalSoul platform — programmatic workspace mutation, event-sourced, audit-logged.
|
|
5
|
+
Project-URL: Homepage, https://github.com/vyomkeshj/esoul-python
|
|
6
|
+
Project-URL: Documentation, https://externalsoul.com/sdk-docs/llm-reference.md
|
|
7
|
+
Project-URL: Repository, https://github.com/vyomkeshj/esoul-python
|
|
8
|
+
Project-URL: Changelog, https://github.com/vyomkeshj/esoul-python/blob/main/CHANGELOG.md
|
|
9
|
+
Author-email: ExternalSoul <hey@vyomkeshj.com>
|
|
10
|
+
License: MIT License
|
|
11
|
+
|
|
12
|
+
Copyright (c) 2026 ExternalSoul
|
|
13
|
+
|
|
14
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
15
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
16
|
+
in the Software without restriction, including without limitation the rights
|
|
17
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
18
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
19
|
+
furnished to do so, subject to the following conditions:
|
|
20
|
+
|
|
21
|
+
The above copyright notice and this permission notice shall be included in all
|
|
22
|
+
copies or substantial portions of the Software.
|
|
23
|
+
|
|
24
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
25
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
26
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
27
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
28
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
29
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
30
|
+
SOFTWARE.
|
|
31
|
+
License-File: LICENSE
|
|
32
|
+
Keywords: automation,esoul,event-sourcing,externalsoul,kinetic,workspace
|
|
33
|
+
Classifier: Development Status :: 3 - Alpha
|
|
34
|
+
Classifier: Intended Audience :: Developers
|
|
35
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
36
|
+
Classifier: Programming Language :: Python
|
|
37
|
+
Classifier: Programming Language :: Python :: 3
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
43
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
44
|
+
Classifier: Typing :: Typed
|
|
45
|
+
Requires-Python: >=3.9
|
|
46
|
+
Requires-Dist: httpx<1.0,>=0.27
|
|
47
|
+
Requires-Dist: pydantic<3.0,>=2.0
|
|
48
|
+
Requires-Dist: typing-extensions>=4.7; python_version < '3.11'
|
|
49
|
+
Provides-Extra: dev
|
|
50
|
+
Requires-Dist: build>=1.0; extra == 'dev'
|
|
51
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
52
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
53
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
54
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
55
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
56
|
+
Requires-Dist: twine>=5.0; extra == 'dev'
|
|
57
|
+
Description-Content-Type: text/markdown
|
|
58
|
+
|
|
59
|
+
# esoul — Python SDK for ExternalSoul
|
|
60
|
+
|
|
61
|
+
`esoul` is the official Python SDK for the **ExternalSoul** platform. It lets
|
|
62
|
+
scripts mutate workspace state from anywhere Python runs — inside the
|
|
63
|
+
platform's E2B sandboxes, on a data scientist's laptop, in a Colab notebook,
|
|
64
|
+
in CI, in a scheduled cron job.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip install esoul
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
import esoul
|
|
72
|
+
|
|
73
|
+
client = esoul.Esoul() # auto-detects credentials
|
|
74
|
+
state = client.describe() # what workspaces + apps + events are available
|
|
75
|
+
print(state.session.workspace_ids)
|
|
76
|
+
|
|
77
|
+
# Low-level dispatch
|
|
78
|
+
result = client.dispatch_event(
|
|
79
|
+
app_id="app_abc123",
|
|
80
|
+
event_name="spreadsheet_add_row",
|
|
81
|
+
event_data={"cells": {"name": "Alice", "email": "a@example.com"}},
|
|
82
|
+
)
|
|
83
|
+
print(result.event_id, result.sequence_num)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Why an SDK?
|
|
87
|
+
|
|
88
|
+
ExternalSoul workspaces are **event-sourced** — every state mutation is a
|
|
89
|
+
typed event on a per-workspace timeline, scrubbable and forkable. The UI,
|
|
90
|
+
agents, and now scripts all dispatch through the **same** reducers; events
|
|
91
|
+
are the source of truth, audit logging is automatic.
|
|
92
|
+
|
|
93
|
+
The SDK is the platform's universal binding for **scripted workspace
|
|
94
|
+
operations**. Common use cases:
|
|
95
|
+
|
|
96
|
+
- Bulk-import 1000 rows into a spreadsheet (one event, not 1000 LLM tool calls)
|
|
97
|
+
- Run an OpenCV pipeline over Drive images and write masks back
|
|
98
|
+
- Programmatic kanban / contacts load from a CSV
|
|
99
|
+
- Notebook-as-workspace-builder
|
|
100
|
+
- Any agent or human writing Python to mutate app state
|
|
101
|
+
|
|
102
|
+
## Authentication
|
|
103
|
+
|
|
104
|
+
`esoul.Esoul()` auto-detects credentials in this order:
|
|
105
|
+
|
|
106
|
+
1. Explicit `token=` kwarg
|
|
107
|
+
2. `ESOUL_TOKEN` environment variable
|
|
108
|
+
3. `/var/run/esoul/token` (the file mode 0600 token written by every E2B
|
|
109
|
+
sandbox at boot — `Esoul()` inside a sandbox Just Works)
|
|
110
|
+
4. `~/.config/esoul/credentials` (TOML file with a `[default]` section
|
|
111
|
+
containing `token = "esoul_pat_..."`)
|
|
112
|
+
|
|
113
|
+
For off-platform use (laptop, CI), create a **Personal Access Token** in
|
|
114
|
+
workspace settings → "Access Tokens", pick the workspaces it can mutate,
|
|
115
|
+
copy the token (shown once), and either:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
# Export inline:
|
|
119
|
+
export ESOUL_TOKEN="esoul_pat_..."
|
|
120
|
+
|
|
121
|
+
# Or write to ~/.config/esoul/credentials:
|
|
122
|
+
mkdir -p ~/.config/esoul
|
|
123
|
+
cat > ~/.config/esoul/credentials <<EOF
|
|
124
|
+
[default]
|
|
125
|
+
token = "esoul_pat_..."
|
|
126
|
+
EOF
|
|
127
|
+
chmod 600 ~/.config/esoul/credentials
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Workspace isolation
|
|
131
|
+
|
|
132
|
+
A given credential is scoped to one or more specific workspaces. Cross-
|
|
133
|
+
workspace dispatch is **structurally impossible** — not a permission check
|
|
134
|
+
in your script, an architectural invariant of the server.
|
|
135
|
+
|
|
136
|
+
## Idempotency
|
|
137
|
+
|
|
138
|
+
Every write is automatically idempotent. The SDK generates a UUID per call
|
|
139
|
+
and reuses it across retries; the server caches the response under
|
|
140
|
+
`(session, key)` for 24h. Same key + same body → identical cached response,
|
|
141
|
+
not a duplicate event. You can override the key for application-level retry
|
|
142
|
+
control:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
client.dispatch_event(
|
|
146
|
+
app_id=...,
|
|
147
|
+
event_name=...,
|
|
148
|
+
event_data=...,
|
|
149
|
+
idempotency_key="my-task-2026-05-15", # caller-controlled
|
|
150
|
+
)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Async client
|
|
154
|
+
|
|
155
|
+
The async client mirrors the sync API exactly:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
import asyncio
|
|
159
|
+
import esoul
|
|
160
|
+
|
|
161
|
+
async def main():
|
|
162
|
+
async with esoul.AsyncEsoul() as client:
|
|
163
|
+
result = await client.dispatch_event(
|
|
164
|
+
app_id=..., event_name=..., event_data=...,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
asyncio.run(main())
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Typed errors
|
|
171
|
+
|
|
172
|
+
Failures surface as Python exception classes mapped from the server's
|
|
173
|
+
`error.code`. You can catch the kind you care about, and `EsoulError`
|
|
174
|
+
catches everything:
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
import esoul
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
client.dispatch_event(...)
|
|
181
|
+
except esoul.WorkspaceAccessDenied as e:
|
|
182
|
+
print(f"This token cannot reach workspace {e.details['workspaceId']}")
|
|
183
|
+
except esoul.IdempotencyConflict as e:
|
|
184
|
+
print("Same key was used with a different request body")
|
|
185
|
+
except esoul.RateLimitError as e:
|
|
186
|
+
time.sleep(e.retry_after_seconds or 1)
|
|
187
|
+
except esoul.AuthError:
|
|
188
|
+
print("Token expired or revoked — get a new one")
|
|
189
|
+
except esoul.EsoulError as e:
|
|
190
|
+
print(f"{e.code}: {e.message}")
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Status
|
|
194
|
+
|
|
195
|
+
**v0.1 — alpha.** Wire surface is stable; per-app typed resources are
|
|
196
|
+
codegen'd and shipping incrementally. The low-level `dispatch_event` /
|
|
197
|
+
`dispatch_batch` / `read_state` paths are production-ready.
|
|
198
|
+
|
|
199
|
+
See [CHANGELOG.md](./CHANGELOG.md) for release notes.
|
|
200
|
+
|
|
201
|
+
## License
|
|
202
|
+
|
|
203
|
+
MIT.
|
esoul-0.1.0/README.md
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# esoul — Python SDK for ExternalSoul
|
|
2
|
+
|
|
3
|
+
`esoul` is the official Python SDK for the **ExternalSoul** platform. It lets
|
|
4
|
+
scripts mutate workspace state from anywhere Python runs — inside the
|
|
5
|
+
platform's E2B sandboxes, on a data scientist's laptop, in a Colab notebook,
|
|
6
|
+
in CI, in a scheduled cron job.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install esoul
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
import esoul
|
|
14
|
+
|
|
15
|
+
client = esoul.Esoul() # auto-detects credentials
|
|
16
|
+
state = client.describe() # what workspaces + apps + events are available
|
|
17
|
+
print(state.session.workspace_ids)
|
|
18
|
+
|
|
19
|
+
# Low-level dispatch
|
|
20
|
+
result = client.dispatch_event(
|
|
21
|
+
app_id="app_abc123",
|
|
22
|
+
event_name="spreadsheet_add_row",
|
|
23
|
+
event_data={"cells": {"name": "Alice", "email": "a@example.com"}},
|
|
24
|
+
)
|
|
25
|
+
print(result.event_id, result.sequence_num)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Why an SDK?
|
|
29
|
+
|
|
30
|
+
ExternalSoul workspaces are **event-sourced** — every state mutation is a
|
|
31
|
+
typed event on a per-workspace timeline, scrubbable and forkable. The UI,
|
|
32
|
+
agents, and now scripts all dispatch through the **same** reducers; events
|
|
33
|
+
are the source of truth, audit logging is automatic.
|
|
34
|
+
|
|
35
|
+
The SDK is the platform's universal binding for **scripted workspace
|
|
36
|
+
operations**. Common use cases:
|
|
37
|
+
|
|
38
|
+
- Bulk-import 1000 rows into a spreadsheet (one event, not 1000 LLM tool calls)
|
|
39
|
+
- Run an OpenCV pipeline over Drive images and write masks back
|
|
40
|
+
- Programmatic kanban / contacts load from a CSV
|
|
41
|
+
- Notebook-as-workspace-builder
|
|
42
|
+
- Any agent or human writing Python to mutate app state
|
|
43
|
+
|
|
44
|
+
## Authentication
|
|
45
|
+
|
|
46
|
+
`esoul.Esoul()` auto-detects credentials in this order:
|
|
47
|
+
|
|
48
|
+
1. Explicit `token=` kwarg
|
|
49
|
+
2. `ESOUL_TOKEN` environment variable
|
|
50
|
+
3. `/var/run/esoul/token` (the file mode 0600 token written by every E2B
|
|
51
|
+
sandbox at boot — `Esoul()` inside a sandbox Just Works)
|
|
52
|
+
4. `~/.config/esoul/credentials` (TOML file with a `[default]` section
|
|
53
|
+
containing `token = "esoul_pat_..."`)
|
|
54
|
+
|
|
55
|
+
For off-platform use (laptop, CI), create a **Personal Access Token** in
|
|
56
|
+
workspace settings → "Access Tokens", pick the workspaces it can mutate,
|
|
57
|
+
copy the token (shown once), and either:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Export inline:
|
|
61
|
+
export ESOUL_TOKEN="esoul_pat_..."
|
|
62
|
+
|
|
63
|
+
# Or write to ~/.config/esoul/credentials:
|
|
64
|
+
mkdir -p ~/.config/esoul
|
|
65
|
+
cat > ~/.config/esoul/credentials <<EOF
|
|
66
|
+
[default]
|
|
67
|
+
token = "esoul_pat_..."
|
|
68
|
+
EOF
|
|
69
|
+
chmod 600 ~/.config/esoul/credentials
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Workspace isolation
|
|
73
|
+
|
|
74
|
+
A given credential is scoped to one or more specific workspaces. Cross-
|
|
75
|
+
workspace dispatch is **structurally impossible** — not a permission check
|
|
76
|
+
in your script, an architectural invariant of the server.
|
|
77
|
+
|
|
78
|
+
## Idempotency
|
|
79
|
+
|
|
80
|
+
Every write is automatically idempotent. The SDK generates a UUID per call
|
|
81
|
+
and reuses it across retries; the server caches the response under
|
|
82
|
+
`(session, key)` for 24h. Same key + same body → identical cached response,
|
|
83
|
+
not a duplicate event. You can override the key for application-level retry
|
|
84
|
+
control:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
client.dispatch_event(
|
|
88
|
+
app_id=...,
|
|
89
|
+
event_name=...,
|
|
90
|
+
event_data=...,
|
|
91
|
+
idempotency_key="my-task-2026-05-15", # caller-controlled
|
|
92
|
+
)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Async client
|
|
96
|
+
|
|
97
|
+
The async client mirrors the sync API exactly:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
import asyncio
|
|
101
|
+
import esoul
|
|
102
|
+
|
|
103
|
+
async def main():
|
|
104
|
+
async with esoul.AsyncEsoul() as client:
|
|
105
|
+
result = await client.dispatch_event(
|
|
106
|
+
app_id=..., event_name=..., event_data=...,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
asyncio.run(main())
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Typed errors
|
|
113
|
+
|
|
114
|
+
Failures surface as Python exception classes mapped from the server's
|
|
115
|
+
`error.code`. You can catch the kind you care about, and `EsoulError`
|
|
116
|
+
catches everything:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
import esoul
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
client.dispatch_event(...)
|
|
123
|
+
except esoul.WorkspaceAccessDenied as e:
|
|
124
|
+
print(f"This token cannot reach workspace {e.details['workspaceId']}")
|
|
125
|
+
except esoul.IdempotencyConflict as e:
|
|
126
|
+
print("Same key was used with a different request body")
|
|
127
|
+
except esoul.RateLimitError as e:
|
|
128
|
+
time.sleep(e.retry_after_seconds or 1)
|
|
129
|
+
except esoul.AuthError:
|
|
130
|
+
print("Token expired or revoked — get a new one")
|
|
131
|
+
except esoul.EsoulError as e:
|
|
132
|
+
print(f"{e.code}: {e.message}")
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Status
|
|
136
|
+
|
|
137
|
+
**v0.1 — alpha.** Wire surface is stable; per-app typed resources are
|
|
138
|
+
codegen'd and shipping incrementally. The low-level `dispatch_event` /
|
|
139
|
+
`dispatch_batch` / `read_state` paths are production-ready.
|
|
140
|
+
|
|
141
|
+
See [CHANGELOG.md](./CHANGELOG.md) for release notes.
|
|
142
|
+
|
|
143
|
+
## License
|
|
144
|
+
|
|
145
|
+
MIT.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.21"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "esoul"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Python SDK for the ExternalSoul platform — programmatic workspace mutation, event-sourced, audit-logged."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { file = "LICENSE" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "ExternalSoul", email = "hey@vyomkeshj.com" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["externalsoul", "esoul", "kinetic", "workspace", "automation", "event-sourcing"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
28
|
+
"Typing :: Typed",
|
|
29
|
+
]
|
|
30
|
+
dependencies = [
|
|
31
|
+
# httpx 0.27 added Timeout + connection-pool improvements we rely on;
|
|
32
|
+
# earlier versions work but we don't test against them.
|
|
33
|
+
"httpx>=0.27,<1.0",
|
|
34
|
+
# pydantic v2 for typed response models. v1 had a different validation
|
|
35
|
+
# API; the codegen pipeline targets v2 exclusively.
|
|
36
|
+
"pydantic>=2.0,<3.0",
|
|
37
|
+
# typing_extensions for TypeAlias / Self on Python 3.9-3.10.
|
|
38
|
+
"typing_extensions>=4.7; python_version<'3.11'",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[project.urls]
|
|
42
|
+
Homepage = "https://github.com/vyomkeshj/esoul-python"
|
|
43
|
+
Documentation = "https://externalsoul.com/sdk-docs/llm-reference.md"
|
|
44
|
+
Repository = "https://github.com/vyomkeshj/esoul-python"
|
|
45
|
+
Changelog = "https://github.com/vyomkeshj/esoul-python/blob/main/CHANGELOG.md"
|
|
46
|
+
|
|
47
|
+
[project.optional-dependencies]
|
|
48
|
+
dev = [
|
|
49
|
+
"mypy>=1.10",
|
|
50
|
+
"ruff>=0.6",
|
|
51
|
+
"pytest>=8.0",
|
|
52
|
+
"pytest-asyncio>=0.23",
|
|
53
|
+
"respx>=0.21", # httpx mocking
|
|
54
|
+
"build>=1.0",
|
|
55
|
+
"twine>=5.0",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
[tool.hatch.version]
|
|
59
|
+
path = "src/esoul/_version.py"
|
|
60
|
+
|
|
61
|
+
[tool.hatch.build.targets.wheel]
|
|
62
|
+
packages = ["src/esoul"]
|
|
63
|
+
|
|
64
|
+
[tool.hatch.build.targets.sdist]
|
|
65
|
+
include = [
|
|
66
|
+
"src/esoul",
|
|
67
|
+
"README.md",
|
|
68
|
+
"LICENSE",
|
|
69
|
+
"CHANGELOG.md",
|
|
70
|
+
"pyproject.toml",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
# ─── mypy ────────────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
[tool.mypy]
|
|
76
|
+
python_version = "3.9"
|
|
77
|
+
strict = true
|
|
78
|
+
warn_return_any = true
|
|
79
|
+
warn_unused_configs = true
|
|
80
|
+
# The generated resources/types modules will land here after codegen.
|
|
81
|
+
# Their typing is stricter than mypy's defaults expect.
|
|
82
|
+
files = ["src/esoul"]
|
|
83
|
+
|
|
84
|
+
[[tool.mypy.overrides]]
|
|
85
|
+
module = "esoul.resources.*"
|
|
86
|
+
# Codegen-emitted modules carry a `# mypy: file generated` marker; we keep
|
|
87
|
+
# strict mode but allow ignore-comments to pile up around generated bits.
|
|
88
|
+
strict = true
|
|
89
|
+
|
|
90
|
+
# ─── ruff ────────────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
[tool.ruff]
|
|
93
|
+
target-version = "py39"
|
|
94
|
+
line-length = 100
|
|
95
|
+
src = ["src"]
|
|
96
|
+
|
|
97
|
+
[tool.ruff.lint]
|
|
98
|
+
select = [
|
|
99
|
+
"E", # pycodestyle errors
|
|
100
|
+
"F", # pyflakes
|
|
101
|
+
"I", # isort
|
|
102
|
+
"B", # flake8-bugbear
|
|
103
|
+
"UP", # pyupgrade (modernise type hints etc.)
|
|
104
|
+
"RUF", # ruff-specific
|
|
105
|
+
]
|
|
106
|
+
ignore = [
|
|
107
|
+
"E501", # line length — formatter handles it
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
[tool.ruff.lint.per-file-ignores]
|
|
111
|
+
"tests/*" = ["B011"] # asserts in tests are fine
|
|
112
|
+
|
|
113
|
+
# ─── pytest ──────────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
[tool.pytest.ini_options]
|
|
116
|
+
testpaths = ["tests"]
|
|
117
|
+
asyncio_mode = "auto"
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""esoul — Python SDK for the ExternalSoul platform.
|
|
2
|
+
|
|
3
|
+
See README.md for usage. The top-level surface:
|
|
4
|
+
|
|
5
|
+
esoul.Esoul — sync client
|
|
6
|
+
esoul.AsyncEsoul — async client
|
|
7
|
+
esoul.Credentials — credential dataclass (typically accessed via client.credentials)
|
|
8
|
+
esoul.<Exception> — typed errors; see exceptions module docstring for the full hierarchy
|
|
9
|
+
|
|
10
|
+
Per-app typed resources (spreadsheet, slideshow, notes, …) ship via the
|
|
11
|
+
codegen pipeline and attach to the client as new attributes; they're
|
|
12
|
+
non-breaking additions.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from ._async_client import AsyncEsoul
|
|
18
|
+
from ._auth import Credentials
|
|
19
|
+
from ._client import Esoul
|
|
20
|
+
from ._version import __version__
|
|
21
|
+
from .exceptions import (
|
|
22
|
+
APIError,
|
|
23
|
+
AuthError,
|
|
24
|
+
DriveError,
|
|
25
|
+
DriveNotConnected,
|
|
26
|
+
EsoulError,
|
|
27
|
+
IdempotencyConflict,
|
|
28
|
+
InvalidRequest,
|
|
29
|
+
MissingCredentialsError,
|
|
30
|
+
NotFoundError,
|
|
31
|
+
RateLimitError,
|
|
32
|
+
TransportError,
|
|
33
|
+
WorkspaceAccessDenied,
|
|
34
|
+
)
|
|
35
|
+
from .resources.agents import (
|
|
36
|
+
InvocationError,
|
|
37
|
+
InvocationHandle,
|
|
38
|
+
InvocationResult,
|
|
39
|
+
InvocationStatus,
|
|
40
|
+
InvocationTimeout,
|
|
41
|
+
)
|
|
42
|
+
from .resources.questions import (
|
|
43
|
+
Answer,
|
|
44
|
+
Question,
|
|
45
|
+
QuestionAlreadyResolved,
|
|
46
|
+
QuestionTimeout,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
__all__ = [
|
|
50
|
+
# version
|
|
51
|
+
"__version__",
|
|
52
|
+
# clients
|
|
53
|
+
"Esoul",
|
|
54
|
+
"AsyncEsoul",
|
|
55
|
+
"Credentials",
|
|
56
|
+
# exceptions (re-exported at top-level for ergonomic `except esoul.AuthError`)
|
|
57
|
+
"EsoulError",
|
|
58
|
+
"MissingCredentialsError",
|
|
59
|
+
"TransportError",
|
|
60
|
+
"APIError",
|
|
61
|
+
"AuthError",
|
|
62
|
+
"WorkspaceAccessDenied",
|
|
63
|
+
"NotFoundError",
|
|
64
|
+
"InvalidRequest",
|
|
65
|
+
"IdempotencyConflict",
|
|
66
|
+
"RateLimitError",
|
|
67
|
+
"DriveNotConnected",
|
|
68
|
+
"DriveError",
|
|
69
|
+
# Stage 12 — agent invocation + workspace HIL queue
|
|
70
|
+
"InvocationStatus",
|
|
71
|
+
"InvocationResult",
|
|
72
|
+
"InvocationHandle",
|
|
73
|
+
"InvocationTimeout",
|
|
74
|
+
"InvocationError",
|
|
75
|
+
"Question",
|
|
76
|
+
"Answer",
|
|
77
|
+
"QuestionTimeout",
|
|
78
|
+
"QuestionAlreadyResolved",
|
|
79
|
+
]
|