semiont 0.6.12.dev2__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.
- semiont/__init__.py +20 -0
- semiont/bus.py +236 -0
- semiont/cache.py +345 -0
- semiont/cached.py +152 -0
- semiont/channel.py +81 -0
- semiont/channels.py +1306 -0
- semiont/client.py +161 -0
- semiont/error_codes.py +232 -0
- semiont/errors.py +147 -0
- semiont/event_bus.py +108 -0
- semiont/events.py +101 -0
- semiont/http/__init__.py +24 -0
- semiont/http/agent.py +232 -0
- semiont/http/content.py +234 -0
- semiont/http/exchange.py +200 -0
- semiont/http/oauth.py +346 -0
- semiont/http/session.py +191 -0
- semiont/http/sse.py +71 -0
- semiont/http/stream.py +683 -0
- semiont/http/transport.py +326 -0
- semiont/identifiers.py +114 -0
- semiont/identity.py +59 -0
- semiont/media_types.py +69 -0
- semiont/media_types_table.py +89 -0
- semiont/model.py +33 -0
- semiont/namespaces/__init__.py +19 -0
- semiont/namespaces/auth.py +32 -0
- semiont/namespaces/beckon.py +47 -0
- semiont/namespaces/bind.py +38 -0
- semiont/namespaces/browse.py +338 -0
- semiont/namespaces/follow.py +238 -0
- semiont/namespaces/frame.py +35 -0
- semiont/namespaces/gather.py +82 -0
- semiont/namespaces/job.py +94 -0
- semiont/namespaces/links.py +66 -0
- semiont/namespaces/live.py +301 -0
- semiont/namespaces/mark.py +174 -0
- semiont/namespaces/match.py +67 -0
- semiont/namespaces/refresher.py +283 -0
- semiont/namespaces/system.py +24 -0
- semiont/namespaces/yield_.py +101 -0
- semiont/oauth_clients.py +27 -0
- semiont/operations.py +337 -0
- semiont/py.typed +0 -0
- semiont/refresh.py +188 -0
- semiont/resume.py +95 -0
- semiont/retry.py +164 -0
- semiont/running.py +110 -0
- semiont/session.py +493 -0
- semiont/sign_in.py +54 -0
- semiont/sign_in_store.py +283 -0
- semiont/storage.py +139 -0
- semiont/telemetry.py +116 -0
- semiont/telemetry_table.py +121 -0
- semiont/testing/__init__.py +62 -0
- semiont/testing/client.py +53 -0
- semiont/testing/content.py +124 -0
- semiont/testing/gateway.py +85 -0
- semiont/testing/transport.py +341 -0
- semiont/timing.py +187 -0
- semiont/transport.py +531 -0
- semiont/types.py +5902 -0
- semiont/watched.py +111 -0
- semiont-0.6.12.dev2.dist-info/METADATA +533 -0
- semiont-0.6.12.dev2.dist-info/RECORD +66 -0
- semiont-0.6.12.dev2.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""What keeps a client's queries true: `specs/src/client/refresh.json` (`semiont.refresh`), applied to the client's caches.
|
|
2
|
+
|
|
3
|
+
Each event on the bus, and the reopening of a dropped stream, asks again for
|
|
4
|
+
the queries its row names, writes the ones whose new value the event carries,
|
|
5
|
+
and ends the ones whose entity is gone.
|
|
6
|
+
|
|
7
|
+
A client has one refresher. It states what each event names (its resource,
|
|
8
|
+
its annotation, the value it carries) and, for each query of the table, which
|
|
9
|
+
cache answers it. Both are a `match` over the table's own names, so a trigger
|
|
10
|
+
or a query added to the table does not pass a type checker until it is
|
|
11
|
+
answered here.
|
|
12
|
+
|
|
13
|
+
An event acts only on a key its cache holds, and the refetches one key is
|
|
14
|
+
asked for inside a window are one refetch (`docs/protocol/CACHE-SEMANTICS.md`
|
|
15
|
+
B12 to B13b, B19, B20).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
from collections.abc import Callable, Hashable, Mapping
|
|
20
|
+
from typing import Final, assert_never, final
|
|
21
|
+
|
|
22
|
+
from pydantic import JsonValue, ValidationError
|
|
23
|
+
|
|
24
|
+
from semiont.bus import decoded
|
|
25
|
+
from semiont.cache import Cache
|
|
26
|
+
from semiont.channel import Channel
|
|
27
|
+
from semiont.channels import (
|
|
28
|
+
BUS_RESUME_GAP,
|
|
29
|
+
FRAME_ENTITY_TYPE_ADDED,
|
|
30
|
+
FRAME_TAG_SCHEMA_ADDED,
|
|
31
|
+
MARK_ADDED,
|
|
32
|
+
MARK_ARCHIVED,
|
|
33
|
+
MARK_BODY_UPDATED,
|
|
34
|
+
MARK_DELETE_OK,
|
|
35
|
+
MARK_ENTITY_TAG_ADDED,
|
|
36
|
+
MARK_ENTITY_TAG_REMOVED,
|
|
37
|
+
MARK_REMOVED,
|
|
38
|
+
MARK_UNARCHIVED,
|
|
39
|
+
YIELD_CLONED,
|
|
40
|
+
YIELD_CREATED,
|
|
41
|
+
YIELD_MOVED,
|
|
42
|
+
YIELD_UPDATED,
|
|
43
|
+
)
|
|
44
|
+
from semiont.events import Events
|
|
45
|
+
from semiont.identifiers import AnnotationId, ResourceId
|
|
46
|
+
from semiont.namespaces.links import Links
|
|
47
|
+
from semiont.namespaces.live import WHOLE, Live, Subject
|
|
48
|
+
from semiont.refresh import CACHE_QUERIES, CACHE_REFRESH, CacheQuery, CacheRefreshReach, CacheRefreshTrigger
|
|
49
|
+
from semiont.transport import ConnectionState, Frame
|
|
50
|
+
from semiont.types import StoredEventResponse
|
|
51
|
+
from semiont.watched import Watched
|
|
52
|
+
|
|
53
|
+
__all__ = ["Refresher"]
|
|
54
|
+
|
|
55
|
+
# The triggers that are events, by the channel each is.
|
|
56
|
+
_EVENTS: Final[Mapping[str, CacheRefreshTrigger]] = {trigger: trigger for trigger in CACHE_REFRESH if trigger != "reopened"}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@final
|
|
60
|
+
class _Window:
|
|
61
|
+
def __init__(self, refetch: Callable[[], None], closing: asyncio.TimerHandle) -> None:
|
|
62
|
+
self.refetch: Final = refetch
|
|
63
|
+
"""What asks again for the window's key."""
|
|
64
|
+
self.closing: Final = closing
|
|
65
|
+
self.owed = False
|
|
66
|
+
"""Whether it was asked for again while the window was open."""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@final
|
|
70
|
+
class _Windows:
|
|
71
|
+
"""B19: per key, the first refetch an event asks for runs at once and opens a window.
|
|
72
|
+
|
|
73
|
+
Any more inside it are owed, and run as one when it closes, which opens
|
|
74
|
+
the next. So a storm of events costs a refetch per key per window, and
|
|
75
|
+
the last event is always reflected.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__(self, lasting_ms: int) -> None:
|
|
79
|
+
self._lasting: Final = lasting_ms / 1000
|
|
80
|
+
self._open: Final[dict[str, _Window]] = {}
|
|
81
|
+
|
|
82
|
+
def run(self, key: str, refetch: Callable[[], None]) -> None:
|
|
83
|
+
"""Ask again for `key`, by `refetch`: what asks for one key is the same whichever event asked."""
|
|
84
|
+
window = self._open.get(key)
|
|
85
|
+
if window is not None:
|
|
86
|
+
window.owed = True
|
|
87
|
+
return
|
|
88
|
+
self._open[key] = _Window(refetch, asyncio.get_running_loop().call_later(self._lasting, self._close, key))
|
|
89
|
+
refetch()
|
|
90
|
+
|
|
91
|
+
def _close(self, key: str) -> None:
|
|
92
|
+
window = self._open.pop(key)
|
|
93
|
+
if window.owed:
|
|
94
|
+
self.run(key, window.refetch)
|
|
95
|
+
|
|
96
|
+
def dispose(self) -> None:
|
|
97
|
+
"""Every window closes, and what each owed is dropped."""
|
|
98
|
+
for window in self._open.values():
|
|
99
|
+
window.closing.cancel()
|
|
100
|
+
self._open.clear()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _annotation_named(payload: Mapping[str, JsonValue]) -> AnnotationId | None:
|
|
104
|
+
named = payload.get("annotationId")
|
|
105
|
+
return AnnotationId.parse(named) if isinstance(named, str) else None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _recorded(channel: Channel[StoredEventResponse], payload: Mapping[str, JsonValue]) -> Subject:
|
|
109
|
+
return Subject(resource=decoded(channel, payload).resource_id)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _named(trigger: CacheRefreshTrigger, payload: Mapping[str, JsonValue]) -> Subject:
|
|
113
|
+
"""What an event names. Each arm states it for its trigger, and a trigger with no arm does not pass a type checker.
|
|
114
|
+
|
|
115
|
+
Raises `ValidationError` for a payload that is not its channel's.
|
|
116
|
+
"""
|
|
117
|
+
match trigger:
|
|
118
|
+
case "reopened":
|
|
119
|
+
# No event: the stream's reopening names nothing.
|
|
120
|
+
return Subject()
|
|
121
|
+
case "bus:resume-gap":
|
|
122
|
+
return Subject(resource=decoded(BUS_RESUME_GAP, payload).scope)
|
|
123
|
+
case "mark:added":
|
|
124
|
+
return Subject(resource=decoded(MARK_ADDED, payload).resource_id)
|
|
125
|
+
case "mark:removed":
|
|
126
|
+
removed = decoded(MARK_REMOVED, payload)
|
|
127
|
+
return Subject(resource=removed.resource_id, annotation=_annotation_named(removed.payload))
|
|
128
|
+
case "mark:delete-ok":
|
|
129
|
+
return Subject(annotation=decoded(MARK_DELETE_OK, payload).response.annotation_id)
|
|
130
|
+
case "mark:body-updated":
|
|
131
|
+
updated = decoded(MARK_BODY_UPDATED, payload)
|
|
132
|
+
if updated.annotation is not None:
|
|
133
|
+
return Subject(when="enriched", resource=updated.resource_id, annotation=updated.annotation.id, written=updated.annotation)
|
|
134
|
+
return Subject(when="unenriched", resource=updated.resource_id, annotation=_annotation_named(updated.payload))
|
|
135
|
+
case "mark:entity-tag-added":
|
|
136
|
+
return _recorded(MARK_ENTITY_TAG_ADDED, payload)
|
|
137
|
+
case "mark:entity-tag-removed":
|
|
138
|
+
return _recorded(MARK_ENTITY_TAG_REMOVED, payload)
|
|
139
|
+
case "mark:archived":
|
|
140
|
+
return _recorded(MARK_ARCHIVED, payload)
|
|
141
|
+
case "mark:unarchived":
|
|
142
|
+
return _recorded(MARK_UNARCHIVED, payload)
|
|
143
|
+
# What is heard by every client: its resource is the one it records, not a scope the client holds.
|
|
144
|
+
case "yield:created":
|
|
145
|
+
return _recorded(YIELD_CREATED, payload)
|
|
146
|
+
case "yield:updated":
|
|
147
|
+
return _recorded(YIELD_UPDATED, payload)
|
|
148
|
+
case "yield:cloned":
|
|
149
|
+
return _recorded(YIELD_CLONED, payload)
|
|
150
|
+
case "yield:moved":
|
|
151
|
+
return _recorded(YIELD_MOVED, payload)
|
|
152
|
+
case "frame:entity-type-added":
|
|
153
|
+
decoded(FRAME_ENTITY_TYPE_ADDED, payload)
|
|
154
|
+
return Subject()
|
|
155
|
+
case "frame:tag-schema-added":
|
|
156
|
+
decoded(FRAME_TAG_SCHEMA_ADDED, payload)
|
|
157
|
+
return Subject()
|
|
158
|
+
case _:
|
|
159
|
+
assert_never(trigger)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@final
|
|
163
|
+
class Refresher:
|
|
164
|
+
"""See the module's documentation. It listens from `start` until its client closes."""
|
|
165
|
+
|
|
166
|
+
def __init__(self, links: Links, live: Live, *, invalidation_window_ms: int) -> None:
|
|
167
|
+
self._links: Final = links
|
|
168
|
+
self._live: Final = live
|
|
169
|
+
self._windows: Final = _Windows(invalidation_window_ms)
|
|
170
|
+
|
|
171
|
+
def start(self) -> None:
|
|
172
|
+
"""Listen, from now on. A client starts it once: when it is held."""
|
|
173
|
+
# Listening before the task that reads has had its first turn: nothing said meanwhile is missed.
|
|
174
|
+
self._links.run(self._listen(self._links.own.frames_among(tuple(_EVENTS))))
|
|
175
|
+
self._links.run(self._reopenings(self._links.wire.transport.state))
|
|
176
|
+
|
|
177
|
+
async def _listen(self, events: Events[Frame]) -> None:
|
|
178
|
+
async for frame in events:
|
|
179
|
+
# Its reader is given the frames of these channels and no others.
|
|
180
|
+
trigger = _EVENTS[frame.channel]
|
|
181
|
+
try:
|
|
182
|
+
subject = _named(trigger, frame.payload)
|
|
183
|
+
except ValidationError:
|
|
184
|
+
# An event that cannot be read: something changed and nothing
|
|
185
|
+
# says what. Everything held is asked for again, as after a gap.
|
|
186
|
+
self._everything_held()
|
|
187
|
+
continue
|
|
188
|
+
self._refresh(trigger, subject)
|
|
189
|
+
|
|
190
|
+
async def _reopenings(self, state: Watched[ConnectionState]) -> None:
|
|
191
|
+
"""B13: the stream is open again having left `open`, which only a drop does.
|
|
192
|
+
|
|
193
|
+
A changed subscription is handed over with the state still `open`,
|
|
194
|
+
and misses nothing.
|
|
195
|
+
"""
|
|
196
|
+
opened = left = False
|
|
197
|
+
# A reopening is seen as one because a transport waits on its
|
|
198
|
+
# connection between leaving `open` and reaching it again, and this
|
|
199
|
+
# task runs in that wait.
|
|
200
|
+
async for now in state:
|
|
201
|
+
if now == "open":
|
|
202
|
+
if left:
|
|
203
|
+
self._refresh("reopened", Subject())
|
|
204
|
+
opened, left = True, False
|
|
205
|
+
else:
|
|
206
|
+
left = opened
|
|
207
|
+
|
|
208
|
+
def _refresh(self, trigger: CacheRefreshTrigger, subject: Subject) -> None:
|
|
209
|
+
row = next((row for row in CACHE_REFRESH[trigger] if row.when == subject.when), None)
|
|
210
|
+
if row is None:
|
|
211
|
+
return
|
|
212
|
+
for query in row.writes:
|
|
213
|
+
self._live.write(query, subject)
|
|
214
|
+
for query in row.removes:
|
|
215
|
+
self._live.remove(query, subject)
|
|
216
|
+
for query in row.refetches:
|
|
217
|
+
self._refetch(query, subject, row.reach)
|
|
218
|
+
|
|
219
|
+
def _everything_held(self) -> None:
|
|
220
|
+
for query in CACHE_QUERIES:
|
|
221
|
+
self._refetch(query, Subject(), "held")
|
|
222
|
+
|
|
223
|
+
def _again[K: Hashable, V](self, cache: Cache[K, V], key: K, window: str) -> None:
|
|
224
|
+
"""Ask again for one key, when the cache holds it (B20), through the key's window (B19)."""
|
|
225
|
+
if cache.known(key):
|
|
226
|
+
self._windows.run(window, lambda: cache.invalidate(key))
|
|
227
|
+
|
|
228
|
+
def _refetch(self, query: CacheQuery, subject: Subject, reach: CacheRefreshReach) -> None:
|
|
229
|
+
"""B7: ask again, for the keys the row reaches, showing what there is meanwhile.
|
|
230
|
+
|
|
231
|
+
Each arm names the cache that answers its query, and a query with no
|
|
232
|
+
arm does not pass a type checker.
|
|
233
|
+
"""
|
|
234
|
+
live = self._live
|
|
235
|
+
|
|
236
|
+
def reached(held: list[ResourceId]) -> list[ResourceId]:
|
|
237
|
+
match reach:
|
|
238
|
+
case "held":
|
|
239
|
+
return held
|
|
240
|
+
case "subject":
|
|
241
|
+
return [] if subject.resource is None else [subject.resource]
|
|
242
|
+
|
|
243
|
+
match query:
|
|
244
|
+
case "resource":
|
|
245
|
+
for resource in reached(live.resource.keys()):
|
|
246
|
+
self._again(live.resource, resource, f"resource/{resource}")
|
|
247
|
+
case "annotations":
|
|
248
|
+
for resource in reached(live.annotations.keys()):
|
|
249
|
+
self._again(live.annotations, resource, f"annotations/{resource}")
|
|
250
|
+
case "events":
|
|
251
|
+
for resource in reached(live.events.keys()):
|
|
252
|
+
self._again(live.events, resource, f"events/{resource}")
|
|
253
|
+
case "referencedBy":
|
|
254
|
+
for resource in reached(live.referenced_by.keys()):
|
|
255
|
+
self._again(live.referenced_by, resource, f"referenced-by/{resource}")
|
|
256
|
+
case "annotation":
|
|
257
|
+
# The annotation the event names; when it names none, each one held of the resource it names.
|
|
258
|
+
if reach == "held":
|
|
259
|
+
annotations = live.annotation.keys()
|
|
260
|
+
elif subject.annotation is not None:
|
|
261
|
+
annotations = [subject.annotation]
|
|
262
|
+
else:
|
|
263
|
+
annotations = [annotation for annotation, of in live.annotation_of.items() if of == subject.resource]
|
|
264
|
+
for annotation in annotations:
|
|
265
|
+
self._again(live.annotation, annotation, f"annotation/{annotation}")
|
|
266
|
+
# An event does not say which lists it changes, nor which searches would now find its resource.
|
|
267
|
+
case "resources":
|
|
268
|
+
self._windows.run("resource-lists", live.lists.invalidate_all)
|
|
269
|
+
case "matchedResources":
|
|
270
|
+
self._windows.run("matched-resources", live.searches.invalidate_all)
|
|
271
|
+
case "entityTypes":
|
|
272
|
+
self._again(live.entity_types, WHOLE, "entity-types")
|
|
273
|
+
case "tagSchemas":
|
|
274
|
+
self._again(live.tag_schemas, WHOLE, "tag-schemas")
|
|
275
|
+
case "agents":
|
|
276
|
+
if live.agents.known(WHOLE):
|
|
277
|
+
self._windows.run("agents", live.invalidate_agents)
|
|
278
|
+
case _:
|
|
279
|
+
assert_never(query)
|
|
280
|
+
|
|
281
|
+
def dispose(self) -> None:
|
|
282
|
+
"""Stop: what a window owed is dropped, and no window opens after. The client's close ends the listening."""
|
|
283
|
+
self._windows.dispose()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""System: what the knowledge base says about itself."""
|
|
2
|
+
|
|
3
|
+
from typing import Final, final
|
|
4
|
+
|
|
5
|
+
from semiont.transport import GatewayOperations
|
|
6
|
+
from semiont.types import HealthResponse, StatusResponse
|
|
7
|
+
|
|
8
|
+
__all__ = ["SystemNamespace"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@final
|
|
12
|
+
class SystemNamespace:
|
|
13
|
+
"""See the module's documentation."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, gateway: GatewayOperations) -> None:
|
|
16
|
+
self._gateway: Final = gateway
|
|
17
|
+
|
|
18
|
+
async def health_check(self) -> HealthResponse:
|
|
19
|
+
"""Whether the gateway is up."""
|
|
20
|
+
return await self._gateway.health_check()
|
|
21
|
+
|
|
22
|
+
async def status(self) -> StatusResponse:
|
|
23
|
+
"""The knowledge base's version and features, and who the caller is to it."""
|
|
24
|
+
return await self._gateway.get_status()
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Yield: creating resources, by upload, by generation, and by cloning."""
|
|
2
|
+
|
|
3
|
+
from typing import Final, final
|
|
4
|
+
|
|
5
|
+
from semiont.channel import Empty
|
|
6
|
+
from semiont.channels import YIELD_CLONE
|
|
7
|
+
from semiont.identifiers import ResourceId
|
|
8
|
+
from semiont.media_types import clone_format, primary_media_type, storage_uri
|
|
9
|
+
from semiont.model import written
|
|
10
|
+
from semiont.namespaces.follow import JobEvent, follow
|
|
11
|
+
from semiont.namespaces.links import Links
|
|
12
|
+
from semiont.operations import YIELD_CLONE_RESOURCE_REQUESTED, YIELD_CLONE_TOKEN_REQUESTED
|
|
13
|
+
from semiont.running import Running
|
|
14
|
+
from semiont.timing import GENERATION_STALL_ASSUMED_TOKENS_COUNT, GENERATION_STALL_FLOOR_MS, GENERATION_STALL_PER_TOKEN_MS
|
|
15
|
+
from semiont.transport import ContentTransport, PutBinaryRequest, Upload
|
|
16
|
+
from semiont.types import (
|
|
17
|
+
CloneResourceWithTokenResponse,
|
|
18
|
+
CreateResourceResponse,
|
|
19
|
+
GatheredContextFocusResource,
|
|
20
|
+
GenerationJobParams,
|
|
21
|
+
JobCreateCommand,
|
|
22
|
+
ResourceDescriptor,
|
|
23
|
+
YieldCloneResourceRequest,
|
|
24
|
+
YieldCloneTokenRequest,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = ["YieldNamespace", "generation_stall_deadline_ms"]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def generation_stall_deadline_ms(max_tokens: float | None) -> int:
|
|
31
|
+
"""How long a followed generation may say nothing before its follower gives up on it.
|
|
32
|
+
|
|
33
|
+
A floor, and a wait that grows with the length asked for: a generation
|
|
34
|
+
says nothing while its model writes, so the silence it is allowed depends
|
|
35
|
+
on how much it was asked to write.
|
|
36
|
+
"""
|
|
37
|
+
tokens = GENERATION_STALL_ASSUMED_TOKENS_COUNT if max_tokens is None else max(max_tokens, 0)
|
|
38
|
+
return max(GENERATION_STALL_FLOOR_MS, round(GENERATION_STALL_PER_TOKEN_MS * tokens))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@final
|
|
42
|
+
class YieldNamespace:
|
|
43
|
+
"""See the module's documentation."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, links: Links, content: ContentTransport) -> None:
|
|
46
|
+
self._links: Final = links
|
|
47
|
+
self._content: Final = content
|
|
48
|
+
|
|
49
|
+
def resource(self, data: PutBinaryRequest) -> Upload:
|
|
50
|
+
"""Upload bytes as a new resource: the upload's progress, and the resource created."""
|
|
51
|
+
return self._content.put_binary(data)
|
|
52
|
+
|
|
53
|
+
def from_context(self, params: GenerationJobParams, *, stall_deadline_ms: int | None = None) -> Running[JobEvent]:
|
|
54
|
+
"""Generate a resource from a gathered context: the job's progress and its completion.
|
|
55
|
+
|
|
56
|
+
The context's focus says what the job is about, so the job names no
|
|
57
|
+
resource. A follower that hears nothing for `stall_deadline_ms`, or
|
|
58
|
+
for `generation_stall_deadline_ms(params.max_tokens)` when none is
|
|
59
|
+
stated, asks for that job to be cancelled and ends as stalled.
|
|
60
|
+
"""
|
|
61
|
+
focus = params.context.focus
|
|
62
|
+
about = focus.resource.id if isinstance(focus, GatheredContextFocusResource) else focus.source_resource.id
|
|
63
|
+
within = generation_stall_deadline_ms(params.max_tokens) if stall_deadline_ms is None else stall_deadline_ms
|
|
64
|
+
create = JobCreateCommand(job_type="generation", params=written(params))
|
|
65
|
+
return follow(self._links, create, resource_id=about, stall_ms=within)
|
|
66
|
+
|
|
67
|
+
async def clone_token(self, resource_id: ResourceId) -> CloneResourceWithTokenResponse:
|
|
68
|
+
"""A token another resource can be created from: a clone of this one."""
|
|
69
|
+
return (await self._links.request(YIELD_CLONE_TOKEN_REQUESTED, YieldCloneTokenRequest(resource_id=resource_id))).response
|
|
70
|
+
|
|
71
|
+
async def from_token(self, token: str) -> ResourceDescriptor:
|
|
72
|
+
"""The resource a clone token was made from."""
|
|
73
|
+
answer = await self._links.request(YIELD_CLONE_RESOURCE_REQUESTED, YieldCloneResourceRequest(token=token))
|
|
74
|
+
return answer.response.source_resource
|
|
75
|
+
|
|
76
|
+
async def create_from_token(
|
|
77
|
+
self, *, token: str, name: str, content: str, archive_original: bool | None = None
|
|
78
|
+
) -> CreateResourceResponse:
|
|
79
|
+
"""Create a resource as a clone of the one `token` was made from.
|
|
80
|
+
|
|
81
|
+
The source is read first. The clone's content then goes by the upload
|
|
82
|
+
path, in the format its source's allows
|
|
83
|
+
(`semiont.media_types.clone_format`) and under a name made from its
|
|
84
|
+
own. `archive_original` archives the source once the clone exists.
|
|
85
|
+
"""
|
|
86
|
+
source = await self.from_token(token)
|
|
87
|
+
format_ = clone_format(primary_media_type(source))
|
|
88
|
+
return await self._content.put_binary(
|
|
89
|
+
PutBinaryRequest(
|
|
90
|
+
name=name,
|
|
91
|
+
file=content.encode(),
|
|
92
|
+
format=format_.media_type,
|
|
93
|
+
storage_uri=storage_uri(name, format_),
|
|
94
|
+
clone_token=token,
|
|
95
|
+
archive_original=archive_original,
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def clone(self) -> None:
|
|
100
|
+
"""Signal: a clone of the open resource is wanted."""
|
|
101
|
+
self._links.signal(YIELD_CLONE, Empty())
|
semiont/oauth_clients.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Generated from specs/src/session/oauth.json; do not edit.
|
|
2
|
+
# Regenerate: node scripts/spec/generate-oauth-clients-python.mjs
|
|
3
|
+
|
|
4
|
+
"""How a Semiont client presents itself to the issuer a knowledge base trusts:
|
|
5
|
+
the clients its realm registers, and the scope every sign-in asks for.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Final
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"BROWSER_CLIENT_ID",
|
|
12
|
+
"SCRIPT_CLIENT_ID",
|
|
13
|
+
"SIGN_IN_SCOPE",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
# An application a person uses: the authorization-code grant with PKCE (RFC 7636), at a redirect
|
|
17
|
+
# address the registration lists.
|
|
18
|
+
BROWSER_CLIENT_ID: Final = "semiont-browser"
|
|
19
|
+
|
|
20
|
+
# A process with no browser of its own, and the launcher: the device authorization grant (RFC
|
|
21
|
+
# 8628).
|
|
22
|
+
SCRIPT_CLIENT_ID: Final = "semiont-cli"
|
|
23
|
+
|
|
24
|
+
# What every sign-in asks for. `offline_access` asks for a refresh token that outlives the
|
|
25
|
+
# issuer's own browser session: a session with a knowledge base is renewed for weeks, not
|
|
26
|
+
# minutes.
|
|
27
|
+
SIGN_IN_SCOPE: Final = "openid email profile offline_access"
|