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
semiont/cached.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""A live query: one of a client's reads that answers from its cache (`semiont.cache`) and stays true as the knowledge base changes.
|
|
2
|
+
|
|
3
|
+
Building one touches nothing, so a query is made wherever it is convenient to
|
|
4
|
+
name it. It is then read one of two ways:
|
|
5
|
+
|
|
6
|
+
- Held with `async with`, it is the live view: the query's state now, and each
|
|
7
|
+
state after it, until the client is closed. Watching is what asks: the first
|
|
8
|
+
watcher of a query nobody has asked starts its fetch. While a query of one
|
|
9
|
+
resource is held, the client holds that resource's scope, so the events that
|
|
10
|
+
keep the query true reach it. A query is watched inside its client's own
|
|
11
|
+
`async with`, which is what listens for those events: before it, watching
|
|
12
|
+
is a `RuntimeError`.
|
|
13
|
+
|
|
14
|
+
async with client.browse.annotations(resource_id) as live:
|
|
15
|
+
async for state in live:
|
|
16
|
+
match state:
|
|
17
|
+
case Ready(value=annotations): ...
|
|
18
|
+
case Failed(error=error): ...
|
|
19
|
+
case Pending(): ...
|
|
20
|
+
|
|
21
|
+
A failure is a state, never a raise: the query lives through it.
|
|
22
|
+
- `await query.fresh()` is the one-shot read: it asks the service now, and
|
|
23
|
+
gives what it answers or raises the failure it met. Every watcher of the
|
|
24
|
+
query is given the answer too.
|
|
25
|
+
|
|
26
|
+
`invalidate` says the value is out of date: it is asked for again, and shown
|
|
27
|
+
meanwhile.
|
|
28
|
+
|
|
29
|
+
A value is the one every reader of the query is given: it is read, not changed.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from collections.abc import AsyncIterator, Callable, Hashable
|
|
33
|
+
from types import TracebackType
|
|
34
|
+
from typing import Final, Protocol, Self, final, override
|
|
35
|
+
|
|
36
|
+
from semiont.cache import Cache, CacheState, Failed, Pending, Ready
|
|
37
|
+
from semiont.identifiers import ResourceId
|
|
38
|
+
from semiont.transport import ResourceHold, Transport
|
|
39
|
+
|
|
40
|
+
__all__ = ["Cached", "Keyed", "Source", "itself", "shown"]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Source[T](Protocol):
|
|
44
|
+
"""What a query answers from: the namespace that made the query supplies it."""
|
|
45
|
+
|
|
46
|
+
async def fresh(self) -> T:
|
|
47
|
+
"""The value now, from the service."""
|
|
48
|
+
...
|
|
49
|
+
|
|
50
|
+
def watch(self) -> AsyncIterator[CacheState[T]]:
|
|
51
|
+
"""The query's state, now and as it changes."""
|
|
52
|
+
...
|
|
53
|
+
|
|
54
|
+
def invalidate(self) -> None:
|
|
55
|
+
"""The value is out of date."""
|
|
56
|
+
...
|
|
57
|
+
|
|
58
|
+
def hold(self) -> ResourceHold | None:
|
|
59
|
+
"""A hold on the scope of the resource the query is of, when it is of one."""
|
|
60
|
+
...
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@final
|
|
64
|
+
class Cached[T]:
|
|
65
|
+
"""See the module's documentation."""
|
|
66
|
+
|
|
67
|
+
def __init__(self, source: Source[T], *, watchable: Callable[[], None]) -> None:
|
|
68
|
+
self._source: Final = source
|
|
69
|
+
self._watchable: Final = watchable
|
|
70
|
+
"""Raises for a watcher nothing would keep true."""
|
|
71
|
+
self._holds: Final[list[ResourceHold | None]] = []
|
|
72
|
+
"""The hold each `async with` on the query took, innermost last."""
|
|
73
|
+
|
|
74
|
+
async def fresh(self) -> T:
|
|
75
|
+
"""The value now, from the service."""
|
|
76
|
+
return await self._source.fresh()
|
|
77
|
+
|
|
78
|
+
def invalidate(self) -> None:
|
|
79
|
+
"""The value is out of date: ask again, showing it meanwhile."""
|
|
80
|
+
self._source.invalidate()
|
|
81
|
+
|
|
82
|
+
async def __aenter__(self) -> AsyncIterator[CacheState[T]]:
|
|
83
|
+
self._watchable()
|
|
84
|
+
# The scope first, so the events that refresh the key are already coming when its value arrives.
|
|
85
|
+
self._holds.append(self._source.hold())
|
|
86
|
+
return self._source.watch()
|
|
87
|
+
|
|
88
|
+
async def __aexit__(self, kind: type[BaseException] | None, error: BaseException | None, trace: TracebackType | None) -> None:
|
|
89
|
+
hold = self._holds.pop()
|
|
90
|
+
if hold is not None:
|
|
91
|
+
hold.release()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def itself[V](value: V) -> V:
|
|
95
|
+
"""A value as it is: the view of a query that shows its cache's value whole."""
|
|
96
|
+
return value
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def shown[V, T](state: CacheState[V], view: Callable[[V], T]) -> CacheState[T]:
|
|
100
|
+
"""A state of a cache's value, as a state of what a query shows of it."""
|
|
101
|
+
match state:
|
|
102
|
+
case Ready(value=value):
|
|
103
|
+
return Ready(view(value))
|
|
104
|
+
case Pending() | Failed():
|
|
105
|
+
return state
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@final
|
|
109
|
+
class _Viewed[V, T]:
|
|
110
|
+
def __init__(self, states: AsyncIterator[CacheState[V]], view: Callable[[V], T]) -> None:
|
|
111
|
+
self._states: Final = states
|
|
112
|
+
self._view: Final = view
|
|
113
|
+
|
|
114
|
+
def __aiter__(self) -> Self:
|
|
115
|
+
return self
|
|
116
|
+
|
|
117
|
+
async def __anext__(self) -> CacheState[T]:
|
|
118
|
+
return shown(await anext(self._states), self._view)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@final
|
|
122
|
+
class Keyed[K: Hashable, V, T](Source[T]):
|
|
123
|
+
"""A query answered by one key of one cache, as `view` shows its value.
|
|
124
|
+
|
|
125
|
+
`scope` is the resource the query is of, and what holds its scope while
|
|
126
|
+
the query is watched.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
def __init__(self, cache: Cache[K, V], key: K, view: Callable[[V], T], *, scope: tuple[Transport, ResourceId] | None = None) -> None:
|
|
130
|
+
self._cache: Final = cache
|
|
131
|
+
self._key: Final = key
|
|
132
|
+
self._view: Final = view
|
|
133
|
+
self._scope: Final = scope
|
|
134
|
+
|
|
135
|
+
@override
|
|
136
|
+
async def fresh(self) -> T:
|
|
137
|
+
return self._view(await self._cache.fetch(self._key))
|
|
138
|
+
|
|
139
|
+
@override
|
|
140
|
+
def watch(self) -> AsyncIterator[CacheState[T]]:
|
|
141
|
+
return _Viewed(self._cache.observe(self._key), self._view)
|
|
142
|
+
|
|
143
|
+
@override
|
|
144
|
+
def invalidate(self) -> None:
|
|
145
|
+
self._cache.invalidate(self._key)
|
|
146
|
+
|
|
147
|
+
@override
|
|
148
|
+
def hold(self) -> ResourceHold | None:
|
|
149
|
+
if self._scope is None:
|
|
150
|
+
return None
|
|
151
|
+
transport, resource_id = self._scope
|
|
152
|
+
return transport.subscribe_to_resource(resource_id)
|
semiont/channel.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""What a channel of the bus is, to a type checker and at run time."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Protocol, final
|
|
5
|
+
|
|
6
|
+
from pydantic import JsonValue
|
|
7
|
+
|
|
8
|
+
from semiont.model import WireModel, written
|
|
9
|
+
|
|
10
|
+
__all__ = ["AnyChannel", "AnyOperation", "Channel", "Empty", "Operation", "Response"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@final
|
|
14
|
+
class Empty(WireModel, frozen=True, extra="forbid"):
|
|
15
|
+
"""The payload of a channel that carries none: the empty object."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@final
|
|
19
|
+
class Response[T](WireModel, frozen=True):
|
|
20
|
+
"""The payload of a reply that wraps what it answers with: `{ response }`."""
|
|
21
|
+
|
|
22
|
+
response: T
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AnyChannel(Protocol):
|
|
26
|
+
"""A channel, whatever it carries: what code that treats every channel alike reads of one."""
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def name(self) -> str: ...
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def payload(self) -> type[WireModel]: ...
|
|
33
|
+
|
|
34
|
+
def decode(self, value: JsonValue) -> WireModel: ...
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@final
|
|
38
|
+
@dataclass(frozen=True, slots=True)
|
|
39
|
+
class Channel[P: WireModel]:
|
|
40
|
+
"""A channel's name, and the type of the payload it carries.
|
|
41
|
+
|
|
42
|
+
`payload` is `P` itself, so the two cannot be written to disagree: a
|
|
43
|
+
`Channel[A]` built with the class `B` does not type-check. And a channel is
|
|
44
|
+
its own payload's and no other's: a `Channel[A]` is not a `Channel[B]`,
|
|
45
|
+
whatever `A` and `B` are to each other, so a function that takes a channel
|
|
46
|
+
and a payload is refused a payload that is another channel's.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
name: str
|
|
50
|
+
payload: type[P]
|
|
51
|
+
|
|
52
|
+
def decode(self, value: JsonValue) -> P:
|
|
53
|
+
"""What a frame on this channel carries, as its payload's type. Raises `ValidationError` for anything else."""
|
|
54
|
+
return self.payload.model_validate(value)
|
|
55
|
+
|
|
56
|
+
def encode(self, payload: P) -> dict[str, JsonValue]:
|
|
57
|
+
"""`payload` as the wire carries it (`semiont.model.written`)."""
|
|
58
|
+
return written(payload)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class AnyOperation(Protocol):
|
|
62
|
+
"""An operation, whatever it asks and answers."""
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def request(self) -> AnyChannel: ...
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def result(self) -> AnyChannel: ...
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def failure(self) -> AnyChannel: ...
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@final
|
|
75
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
76
|
+
class Operation[Request: WireModel, Result: WireModel, Failure: WireModel]:
|
|
77
|
+
"""A request, and the two channels it is answered on."""
|
|
78
|
+
|
|
79
|
+
request: Channel[Request]
|
|
80
|
+
result: Channel[Result]
|
|
81
|
+
failure: Channel[Failure]
|