hugpy-control 0.2.0a0__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.
- hugpy_control/__init__.py +49 -0
- hugpy_control/bus.py +240 -0
- hugpy_control/calllog.py +131 -0
- hugpy_control/job_schemas.py +35 -0
- hugpy_control/jobs.py +1054 -0
- hugpy_control/principals.py +339 -0
- hugpy_control/py.typed +0 -0
- hugpy_control/settings.py +214 -0
- hugpy_control/shared.py +939 -0
- hugpy_control-0.2.0a0.dist-info/METADATA +35 -0
- hugpy_control-0.2.0a0.dist-info/RECORD +14 -0
- hugpy_control-0.2.0a0.dist-info/WHEEL +5 -0
- hugpy_control-0.2.0a0.dist-info/licenses/LICENSE +41 -0
- hugpy_control-0.2.0a0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""hugpy-control: the job/settings/principal control plane of the Hugpy ecosystem.
|
|
2
|
+
|
|
3
|
+
Submodules (all stdlib + ``hugpy_platform``; none import an engine, fleet or
|
|
4
|
+
server package):
|
|
5
|
+
|
|
6
|
+
bus in-process typed message bus + topic constants
|
|
7
|
+
jobs JobStore — the one all-transport job store (chat, media, downloads)
|
|
8
|
+
shared SqliteMirror — the cross-process mirror the JobStore rides on
|
|
9
|
+
settings SettingsStore — namespaced runtime settings (F4)
|
|
10
|
+
principals PrincipalStore — identities, tokens, capability checks (F2)
|
|
11
|
+
calllog append-only call log + tail reader
|
|
12
|
+
job_schemas compat re-export of the download-job names
|
|
13
|
+
|
|
14
|
+
Submodules are resolved lazily (PEP 562) so ``import hugpy_control`` stays
|
|
15
|
+
side-effect free: the store singletons are only created when the module that
|
|
16
|
+
owns them is first imported.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import importlib
|
|
21
|
+
|
|
22
|
+
try: # the installed distribution's version: the workspace tag/commit, never a literal
|
|
23
|
+
from importlib.metadata import version as _dist_version
|
|
24
|
+
__version__ = _dist_version("hugpy-control")
|
|
25
|
+
except Exception: # noqa: BLE001 — source tree without metadata
|
|
26
|
+
__version__ = "0.0.0+unknown"
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"__version__",
|
|
30
|
+
"bus",
|
|
31
|
+
"calllog",
|
|
32
|
+
"job_schemas",
|
|
33
|
+
"jobs",
|
|
34
|
+
"principals",
|
|
35
|
+
"settings",
|
|
36
|
+
"shared",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
_SUBMODULES = frozenset(n for n in __all__ if not n.startswith("__"))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def __getattr__(name: str):
|
|
43
|
+
if name in _SUBMODULES:
|
|
44
|
+
return importlib.import_module(f"{__name__}.{name}")
|
|
45
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def __dir__():
|
|
49
|
+
return sorted(set(globals()) | _SUBMODULES)
|
hugpy_control/bus.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""F1 — one typed, frozen, addressed message bus. Control rides the same bus.
|
|
2
|
+
|
|
3
|
+
Scope (deliberate): this is the STATE/CONTROL plane, not the token data
|
|
4
|
+
plane. Token streams stay on their SSE pipes — publishing every token through
|
|
5
|
+
a fan-out bus would tax the hot path for nothing. What travels here:
|
|
6
|
+
|
|
7
|
+
job.created / job.status / job.done lifecycle (published by the
|
|
8
|
+
JobStore adapter, see wire below)
|
|
9
|
+
control.cancel stop a generation, any transport
|
|
10
|
+
catalog.changed on-disk model inventory changed
|
|
11
|
+
(storage publishes, engine consumes)
|
|
12
|
+
control.restart / control.* (future) worker ops, module updates
|
|
13
|
+
worker.* / keeper.* (future) registries, health, keeper comms
|
|
14
|
+
|
|
15
|
+
Envelope: BusMessage — frozen, addressed (principal / channel / target /
|
|
16
|
+
job_id), serializable (to_dict/from_dict), so an HTTP or SSE relay can carry
|
|
17
|
+
it across processes verbatim. In-process today (one bus per process, same
|
|
18
|
+
one-gunicorn-process model as the JobStore); remote transports reach it
|
|
19
|
+
through thin route adapters that publish/subscribe on their behalf.
|
|
20
|
+
|
|
21
|
+
Delivery: each Subscription owns a bounded queue. Publishing never blocks —
|
|
22
|
+
on overflow the OLDEST message is dropped and the drop is counted loudly on
|
|
23
|
+
the subscription (a slow consumer must not stall cancels for everyone else).
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import logging
|
|
28
|
+
import queue
|
|
29
|
+
import threading
|
|
30
|
+
import time
|
|
31
|
+
import uuid
|
|
32
|
+
from dataclasses import dataclass, field
|
|
33
|
+
from typing import Any, Iterator, Optional
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
# Topic conventions. Match exact ("control.cancel") or prefix-wildcard
|
|
38
|
+
# ("job.*" matches job.created, job.status, ...; "*" matches everything).
|
|
39
|
+
TOPIC_JOB_CREATED = "job.created"
|
|
40
|
+
TOPIC_JOB_STATUS = "job.status"
|
|
41
|
+
TOPIC_JOB_DONE = "job.done" # any terminal state; payload carries which
|
|
42
|
+
TOPIC_CONTROL_CANCEL = "control.cancel"
|
|
43
|
+
# Catalog invalidation (partition protocol, see EXTRACTION_GUIDE §4): published by
|
|
44
|
+
# hugpy_storage after a download/delete changes what is on disk; consumed by
|
|
45
|
+
# hugpy_engine to drop its catalog/manifest caches. Payload convention:
|
|
46
|
+
# {"model_key": str|None, "reason": "download"|"delete"|"prune"|"reconcile"|...}.
|
|
47
|
+
TOPIC_CATALOG_CHANGED = "catalog.changed"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class BusMessage:
|
|
52
|
+
topic: str
|
|
53
|
+
id: str = field(default_factory=lambda: uuid.uuid4().hex)
|
|
54
|
+
ts: float = field(default_factory=time.time)
|
|
55
|
+
source: Optional[str] = None # transport/component that published
|
|
56
|
+
principal: Optional[str] = None # who caused it (F2 threads through here)
|
|
57
|
+
channel: Optional[str] = None # conversational context
|
|
58
|
+
target: Optional[str] = None # addressed recipient; None = broadcast
|
|
59
|
+
job_id: Optional[str] = None
|
|
60
|
+
payload: dict = field(default_factory=dict)
|
|
61
|
+
|
|
62
|
+
def to_dict(self) -> dict[str, Any]:
|
|
63
|
+
return {
|
|
64
|
+
"topic": self.topic, "id": self.id, "ts": self.ts,
|
|
65
|
+
"source": self.source, "principal": self.principal,
|
|
66
|
+
"channel": self.channel, "target": self.target,
|
|
67
|
+
"job_id": self.job_id, "payload": dict(self.payload),
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def from_dict(cls, d: dict) -> "BusMessage":
|
|
72
|
+
return cls(
|
|
73
|
+
topic=str(d.get("topic") or ""),
|
|
74
|
+
id=str(d.get("id") or uuid.uuid4().hex),
|
|
75
|
+
ts=float(d.get("ts") or time.time()),
|
|
76
|
+
source=d.get("source"), principal=d.get("principal"),
|
|
77
|
+
channel=d.get("channel"), target=d.get("target"),
|
|
78
|
+
job_id=d.get("job_id"), payload=dict(d.get("payload") or {}),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _topic_matches(pattern: str, topic: str) -> bool:
|
|
83
|
+
if pattern == "*" or pattern == topic:
|
|
84
|
+
return True
|
|
85
|
+
if pattern.endswith(".*"):
|
|
86
|
+
return topic.startswith(pattern[:-1])
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class Subscription:
|
|
91
|
+
"""One consumer's mailbox. Iterate it (blocking) or get(timeout=...).
|
|
92
|
+
close() detaches it from the bus and unblocks any pending get."""
|
|
93
|
+
|
|
94
|
+
_SENTINEL = object()
|
|
95
|
+
|
|
96
|
+
def __init__(self, bus: "Bus", topics: tuple[str, ...],
|
|
97
|
+
target: Optional[str], maxsize: int) -> None:
|
|
98
|
+
self._bus = bus
|
|
99
|
+
self.topics = topics
|
|
100
|
+
self.target = target
|
|
101
|
+
self._q: "queue.Queue[Any]" = queue.Queue(maxsize=maxsize)
|
|
102
|
+
self.dropped = 0
|
|
103
|
+
self.closed = False
|
|
104
|
+
|
|
105
|
+
def _matches(self, msg: BusMessage) -> bool:
|
|
106
|
+
if self.target is not None and msg.target not in (None, self.target):
|
|
107
|
+
return False
|
|
108
|
+
return any(_topic_matches(p, msg.topic) for p in self.topics)
|
|
109
|
+
|
|
110
|
+
def _offer(self, msg: BusMessage) -> None:
|
|
111
|
+
while True:
|
|
112
|
+
try:
|
|
113
|
+
self._q.put_nowait(msg)
|
|
114
|
+
return
|
|
115
|
+
except queue.Full:
|
|
116
|
+
try:
|
|
117
|
+
self._q.get_nowait()
|
|
118
|
+
self.dropped += 1
|
|
119
|
+
if self.dropped in (1, 100) or self.dropped % 1000 == 0:
|
|
120
|
+
logger.warning(
|
|
121
|
+
"bus subscription %s dropped %d message(s) "
|
|
122
|
+
"(slow consumer)", self.topics, self.dropped)
|
|
123
|
+
except queue.Empty:
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
def get(self, timeout: Optional[float] = None) -> Optional[BusMessage]:
|
|
127
|
+
try:
|
|
128
|
+
item = self._q.get(timeout=timeout)
|
|
129
|
+
except queue.Empty:
|
|
130
|
+
return None
|
|
131
|
+
return None if item is self._SENTINEL else item
|
|
132
|
+
|
|
133
|
+
def __iter__(self) -> Iterator[BusMessage]:
|
|
134
|
+
while not self.closed:
|
|
135
|
+
item = self._q.get()
|
|
136
|
+
if item is self._SENTINEL:
|
|
137
|
+
return
|
|
138
|
+
yield item
|
|
139
|
+
|
|
140
|
+
def close(self) -> None:
|
|
141
|
+
self.closed = True
|
|
142
|
+
self._bus._detach(self)
|
|
143
|
+
try:
|
|
144
|
+
self._q.put_nowait(self._SENTINEL)
|
|
145
|
+
except queue.Full:
|
|
146
|
+
pass
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class Bus:
|
|
150
|
+
def __init__(self) -> None:
|
|
151
|
+
self._subs: list[Subscription] = []
|
|
152
|
+
self._lock = threading.Lock()
|
|
153
|
+
|
|
154
|
+
def subscribe(self, *topics: str, target: Optional[str] = None,
|
|
155
|
+
maxsize: int = 256) -> Subscription:
|
|
156
|
+
sub = Subscription(self, topics or ("*",), target, maxsize)
|
|
157
|
+
with self._lock:
|
|
158
|
+
self._subs.append(sub)
|
|
159
|
+
return sub
|
|
160
|
+
|
|
161
|
+
def _detach(self, sub: Subscription) -> None:
|
|
162
|
+
with self._lock:
|
|
163
|
+
try:
|
|
164
|
+
self._subs.remove(sub)
|
|
165
|
+
except ValueError:
|
|
166
|
+
pass
|
|
167
|
+
|
|
168
|
+
def publish(self, topic: Optional[str] = None, *,
|
|
169
|
+
msg: Optional[BusMessage] = None, **fields: Any) -> BusMessage:
|
|
170
|
+
"""publish("control.cancel", job_id=...) or publish(msg=BusMessage(...))."""
|
|
171
|
+
if msg is None:
|
|
172
|
+
if not topic:
|
|
173
|
+
raise ValueError("publish() needs a topic or a msg")
|
|
174
|
+
msg = BusMessage(topic=topic, **fields)
|
|
175
|
+
with self._lock:
|
|
176
|
+
subs = list(self._subs)
|
|
177
|
+
for sub in subs:
|
|
178
|
+
if not sub.closed and sub._matches(msg):
|
|
179
|
+
sub._offer(msg)
|
|
180
|
+
return msg
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
bus = Bus()
|
|
184
|
+
|
|
185
|
+
_WIRED: dict[tuple[int, int], threading.Thread] = {}
|
|
186
|
+
_WIRED_LOCK = threading.Lock()
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def wire_cancel(the_bus: Optional[Bus] = None, store=None) -> threading.Thread:
|
|
190
|
+
"""F1.3 — control messages act through the same substrate everywhere:
|
|
191
|
+
a daemon thread subscribes to control.cancel and calls JobStore.cancel,
|
|
192
|
+
which fires the cancel handle the owning stream attached. Idempotent per
|
|
193
|
+
(bus, store) pair; call it from every process entrypoint that serves jobs
|
|
194
|
+
(flask app factory, worker agent main)."""
|
|
195
|
+
from hugpy_control.jobs import job_store as _default_store
|
|
196
|
+
the_bus = the_bus or bus
|
|
197
|
+
store = store if store is not None else _default_store
|
|
198
|
+
key = (id(the_bus), id(store))
|
|
199
|
+
with _WIRED_LOCK:
|
|
200
|
+
th = _WIRED.get(key)
|
|
201
|
+
if th is not None and th.is_alive():
|
|
202
|
+
return th
|
|
203
|
+
sub = the_bus.subscribe(TOPIC_CONTROL_CANCEL)
|
|
204
|
+
|
|
205
|
+
def _run() -> None:
|
|
206
|
+
for m in sub:
|
|
207
|
+
if m.job_id:
|
|
208
|
+
store.cancel(m.job_id,
|
|
209
|
+
reason=str(m.payload.get("reason") or ""))
|
|
210
|
+
|
|
211
|
+
th = threading.Thread(target=_run, name="comms-cancel", daemon=True)
|
|
212
|
+
th.start()
|
|
213
|
+
_WIRED[key] = th
|
|
214
|
+
return th
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def wire_job_events(the_bus: Optional[Bus] = None, store=None,
|
|
218
|
+
source: Optional[str] = None) -> None:
|
|
219
|
+
"""Publish job lifecycle transitions onto the bus (job.created /
|
|
220
|
+
job.status / job.done) via the store's on_change hook — the store never
|
|
221
|
+
imports the bus, this adapter is the one seam between them."""
|
|
222
|
+
from hugpy_control.jobs import job_store as _default_store, TERMINAL_STATUSES
|
|
223
|
+
the_bus = the_bus or bus
|
|
224
|
+
store = store if store is not None else _default_store
|
|
225
|
+
|
|
226
|
+
def _on_change(job, prior: str) -> None:
|
|
227
|
+
status = job.to_dict()["status"]
|
|
228
|
+
if not prior:
|
|
229
|
+
topic = TOPIC_JOB_CREATED
|
|
230
|
+
elif status in TERMINAL_STATUSES:
|
|
231
|
+
topic = TOPIC_JOB_DONE
|
|
232
|
+
else:
|
|
233
|
+
topic = TOPIC_JOB_STATUS
|
|
234
|
+
the_bus.publish(topic, source=source, principal=job.principal,
|
|
235
|
+
channel=job.channel, target=job.worker,
|
|
236
|
+
job_id=job.id,
|
|
237
|
+
payload={"status": status, "prior": prior,
|
|
238
|
+
"kind": job.kind, "model_key": job.model_key})
|
|
239
|
+
|
|
240
|
+
store.on_change = _on_change
|
hugpy_control/calllog.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""CALL-LOG-20260910: append-only call log (one JSON line per event) + tail reader.
|
|
2
|
+
|
|
3
|
+
Events: phase "start" (job created; carries client/ua/route when a request
|
|
4
|
+
context exists) and phase "end" (job finished; carries status, worker, tokens,
|
|
5
|
+
duration_ms, error). The reader merges the two by job id, newest first.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import socket
|
|
13
|
+
import threading
|
|
14
|
+
import time
|
|
15
|
+
|
|
16
|
+
log = logging.getLogger(__name__)
|
|
17
|
+
_LOCK = threading.Lock()
|
|
18
|
+
_HOST = socket.gethostname()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def path() -> str:
|
|
22
|
+
env = (os.environ.get("HUGPY_CALL_LOG") or "").strip()
|
|
23
|
+
if env:
|
|
24
|
+
return env
|
|
25
|
+
db = (os.environ.get("HUGPY_COMMS_DB") or "").strip()
|
|
26
|
+
base = os.path.dirname(db) if db else ""
|
|
27
|
+
if not base:
|
|
28
|
+
try:
|
|
29
|
+
from hugpy_platform.constants import DEFAULT_ROOT
|
|
30
|
+
base = os.path.join(str(DEFAULT_ROOT), "comms")
|
|
31
|
+
except Exception: # noqa: BLE001
|
|
32
|
+
base = os.path.expanduser("~/.hugpy")
|
|
33
|
+
return os.path.join(base, "calls.jsonl")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _request_context() -> dict:
|
|
37
|
+
try:
|
|
38
|
+
from flask import has_request_context, request
|
|
39
|
+
if not has_request_context():
|
|
40
|
+
return {}
|
|
41
|
+
xff = (request.headers.get("X-Forwarded-For") or "").split(",")[0].strip()
|
|
42
|
+
return {
|
|
43
|
+
"client": xff or request.remote_addr,
|
|
44
|
+
"ua": (request.headers.get("User-Agent") or "")[:160],
|
|
45
|
+
"route": request.path,
|
|
46
|
+
"method": request.method,
|
|
47
|
+
"host": request.host,
|
|
48
|
+
}
|
|
49
|
+
except Exception: # noqa: BLE001
|
|
50
|
+
return {}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _err_text(err) -> str | None:
|
|
54
|
+
if err is None:
|
|
55
|
+
return None
|
|
56
|
+
for attr in ("message", "msg"):
|
|
57
|
+
v = getattr(err, attr, None)
|
|
58
|
+
if v:
|
|
59
|
+
return str(v)[:400]
|
|
60
|
+
if isinstance(err, dict):
|
|
61
|
+
return str(err.get("message") or err)[:400]
|
|
62
|
+
return str(err)[:400]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def record(phase: str, job, **extra) -> None:
|
|
66
|
+
"""Append one event for `job`. Never raises — logging must not break serving."""
|
|
67
|
+
try:
|
|
68
|
+
row = {
|
|
69
|
+
"ts": time.time(), "phase": phase, "node": _HOST,
|
|
70
|
+
"id": getattr(job, "id", None), "kind": getattr(job, "kind", None),
|
|
71
|
+
"model_key": getattr(job, "model_key", None), "model": getattr(job, "model_name", None),
|
|
72
|
+
"principal": getattr(job, "principal", None), "transport": getattr(job, "transport", None),
|
|
73
|
+
"channel": getattr(job, "channel", None), "worker": getattr(job, "worker", None),
|
|
74
|
+
"status": getattr(job, "status", None), "tokens": getattr(job, "tokens", 0),
|
|
75
|
+
"started_ts": getattr(job, "started_ts", None),
|
|
76
|
+
}
|
|
77
|
+
if phase == "start":
|
|
78
|
+
row.update(_request_context())
|
|
79
|
+
else:
|
|
80
|
+
st = getattr(job, "started_ts", None)
|
|
81
|
+
row["duration_ms"] = int((time.time() - st) * 1000) if st else None
|
|
82
|
+
row["error"] = _err_text(getattr(job, "error", None))
|
|
83
|
+
row.update(extra)
|
|
84
|
+
line = json.dumps(row, default=str)
|
|
85
|
+
p = path()
|
|
86
|
+
os.makedirs(os.path.dirname(p), exist_ok=True)
|
|
87
|
+
with _LOCK, open(p, "a", encoding="utf-8") as fh:
|
|
88
|
+
fh.write(line + "\n")
|
|
89
|
+
except Exception: # noqa: BLE001
|
|
90
|
+
log.debug("call log write failed", exc_info=True)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def read(limit: int = 300, since: float | None = None, tail_bytes: int = 4 * 1024 * 1024) -> list[dict]:
|
|
94
|
+
"""Newest-first merged calls from the tail of the log."""
|
|
95
|
+
p = path()
|
|
96
|
+
try:
|
|
97
|
+
with open(p, "rb") as fh:
|
|
98
|
+
fh.seek(0, 2)
|
|
99
|
+
size = fh.tell()
|
|
100
|
+
fh.seek(max(0, size - tail_bytes))
|
|
101
|
+
data = fh.read().decode("utf-8", errors="replace")
|
|
102
|
+
except OSError:
|
|
103
|
+
return []
|
|
104
|
+
lines = data.split("\n")
|
|
105
|
+
if size > tail_bytes and lines:
|
|
106
|
+
lines = lines[1:] # drop the partial first line
|
|
107
|
+
calls: dict = {}
|
|
108
|
+
order: list = []
|
|
109
|
+
for line in lines:
|
|
110
|
+
if not line.strip():
|
|
111
|
+
continue
|
|
112
|
+
try:
|
|
113
|
+
ev = json.loads(line)
|
|
114
|
+
except ValueError:
|
|
115
|
+
continue
|
|
116
|
+
jid = ev.get("id") or f"anon-{ev.get('ts')}"
|
|
117
|
+
if jid not in calls:
|
|
118
|
+
calls[jid] = {}
|
|
119
|
+
order.append(jid)
|
|
120
|
+
cur = calls[jid]
|
|
121
|
+
if ev.get("phase") == "start":
|
|
122
|
+
cur.update({k: v for k, v in ev.items() if k != "phase"})
|
|
123
|
+
cur.setdefault("started_ts", ev.get("ts"))
|
|
124
|
+
else:
|
|
125
|
+
cur.update({k: v for k, v in ev.items() if k not in ("phase", "ts") and v is not None})
|
|
126
|
+
cur["ended_ts"] = ev.get("ts")
|
|
127
|
+
rows = [calls[j] for j in order]
|
|
128
|
+
if since:
|
|
129
|
+
rows = [r for r in rows if (r.get("started_ts") or r.get("ts") or 0) >= since]
|
|
130
|
+
rows.sort(key=lambda r: r.get("started_ts") or r.get("ts") or 0, reverse=True)
|
|
131
|
+
return rows[:limit]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Compat shim for the download-job schema names.
|
|
2
|
+
|
|
3
|
+
The Job/JobStore that used to live here (download jobs only) is now the shared,
|
|
4
|
+
all-transport store in :mod:`hugpy_control.jobs` (F5). Download callers keep
|
|
5
|
+
importing from here unchanged; the singleton is the same store every other
|
|
6
|
+
transport enqueues into.
|
|
7
|
+
"""
|
|
8
|
+
# The Job/JobStore that used to live here (download jobs only) is now the
|
|
9
|
+
# shared, all-transport store in hugpy_control.jobs (F5): one
|
|
10
|
+
# schema for chat requests AND downloads, canonical lifecycle
|
|
11
|
+
# pending→processing→streaming→(done|cancelled|failed), with the old
|
|
12
|
+
# queued/running/completed names normalized on write. Download callers keep
|
|
13
|
+
# importing from here unchanged; the singleton is the same store every other
|
|
14
|
+
# transport enqueues into.
|
|
15
|
+
from hugpy_control.jobs import (
|
|
16
|
+
CANONICAL_STATUSES,
|
|
17
|
+
LEGACY_FOR_CANONICAL,
|
|
18
|
+
TERMINAL_STATUSES,
|
|
19
|
+
Job,
|
|
20
|
+
JobError,
|
|
21
|
+
JobStore,
|
|
22
|
+
job_store,
|
|
23
|
+
normalize_status,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"CANONICAL_STATUSES",
|
|
28
|
+
"LEGACY_FOR_CANONICAL",
|
|
29
|
+
"TERMINAL_STATUSES",
|
|
30
|
+
"Job",
|
|
31
|
+
"JobError",
|
|
32
|
+
"JobStore",
|
|
33
|
+
"job_store",
|
|
34
|
+
"normalize_status",
|
|
35
|
+
]
|