triggerware 0.1.2__tar.gz → 1.0.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,38 @@
1
+ Metadata-Version: 2.3
2
+ Name: triggerware
3
+ Version: 1.0.0
4
+ Summary: Async Python SDK for Triggerware
5
+ Requires-Dist: tw2-jsonrpc
6
+ Requires-Dist: pyyaml
7
+ Requires-Dist: tw2-jsonrpc[websockets] ; extra == 'websockets'
8
+ Requires-Python: >=3.11
9
+ Provides-Extra: websockets
10
+ Description-Content-Type: text/markdown
11
+
12
+ # Triggerware Python SDK
13
+
14
+ Async Python SDK for Triggerware.
15
+
16
+ Install with the `websockets` extra when using cloud/WebSocket connections:
17
+
18
+ ```sh
19
+ uv add "triggerware[websockets]"
20
+ ```
21
+
22
+ ```python
23
+ from triggerware import TriggerwareClient
24
+
25
+ client = await TriggerwareClient.connect_tcp("localhost:5221")
26
+
27
+ health = await client.system.health()
28
+ cursor = await client.cursors.open(sql="select * from my_connector")
29
+
30
+ async for row in cursor:
31
+ print(row)
32
+ ```
33
+
34
+ WebSocket connections authorize immediately with the provided API key:
35
+
36
+ ```python
37
+ client = await TriggerwareClient.connect_ws("wss://example.com/rpc", "your-api-key-here")
38
+ ```
@@ -0,0 +1,27 @@
1
+ # Triggerware Python SDK
2
+
3
+ Async Python SDK for Triggerware.
4
+
5
+ Install with the `websockets` extra when using cloud/WebSocket connections:
6
+
7
+ ```sh
8
+ uv add "triggerware[websockets]"
9
+ ```
10
+
11
+ ```python
12
+ from triggerware import TriggerwareClient
13
+
14
+ client = await TriggerwareClient.connect_tcp("localhost:5221")
15
+
16
+ health = await client.system.health()
17
+ cursor = await client.cursors.open(sql="select * from my_connector")
18
+
19
+ async for row in cursor:
20
+ print(row)
21
+ ```
22
+
23
+ WebSocket connections authorize immediately with the provided API key:
24
+
25
+ ```python
26
+ client = await TriggerwareClient.connect_ws("wss://example.com/rpc", "your-api-key-here")
27
+ ```
@@ -0,0 +1,14 @@
1
+ [project]
2
+ name = "triggerware"
3
+ version = "1.0.0"
4
+ description = "Async Python SDK for Triggerware"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ dependencies = ["tw2-jsonrpc", "pyyaml"]
8
+
9
+ [project.optional-dependencies]
10
+ websockets = ["tw2-jsonrpc[websockets]"]
11
+
12
+ [build-system]
13
+ requires = ["uv_build>=0.10.0,<0.11.0"]
14
+ build-backend = "uv_build"
@@ -0,0 +1,39 @@
1
+ from ._client import TriggerwareClient
2
+ from ._configuration import Configuration
3
+ from ._connectors import (
4
+ ConnectorColumn,
5
+ ConnectorGenerator,
6
+ ConnectorMetadata,
7
+ ConnectorRuntime,
8
+ connector_metadata_from_string,
9
+ )
10
+ from ._cursors import CursorColumn
11
+ from ._extensions import Extension, ExtensionDescription, ExtensionRuntime
12
+ from ._logging import LogLevel
13
+ from ._queries import QuerySignature
14
+ from ._runtimes import Runtime
15
+ from ._triggers import (
16
+ TriggerCallback,
17
+ TriggerDelta,
18
+ TriggerSignature,
19
+ )
20
+
21
+ __all__ = [
22
+ "Configuration",
23
+ "ConnectorColumn",
24
+ "ConnectorGenerator",
25
+ "ConnectorMetadata",
26
+ "ConnectorRuntime",
27
+ "connector_metadata_from_string",
28
+ "CursorColumn",
29
+ "Extension",
30
+ "ExtensionDescription",
31
+ "ExtensionRuntime",
32
+ "LogLevel",
33
+ "QuerySignature",
34
+ "Runtime",
35
+ "TriggerCallback",
36
+ "TriggerDelta",
37
+ "TriggerSignature",
38
+ "TriggerwareClient",
39
+ ]
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Self
4
+
5
+ import tw2_jsonrpc as rpc
6
+
7
+ from ._configuration import ConfigurationService
8
+ from ._connectors import ConnectorsService
9
+ from ._cursors import CursorsService
10
+ from ._extensions import ExtensionsService
11
+ from ._logging import LoggingService
12
+ from ._prompts import PromptsService
13
+ from ._queries import QueriesService
14
+ from ._runtimes import RuntimesService
15
+ from ._system import SystemService
16
+ from ._triggers import TriggersService
17
+
18
+
19
+ class TriggerwareClient:
20
+
21
+ def __init__(self, peer: rpc.Peer) -> None:
22
+ self._peer = peer
23
+ self.configuration = ConfigurationService(peer)
24
+ self.connectors = ConnectorsService(peer)
25
+ self.cursors = CursorsService(peer)
26
+ self.extensions = ExtensionsService(peer)
27
+ self.logging = LoggingService(peer)
28
+ self.prompts = PromptsService(peer)
29
+ self.queries = QueriesService(peer)
30
+ self.runtimes = RuntimesService(peer)
31
+ self.system = SystemService(peer)
32
+ self.triggers = TriggersService(peer)
33
+
34
+ # do we need enter? exit? close?
35
+
36
+ @classmethod
37
+ async def connect_tcp(cls, address: str) -> Self:
38
+ host, port_text = address.rsplit(":", 1)
39
+ peer = await rpc.connect_tcp(host, int(port_text))
40
+ return cls(peer)
41
+
42
+ @classmethod
43
+ async def connect_ws(cls, url: str, api_key: str) -> Self:
44
+ peer = await rpc.connect_ws(url)
45
+ client = cls(peer)
46
+ await client.system.authorize(api_key)
47
+ return client
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import NotRequired, TypedDict
4
+
5
+ import tw2_jsonrpc as rpc
6
+
7
+
8
+ class Configuration(TypedDict):
9
+ default_timeout_ms: NotRequired[int]
10
+ default_batch_size: NotRequired[int]
11
+ default_trigger_batch_size: NotRequired[int]
12
+ llm_provider: NotRequired[str]
13
+ llm_model: NotRequired[str]
14
+ max_log_file_size: NotRequired[str]
15
+
16
+
17
+ class ConfigurationService:
18
+ def __init__(self, peer: rpc.Peer):
19
+ self._peer = peer
20
+
21
+ async def get(self) -> Configuration:
22
+ return await self._peer.call("configuration.get")
23
+
24
+ async def set(
25
+ self,
26
+ *,
27
+ default_timeout_ms: int | None = None,
28
+ default_batch_size: int | None = None,
29
+ default_trigger_batch_size: int | None = None,
30
+ llm_provider: str | None = None,
31
+ llm_model: str | None = None,
32
+ llm_api_key: str | None = None,
33
+ max_log_file_size: str | None = None,
34
+ ) -> Configuration:
35
+ params = {
36
+ "default_timeout_ms": default_timeout_ms,
37
+ "default_batch_size": default_batch_size,
38
+ "default_trigger_batch_size": default_trigger_batch_size,
39
+ "llm_provider": llm_provider,
40
+ "llm_model": llm_model,
41
+ "llm_api_key": llm_api_key,
42
+ "max_log_file_size": max_log_file_size,
43
+ }
44
+ return await self._peer.call(
45
+ "configuration.set",
46
+ {key: value for key, value in params.items() if value is not None},
47
+ )
48
+
49
+ async def refresh(self) -> Configuration:
50
+ return await self._peer.call("configuration.refresh")
@@ -0,0 +1,171 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any, Literal, NotRequired, TypedDict
5
+
6
+ import tw2_jsonrpc as rpc
7
+ import yaml
8
+
9
+
10
+ ConnectorRuntime = Literal["lua", "python", "node"]
11
+
12
+
13
+ class ConnectorColumn(TypedDict):
14
+ name: str
15
+ type: str
16
+
17
+
18
+ class ConnectorGenerator(TypedDict):
19
+ name: str
20
+ inputs: list[str]
21
+
22
+
23
+ class ConnectorMetadata(TypedDict):
24
+ description: NotRequired[str]
25
+ columns: list[ConnectorColumn]
26
+ generators: list[ConnectorGenerator]
27
+ config_schema: NotRequired[dict[str, Any]]
28
+
29
+
30
+ class ConnectorExtension(TypedDict):
31
+ name: str
32
+ tag: NotRequired[str]
33
+
34
+
35
+ class Connector(TypedDict):
36
+ name: str
37
+ meta: ConnectorMetadata
38
+ code: str
39
+ runtime: NotRequired[ConnectorRuntime]
40
+ extension: NotRequired[ConnectorExtension]
41
+ configured: bool
42
+
43
+
44
+ class ConnectorsService:
45
+ def __init__(self, peer: rpc.Peer):
46
+ self._peer = peer
47
+
48
+ async def create(
49
+ self,
50
+ name: str,
51
+ *,
52
+ prompt: str | None = None,
53
+ code: str | None = None,
54
+ meta: ConnectorMetadata | str | None = None,
55
+ runtime: ConnectorRuntime | None = None,
56
+ ) -> Connector:
57
+ connector = await self._peer.call(
58
+ "connectors.create",
59
+ self._definition_params(
60
+ name=name,
61
+ prompt=prompt,
62
+ code=code,
63
+ meta=meta,
64
+ runtime=runtime,
65
+ ),
66
+ )
67
+ connector["name"] = name
68
+ return connector
69
+
70
+ async def get(self, name: str) -> Connector:
71
+ connector = await self._peer.call("connectors.get", {"name": name})
72
+ connector["name"] = name
73
+ return connector
74
+
75
+ async def update(
76
+ self,
77
+ name: str,
78
+ *,
79
+ prompt: str | None = None,
80
+ code: str | None = None,
81
+ meta: ConnectorMetadata | str | None = None,
82
+ runtime: ConnectorRuntime | None = None,
83
+ ) -> Connector:
84
+ connector = await self._peer.call(
85
+ "connectors.update",
86
+ self._definition_params(
87
+ name=name,
88
+ prompt=prompt,
89
+ code=code,
90
+ meta=meta,
91
+ runtime=runtime,
92
+ ),
93
+ )
94
+ connector["name"] = name
95
+ return connector
96
+
97
+ async def list(self) -> list[str]:
98
+ response = await self._peer.call("connectors.list")
99
+ return response["connectors"]
100
+
101
+ async def delete(self, name: str) -> None:
102
+ await self._peer.call("connectors.delete", {"name": name})
103
+
104
+ async def sync(
105
+ self,
106
+ *,
107
+ connectors: list[str] | None = None,
108
+ extensions: list[str] | None = None,
109
+ ) -> None:
110
+ params = {
111
+ "connectors": connectors,
112
+ "extensions": extensions,
113
+ }
114
+ await self._peer.call(
115
+ "connectors.sync",
116
+ {key: value for key, value in params.items() if value is not None},
117
+ )
118
+
119
+ async def configure(self, connector: str | Connector, **config: Any) -> None:
120
+ if isinstance(connector, str):
121
+ name = connector
122
+ attached_connector = None
123
+ else:
124
+ name = connector["name"]
125
+ attached_connector = connector
126
+
127
+ await self._peer.call(
128
+ "connectors.configure",
129
+ {
130
+ "name": name,
131
+ "config": config,
132
+ },
133
+ )
134
+
135
+ if attached_connector is not None:
136
+ attached_connector["configured"] = True
137
+
138
+ def _definition_params(
139
+ self,
140
+ *,
141
+ name: str,
142
+ prompt: str | None,
143
+ code: str | None,
144
+ meta: ConnectorMetadata | str | None,
145
+ runtime: ConnectorRuntime | None,
146
+ ) -> dict[str, Any]:
147
+ params: dict[str, Any] = {"name": name}
148
+
149
+ if prompt is not None:
150
+ params["prompt"] = prompt
151
+ else:
152
+ if code is None or meta is None:
153
+ raise ValueError("either prompt or both code and meta must be supplied")
154
+ params["code"] = code
155
+ params["meta"] = connector_metadata_from_string(meta) if isinstance(meta, str) else meta
156
+
157
+ if runtime is not None:
158
+ params["runtime"] = runtime
159
+
160
+ return params
161
+
162
+
163
+ def connector_metadata_from_string(contents: str) -> ConnectorMetadata:
164
+ try:
165
+ parsed = yaml.safe_load(contents)
166
+ except yaml.YAMLError:
167
+ parsed = json.loads(contents)
168
+
169
+ if not isinstance(parsed, dict):
170
+ raise ValueError("connector metadata must be an object")
171
+ return parsed # type: ignore[return-value]
@@ -0,0 +1,113 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, NotRequired, TypedDict
4
+
5
+ import tw2_jsonrpc as rpc
6
+
7
+
8
+ class CursorColumn(TypedDict):
9
+ name: str
10
+ type: str
11
+
12
+
13
+ class CursorOpen(TypedDict):
14
+ sql: str
15
+ signature: list[CursorColumn]
16
+ rows: list[list[Any]]
17
+ cursor: str
18
+ exhausted: bool
19
+
20
+
21
+ class CursorFetch(TypedDict):
22
+ rows: list[list[Any]]
23
+ exhausted: bool
24
+
25
+
26
+ class Cursor:
27
+ def __init__(
28
+ self,
29
+ peer: rpc.Peer,
30
+ *,
31
+ cursor: str,
32
+ sql: str,
33
+ signature: list[CursorColumn],
34
+ rows: list[list[Any]],
35
+ exhausted: bool,
36
+ ):
37
+ self._peer = peer
38
+ self._cursor = cursor
39
+ self.sql = sql
40
+ self.signature = signature
41
+ self.exhausted = exhausted
42
+ self._cache = rows
43
+ self._cache_position = 0
44
+
45
+ async def fetch(self) -> list[list[Any]]:
46
+ if self.exhausted:
47
+ return []
48
+
49
+ response: CursorFetch = await self._peer.call(
50
+ "cursors.fetch",
51
+ {"cursor": self._cursor},
52
+ )
53
+ self.exhausted = response["exhausted"]
54
+ self._cache.extend(response["rows"])
55
+ return response["rows"]
56
+
57
+ async def close(self) -> None:
58
+ await self._peer.call("cursors.close", {"cursor": self._cursor})
59
+ self.exhausted = True
60
+
61
+ def __aiter__(self) -> Cursor:
62
+ return self
63
+
64
+ async def __anext__(self) -> list[Any]:
65
+ if self._cache_position < len(self._cache):
66
+ row = self._cache[self._cache_position]
67
+ self._cache_position += 1
68
+ return row
69
+
70
+ if self.exhausted:
71
+ raise StopAsyncIteration
72
+
73
+ await self.fetch()
74
+
75
+ if self._cache_position < len(self._cache):
76
+ row = self._cache[self._cache_position]
77
+ self._cache_position += 1
78
+ return row
79
+
80
+ raise StopAsyncIteration
81
+
82
+
83
+ class CursorsService:
84
+ def __init__(self, peer: rpc.Peer):
85
+ self._peer = peer
86
+
87
+ async def open(
88
+ self,
89
+ *,
90
+ sql: str | None = None,
91
+ prompt: str | None = None,
92
+ batch_size: int | None = None,
93
+ timeout_ms: int | None = None,
94
+ ) -> Cursor:
95
+ params = _run_params(
96
+ sql=sql,
97
+ prompt=prompt,
98
+ batch_size=batch_size,
99
+ timeout_ms=timeout_ms,
100
+ )
101
+ response: CursorOpen = await self._peer.call("cursors.open", params)
102
+ return Cursor(
103
+ self._peer,
104
+ cursor=response["cursor"],
105
+ sql=response["sql"],
106
+ signature=response["signature"],
107
+ rows=response["rows"],
108
+ exhausted=response["exhausted"],
109
+ )
110
+
111
+
112
+ def _run_params(**values: Any) -> dict[str, Any]:
113
+ return {key: value for key, value in values.items() if value is not None}
@@ -0,0 +1,97 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Literal, NotRequired, TypedDict
4
+
5
+ import tw2_jsonrpc as rpc
6
+
7
+
8
+ ExtensionRuntime = Literal["lua", "python", "node"]
9
+
10
+
11
+ class Extension(TypedDict):
12
+ name: str
13
+ tag: NotRequired[str]
14
+ config: dict[str, Any] | None
15
+ connectors: list[str]
16
+
17
+
18
+ class ExtensionDescription(TypedDict):
19
+ description: str
20
+ config_schema: dict[str, Any]
21
+ runtime: NotRequired[ExtensionRuntime]
22
+ dependencies: NotRequired[list[str]]
23
+ connectors: list[str]
24
+
25
+
26
+ class ExtensionsService:
27
+ def __init__(self, peer: rpc.Peer):
28
+ self._peer = peer
29
+
30
+ async def install(
31
+ self,
32
+ *,
33
+ name: str | None = None,
34
+ url: str | None = None,
35
+ tag: str | None = None,
36
+ config: dict[str, Any] | None = None,
37
+ ) -> list[str]:
38
+ response = await self._peer.call(
39
+ "extensions.install",
40
+ _extension_params(name=name, url=url, tag=tag, config=config or {}),
41
+ )
42
+ return response["connectors"]
43
+
44
+ async def get(self, name: str, *, tag: str | None = None) -> Extension:
45
+ response = await self._peer.call(
46
+ "extensions.get",
47
+ _extension_params(name=name, tag=tag),
48
+ )
49
+ extension: Extension = {
50
+ "name": name,
51
+ "config": response["config"],
52
+ "connectors": response["connectors"],
53
+ }
54
+ if tag is not None:
55
+ extension["tag"] = tag
56
+ return extension
57
+
58
+ async def list(self) -> list[str]:
59
+ response = await self._peer.call("extensions.list")
60
+ return response["extensions"]
61
+
62
+ async def configure(
63
+ self,
64
+ name: str,
65
+ *,
66
+ tag: str | None = None,
67
+ config: dict[str, Any],
68
+ ) -> None:
69
+ await self._peer.call(
70
+ "extensions.configure",
71
+ _extension_params(name=name, tag=tag, config=config),
72
+ )
73
+
74
+ async def catalog(self) -> list[str]:
75
+ response = await self._peer.call("extensions.catalog")
76
+ return response["extensions"]
77
+
78
+ async def describe(
79
+ self,
80
+ *,
81
+ name: str | None = None,
82
+ url: str | None = None,
83
+ ) -> ExtensionDescription:
84
+ return await self._peer.call(
85
+ "extensions.describe",
86
+ _extension_params(name=name, url=url),
87
+ )
88
+
89
+ async def uninstall(self, name: str, *, tag: str | None = None) -> None:
90
+ await self._peer.call(
91
+ "extensions.uninstall",
92
+ _extension_params(name=name, tag=tag),
93
+ )
94
+
95
+
96
+ def _extension_params(**values: Any) -> dict[str, Any]:
97
+ return {key: value for key, value in values.items() if value is not None}
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Literal
4
+
5
+ import tw2_jsonrpc as rpc
6
+
7
+
8
+ LogLevel = Literal["debug", "info", "error"]
9
+
10
+
11
+ class LoggingService:
12
+ def __init__(self, peer: rpc.Peer):
13
+ self._peer = peer
14
+
15
+ async def set(self, level: LogLevel) -> None:
16
+ await self._peer.call("logging.set", {"level": level})
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ import tw2_jsonrpc as rpc
4
+
5
+
6
+ class PromptsService:
7
+ def __init__(self, peer: rpc.Peer):
8
+ self._peer = peer
9
+
10
+ async def get(self, name: str) -> str:
11
+ response = await self._peer.call("prompts.get", {"name": name})
12
+ return response["prompt"]
13
+
14
+ async def set(self, name: str, prompt: str) -> None:
15
+ await self._peer.call(
16
+ "prompts.set",
17
+ {
18
+ "name": name,
19
+ "prompt": prompt,
20
+ },
21
+ )
22
+
23
+ async def reset(self, name: str | None = None) -> None:
24
+ params = {} if name is None else {"name": name}
25
+ await self._peer.call("prompts.reset", params)