crtrs-driver 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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 CREATORS
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.
@@ -0,0 +1,39 @@
1
+ Metadata-Version: 2.4
2
+ Name: crtrs-driver
3
+ Version: 0.1.0
4
+ Summary: Python client for Driver cloud — run agents, stream events.
5
+ Project-URL: Homepage, https://driver.tors.app
6
+ Project-URL: Issues, https://github.com/CREATORS-INDUSTRIES/driver-python/issues
7
+ Author: CREATORS
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: agent,ai,cloud,crtrs,driver,sse
11
+ Requires-Python: >=3.8
12
+ Description-Content-Type: text/markdown
13
+
14
+ # crtrs-driver
15
+
16
+ Python client for Driver. Run agents, stream events.
17
+
18
+ ```sh
19
+ pip install crtrs-driver
20
+ ```
21
+
22
+ ```python
23
+ from crtrs.driver import Driver
24
+
25
+ driver = Driver(api_key="dr_...") # or DRIVER_API_KEY
26
+
27
+ # Stream events.
28
+ for ev in driver.stream("what is https://ycombinator.com about?"):
29
+ if ev["kind"] == "action":
30
+ print(ev["tool"])
31
+
32
+ # ...or run to completion.
33
+ done = driver.run("what is https://ycombinator.com about?")
34
+ print(done["result"])
35
+ ```
36
+
37
+ Events: `plan`, `plan_item_start`, `action`, `done`, `fatal`.
38
+
39
+ MIT
@@ -0,0 +1,26 @@
1
+ # crtrs-driver
2
+
3
+ Python client for Driver. Run agents, stream events.
4
+
5
+ ```sh
6
+ pip install crtrs-driver
7
+ ```
8
+
9
+ ```python
10
+ from crtrs.driver import Driver
11
+
12
+ driver = Driver(api_key="dr_...") # or DRIVER_API_KEY
13
+
14
+ # Stream events.
15
+ for ev in driver.stream("what is https://ycombinator.com about?"):
16
+ if ev["kind"] == "action":
17
+ print(ev["tool"])
18
+
19
+ # ...or run to completion.
20
+ done = driver.run("what is https://ycombinator.com about?")
21
+ print(done["result"])
22
+ ```
23
+
24
+ Events: `plan`, `plan_item_start`, `action`, `done`, `fatal`.
25
+
26
+ MIT
@@ -0,0 +1,57 @@
1
+ """Manual example: run a real prompt against Driver cloud and print events live.
2
+
3
+ export DRIVER_API_KEY=dr_xxxxxxxx
4
+ python examples/run.py "what is https://ycombinator.com about?"
5
+
6
+ Optional:
7
+ export DRIVER_BASE_URL=https://driver.tors.app
8
+ export DRIVER_DEBUG=1 # dump raw events
9
+ """
10
+
11
+ import os
12
+ import sys
13
+
14
+ from crtrs.driver import Driver, DriverError
15
+
16
+
17
+ def main() -> int:
18
+ if not os.environ.get("DRIVER_API_KEY"):
19
+ print("set DRIVER_API_KEY (dr_...) — get one from the dashboard", file=sys.stderr)
20
+ return 2
21
+
22
+ prompt = " ".join(sys.argv[1:]) or "what is https://ycombinator.com about?"
23
+ driver = Driver() # reads DRIVER_API_KEY / DRIVER_BASE_URL from env
24
+ debug = bool(os.environ.get("DRIVER_DEBUG"))
25
+
26
+ print(f"prompt: {prompt}")
27
+ done = None
28
+ try:
29
+ for ev in driver.stream(prompt):
30
+ if debug:
31
+ print("RAW", ev, file=sys.stderr)
32
+ kind = ev["kind"]
33
+ if kind == "plan":
34
+ print("\n[plan]")
35
+ for i, it in enumerate(ev.get("items") or []):
36
+ print(f" {i + 1}. {it}")
37
+ elif kind == "plan_item_start":
38
+ print(f"\n[->] {ev['num'] + 1}. {ev['def']}")
39
+ elif kind == "action":
40
+ net = " [net]" if ev.get("is_network") else ""
41
+ print(f" . {ev['tool']}{net}")
42
+ elif kind == "done":
43
+ done = ev
44
+ except DriverError as e:
45
+ print(f"\nfatal: {e}", file=sys.stderr)
46
+ return 1
47
+
48
+ if done is not None:
49
+ print(f"\n[done] steps={done.get('steps')} errors={done.get('errors')}")
50
+ print("RESULT:", done.get("result"))
51
+ for d in done.get("data") or []:
52
+ print(f" {d['var']} ({d['label']}): {len(d['value'])} chars")
53
+ return 0
54
+
55
+
56
+ if __name__ == "__main__":
57
+ raise SystemExit(main())
@@ -0,0 +1,21 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "crtrs-driver"
7
+ version = "0.1.0"
8
+ description = "Python client for Driver cloud — run agents, stream events."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "CREATORS" }]
13
+ keywords = ["driver", "agent", "ai", "sse", "cloud", "crtrs"]
14
+ dependencies = []
15
+
16
+ [project.urls]
17
+ Homepage = "https://driver.tors.app"
18
+ Issues = "https://github.com/CREATORS-INDUSTRIES/driver-python/issues"
19
+
20
+ [tool.hatch.build.targets.wheel]
21
+ packages = ["src/crtrs"]
@@ -0,0 +1,154 @@
1
+ """Python client for Driver cloud — run agents, stream events.
2
+
3
+ Mirror of the Node client (`@crtrs/driver`): POST a prompt to
4
+ ``/api/driver/run`` with a ``dr_`` API key, then stream the agent's events back
5
+ over SSE in real time.
6
+
7
+ Only five event kinds are surfaced — ``plan``, ``plan_item_start``, ``action``,
8
+ ``done``, ``fatal`` — and nothing else (internal kinds, raw tool ids, action
9
+ args and raw error messages are dropped client-side).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import os
16
+ import urllib.error
17
+ import urllib.request
18
+ from typing import Callable, Dict, Iterator, List, Optional
19
+
20
+ __all__ = ["Driver", "DriverError"]
21
+ __version__ = "0.1.0"
22
+
23
+ DEFAULT_BASE_URL = "https://driver.tors.app"
24
+ RUN_PATH = "/api/driver/run"
25
+
26
+ # The only event kinds the client surfaces; everything else is dropped.
27
+ ALLOWED_KINDS = frozenset({"plan", "plan_item_start", "action", "done", "fatal"})
28
+
29
+ Event = Dict[str, object]
30
+
31
+
32
+ class DriverError(Exception):
33
+ """Raised on a failed run or a ``fatal`` event (carries the error category)."""
34
+
35
+
36
+ class Driver:
37
+ """Client for Driver cloud.
38
+
39
+ :param api_key: the ``dr_…`` API key (machine credential). Falls back to the
40
+ ``DRIVER_API_KEY`` environment variable.
41
+ :param base_url: cloud base URL. Falls back to ``DRIVER_BASE_URL`` then the
42
+ default (``https://driver.tors.app``).
43
+ :param timeout: per-read socket timeout in seconds (``None`` = no timeout).
44
+ """
45
+
46
+ def __init__(
47
+ self,
48
+ api_key: Optional[str] = None,
49
+ base_url: Optional[str] = None,
50
+ timeout: Optional[float] = None,
51
+ ) -> None:
52
+ key = api_key or os.environ.get("DRIVER_API_KEY")
53
+ if not key:
54
+ raise ValueError(
55
+ "Driver: missing api_key (pass api_key= or set DRIVER_API_KEY)"
56
+ )
57
+ self.api_key = key
58
+ self.base_url = (base_url or os.environ.get("DRIVER_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
59
+ self.timeout = timeout
60
+
61
+ def stream(self, prompt: str) -> Iterator[Event]:
62
+ """Run a prompt and yield each agent event as it arrives.
63
+
64
+ Yields the five allowlisted event kinds. Raises :class:`DriverError` on a
65
+ ``fatal`` event or a non-2xx response.
66
+ """
67
+ req = urllib.request.Request(
68
+ self.base_url + RUN_PATH,
69
+ data=json.dumps({"prompt": prompt}).encode("utf-8"),
70
+ method="POST",
71
+ headers={
72
+ "Authorization": f"Bearer {self.api_key}",
73
+ "Content-Type": "application/json",
74
+ "Accept": "text/event-stream",
75
+ # Default urllib UA ("Python-urllib/3.x") trips Cloudflare bot
76
+ # filtering and gets a 403; send our own instead.
77
+ "User-Agent": f"crtrs-driver/{__version__}",
78
+ },
79
+ )
80
+ try:
81
+ resp = urllib.request.urlopen(req, timeout=self.timeout)
82
+ except urllib.error.HTTPError as e:
83
+ body = _safe_read(e)
84
+ raise DriverError(
85
+ f"Driver run failed: HTTP {e.code} {e.reason}" + (f" — {body}" if body else "")
86
+ ) from None
87
+
88
+ with resp:
89
+ for ev in _parse_sse(resp):
90
+ # Allowlist: only surface the public kinds so internal events
91
+ # can never leak to the caller.
92
+ if ev.get("kind") not in ALLOWED_KINDS:
93
+ continue
94
+ if ev["kind"] == "fatal":
95
+ # Only the error category is exposed; raw message stays server-side.
96
+ raise DriverError(str(ev.get("semantic") or "fatal"))
97
+ yield ev
98
+
99
+ def run(self, prompt: str, on_event: Optional[Callable[[Event], None]] = None) -> Optional[Event]:
100
+ """Run a prompt to completion.
101
+
102
+ :param on_event: optional callback invoked for every event.
103
+ :returns: the final ``done`` event, or ``None`` if the stream ended
104
+ without one.
105
+ """
106
+ done: Optional[Event] = None
107
+ for ev in self.stream(prompt):
108
+ if on_event is not None:
109
+ on_event(ev)
110
+ if ev["kind"] == "done":
111
+ done = ev
112
+ return done
113
+
114
+
115
+ def _safe_read(e: urllib.error.HTTPError) -> str:
116
+ try:
117
+ return e.read().decode("utf-8", "replace")[:500]
118
+ except Exception:
119
+ return ""
120
+
121
+
122
+ def _parse_sse(resp) -> Iterator[Event]:
123
+ """Yield parsed JSON events from an SSE stream.
124
+
125
+ Handles multi-line ``data:`` fields and blank-line event delimiters.
126
+ ``data`` blocks that aren't valid JSON are skipped (never surfaced as raw
127
+ text).
128
+ """
129
+ data_lines: List[str] = []
130
+ for raw in resp:
131
+ line = raw.decode("utf-8", "replace").rstrip("\r\n")
132
+ if line == "": # blank line = end of one SSE event
133
+ ev = _parse_event(data_lines)
134
+ data_lines = []
135
+ if ev is not None:
136
+ yield ev
137
+ continue
138
+ if line.startswith(":"): # comment / heartbeat
139
+ continue
140
+ if line.startswith("data:"):
141
+ data_lines.append(line[5:].lstrip(" "))
142
+
143
+ ev = _parse_event(data_lines) # flush trailing event with no final blank line
144
+ if ev is not None:
145
+ yield ev
146
+
147
+
148
+ def _parse_event(data_lines: List[str]) -> Optional[Event]:
149
+ if not data_lines:
150
+ return None
151
+ try:
152
+ return json.loads("\n".join(data_lines))
153
+ except json.JSONDecodeError:
154
+ return None # non-JSON data is dropped, never surfaced as raw text
@@ -0,0 +1,80 @@
1
+ """Smoke test: API surface + SSE parsing against a mock urlopen (no network)."""
2
+
3
+ import io
4
+ import json
5
+ import os
6
+ import sys
7
+
8
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
9
+
10
+ import crtrs.driver as crtrs_driver
11
+ from crtrs.driver import Driver, DriverError
12
+
13
+
14
+ class FakeResponse(io.BytesIO):
15
+ """Byte stream that also works as a context manager (like an HTTP response)."""
16
+
17
+ def __enter__(self):
18
+ return self
19
+
20
+ def __exit__(self, *a):
21
+ self.close()
22
+ return False
23
+
24
+
25
+ def make_urlopen(captured, body, expect_prompt):
26
+ def fake_urlopen(req, timeout=None):
27
+ captured["url"] = req.full_url
28
+ captured["auth"] = req.headers.get("Authorization")
29
+ assert json.loads(req.data) == {"prompt": expect_prompt}
30
+ return FakeResponse(body.encode("utf-8"))
31
+
32
+ return fake_urlopen
33
+
34
+
35
+ def test_requires_api_key():
36
+ os.environ.pop("DRIVER_API_KEY", None)
37
+ try:
38
+ Driver()
39
+ except ValueError as e:
40
+ assert "missing api_key" in str(e)
41
+ else:
42
+ raise AssertionError("should require an api_key")
43
+
44
+
45
+ def test_stream_allowlist():
46
+ captured = {}
47
+ body = (
48
+ 'data: {"kind":"plan","items":["a","b"]}\n\n'
49
+ 'data: {"kind":"step","def":"internal leak"}\n\n' # hidden kind: dropped
50
+ "data: not-json should be dropped\n\n" # non-JSON: dropped
51
+ 'data: {"kind":"action","tool":"http fetching","is_network":true}\n\n'
52
+ 'data: {"kind":"done","result":"ok","steps":2,"errors":0}\n\n'
53
+ )
54
+ crtrs_driver.urllib.request.urlopen = make_urlopen(captured, body, "hi")
55
+
56
+ driver = Driver(api_key="dr_test")
57
+ kinds = [ev["kind"] for ev in driver.stream("hi")]
58
+
59
+ assert captured["url"].endswith("/api/driver/run")
60
+ assert captured["auth"] == "Bearer dr_test"
61
+ assert kinds == ["plan", "action", "done"], kinds
62
+
63
+
64
+ def test_fatal_raises():
65
+ body = 'data: {"kind":"fatal","semantic":"provider"}\n\n'
66
+ crtrs_driver.urllib.request.urlopen = make_urlopen({}, body, "boom")
67
+ driver = Driver(api_key="dr_test")
68
+ try:
69
+ list(driver.stream("boom"))
70
+ except DriverError as e:
71
+ assert str(e) == "provider"
72
+ else:
73
+ raise AssertionError("fatal should raise DriverError")
74
+
75
+
76
+ if __name__ == "__main__":
77
+ test_requires_api_key()
78
+ test_stream_allowlist()
79
+ test_fatal_raises()
80
+ print("ok — crtrs-driver smoke test passed")
@@ -0,0 +1,8 @@
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.8"
4
+
5
+ [[package]]
6
+ name = "crtrs-driver"
7
+ version = "0.1.0"
8
+ source = { editable = "." }