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.
Files changed (66) hide show
  1. semiont/__init__.py +20 -0
  2. semiont/bus.py +236 -0
  3. semiont/cache.py +345 -0
  4. semiont/cached.py +152 -0
  5. semiont/channel.py +81 -0
  6. semiont/channels.py +1306 -0
  7. semiont/client.py +161 -0
  8. semiont/error_codes.py +232 -0
  9. semiont/errors.py +147 -0
  10. semiont/event_bus.py +108 -0
  11. semiont/events.py +101 -0
  12. semiont/http/__init__.py +24 -0
  13. semiont/http/agent.py +232 -0
  14. semiont/http/content.py +234 -0
  15. semiont/http/exchange.py +200 -0
  16. semiont/http/oauth.py +346 -0
  17. semiont/http/session.py +191 -0
  18. semiont/http/sse.py +71 -0
  19. semiont/http/stream.py +683 -0
  20. semiont/http/transport.py +326 -0
  21. semiont/identifiers.py +114 -0
  22. semiont/identity.py +59 -0
  23. semiont/media_types.py +69 -0
  24. semiont/media_types_table.py +89 -0
  25. semiont/model.py +33 -0
  26. semiont/namespaces/__init__.py +19 -0
  27. semiont/namespaces/auth.py +32 -0
  28. semiont/namespaces/beckon.py +47 -0
  29. semiont/namespaces/bind.py +38 -0
  30. semiont/namespaces/browse.py +338 -0
  31. semiont/namespaces/follow.py +238 -0
  32. semiont/namespaces/frame.py +35 -0
  33. semiont/namespaces/gather.py +82 -0
  34. semiont/namespaces/job.py +94 -0
  35. semiont/namespaces/links.py +66 -0
  36. semiont/namespaces/live.py +301 -0
  37. semiont/namespaces/mark.py +174 -0
  38. semiont/namespaces/match.py +67 -0
  39. semiont/namespaces/refresher.py +283 -0
  40. semiont/namespaces/system.py +24 -0
  41. semiont/namespaces/yield_.py +101 -0
  42. semiont/oauth_clients.py +27 -0
  43. semiont/operations.py +337 -0
  44. semiont/py.typed +0 -0
  45. semiont/refresh.py +188 -0
  46. semiont/resume.py +95 -0
  47. semiont/retry.py +164 -0
  48. semiont/running.py +110 -0
  49. semiont/session.py +493 -0
  50. semiont/sign_in.py +54 -0
  51. semiont/sign_in_store.py +283 -0
  52. semiont/storage.py +139 -0
  53. semiont/telemetry.py +116 -0
  54. semiont/telemetry_table.py +121 -0
  55. semiont/testing/__init__.py +62 -0
  56. semiont/testing/client.py +53 -0
  57. semiont/testing/content.py +124 -0
  58. semiont/testing/gateway.py +85 -0
  59. semiont/testing/transport.py +341 -0
  60. semiont/timing.py +187 -0
  61. semiont/transport.py +531 -0
  62. semiont/types.py +5902 -0
  63. semiont/watched.py +111 -0
  64. semiont-0.6.12.dev2.dist-info/METADATA +533 -0
  65. semiont-0.6.12.dev2.dist-info/RECORD +66 -0
  66. 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]