tikeo 0.1.901__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.
- tikeo-0.1.901/PKG-INFO +40 -0
- tikeo-0.1.901/README.md +16 -0
- tikeo-0.1.901/pyproject.toml +43 -0
- tikeo-0.1.901/setup.cfg +4 -0
- tikeo-0.1.901/src/tikeo/__init__.py +73 -0
- tikeo-0.1.901/src/tikeo/client.py +250 -0
- tikeo-0.1.901/src/tikeo/config.py +119 -0
- tikeo-0.1.901/src/tikeo/errors.py +13 -0
- tikeo-0.1.901/src/tikeo/management.py +139 -0
- tikeo-0.1.901/src/tikeo/proto/worker.proto +188 -0
- tikeo-0.1.901/src/tikeo/proto_loader.py +28 -0
- tikeo-0.1.901/src/tikeo/runtime_dirs.py +124 -0
- tikeo-0.1.901/src/tikeo/sandbox_tools.py +166 -0
- tikeo-0.1.901/src/tikeo/script.py +394 -0
- tikeo-0.1.901/src/tikeo/task.py +64 -0
- tikeo-0.1.901/src/tikeo.egg-info/PKG-INFO +40 -0
- tikeo-0.1.901/src/tikeo.egg-info/SOURCES.txt +19 -0
- tikeo-0.1.901/src/tikeo.egg-info/dependency_links.txt +1 -0
- tikeo-0.1.901/src/tikeo.egg-info/requires.txt +7 -0
- tikeo-0.1.901/src/tikeo.egg-info/top_level.txt +1 -0
- tikeo-0.1.901/tests/test_sdk.py +219 -0
tikeo-0.1.901/PKG-INFO
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tikeo
|
|
3
|
+
Version: 0.1.901
|
|
4
|
+
Summary: Python Worker SDK for tikeo
|
|
5
|
+
Author: tikeo contributors
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/yhyzgn/tikeo
|
|
8
|
+
Project-URL: Repository, https://github.com/yhyzgn/tikeo
|
|
9
|
+
Project-URL: Issues, https://github.com/yhyzgn/tikeo/issues
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Requires-Python: >=3.11
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: grpcio>=1.76.0
|
|
19
|
+
Requires-Dist: grpcio-tools>=1.76.0
|
|
20
|
+
Requires-Dist: protobuf>=6.0.0
|
|
21
|
+
Requires-Dist: requests>=2.32.0
|
|
22
|
+
Provides-Extra: test
|
|
23
|
+
Requires-Dist: pytest>=9.0.0; extra == "test"
|
|
24
|
+
|
|
25
|
+
# tikeo Python Worker SDK
|
|
26
|
+
|
|
27
|
+
Python SDK aligned with the Rust, Go, and Java Worker SDKs.
|
|
28
|
+
|
|
29
|
+
Highlights:
|
|
30
|
+
|
|
31
|
+
- Worker Tunnel client with structured capabilities.
|
|
32
|
+
- Task processors with precise task-scoped logs.
|
|
33
|
+
- Management API client using `x-tikeo-api-key`.
|
|
34
|
+
- Script runners for SRT, Deno, container, local development, and fail-closed unavailable handlers.
|
|
35
|
+
- Default script sandbox resolution: `srt` for shell/Python/PowerShell/PHP/Groovy/Rhai, `deno` for JavaScript/TypeScript.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
python -m pip install -e .[test]
|
|
39
|
+
python -m pytest
|
|
40
|
+
```
|
tikeo-0.1.901/README.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# tikeo Python Worker SDK
|
|
2
|
+
|
|
3
|
+
Python SDK aligned with the Rust, Go, and Java Worker SDKs.
|
|
4
|
+
|
|
5
|
+
Highlights:
|
|
6
|
+
|
|
7
|
+
- Worker Tunnel client with structured capabilities.
|
|
8
|
+
- Task processors with precise task-scoped logs.
|
|
9
|
+
- Management API client using `x-tikeo-api-key`.
|
|
10
|
+
- Script runners for SRT, Deno, container, local development, and fail-closed unavailable handlers.
|
|
11
|
+
- Default script sandbox resolution: `srt` for shell/Python/PowerShell/PHP/Groovy/Rhai, `deno` for JavaScript/TypeScript.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
python -m pip install -e .[test]
|
|
15
|
+
python -m pytest
|
|
16
|
+
```
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "tikeo"
|
|
3
|
+
version = "0.1.901"
|
|
4
|
+
description = "Python Worker SDK for tikeo"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = "Apache-2.0"
|
|
8
|
+
authors = [{ name = "tikeo contributors" }]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 3 - Alpha",
|
|
11
|
+
"Intended Audience :: Developers",
|
|
12
|
+
"Programming Language :: Python :: 3",
|
|
13
|
+
"Programming Language :: Python :: 3.11",
|
|
14
|
+
"Programming Language :: Python :: 3.12",
|
|
15
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
16
|
+
]
|
|
17
|
+
dependencies = [
|
|
18
|
+
"grpcio>=1.76.0",
|
|
19
|
+
"grpcio-tools>=1.76.0",
|
|
20
|
+
"protobuf>=6.0.0",
|
|
21
|
+
"requests>=2.32.0",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
test = ["pytest>=9.0.0"]
|
|
26
|
+
|
|
27
|
+
[build-system]
|
|
28
|
+
requires = ["setuptools>=80", "wheel"]
|
|
29
|
+
build-backend = "setuptools.build_meta"
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.packages.find]
|
|
32
|
+
where = ["src"]
|
|
33
|
+
|
|
34
|
+
[tool.setuptools.package-data]
|
|
35
|
+
tikeo = ["proto/*.proto"]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://github.com/yhyzgn/tikeo"
|
|
39
|
+
Repository = "https://github.com/yhyzgn/tikeo"
|
|
40
|
+
Issues = "https://github.com/yhyzgn/tikeo/issues"
|
|
41
|
+
|
|
42
|
+
[tool.pytest.ini_options]
|
|
43
|
+
pythonpath = ["src"]
|
tikeo-0.1.901/setup.cfg
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Python Worker SDK for tikeo."""
|
|
2
|
+
|
|
3
|
+
from .client import Client, Heartbeat, Registration, Session, grpc_target
|
|
4
|
+
from .config import (
|
|
5
|
+
PluginProcessorCapability,
|
|
6
|
+
ScriptRunnerCapability,
|
|
7
|
+
WorkerCapabilities,
|
|
8
|
+
WorkerConfig,
|
|
9
|
+
local_config,
|
|
10
|
+
)
|
|
11
|
+
from .management import (
|
|
12
|
+
API_KEY_HEADER,
|
|
13
|
+
CreateJobRequest,
|
|
14
|
+
JobDefinition,
|
|
15
|
+
JobRetryPolicy,
|
|
16
|
+
ManagementClient,
|
|
17
|
+
api_job,
|
|
18
|
+
default_job_retry_policy,
|
|
19
|
+
plugin_api_job,
|
|
20
|
+
script_api_job,
|
|
21
|
+
)
|
|
22
|
+
from .script import (
|
|
23
|
+
ContainerScriptRunner,
|
|
24
|
+
DenoScriptRunner,
|
|
25
|
+
LocalCommandScriptRunner,
|
|
26
|
+
SandboxToolResolver,
|
|
27
|
+
ScriptRunnerRegistry,
|
|
28
|
+
ScriptRunnerTask,
|
|
29
|
+
SrtScriptRunner,
|
|
30
|
+
UnavailableScriptRunner,
|
|
31
|
+
default_sandbox_backend,
|
|
32
|
+
normalize_script_language,
|
|
33
|
+
normalize_script_sandbox_backend,
|
|
34
|
+
)
|
|
35
|
+
from .task import TaskContext, TaskOutcome, TaskProcessor, failed, succeeded
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"API_KEY_HEADER",
|
|
39
|
+
"Client",
|
|
40
|
+
"ContainerScriptRunner",
|
|
41
|
+
"CreateJobRequest",
|
|
42
|
+
"DenoScriptRunner",
|
|
43
|
+
"Heartbeat",
|
|
44
|
+
"JobDefinition",
|
|
45
|
+
"JobRetryPolicy",
|
|
46
|
+
"LocalCommandScriptRunner",
|
|
47
|
+
"ManagementClient",
|
|
48
|
+
"PluginProcessorCapability",
|
|
49
|
+
"Registration",
|
|
50
|
+
"SandboxToolResolver",
|
|
51
|
+
"ScriptRunnerCapability",
|
|
52
|
+
"ScriptRunnerRegistry",
|
|
53
|
+
"ScriptRunnerTask",
|
|
54
|
+
"Session",
|
|
55
|
+
"SrtScriptRunner",
|
|
56
|
+
"TaskContext",
|
|
57
|
+
"TaskOutcome",
|
|
58
|
+
"TaskProcessor",
|
|
59
|
+
"UnavailableScriptRunner",
|
|
60
|
+
"WorkerCapabilities",
|
|
61
|
+
"WorkerConfig",
|
|
62
|
+
"api_job",
|
|
63
|
+
"default_job_retry_policy",
|
|
64
|
+
"default_sandbox_backend",
|
|
65
|
+
"failed",
|
|
66
|
+
"grpc_target",
|
|
67
|
+
"local_config",
|
|
68
|
+
"normalize_script_language",
|
|
69
|
+
"normalize_script_sandbox_backend",
|
|
70
|
+
"plugin_api_job",
|
|
71
|
+
"script_api_job",
|
|
72
|
+
"succeeded",
|
|
73
|
+
]
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""Worker Tunnel client and session implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import queue
|
|
6
|
+
import sys
|
|
7
|
+
import threading
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from typing import Any
|
|
11
|
+
from urllib.parse import urlparse
|
|
12
|
+
|
|
13
|
+
import grpc
|
|
14
|
+
|
|
15
|
+
from .config import WorkerCapabilities, WorkerConfig
|
|
16
|
+
from .proto_loader import worker_modules
|
|
17
|
+
from .script import ScriptRunnerRegistry, ScriptRunnerTask
|
|
18
|
+
from .task import TaskContext, TaskOutcome, TaskProcessor, failed
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(slots=True)
|
|
22
|
+
class Registration:
|
|
23
|
+
client_instance_id: str
|
|
24
|
+
namespace: str
|
|
25
|
+
app: str
|
|
26
|
+
name: str
|
|
27
|
+
region: str
|
|
28
|
+
version: str
|
|
29
|
+
cluster: str
|
|
30
|
+
capabilities: list[str]
|
|
31
|
+
labels: dict[str, str]
|
|
32
|
+
structured: WorkerCapabilities
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(slots=True)
|
|
36
|
+
class Heartbeat:
|
|
37
|
+
worker_id: str
|
|
38
|
+
sequence: int
|
|
39
|
+
generation: int
|
|
40
|
+
fencing_token: str
|
|
41
|
+
sent_at: datetime
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Client:
|
|
45
|
+
"""Python Worker client matching the Rust/Go SDK boundary."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, config: WorkerConfig) -> None:
|
|
48
|
+
config.validate()
|
|
49
|
+
config.normalize()
|
|
50
|
+
self._config = config
|
|
51
|
+
self._seq = 0
|
|
52
|
+
self._open = False
|
|
53
|
+
|
|
54
|
+
def registration(self) -> Registration:
|
|
55
|
+
return Registration(
|
|
56
|
+
client_instance_id=self._config.client_instance_id,
|
|
57
|
+
namespace=self._config.namespace,
|
|
58
|
+
app=self._config.app,
|
|
59
|
+
name=self._config.name,
|
|
60
|
+
region=self._config.region,
|
|
61
|
+
version=self._config.version,
|
|
62
|
+
cluster=self._config.cluster,
|
|
63
|
+
capabilities=list(self._config.capabilities),
|
|
64
|
+
labels=dict(self._config.labels),
|
|
65
|
+
structured=self._config.structured,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def start_dry_run(self, processor: TaskProcessor) -> None:
|
|
69
|
+
if processor is None:
|
|
70
|
+
raise ValueError("tikeo task processor is required")
|
|
71
|
+
self._open = True
|
|
72
|
+
|
|
73
|
+
def next_heartbeat(self, worker_id: str, fencing_token: str, generation: int) -> Heartbeat:
|
|
74
|
+
if not self._open:
|
|
75
|
+
raise RuntimeError("tikeo worker client is not started")
|
|
76
|
+
if not worker_id:
|
|
77
|
+
raise ValueError("tikeo worker id is required")
|
|
78
|
+
self._seq += 1
|
|
79
|
+
return Heartbeat(worker_id, self._seq, generation, fencing_token, datetime.now(timezone.utc))
|
|
80
|
+
|
|
81
|
+
def close(self) -> None:
|
|
82
|
+
self._open = False
|
|
83
|
+
|
|
84
|
+
def connect_grpc(self) -> grpc.Channel:
|
|
85
|
+
return grpc.insecure_channel(grpc_target(self._config.endpoint))
|
|
86
|
+
|
|
87
|
+
def connect(self) -> "Session":
|
|
88
|
+
pb2, pb2_grpc = worker_modules()
|
|
89
|
+
channel = self.connect_grpc()
|
|
90
|
+
stub = pb2_grpc.WorkerTunnelServiceStub(channel)
|
|
91
|
+
outbound: queue.Queue[Any] = queue.Queue()
|
|
92
|
+
|
|
93
|
+
def messages():
|
|
94
|
+
while True:
|
|
95
|
+
item = outbound.get()
|
|
96
|
+
if item is None:
|
|
97
|
+
return
|
|
98
|
+
yield item
|
|
99
|
+
|
|
100
|
+
stream = stub.OpenTunnel(messages())
|
|
101
|
+
outbound.put(self._register_message(pb2))
|
|
102
|
+
ack = next(stream)
|
|
103
|
+
registered = getattr(ack, "registered", None)
|
|
104
|
+
if not registered or not registered.worker_id:
|
|
105
|
+
channel.close()
|
|
106
|
+
raise RuntimeError("tikeo worker expected registration ack")
|
|
107
|
+
return Session(pb2, channel, stream, outbound, registered.worker_id, registered.lease_seconds, registered.generation, registered.fencing_token, self._config.heartbeat_every.total_seconds())
|
|
108
|
+
|
|
109
|
+
def _register_message(self, pb2: Any) -> Any:
|
|
110
|
+
register = pb2.RegisterWorker(
|
|
111
|
+
client_instance_id=self._config.client_instance_id,
|
|
112
|
+
app=self._config.app,
|
|
113
|
+
namespace=self._config.namespace,
|
|
114
|
+
cluster=self._config.cluster,
|
|
115
|
+
region=self._config.region,
|
|
116
|
+
capabilities=list(self._config.capabilities),
|
|
117
|
+
labels=dict(self._config.labels),
|
|
118
|
+
structured_capabilities=_to_proto_capabilities(pb2, self._config.structured),
|
|
119
|
+
election=pb2.WorkerClusterElection(enabled=True, priority=100),
|
|
120
|
+
)
|
|
121
|
+
return pb2.WorkerMessage(register=register)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class Session:
|
|
125
|
+
def __init__(self, pb2: Any, channel: grpc.Channel, stream: Any, outbound: queue.Queue[Any], worker_id: str, lease_seconds: int, generation: int, fencing_token: str, heartbeat_every: float) -> None:
|
|
126
|
+
self._pb2 = pb2
|
|
127
|
+
self._channel = channel
|
|
128
|
+
self._stream = stream
|
|
129
|
+
self._outbound = outbound
|
|
130
|
+
self._worker_id = worker_id
|
|
131
|
+
self._lease_seconds = lease_seconds
|
|
132
|
+
self._generation = generation
|
|
133
|
+
self._fencing_token = fencing_token
|
|
134
|
+
self._heartbeat_every = heartbeat_every
|
|
135
|
+
self._sequence = 0
|
|
136
|
+
self._log_sequence = 0
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def worker_id(self) -> str:
|
|
140
|
+
return self._worker_id
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def lease_seconds(self) -> int:
|
|
144
|
+
return self._lease_seconds
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def generation(self) -> int:
|
|
148
|
+
return self._generation
|
|
149
|
+
|
|
150
|
+
def send_heartbeat(self) -> int:
|
|
151
|
+
self._sequence += 1
|
|
152
|
+
self._outbound.put(self._pb2.WorkerMessage(heartbeat=self._pb2.Heartbeat(worker_id=self._worker_id, sequence=self._sequence, generation=self._generation, fencing_token=self._fencing_token)))
|
|
153
|
+
return self._sequence
|
|
154
|
+
|
|
155
|
+
def start_heartbeat(self) -> threading.Event:
|
|
156
|
+
stop = threading.Event()
|
|
157
|
+
|
|
158
|
+
def loop() -> None:
|
|
159
|
+
self.send_heartbeat()
|
|
160
|
+
while not stop.wait(self._heartbeat_every):
|
|
161
|
+
self.send_heartbeat()
|
|
162
|
+
|
|
163
|
+
threading.Thread(target=loop, daemon=True).start()
|
|
164
|
+
return stop
|
|
165
|
+
|
|
166
|
+
def emit_task_log(self, instance_id: str, assignment_token: str, level: str, message: str) -> int:
|
|
167
|
+
self._log_sequence += 1
|
|
168
|
+
self._outbound.put(self._pb2.WorkerMessage(task_log=self._pb2.TaskLog(worker_id=self._worker_id, instance_id=instance_id, level=level or "info", message=message, sequence=self._log_sequence, assignment_token=assignment_token)))
|
|
169
|
+
return self._log_sequence
|
|
170
|
+
|
|
171
|
+
def process_next(self, processor: TaskProcessor, scripts: ScriptRunnerRegistry | None = None) -> TaskOutcome:
|
|
172
|
+
for message in self._stream:
|
|
173
|
+
task = getattr(message, "dispatch_task", None)
|
|
174
|
+
if not task or not task.instance_id:
|
|
175
|
+
continue
|
|
176
|
+
self._emit_task_log_safely(task, "info", f"received task {task.instance_id} processor={task.processor_name}")
|
|
177
|
+
outcome = process_dispatch_task(processor, scripts, task, lambda level, msg: self._emit_task_log_safely(task, level, msg))
|
|
178
|
+
level = "info" if outcome.success else "error"
|
|
179
|
+
self._emit_task_log_safely(task, level, f"completed task {task.instance_id} success={str(outcome.success).lower()} message={outcome.message}")
|
|
180
|
+
self._outbound.put(self._pb2.WorkerMessage(task_result=self._pb2.TaskResult(worker_id=self._worker_id, instance_id=task.instance_id, success=outcome.success, message=outcome.message, assignment_token=task.assignment_token)))
|
|
181
|
+
return outcome
|
|
182
|
+
raise RuntimeError("worker tunnel closed")
|
|
183
|
+
|
|
184
|
+
def close(self) -> None:
|
|
185
|
+
self._outbound.put(self._pb2.WorkerMessage(unregister=self._pb2.UnregisterWorker(worker_id=self._worker_id, generation=self._generation, fencing_token=self._fencing_token)))
|
|
186
|
+
self._outbound.put(None)
|
|
187
|
+
self._channel.close()
|
|
188
|
+
|
|
189
|
+
def _emit_task_log_safely(self, task: Any, level: str, message: str) -> None:
|
|
190
|
+
print_task_log_locally(level, message)
|
|
191
|
+
self.emit_task_log(task.instance_id, task.assignment_token, level, message)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def process_dispatch_task(processor: TaskProcessor, scripts: ScriptRunnerRegistry | None, task: Any, log: callable) -> TaskOutcome:
|
|
195
|
+
try:
|
|
196
|
+
binding = getattr(task, "processor_binding", None)
|
|
197
|
+
if binding and binding.HasField("script"):
|
|
198
|
+
script = binding.script
|
|
199
|
+
runner = scripts.get(script.language) if scripts else None
|
|
200
|
+
if runner is None:
|
|
201
|
+
return failed(f"script runner is not registered for language: {script.language}")
|
|
202
|
+
return runner.run(ScriptRunnerTask(
|
|
203
|
+
script_id=script.script_id,
|
|
204
|
+
version_id=script.version_id,
|
|
205
|
+
version_number=script.version_number,
|
|
206
|
+
language=script.language,
|
|
207
|
+
content=bytes(script.content),
|
|
208
|
+
content_sha256=script.content_sha256,
|
|
209
|
+
timeout_ms=script.timeout_ms or 30_000,
|
|
210
|
+
max_output_bytes=script.max_output_bytes or 1024 * 1024,
|
|
211
|
+
allow_network=script.allow_network,
|
|
212
|
+
allowed_env_vars=list(script.allowed_env_vars),
|
|
213
|
+
read_only_paths=list(script.read_only_paths),
|
|
214
|
+
writable_paths=list(script.writable_paths),
|
|
215
|
+
secret_refs=list(script.secret_refs),
|
|
216
|
+
allowed_network_hosts=list(script.allowed_network_hosts),
|
|
217
|
+
sandbox_backend=getattr(script, "sandbox_backend", ""),
|
|
218
|
+
instance_id=task.instance_id,
|
|
219
|
+
job_id=task.job_id,
|
|
220
|
+
log=log,
|
|
221
|
+
))
|
|
222
|
+
return processor(TaskContext(instance_id=task.instance_id, job_id=task.job_id, processor_name=task.processor_name or task.job_id, payload=bytes(task.payload), log=log))
|
|
223
|
+
except Exception as exc:
|
|
224
|
+
return failed(str(exc))
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def print_task_log_locally(level: str, message: str) -> None:
|
|
228
|
+
line = f"[tikeo-worker] {message}"
|
|
229
|
+
stream = sys.stderr if level.lower() == "error" else sys.stdout
|
|
230
|
+
print(line, file=stream)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def grpc_target(endpoint: str) -> str:
|
|
234
|
+
value = endpoint.strip()
|
|
235
|
+
if not value:
|
|
236
|
+
raise ValueError("tikeo worker endpoint is required")
|
|
237
|
+
parsed = urlparse(value)
|
|
238
|
+
if parsed.scheme in {"http", "https"}:
|
|
239
|
+
if not parsed.netloc:
|
|
240
|
+
raise ValueError("tikeo worker endpoint host is required")
|
|
241
|
+
return parsed.netloc
|
|
242
|
+
return value
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _to_proto_capabilities(pb2: Any, capabilities: WorkerCapabilities) -> Any:
|
|
246
|
+
out = pb2.WorkerCapabilities(tags=list(capabilities.tags))
|
|
247
|
+
out.sdk_processors.extend(pb2.SdkProcessorCapability(name=name) for name in capabilities.sdk_processors)
|
|
248
|
+
out.script_runners.extend(pb2.ScriptRunnerCapability(language=r.language, sandbox_backend=r.sandbox_backend) for r in capabilities.script_runners)
|
|
249
|
+
out.plugin_processors.extend(pb2.PluginProcessorCapability(type=p.type, processor_names=list(p.processor_names)) for p in capabilities.plugin_processors)
|
|
250
|
+
return out
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Worker configuration and structured capability models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from datetime import timedelta
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _append_unique(values: list[str], value: str) -> None:
|
|
10
|
+
item = value.strip()
|
|
11
|
+
if item and item not in values:
|
|
12
|
+
values.append(item)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _normalized(values: list[str]) -> list[str]:
|
|
16
|
+
out: list[str] = []
|
|
17
|
+
for value in values:
|
|
18
|
+
_append_unique(out, value)
|
|
19
|
+
return out
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(slots=True)
|
|
23
|
+
class ScriptRunnerCapability:
|
|
24
|
+
"""Structured script runner capability advertised by a Worker."""
|
|
25
|
+
|
|
26
|
+
language: str
|
|
27
|
+
sandbox_backend: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(slots=True)
|
|
31
|
+
class PluginProcessorCapability:
|
|
32
|
+
"""Structured plugin processor capability advertised by a Worker."""
|
|
33
|
+
|
|
34
|
+
type: str
|
|
35
|
+
processor_names: list[str] = field(default_factory=list)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(slots=True)
|
|
39
|
+
class WorkerCapabilities:
|
|
40
|
+
"""Structured worker capabilities; routing must use these fields."""
|
|
41
|
+
|
|
42
|
+
tags: list[str] = field(default_factory=list)
|
|
43
|
+
sdk_processors: list[str] = field(default_factory=list)
|
|
44
|
+
script_runners: list[ScriptRunnerCapability] = field(default_factory=list)
|
|
45
|
+
plugin_processors: list[PluginProcessorCapability] = field(default_factory=list)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(slots=True)
|
|
49
|
+
class WorkerConfig:
|
|
50
|
+
"""One outbound Worker Tunnel client instance configuration."""
|
|
51
|
+
|
|
52
|
+
endpoint: str
|
|
53
|
+
client_instance_id: str
|
|
54
|
+
namespace: str = "default"
|
|
55
|
+
app: str = "default"
|
|
56
|
+
name: str = ""
|
|
57
|
+
region: str = "local"
|
|
58
|
+
version: str = "dev"
|
|
59
|
+
cluster: str = "local"
|
|
60
|
+
capabilities: list[str] = field(default_factory=list)
|
|
61
|
+
labels: dict[str, str] = field(default_factory=dict)
|
|
62
|
+
structured: WorkerCapabilities = field(default_factory=WorkerCapabilities)
|
|
63
|
+
heartbeat_every: timedelta = timedelta(seconds=10)
|
|
64
|
+
|
|
65
|
+
def __post_init__(self) -> None:
|
|
66
|
+
if not self.name:
|
|
67
|
+
self.name = self.client_instance_id
|
|
68
|
+
|
|
69
|
+
def add_tag(self, tag: str) -> None:
|
|
70
|
+
_append_unique(self.structured.tags, tag)
|
|
71
|
+
|
|
72
|
+
def add_sdk_processor(self, name: str) -> None:
|
|
73
|
+
_append_unique(self.structured.sdk_processors, name)
|
|
74
|
+
|
|
75
|
+
def add_script_runner(self, language: str, sandbox_backend: str) -> None:
|
|
76
|
+
language = language.strip()
|
|
77
|
+
if not language:
|
|
78
|
+
return
|
|
79
|
+
if any(runner.language == language for runner in self.structured.script_runners):
|
|
80
|
+
return
|
|
81
|
+
self.structured.script_runners.append(ScriptRunnerCapability(language, sandbox_backend.strip()))
|
|
82
|
+
|
|
83
|
+
def add_plugin_processor(self, processor_type: str, processor_name: str) -> None:
|
|
84
|
+
processor_type = processor_type.strip()
|
|
85
|
+
processor_name = processor_name.strip()
|
|
86
|
+
if not processor_type or not processor_name:
|
|
87
|
+
return
|
|
88
|
+
for plugin in self.structured.plugin_processors:
|
|
89
|
+
if plugin.type == processor_type:
|
|
90
|
+
_append_unique(plugin.processor_names, processor_name)
|
|
91
|
+
return
|
|
92
|
+
self.structured.plugin_processors.append(PluginProcessorCapability(processor_type, [processor_name]))
|
|
93
|
+
|
|
94
|
+
def validate(self) -> None:
|
|
95
|
+
for label, value in {
|
|
96
|
+
"tikeo worker endpoint": self.endpoint,
|
|
97
|
+
"tikeo client instance id": self.client_instance_id,
|
|
98
|
+
"tikeo worker namespace": self.namespace,
|
|
99
|
+
"tikeo worker app": self.app,
|
|
100
|
+
"tikeo worker name": self.name,
|
|
101
|
+
"tikeo worker cluster": self.cluster,
|
|
102
|
+
}.items():
|
|
103
|
+
if not value.strip():
|
|
104
|
+
raise ValueError(f"{label} is required")
|
|
105
|
+
if self.heartbeat_every.total_seconds() <= 0:
|
|
106
|
+
raise ValueError("tikeo heartbeat interval must be positive")
|
|
107
|
+
|
|
108
|
+
def normalize(self) -> None:
|
|
109
|
+
self.capabilities = _normalized(self.capabilities)
|
|
110
|
+
self.structured.tags = _normalized(self.structured.tags)
|
|
111
|
+
self.structured.sdk_processors = _normalized(self.structured.sdk_processors)
|
|
112
|
+
for plugin in self.structured.plugin_processors:
|
|
113
|
+
plugin.processor_names = _normalized(plugin.processor_names)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def local_config(endpoint: str, client_instance_id: str) -> WorkerConfig:
|
|
117
|
+
"""Return a development-friendly worker config."""
|
|
118
|
+
|
|
119
|
+
return WorkerConfig(endpoint=endpoint, client_instance_id=client_instance_id)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""SDK error types."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class WorkerSdkError(Exception):
|
|
5
|
+
"""Base Python Worker SDK error."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ScriptExecutionError(WorkerSdkError):
|
|
9
|
+
"""Dynamic script execution failed."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ManagementRequestError(WorkerSdkError):
|
|
13
|
+
"""Management API request failed."""
|