omina-client 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,14 @@
1
+ /target
2
+ /keys/
3
+ .venv/
4
+ **/.venv/
5
+ **/__pycache__/
6
+ **/*.py[cod]
7
+ **/.pytest_cache/
8
+ **/dist/
9
+ /training_runs/
10
+ /data/telemetry_50k/
11
+ /models/minilm-l6-h384/model.safetensors
12
+ /bundles/compiled_from_cli.omina
13
+ /bundles/drone_flight.omina
14
+ /reports/**/predictions.jsonl
@@ -0,0 +1,90 @@
1
+ Metadata-Version: 2.5
2
+ Name: omina-client
3
+ Version: 1.0.0
4
+ Summary: Official Python client SDK for the Omina edge intelligence daemon
5
+ Author: Omina Project Maintainers
6
+ License-Expression: Apache-2.0
7
+ Classifier: License :: OSI Approved :: Apache Software License
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
11
+ Requires-Python: >=3.9
12
+ Requires-Dist: httpx>=0.27.0
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
15
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
16
+ Description-Content-Type: text/markdown
17
+
18
+ # Omina Python Client SDK (`omina-client`)
19
+
20
+ The official Python client SDK for the **Omina** intelligence daemon.
21
+
22
+ Designed for downstream systems engineers, telemetry monitoring pipelines, robotics scripts, and automated control loops.
23
+
24
+ ## Features
25
+
26
+ - **Type-Safe Primitive Extraction**: Direct extraction of continuous `Gauge`, binary `Gate`, categorical `Route`, semantic `Noul`, discrete `Choice`, and ordinal `Score` heads.
27
+ - **Transparent Backpressure Resilience**: Automatic exponential backoff with full jitter on HTTP `429 OVERLOADED` and `503 DEADLINE_EXCEEDED` respecting `Retry-After` headers.
28
+ - **Hardware Fallback Awareness**: Immediate detection of neural graph fallbacks (`response.is_fallback`, `fallback_triggered`).
29
+ - **Synchronous & Asynchronous**: High-performance sync (`OminaClient`) and async (`AsyncOminaClient`) clients built on top of `httpx`.
30
+
31
+ ## Installation
32
+
33
+ ```sh
34
+ pip install omina-client
35
+ ```
36
+
37
+ ## Quickstart
38
+
39
+ ```python
40
+ from omina_client import OminaClient
41
+ from omina_client.exceptions import OminaOverloadError
42
+
43
+ client = OminaClient(
44
+ base_url="http://localhost:8080",
45
+ max_retries=3,
46
+ base_backoff_ms=50
47
+ )
48
+
49
+ try:
50
+ response = client.evaluate(
51
+ domain_id="drone_flight",
52
+ context_log="Winds at 40 knots. Vision sensor degraded. GPS altitude spiking."
53
+ )
54
+
55
+ # 1. Dictionary-like or attribute access
56
+ altitude = response.gauges["altitude"].predicted_value
57
+ sigma = response.gauges["altitude"].uncertainty_sigma
58
+ is_breached = response.gates["geofence_breach"].decision
59
+ maneuver = response.routes["maneuver"].selected
60
+
61
+ # 2. Direct helper access
62
+ alt = response.gauge("altitude").predicted_value
63
+ breached = response.gate("geofence_breach") # returns bool
64
+ man = response.route("maneuver") # returns str
65
+
66
+ # 3. Fallback inspection
67
+ if response.gauges["altitude"].fallback_triggered:
68
+ print("Warning: Altitude gauge executed safe fallback envelope!")
69
+ if response.is_fallback:
70
+ print("Degraded heads:", response.fallback_heads)
71
+
72
+ print(f"Maneuver: {maneuver}, Altitude: {altitude}m (±{sigma})")
73
+
74
+ except OminaOverloadError as e:
75
+ print(f"Edge daemon saturated after {e.retries} retries: {e}")
76
+ ```
77
+
78
+ ## Async Usage
79
+
80
+ ```python
81
+ import asyncio
82
+ from omina_client import AsyncOminaClient
83
+
84
+ async def main():
85
+ async with AsyncOminaClient("http://localhost:8080") as client:
86
+ response = await client.evaluate("drone_flight", "Clear skies, nominal telemetry")
87
+ print("Altitude:", response.gauge("altitude").predicted_value)
88
+
89
+ asyncio.run(main())
90
+ ```
@@ -0,0 +1,73 @@
1
+ # Omina Python Client SDK (`omina-client`)
2
+
3
+ The official Python client SDK for the **Omina** intelligence daemon.
4
+
5
+ Designed for downstream systems engineers, telemetry monitoring pipelines, robotics scripts, and automated control loops.
6
+
7
+ ## Features
8
+
9
+ - **Type-Safe Primitive Extraction**: Direct extraction of continuous `Gauge`, binary `Gate`, categorical `Route`, semantic `Noul`, discrete `Choice`, and ordinal `Score` heads.
10
+ - **Transparent Backpressure Resilience**: Automatic exponential backoff with full jitter on HTTP `429 OVERLOADED` and `503 DEADLINE_EXCEEDED` respecting `Retry-After` headers.
11
+ - **Hardware Fallback Awareness**: Immediate detection of neural graph fallbacks (`response.is_fallback`, `fallback_triggered`).
12
+ - **Synchronous & Asynchronous**: High-performance sync (`OminaClient`) and async (`AsyncOminaClient`) clients built on top of `httpx`.
13
+
14
+ ## Installation
15
+
16
+ ```sh
17
+ pip install omina-client
18
+ ```
19
+
20
+ ## Quickstart
21
+
22
+ ```python
23
+ from omina_client import OminaClient
24
+ from omina_client.exceptions import OminaOverloadError
25
+
26
+ client = OminaClient(
27
+ base_url="http://localhost:8080",
28
+ max_retries=3,
29
+ base_backoff_ms=50
30
+ )
31
+
32
+ try:
33
+ response = client.evaluate(
34
+ domain_id="drone_flight",
35
+ context_log="Winds at 40 knots. Vision sensor degraded. GPS altitude spiking."
36
+ )
37
+
38
+ # 1. Dictionary-like or attribute access
39
+ altitude = response.gauges["altitude"].predicted_value
40
+ sigma = response.gauges["altitude"].uncertainty_sigma
41
+ is_breached = response.gates["geofence_breach"].decision
42
+ maneuver = response.routes["maneuver"].selected
43
+
44
+ # 2. Direct helper access
45
+ alt = response.gauge("altitude").predicted_value
46
+ breached = response.gate("geofence_breach") # returns bool
47
+ man = response.route("maneuver") # returns str
48
+
49
+ # 3. Fallback inspection
50
+ if response.gauges["altitude"].fallback_triggered:
51
+ print("Warning: Altitude gauge executed safe fallback envelope!")
52
+ if response.is_fallback:
53
+ print("Degraded heads:", response.fallback_heads)
54
+
55
+ print(f"Maneuver: {maneuver}, Altitude: {altitude}m (±{sigma})")
56
+
57
+ except OminaOverloadError as e:
58
+ print(f"Edge daemon saturated after {e.retries} retries: {e}")
59
+ ```
60
+
61
+ ## Async Usage
62
+
63
+ ```python
64
+ import asyncio
65
+ from omina_client import AsyncOminaClient
66
+
67
+ async def main():
68
+ async with AsyncOminaClient("http://localhost:8080") as client:
69
+ response = await client.evaluate("drone_flight", "Clear skies, nominal telemetry")
70
+ print("Altitude:", response.gauge("altitude").predicted_value)
71
+
72
+ asyncio.run(main())
73
+ ```
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "omina-client"
7
+ version = "1.0.0"
8
+ description = "Official Python client SDK for the Omina edge intelligence daemon"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "Apache-2.0"
12
+ authors = [
13
+ { name = "Omina Project Maintainers" }
14
+ ]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "License :: OSI Approved :: Apache Software License",
18
+ "Operating System :: OS Independent",
19
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
20
+ ]
21
+ dependencies = [
22
+ "httpx>=0.27.0",
23
+ ]
24
+
25
+ [project.optional-dependencies]
26
+ dev = [
27
+ "pytest>=8.0.0",
28
+ "pytest-asyncio>=0.23.0",
29
+ ]
30
+
31
+ [tool.pytest.ini_options]
32
+ asyncio_mode = "auto"
33
+ testpaths = ["tests"]
34
+
35
+ [tool.hatch.build.targets.wheel]
36
+ packages = ["src/omina_client"]
@@ -0,0 +1,50 @@
1
+ """Omina Official Python Client SDK."""
2
+
3
+ from omina_client.client import AsyncOminaClient, OminaClient
4
+ from omina_client.exceptions import (
5
+ OminaBadRequestError,
6
+ OminaDeadlineError,
7
+ OminaError,
8
+ OminaHeadNotFoundError,
9
+ OminaInternalServerError,
10
+ OminaNotFoundError,
11
+ OminaOverloadError,
12
+ OminaSchemaError,
13
+ OminaUnprocessableEntityError,
14
+ )
15
+ from omina_client.models import (
16
+ ChoiceResult,
17
+ EvaluationResponse,
18
+ GateResult,
19
+ GaugeResult,
20
+ MultiDomainResponse,
21
+ NoulResult,
22
+ RouteResult,
23
+ ScoreResult,
24
+ SystemHealthResponse,
25
+ )
26
+
27
+ __version__ = "1.0.0"
28
+
29
+ __all__ = [
30
+ "OminaClient",
31
+ "AsyncOminaClient",
32
+ "EvaluationResponse",
33
+ "MultiDomainResponse",
34
+ "SystemHealthResponse",
35
+ "GaugeResult",
36
+ "GateResult",
37
+ "RouteResult",
38
+ "NoulResult",
39
+ "ChoiceResult",
40
+ "ScoreResult",
41
+ "OminaError",
42
+ "OminaOverloadError",
43
+ "OminaDeadlineError",
44
+ "OminaNotFoundError",
45
+ "OminaBadRequestError",
46
+ "OminaUnprocessableEntityError",
47
+ "OminaInternalServerError",
48
+ "OminaHeadNotFoundError",
49
+ "OminaSchemaError",
50
+ ]
@@ -0,0 +1,280 @@
1
+ """Synchronous and Asynchronous HTTP clients for the Omina Edge Serving Daemon."""
2
+
3
+ import time
4
+ import asyncio
5
+ from typing import Any, Dict, List, Optional
6
+ import httpx
7
+
8
+ from omina_client.exceptions import (
9
+ OminaBadRequestError,
10
+ OminaDeadlineError,
11
+ OminaError,
12
+ OminaInternalServerError,
13
+ OminaNotFoundError,
14
+ OminaOverloadError,
15
+ OminaUnprocessableEntityError,
16
+ )
17
+ from omina_client.models import (
18
+ EvaluationResponse,
19
+ MultiDomainResponse,
20
+ SystemHealthResponse,
21
+ )
22
+ from omina_client.retry import compute_backoff, is_retryable_status, parse_retry_after
23
+
24
+
25
+ def _extract_error_message(resp: httpx.Response) -> str:
26
+ try:
27
+ data = resp.json()
28
+ if isinstance(data, dict):
29
+ err = data.get("error")
30
+ details = data.get("details")
31
+ if err and details:
32
+ return f"{err}: {details}"
33
+ if err:
34
+ return str(err)
35
+ except Exception:
36
+ pass
37
+ text = resp.text.strip()
38
+ return text if text else f"HTTP {resp.status_code}"
39
+
40
+
41
+ class OminaClient:
42
+ """Synchronous client for the Omina edge intelligence daemon with built-in backpressure handling."""
43
+
44
+ def __init__(
45
+ self,
46
+ base_url: str,
47
+ max_retries: int = 3,
48
+ base_backoff_ms: float = 50.0,
49
+ max_backoff_ms: float = 2000.0,
50
+ timeout: float = 10.0,
51
+ bearer_token: Optional[str] = None,
52
+ ):
53
+ self.base_url = base_url.rstrip("/")
54
+ self.max_retries = max_retries
55
+ self.base_backoff_ms = base_backoff_ms
56
+ self.max_backoff_ms = max_backoff_ms
57
+ self.timeout = timeout
58
+ self.bearer_token = bearer_token
59
+
60
+ headers = {"Content-Type": "application/json"}
61
+ if self.bearer_token:
62
+ headers["Authorization"] = f"Bearer {self.bearer_token}"
63
+
64
+ self._client = httpx.Client(base_url=self.base_url, headers=headers, timeout=self.timeout)
65
+
66
+ def close(self) -> None:
67
+ self._client.close()
68
+
69
+ def __enter__(self) -> "OminaClient":
70
+ return self
71
+
72
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
73
+ self.close()
74
+
75
+ def evaluate(self, domain_id: str, context_log: str) -> EvaluationResponse:
76
+ """Evaluates a context text log against a registered domain head."""
77
+ payload = {"domain_id": domain_id, "context_log": context_log}
78
+ data = self._dispatch_post("/v1/evaluate", payload)
79
+ return EvaluationResponse.from_dict(data)
80
+
81
+ def evaluate_frame(
82
+ self, context_log: str, domains: Optional[List[str]] = None
83
+ ) -> MultiDomainResponse:
84
+ """Evaluates a context frame across multiple or all registered domains."""
85
+ payload: Dict[str, Any] = {"context_log": context_log}
86
+ if domains is not None:
87
+ payload["domains"] = domains
88
+ data = self._dispatch_post("/v1/evaluate/frame", payload)
89
+ return MultiDomainResponse.from_dict(data)
90
+
91
+ def health(self) -> SystemHealthResponse:
92
+ """Checks daemon health and resident encoder state via GET /system/health."""
93
+ data = self._dispatch_get("/system/health")
94
+ return SystemHealthResponse.from_dict(data)
95
+
96
+ def health_check(self) -> SystemHealthResponse:
97
+ """Alias for health()."""
98
+ return self.health()
99
+
100
+ def _dispatch_post(self, path: str, payload: Dict[str, Any]) -> Dict[str, Any]:
101
+ attempt = 0
102
+ while True:
103
+ try:
104
+ resp = self._client.post(path, json=payload)
105
+ if resp.is_success:
106
+ return resp.json()
107
+
108
+ status = resp.status_code
109
+ if is_retryable_status(status):
110
+ retry_after = parse_retry_after(resp.headers)
111
+ err_msg = _extract_error_message(resp)
112
+
113
+ if attempt >= self.max_retries:
114
+ if status == 429:
115
+ raise OminaOverloadError(err_msg, retries=attempt, retry_after=retry_after)
116
+ else:
117
+ raise OminaDeadlineError(err_msg, retries=attempt)
118
+
119
+ delay = compute_backoff(
120
+ attempt, self.base_backoff_ms, self.max_backoff_ms, retry_after
121
+ )
122
+ time.sleep(delay)
123
+ attempt += 1
124
+ continue
125
+
126
+ # Non-retryable error
127
+ err_msg = _extract_error_message(resp)
128
+ self._raise_for_status(status, err_msg)
129
+
130
+ except httpx.TransportError as e:
131
+ if attempt < self.max_retries:
132
+ delay = compute_backoff(attempt, self.base_backoff_ms, self.max_backoff_ms)
133
+ time.sleep(delay)
134
+ attempt += 1
135
+ continue
136
+ raise OminaError(f"HTTP transport failure: {e}") from e
137
+
138
+ def _dispatch_get(self, path: str) -> Dict[str, Any]:
139
+ try:
140
+ resp = self._client.get(path)
141
+ if resp.is_success:
142
+ return resp.json()
143
+ err_msg = _extract_error_message(resp)
144
+ self._raise_for_status(resp.status_code, err_msg)
145
+ return resp.json()
146
+ except httpx.TransportError as e:
147
+ raise OminaError(f"HTTP transport failure: {e}") from e
148
+
149
+ def _raise_for_status(self, status: int, err_msg: str) -> None:
150
+ if status == 404:
151
+ raise OminaNotFoundError(err_msg)
152
+ elif status == 400:
153
+ raise OminaBadRequestError(err_msg)
154
+ elif status == 422:
155
+ raise OminaUnprocessableEntityError(err_msg)
156
+ elif status == 500:
157
+ raise OminaInternalServerError(err_msg)
158
+ else:
159
+ raise OminaError(f"HTTP {status}: {err_msg}")
160
+
161
+
162
+ class AsyncOminaClient:
163
+ """Asynchronous client for the Omina edge intelligence daemon with built-in backpressure handling."""
164
+
165
+ def __init__(
166
+ self,
167
+ base_url: str,
168
+ max_retries: int = 3,
169
+ base_backoff_ms: float = 50.0,
170
+ max_backoff_ms: float = 2000.0,
171
+ timeout: float = 10.0,
172
+ bearer_token: Optional[str] = None,
173
+ ):
174
+ self.base_url = base_url.rstrip("/")
175
+ self.max_retries = max_retries
176
+ self.base_backoff_ms = base_backoff_ms
177
+ self.max_backoff_ms = max_backoff_ms
178
+ self.timeout = timeout
179
+ self.bearer_token = bearer_token
180
+
181
+ headers = {"Content-Type": "application/json"}
182
+ if self.bearer_token:
183
+ headers["Authorization"] = f"Bearer {self.bearer_token}"
184
+
185
+ self._client = httpx.AsyncClient(base_url=self.base_url, headers=headers, timeout=self.timeout)
186
+
187
+ async def aclose(self) -> None:
188
+ await self._client.aclose()
189
+
190
+ async def __aenter__(self) -> "AsyncOminaClient":
191
+ return self
192
+
193
+ async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
194
+ await self.aclose()
195
+
196
+ async def evaluate(self, domain_id: str, context_log: str) -> EvaluationResponse:
197
+ """Evaluates a context text log against a registered domain head."""
198
+ payload = {"domain_id": domain_id, "context_log": context_log}
199
+ data = await self._dispatch_post("/v1/evaluate", payload)
200
+ return EvaluationResponse.from_dict(data)
201
+
202
+ async def evaluate_frame(
203
+ self, context_log: str, domains: Optional[List[str]] = None
204
+ ) -> MultiDomainResponse:
205
+ """Evaluates a context frame across multiple or all registered domains."""
206
+ payload: Dict[str, Any] = {"context_log": context_log}
207
+ if domains is not None:
208
+ payload["domains"] = domains
209
+ data = await self._dispatch_post("/v1/evaluate/frame", payload)
210
+ return MultiDomainResponse.from_dict(data)
211
+
212
+ async def health(self) -> SystemHealthResponse:
213
+ """Checks daemon health and resident encoder state via GET /system/health."""
214
+ data = await self._dispatch_get("/system/health")
215
+ return SystemHealthResponse.from_dict(data)
216
+
217
+ async def health_check(self) -> SystemHealthResponse:
218
+ """Alias for health()."""
219
+ return await self.health()
220
+
221
+ async def _dispatch_post(self, path: str, payload: Dict[str, Any]) -> Dict[str, Any]:
222
+ attempt = 0
223
+ while True:
224
+ try:
225
+ resp = await self._client.post(path, json=payload)
226
+ if resp.is_success:
227
+ return resp.json()
228
+
229
+ status = resp.status_code
230
+ if is_retryable_status(status):
231
+ retry_after = parse_retry_after(resp.headers)
232
+ err_msg = _extract_error_message(resp)
233
+
234
+ if attempt >= self.max_retries:
235
+ if status == 429:
236
+ raise OminaOverloadError(err_msg, retries=attempt, retry_after=retry_after)
237
+ else:
238
+ raise OminaDeadlineError(err_msg, retries=attempt)
239
+
240
+ delay = compute_backoff(
241
+ attempt, self.base_backoff_ms, self.max_backoff_ms, retry_after
242
+ )
243
+ await asyncio.sleep(delay)
244
+ attempt += 1
245
+ continue
246
+
247
+ # Non-retryable error
248
+ err_msg = _extract_error_message(resp)
249
+ self._raise_for_status(status, err_msg)
250
+
251
+ except httpx.TransportError as e:
252
+ if attempt < self.max_retries:
253
+ delay = compute_backoff(attempt, self.base_backoff_ms, self.max_backoff_ms)
254
+ await asyncio.sleep(delay)
255
+ attempt += 1
256
+ continue
257
+ raise OminaError(f"HTTP transport failure: {e}") from e
258
+
259
+ async def _dispatch_get(self, path: str) -> Dict[str, Any]:
260
+ try:
261
+ resp = await self._client.get(path)
262
+ if resp.is_success:
263
+ return resp.json()
264
+ err_msg = _extract_error_message(resp)
265
+ self._raise_for_status(resp.status_code, err_msg)
266
+ return resp.json()
267
+ except httpx.TransportError as e:
268
+ raise OminaError(f"HTTP transport failure: {e}") from e
269
+
270
+ def _raise_for_status(self, status: int, err_msg: str) -> None:
271
+ if status == 404:
272
+ raise OminaNotFoundError(err_msg)
273
+ elif status == 400:
274
+ raise OminaBadRequestError(err_msg)
275
+ elif status == 422:
276
+ raise OminaUnprocessableEntityError(err_msg)
277
+ elif status == 500:
278
+ raise OminaInternalServerError(err_msg)
279
+ else:
280
+ raise OminaError(f"HTTP {status}: {err_msg}")
@@ -0,0 +1,61 @@
1
+ """Exceptions emitted by the Omina Client SDK."""
2
+
3
+ from typing import Optional
4
+
5
+
6
+ class OminaError(Exception):
7
+ """Base exception for all Omina client errors."""
8
+ pass
9
+
10
+
11
+ class OminaOverloadError(OminaError):
12
+ """Raised when the edge daemon's inference queue is saturated (HTTP 429)."""
13
+
14
+ def __init__(self, message: str, retries: int = 0, retry_after: Optional[float] = None):
15
+ super().__init__(f"{message} (exhausted {retries} retries)")
16
+ self.message = message
17
+ self.retries = retries
18
+ self.retry_after = retry_after
19
+
20
+
21
+ class OminaDeadlineError(OminaError):
22
+ """Raised when an inference request exceeds the server-side deadline (HTTP 503/504)."""
23
+
24
+ def __init__(self, message: str, retries: int = 0):
25
+ super().__init__(f"{message} (exhausted {retries} retries)")
26
+ self.message = message
27
+ self.retries = retries
28
+
29
+
30
+ class OminaNotFoundError(OminaError):
31
+ """Raised when the requested domain or endpoint does not exist on the daemon (HTTP 404)."""
32
+ pass
33
+
34
+
35
+ class OminaBadRequestError(OminaError):
36
+ """Raised when the client request payload is malformed (HTTP 400)."""
37
+ pass
38
+
39
+
40
+ class OminaUnprocessableEntityError(OminaError):
41
+ """Raised when the request payload cannot be processed (HTTP 422)."""
42
+ pass
43
+
44
+
45
+ class OminaInternalServerError(OminaError):
46
+ """Raised when the daemon encounters an unhandled internal error (HTTP 500)."""
47
+ pass
48
+
49
+
50
+ class OminaHeadNotFoundError(OminaError):
51
+ """Raised when attempting to retrieve an unknown head ID from an evaluation response."""
52
+
53
+ def __init__(self, head_id: str, domain_id: str):
54
+ super().__init__(f"Head '{head_id}' not found in domain '{domain_id}' response")
55
+ self.head_id = head_id
56
+ self.domain_id = domain_id
57
+
58
+
59
+ class OminaSchemaError(OminaError):
60
+ """Raised when a response fails schema validation or JSON decoding."""
61
+ pass