kaether 0.1.0__py3-none-any.whl
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.
- aether_sdk/__init__.py +18 -0
- aether_sdk/client.py +277 -0
- aether_sdk/exceptions.py +19 -0
- aether_sdk/models.py +177 -0
- aether_sdk/pressure.py +21 -0
- kaether-0.1.0.dist-info/METADATA +9 -0
- kaether-0.1.0.dist-info/RECORD +9 -0
- kaether-0.1.0.dist-info/WHEEL +5 -0
- kaether-0.1.0.dist-info/top_level.txt +1 -0
aether_sdk/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from aether_sdk.client import Aether, AsyncAether
|
|
2
|
+
from aether_sdk.exceptions import AetherError, AetherHTTPError, AetherJobError
|
|
3
|
+
from aether_sdk.models import Forecast, GhostResult, IngestResult, TimelineFrame
|
|
4
|
+
from aether_sdk.pressure import pressure_band, pressure_color
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"Aether",
|
|
8
|
+
"AetherError",
|
|
9
|
+
"AetherHTTPError",
|
|
10
|
+
"AetherJobError",
|
|
11
|
+
"AsyncAether",
|
|
12
|
+
"Forecast",
|
|
13
|
+
"GhostResult",
|
|
14
|
+
"IngestResult",
|
|
15
|
+
"TimelineFrame",
|
|
16
|
+
"pressure_band",
|
|
17
|
+
"pressure_color",
|
|
18
|
+
]
|
aether_sdk/client.py
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from aether_sdk.exceptions import AetherHTTPError, AetherJobError
|
|
10
|
+
from aether_sdk.models import (
|
|
11
|
+
ChangeSet,
|
|
12
|
+
Forecast,
|
|
13
|
+
GraphSlice,
|
|
14
|
+
IngestResult,
|
|
15
|
+
JobAccepted,
|
|
16
|
+
JobStatus,
|
|
17
|
+
UniverseSummary,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
ProgressFn = Callable[[int, str], None]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _raise_for_status(response: httpx.Response) -> None:
|
|
24
|
+
if response.is_success:
|
|
25
|
+
return
|
|
26
|
+
detail = response.text
|
|
27
|
+
try:
|
|
28
|
+
payload = response.json()
|
|
29
|
+
if isinstance(payload, dict):
|
|
30
|
+
detail = str(payload.get("detail", payload))
|
|
31
|
+
except ValueError:
|
|
32
|
+
pass
|
|
33
|
+
raise AetherHTTPError(response.status_code, detail)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Aether:
|
|
37
|
+
"""Sync client for the Aether engine HTTP API."""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
base_url: str = "http://127.0.0.1:8000",
|
|
42
|
+
*,
|
|
43
|
+
timeout: float = 60.0,
|
|
44
|
+
headers: dict[str, str] | None = None,
|
|
45
|
+
transport: httpx.BaseTransport | None = None,
|
|
46
|
+
) -> None:
|
|
47
|
+
self.base_url = base_url.rstrip("/")
|
|
48
|
+
self._client = httpx.Client(
|
|
49
|
+
base_url=self.base_url,
|
|
50
|
+
timeout=timeout,
|
|
51
|
+
headers=headers,
|
|
52
|
+
transport=transport,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def close(self) -> None:
|
|
56
|
+
self._client.close()
|
|
57
|
+
|
|
58
|
+
def __enter__(self) -> Aether:
|
|
59
|
+
return self
|
|
60
|
+
|
|
61
|
+
def __exit__(self, *exc: object) -> None:
|
|
62
|
+
self.close()
|
|
63
|
+
|
|
64
|
+
def health(self) -> dict[str, Any]:
|
|
65
|
+
return self._get("/health")
|
|
66
|
+
|
|
67
|
+
def universes(self) -> list[UniverseSummary]:
|
|
68
|
+
rows = self._get("/v1/universes")
|
|
69
|
+
return [UniverseSummary.model_validate(row) for row in rows]
|
|
70
|
+
|
|
71
|
+
def ingest(
|
|
72
|
+
self,
|
|
73
|
+
*,
|
|
74
|
+
path: str = "",
|
|
75
|
+
url: str = "",
|
|
76
|
+
pr_ref: str = "",
|
|
77
|
+
velocity: float | None = None,
|
|
78
|
+
wait: bool = True,
|
|
79
|
+
poll_interval: float = 0.35,
|
|
80
|
+
on_progress: ProgressFn | None = None,
|
|
81
|
+
) -> IngestResult | JobAccepted:
|
|
82
|
+
"""Start an ingest job. By default waits until the forecast is ready."""
|
|
83
|
+
body = {
|
|
84
|
+
"path": path,
|
|
85
|
+
"url": url,
|
|
86
|
+
"pr_ref": pr_ref,
|
|
87
|
+
"velocity_override": velocity,
|
|
88
|
+
}
|
|
89
|
+
accepted = JobAccepted.model_validate(self._post("/v1/jobs", body))
|
|
90
|
+
if not wait:
|
|
91
|
+
return accepted
|
|
92
|
+
return self.wait_job(accepted.job_id, poll_interval=poll_interval, on_progress=on_progress)
|
|
93
|
+
|
|
94
|
+
def ingest_now(
|
|
95
|
+
self,
|
|
96
|
+
*,
|
|
97
|
+
path: str = "",
|
|
98
|
+
url: str = "",
|
|
99
|
+
pr_ref: str = "",
|
|
100
|
+
velocity: float | None = None,
|
|
101
|
+
) -> IngestResult:
|
|
102
|
+
"""Blocking ingest on the request thread (small repos)."""
|
|
103
|
+
return IngestResult.model_validate(
|
|
104
|
+
self._post(
|
|
105
|
+
"/v1/universes",
|
|
106
|
+
{
|
|
107
|
+
"path": path,
|
|
108
|
+
"url": url,
|
|
109
|
+
"pr_ref": pr_ref,
|
|
110
|
+
"velocity_override": velocity,
|
|
111
|
+
},
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def job(self, job_id: str) -> JobStatus:
|
|
116
|
+
return JobStatus.model_validate(self._get(f"/v1/jobs/{job_id}"))
|
|
117
|
+
|
|
118
|
+
def wait_job(
|
|
119
|
+
self,
|
|
120
|
+
job_id: str,
|
|
121
|
+
*,
|
|
122
|
+
poll_interval: float = 0.35,
|
|
123
|
+
on_progress: ProgressFn | None = None,
|
|
124
|
+
) -> IngestResult:
|
|
125
|
+
while True:
|
|
126
|
+
status = self.job(job_id)
|
|
127
|
+
if on_progress:
|
|
128
|
+
on_progress(status.percent, status.stage)
|
|
129
|
+
if status.failed:
|
|
130
|
+
raise AetherJobError(job_id, status.error or "ingest failed")
|
|
131
|
+
if status.done and status.result:
|
|
132
|
+
return status.result
|
|
133
|
+
time.sleep(poll_interval)
|
|
134
|
+
|
|
135
|
+
def forecast(self, universe_id: str, horizon_months: int = 24) -> Forecast:
|
|
136
|
+
return Forecast.model_validate(
|
|
137
|
+
self._get(f"/v1/universes/{universe_id}/forecast", {"horizon_months": horizon_months})
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def graph(self, universe_id: str, t: int = 0) -> GraphSlice:
|
|
141
|
+
return GraphSlice.model_validate(self._get(f"/v1/universes/{universe_id}/graph", {"t": t}))
|
|
142
|
+
|
|
143
|
+
def attach_changeset(self, universe_id: str, changeset: ChangeSet | dict[str, Any]) -> ChangeSet:
|
|
144
|
+
payload = changeset.model_dump() if isinstance(changeset, ChangeSet) else changeset
|
|
145
|
+
return ChangeSet.model_validate(self._post(f"/v1/universes/{universe_id}/changesets", payload))
|
|
146
|
+
|
|
147
|
+
def _get(self, path: str, params: dict[str, Any] | None = None) -> Any:
|
|
148
|
+
response = self._client.get(path, params=params)
|
|
149
|
+
_raise_for_status(response)
|
|
150
|
+
return response.json()
|
|
151
|
+
|
|
152
|
+
def _post(self, path: str, body: dict[str, Any]) -> Any:
|
|
153
|
+
response = self._client.post(path, json=body)
|
|
154
|
+
_raise_for_status(response)
|
|
155
|
+
return response.json()
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class AsyncAether:
|
|
159
|
+
"""Async client for the Aether engine HTTP API."""
|
|
160
|
+
|
|
161
|
+
def __init__(
|
|
162
|
+
self,
|
|
163
|
+
base_url: str = "http://127.0.0.1:8000",
|
|
164
|
+
*,
|
|
165
|
+
timeout: float = 60.0,
|
|
166
|
+
headers: dict[str, str] | None = None,
|
|
167
|
+
transport: httpx.AsyncBaseTransport | None = None,
|
|
168
|
+
) -> None:
|
|
169
|
+
self.base_url = base_url.rstrip("/")
|
|
170
|
+
self._client = httpx.AsyncClient(
|
|
171
|
+
base_url=self.base_url,
|
|
172
|
+
timeout=timeout,
|
|
173
|
+
headers=headers,
|
|
174
|
+
transport=transport,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
async def aclose(self) -> None:
|
|
178
|
+
await self._client.aclose()
|
|
179
|
+
|
|
180
|
+
async def __aenter__(self) -> AsyncAether:
|
|
181
|
+
return self
|
|
182
|
+
|
|
183
|
+
async def __aexit__(self, *exc: object) -> None:
|
|
184
|
+
await self.aclose()
|
|
185
|
+
|
|
186
|
+
async def health(self) -> dict[str, Any]:
|
|
187
|
+
return await self._get("/health")
|
|
188
|
+
|
|
189
|
+
async def universes(self) -> list[UniverseSummary]:
|
|
190
|
+
rows = await self._get("/v1/universes")
|
|
191
|
+
return [UniverseSummary.model_validate(row) for row in rows]
|
|
192
|
+
|
|
193
|
+
async def ingest(
|
|
194
|
+
self,
|
|
195
|
+
*,
|
|
196
|
+
path: str = "",
|
|
197
|
+
url: str = "",
|
|
198
|
+
pr_ref: str = "",
|
|
199
|
+
velocity: float | None = None,
|
|
200
|
+
wait: bool = True,
|
|
201
|
+
poll_interval: float = 0.35,
|
|
202
|
+
on_progress: ProgressFn | None = None,
|
|
203
|
+
) -> IngestResult | JobAccepted:
|
|
204
|
+
body = {
|
|
205
|
+
"path": path,
|
|
206
|
+
"url": url,
|
|
207
|
+
"pr_ref": pr_ref,
|
|
208
|
+
"velocity_override": velocity,
|
|
209
|
+
}
|
|
210
|
+
accepted = JobAccepted.model_validate(await self._post("/v1/jobs", body))
|
|
211
|
+
if not wait:
|
|
212
|
+
return accepted
|
|
213
|
+
return await self.wait_job(accepted.job_id, poll_interval=poll_interval, on_progress=on_progress)
|
|
214
|
+
|
|
215
|
+
async def ingest_now(
|
|
216
|
+
self,
|
|
217
|
+
*,
|
|
218
|
+
path: str = "",
|
|
219
|
+
url: str = "",
|
|
220
|
+
pr_ref: str = "",
|
|
221
|
+
velocity: float | None = None,
|
|
222
|
+
) -> IngestResult:
|
|
223
|
+
return IngestResult.model_validate(
|
|
224
|
+
await self._post(
|
|
225
|
+
"/v1/universes",
|
|
226
|
+
{
|
|
227
|
+
"path": path,
|
|
228
|
+
"url": url,
|
|
229
|
+
"pr_ref": pr_ref,
|
|
230
|
+
"velocity_override": velocity,
|
|
231
|
+
},
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
async def job(self, job_id: str) -> JobStatus:
|
|
236
|
+
return JobStatus.model_validate(await self._get(f"/v1/jobs/{job_id}"))
|
|
237
|
+
|
|
238
|
+
async def wait_job(
|
|
239
|
+
self,
|
|
240
|
+
job_id: str,
|
|
241
|
+
*,
|
|
242
|
+
poll_interval: float = 0.35,
|
|
243
|
+
on_progress: ProgressFn | None = None,
|
|
244
|
+
) -> IngestResult:
|
|
245
|
+
import asyncio
|
|
246
|
+
|
|
247
|
+
while True:
|
|
248
|
+
status = await self.job(job_id)
|
|
249
|
+
if on_progress:
|
|
250
|
+
on_progress(status.percent, status.stage)
|
|
251
|
+
if status.failed:
|
|
252
|
+
raise AetherJobError(job_id, status.error or "ingest failed")
|
|
253
|
+
if status.done and status.result:
|
|
254
|
+
return status.result
|
|
255
|
+
await asyncio.sleep(poll_interval)
|
|
256
|
+
|
|
257
|
+
async def forecast(self, universe_id: str, horizon_months: int = 24) -> Forecast:
|
|
258
|
+
return Forecast.model_validate(
|
|
259
|
+
await self._get(f"/v1/universes/{universe_id}/forecast", {"horizon_months": horizon_months})
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
async def graph(self, universe_id: str, t: int = 0) -> GraphSlice:
|
|
263
|
+
return GraphSlice.model_validate(await self._get(f"/v1/universes/{universe_id}/graph", {"t": t}))
|
|
264
|
+
|
|
265
|
+
async def attach_changeset(self, universe_id: str, changeset: ChangeSet | dict[str, Any]) -> ChangeSet:
|
|
266
|
+
payload = changeset.model_dump() if isinstance(changeset, ChangeSet) else changeset
|
|
267
|
+
return ChangeSet.model_validate(await self._post(f"/v1/universes/{universe_id}/changesets", payload))
|
|
268
|
+
|
|
269
|
+
async def _get(self, path: str, params: dict[str, Any] | None = None) -> Any:
|
|
270
|
+
response = await self._client.get(path, params=params)
|
|
271
|
+
_raise_for_status(response)
|
|
272
|
+
return response.json()
|
|
273
|
+
|
|
274
|
+
async def _post(self, path: str, body: dict[str, Any]) -> Any:
|
|
275
|
+
response = await self._client.post(path, json=body)
|
|
276
|
+
_raise_for_status(response)
|
|
277
|
+
return response.json()
|
aether_sdk/exceptions.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AetherError(Exception):
|
|
5
|
+
"""Base SDK error."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AetherHTTPError(AetherError):
|
|
9
|
+
def __init__(self, status_code: int, detail: str) -> None:
|
|
10
|
+
self.status_code = status_code
|
|
11
|
+
self.detail = detail
|
|
12
|
+
super().__init__(f"{status_code}: {detail}")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AetherJobError(AetherError):
|
|
16
|
+
def __init__(self, job_id: str, error: str) -> None:
|
|
17
|
+
self.job_id = job_id
|
|
18
|
+
self.error = error
|
|
19
|
+
super().__init__(f"job {job_id} failed: {error}")
|
aether_sdk/models.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Literal
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from aether_sdk.pressure import Band, pressure_band
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class IngestResult(BaseModel):
|
|
11
|
+
universe_id: str
|
|
12
|
+
repo_path: str
|
|
13
|
+
license: str = ""
|
|
14
|
+
snapshots: int = 0
|
|
15
|
+
velocity_commits_per_week: float = 1.0
|
|
16
|
+
warnings: list[str] = Field(default_factory=list)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class JobAccepted(BaseModel):
|
|
20
|
+
job_id: str
|
|
21
|
+
status: str
|
|
22
|
+
percent: int = 0
|
|
23
|
+
stage: str = "queued"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class JobStatus(BaseModel):
|
|
27
|
+
job_id: str
|
|
28
|
+
status: str
|
|
29
|
+
percent: int = 0
|
|
30
|
+
stage: str = ""
|
|
31
|
+
error: str = ""
|
|
32
|
+
result: IngestResult | None = None
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def done(self) -> bool:
|
|
36
|
+
return self.status == "done"
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def failed(self) -> bool:
|
|
40
|
+
return self.status == "error"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class UniverseSummary(BaseModel):
|
|
44
|
+
id: str
|
|
45
|
+
repo_path: str
|
|
46
|
+
license: str = ""
|
|
47
|
+
velocity: float = 1.0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class NodeMetrics(BaseModel):
|
|
51
|
+
node_id: str
|
|
52
|
+
kind: str
|
|
53
|
+
label: str
|
|
54
|
+
path: str = ""
|
|
55
|
+
lang: str = ""
|
|
56
|
+
mass: float
|
|
57
|
+
velocity: float
|
|
58
|
+
momentum: float
|
|
59
|
+
pressure: float
|
|
60
|
+
dependents: int = 0
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def band(self) -> Band:
|
|
64
|
+
return pressure_band(self.pressure)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Collision(BaseModel):
|
|
68
|
+
a: str
|
|
69
|
+
b: str
|
|
70
|
+
reason: str
|
|
71
|
+
intensity: float
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class TimelineFrame(BaseModel):
|
|
75
|
+
t_index: int
|
|
76
|
+
months_ahead: float
|
|
77
|
+
label: str
|
|
78
|
+
cells: list[NodeMetrics] = Field(default_factory=list)
|
|
79
|
+
collisions: list[Collision] = Field(default_factory=list)
|
|
80
|
+
narrative: str = ""
|
|
81
|
+
narrative_kind: Literal["rising_pressure", "bottleneck", "stable"] = "stable"
|
|
82
|
+
|
|
83
|
+
def hottest(self, n: int = 8) -> list[NodeMetrics]:
|
|
84
|
+
return sorted(self.cells, key=lambda c: c.pressure, reverse=True)[:n]
|
|
85
|
+
|
|
86
|
+
def by_band(self) -> dict[Band, list[NodeMetrics]]:
|
|
87
|
+
out: dict[Band, list[NodeMetrics]] = {"calm": [], "watch": [], "high-pressure": []}
|
|
88
|
+
for cell in self.cells:
|
|
89
|
+
out[cell.band].append(cell)
|
|
90
|
+
return out
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class ImpactedNode(BaseModel):
|
|
94
|
+
node_id: str
|
|
95
|
+
label: str
|
|
96
|
+
kind: str
|
|
97
|
+
intensity: float
|
|
98
|
+
path_type: str = ""
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class ButterflyFrame(BaseModel):
|
|
102
|
+
months: float
|
|
103
|
+
origin_ids: list[str] = Field(default_factory=list)
|
|
104
|
+
impacted: list[ImpactedNode] = Field(default_factory=list)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class GhostResult(BaseModel):
|
|
108
|
+
intent: str
|
|
109
|
+
title: str
|
|
110
|
+
verdict: Literal["pass", "warn", "fail"]
|
|
111
|
+
extensibility: float
|
|
112
|
+
files_touched: list[str] = Field(default_factory=list)
|
|
113
|
+
core_mass_hits: list[str] = Field(default_factory=list)
|
|
114
|
+
new_cycles: int = 0
|
|
115
|
+
contract_breaks: list[str] = Field(default_factory=list)
|
|
116
|
+
note: str = ""
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class CostBand(BaseModel):
|
|
120
|
+
month: int
|
|
121
|
+
compute: float
|
|
122
|
+
storage: float
|
|
123
|
+
egress: float
|
|
124
|
+
drivers: list[str] = Field(default_factory=list)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class Forecast(BaseModel):
|
|
128
|
+
universe_id: str
|
|
129
|
+
repo_path: str
|
|
130
|
+
ir_version: int = 1
|
|
131
|
+
horizon_months: int = 24
|
|
132
|
+
velocity_commits_per_week: float = 1.0
|
|
133
|
+
timeline: list[TimelineFrame] = Field(default_factory=list)
|
|
134
|
+
butterflies: list[ButterflyFrame] = Field(default_factory=list)
|
|
135
|
+
ghosts: list[GhostResult] = Field(default_factory=list)
|
|
136
|
+
costs: list[CostBand] = Field(default_factory=list)
|
|
137
|
+
warnings: list[str] = Field(default_factory=list)
|
|
138
|
+
heuristic: bool = True
|
|
139
|
+
license: str = ""
|
|
140
|
+
|
|
141
|
+
def frame(self, months: float = 8) -> TimelineFrame | None:
|
|
142
|
+
if not self.timeline:
|
|
143
|
+
return None
|
|
144
|
+
return min(self.timeline, key=lambda f: abs(f.months_ahead - months))
|
|
145
|
+
|
|
146
|
+
def hottest(self, n: int = 8, months: float = 8) -> list[NodeMetrics]:
|
|
147
|
+
frame = self.frame(months)
|
|
148
|
+
return frame.hottest(n) if frame else []
|
|
149
|
+
|
|
150
|
+
def storms(self, months: float = 8) -> list[Collision]:
|
|
151
|
+
frame = self.frame(months)
|
|
152
|
+
return list(frame.collisions) if frame else []
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class Mutation(BaseModel):
|
|
156
|
+
op: Literal["add", "remove", "retarget"]
|
|
157
|
+
node: dict[str, Any] | None = None
|
|
158
|
+
edge: dict[str, Any] | None = None
|
|
159
|
+
retarget_src: str = ""
|
|
160
|
+
retarget_dst: str = ""
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class ChangeSet(BaseModel):
|
|
164
|
+
id: str = ""
|
|
165
|
+
universe_id: str = ""
|
|
166
|
+
label: str = "changeset"
|
|
167
|
+
summary: str = ""
|
|
168
|
+
mutations: list[Mutation] = Field(default_factory=list)
|
|
169
|
+
touched_paths: list[str] = Field(default_factory=list)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class GraphSlice(BaseModel):
|
|
173
|
+
commit_sha: str = ""
|
|
174
|
+
authored_at: str = ""
|
|
175
|
+
nodes: list[dict[str, Any]] = Field(default_factory=list)
|
|
176
|
+
edges: list[dict[str, Any]] = Field(default_factory=list)
|
|
177
|
+
contracts: list[dict[str, Any]] = Field(default_factory=list)
|
aether_sdk/pressure.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
Band = Literal["calm", "watch", "high-pressure"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def pressure_band(pressure: float) -> Band:
|
|
9
|
+
if pressure < 0.6:
|
|
10
|
+
return "calm"
|
|
11
|
+
if pressure < 1.2:
|
|
12
|
+
return "watch"
|
|
13
|
+
return "high-pressure"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def pressure_color(pressure: float) -> str:
|
|
17
|
+
if pressure < 0.6:
|
|
18
|
+
return "#3d9ee0"
|
|
19
|
+
if pressure < 1.2:
|
|
20
|
+
return "#d4a017"
|
|
21
|
+
return "#e05a4f"
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kaether
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the Aether software physics engine
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: httpx>=0.27.0
|
|
7
|
+
Requires-Dist: pydantic>=2.9.0
|
|
8
|
+
Provides-Extra: dev
|
|
9
|
+
Requires-Dist: pytest>=8.3.0; extra == "dev"
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
aether_sdk/__init__.py,sha256=E4T8HK5w607wBmzOjXKIpac0fh70SNpMGZDwFTGS4DA,503
|
|
2
|
+
aether_sdk/client.py,sha256=_vVZvFjqMam6gHtwEdxY_nwd3e0H32KBJ7KLDxUwC5w,8852
|
|
3
|
+
aether_sdk/exceptions.py,sha256=SQa4x1O5XRfb6YVRGvO0YDA5rY1PHRM0-X9lTKZ_PbE,523
|
|
4
|
+
aether_sdk/models.py,sha256=a7c4jIkXFz0hDt8Wi9MTvoE-_eKi6wgwBcCrTwKvvXc,4560
|
|
5
|
+
aether_sdk/pressure.py,sha256=_7b2GKc-6imJ13gF3WtY7_S4TCyD4Z4Mzy1Un0rVTyw,440
|
|
6
|
+
kaether-0.1.0.dist-info/METADATA,sha256=7EfUfo9X4ueiybkv77pjwzUTJnn5D1A1LpbqKTLNx4s,259
|
|
7
|
+
kaether-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
8
|
+
kaether-0.1.0.dist-info/top_level.txt,sha256=uov1M5b-1qQVZ7HLAXfNaJ58ZlW1nAfz_rjidGiRVSY,11
|
|
9
|
+
kaether-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
aether_sdk
|