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 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
+ ```
@@ -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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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."""