ommlds 0.0.0.dev500__py3-none-any.whl → 0.0.0.dev501__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 (30) hide show
  1. ommlds/cli/sessions/chat/interfaces/textual/app.py +1 -1
  2. ommlds/minichain/__init__.py +9 -2
  3. ommlds/minichain/_dataclasses.py +337 -81
  4. ommlds/minichain/backends/impls/openai/format.py +1 -1
  5. ommlds/minichain/chat/messages.py +1 -1
  6. ommlds/minichain/chat/stream/joining.py +36 -12
  7. ommlds/minichain/chat/transforms/metadata.py +3 -3
  8. ommlds/minichain/content/standard.py +1 -1
  9. ommlds/minichain/content/transform/json.py +1 -1
  10. ommlds/minichain/content/transform/metadata.py +1 -1
  11. ommlds/minichain/content/transform/standard.py +2 -2
  12. ommlds/minichain/content/transform/strings.py +1 -1
  13. ommlds/minichain/content/transform/templates.py +1 -1
  14. ommlds/minichain/metadata.py +13 -16
  15. ommlds/minichain/services/__init__.py +155 -0
  16. ommlds/minichain/services/_marshal.py +46 -10
  17. ommlds/minichain/services/_origclasses.py +11 -0
  18. ommlds/minichain/services/_typedvalues.py +8 -3
  19. ommlds/minichain/services/requests.py +73 -3
  20. ommlds/minichain/services/responses.py +73 -3
  21. ommlds/minichain/services/services.py +9 -0
  22. ommlds/minichain/tools/reflect.py +3 -3
  23. ommlds/minichain/wrappers/instrument.py +146 -0
  24. ommlds/minichain/wrappers/services.py +26 -0
  25. {ommlds-0.0.0.dev500.dist-info → ommlds-0.0.0.dev501.dist-info}/METADATA +4 -4
  26. {ommlds-0.0.0.dev500.dist-info → ommlds-0.0.0.dev501.dist-info}/RECORD +30 -29
  27. {ommlds-0.0.0.dev500.dist-info → ommlds-0.0.0.dev501.dist-info}/WHEEL +0 -0
  28. {ommlds-0.0.0.dev500.dist-info → ommlds-0.0.0.dev501.dist-info}/entry_points.txt +0 -0
  29. {ommlds-0.0.0.dev500.dist-info → ommlds-0.0.0.dev501.dist-info}/licenses/LICENSE +0 -0
  30. {ommlds-0.0.0.dev500.dist-info → ommlds-0.0.0.dev501.dist-info}/top_level.txt +0 -0
@@ -145,7 +145,7 @@ def build_mc_ai_delta(delta: pt.ChatCompletionChunkChoiceDelta) -> AiDelta:
145
145
  )
146
146
 
147
147
  else:
148
- raise ValueError(delta)
148
+ return ContentAiDelta('')
149
149
 
150
150
 
151
151
  ##
@@ -41,7 +41,7 @@ class Message( # noqa
41
41
  def replace(self, **kwargs: ta.Any) -> ta.Self:
42
42
  if (n := dc.replace_is_not(self, **kwargs)) is self:
43
43
  return self
44
- return n.update_metadata(MessageOriginal(self), discard=[MessageOriginal], override=True)
44
+ return n.with_metadata(MessageOriginal(self), discard=[MessageOriginal], override=True)
45
45
 
46
46
 
47
47
  Chat: ta.TypeAlias = ta.Sequence[Message]
@@ -22,8 +22,9 @@ class AiDeltaJoiner:
22
22
  def __init__(self) -> None:
23
23
  super().__init__()
24
24
 
25
- self._deltas: list[AiDelta] = []
26
- self._messages: list[AnyAiMessage] = []
25
+ self._all: list[AiDelta] = []
26
+ self._queue: list[AiDelta] = []
27
+ self._out: list[AnyAiMessage] = []
27
28
 
28
29
  def _build_joined(self, deltas: ta.Sequence[AiDelta]) -> AnyAiMessage:
29
30
  dty = check.single(set(map(type, check.not_empty(deltas))))
@@ -43,6 +44,9 @@ class AiDeltaJoiner:
43
44
 
44
45
  ra = ''.join(filter(None, (td.raw_args for td in tds)))
45
46
 
47
+ if not ra:
48
+ ra = '{}'
49
+
46
50
  return ToolUseMessage(ToolUse(
47
51
  id=tds[0].id,
48
52
  name=check.non_empty_str(tds[0].name),
@@ -53,19 +57,39 @@ class AiDeltaJoiner:
53
57
  else:
54
58
  raise TypeError(dty)
55
59
 
56
- def _maybe_join(self) -> None:
57
- if not self._deltas:
60
+ def _join(self) -> None:
61
+ if not self._queue:
58
62
  return
59
63
 
60
- self._messages.append(self._build_joined(self._deltas))
61
- self._deltas.clear()
64
+ self._out.append(self._build_joined(self._queue))
65
+ self._queue.clear()
66
+
67
+ def _should_join(self, *, new: AiDelta | None = None) -> bool:
68
+ if not self._queue:
69
+ return False
70
+
71
+ if new is not None and type(self._queue[0]) is not type(new):
72
+ return True
73
+
74
+ if (
75
+ isinstance(d0 := self._queue[0], PartialToolUseAiDelta) and
76
+ isinstance(new, PartialToolUseAiDelta) and
77
+ d0.id is not None and
78
+ new.id is not None and
79
+ d0.id != new.id
80
+ ):
81
+ return True
82
+
83
+ return False
62
84
 
63
85
  def _add_one(self, d: AiDelta) -> None:
64
- if self._deltas and type(self._deltas[0]) is not type(d):
65
- self._maybe_join()
86
+ if self._should_join(new=d):
87
+ self._join()
88
+
89
+ self._all.append(d)
66
90
 
67
91
  if isinstance(d, ToolUseAiDelta):
68
- self._messages.append(ToolUseMessage(ToolUse(
92
+ self._out.append(ToolUseMessage(ToolUse(
69
93
  id=d.id,
70
94
  name=check.not_none(d.name),
71
95
  args=d.args or {},
@@ -73,13 +97,13 @@ class AiDeltaJoiner:
73
97
  )))
74
98
 
75
99
  else:
76
- self._deltas.append(d)
100
+ self._queue.append(d)
77
101
 
78
102
  def add(self, deltas: AiDeltas) -> None:
79
103
  for d in deltas:
80
104
  self._add_one(d)
81
105
 
82
106
  def build(self) -> AiChat:
83
- self._maybe_join()
107
+ self._join()
84
108
 
85
- return list(self._messages)
109
+ return list(self._out)
@@ -26,7 +26,7 @@ class UuidAddingMessageTransform(MessageTransform):
26
26
 
27
27
  def transform_message(self, m: Message) -> Chat:
28
28
  if Uuid not in m.metadata:
29
- m = m.update_metadata(Uuid(self.uuid_factory()))
29
+ m = m.with_metadata(Uuid(self.uuid_factory()))
30
30
  return [m]
31
31
 
32
32
 
@@ -36,7 +36,7 @@ class CreatedAtAddingMessageTransform(MessageTransform):
36
36
 
37
37
  def transform_message(self, m: Message) -> Chat:
38
38
  if CreatedAt not in m.metadata:
39
- m = m.update_metadata(CreatedAt(self.clock()))
39
+ m = m.with_metadata(CreatedAt(self.clock()))
40
40
  return [m]
41
41
 
42
42
 
@@ -54,6 +54,6 @@ class OriginAddingMessageTransform(MessageTransform):
54
54
 
55
55
  def transform_message(self, m: Message) -> Chat:
56
56
  return [
57
- o.update_metadata(TransformedMessageOrigin(m)) if TransformedMessageOrigin not in o.metadata else m
57
+ o.with_metadata(TransformedMessageOrigin(m)) if TransformedMessageOrigin not in o.metadata else m
58
58
  for o in self.child.transform_message(m)
59
59
  ]
@@ -29,4 +29,4 @@ class StandardContent( # noqa
29
29
  def replace(self, **kwargs: ta.Any) -> ta.Self:
30
30
  if (n := dc.replace_is_not(self, **kwargs)) is self:
31
31
  return self
32
- return n.update_metadata(ContentOriginal(self), discard=[ContentOriginal], override=True)
32
+ return n.with_metadata(ContentOriginal(self), discard=[ContentOriginal], override=True)
@@ -52,4 +52,4 @@ class JsonContentRenderer(ContentTransform[None]):
52
52
  case _:
53
53
  raise ValueError(self._code)
54
54
 
55
- return nc.update_metadata(ContentOriginal(c))
55
+ return nc.with_metadata(ContentOriginal(c))
@@ -9,7 +9,7 @@ from ..visitors import ContentTransform
9
9
 
10
10
  class OriginalMetadataStrippingContentTransform(ContentTransform[None]):
11
11
  def visit_standard_content(self, c: StandardContent, ctx: None) -> StandardContent:
12
- return c.discard_metadata(ContentOriginal)
12
+ return c.with_metadata(discard=[ContentOriginal])
13
13
 
14
14
 
15
15
  def strip_content_original_metadata(c: Content) -> Content:
@@ -26,7 +26,7 @@ class LiftToStandardContentTransform(ContentTransform[None]):
26
26
  self._sequence_mode = sequence_mode
27
27
 
28
28
  def visit_str(self, s: str, ctx: None) -> Content:
29
- return TextContent(s).update_metadata(ContentOriginal(s))
29
+ return TextContent(s).with_metadata(ContentOriginal(s))
30
30
 
31
31
  def visit_sequence(self, c: ta.Sequence[Content], ctx: None) -> Content:
32
32
  cc = check.isinstance(super().visit_sequence(c, ctx), collections.abc.Sequence)
@@ -40,4 +40,4 @@ class LiftToStandardContentTransform(ContentTransform[None]):
40
40
  case _:
41
41
  raise ValueError(self._sequence_mode)
42
42
 
43
- return nc.update_metadata(ContentOriginal(c))
43
+ return nc.with_metadata(ContentOriginal(c))
@@ -20,7 +20,7 @@ class StringFnContentTransform(ContentTransform[None]):
20
20
  fn: ta.Callable[[str], str]
21
21
 
22
22
  def visit_str(self, c: str, ctx: None) -> TextContent:
23
- return TextContent(self.fn(c)).update_metadata(ContentOriginal(c))
23
+ return TextContent(self.fn(c)).with_metadata(ContentOriginal(c))
24
24
 
25
25
  def visit_text_content(self, c: TextContent, ctx: None) -> TextContent:
26
26
  return c.replace(s=self.fn(c.s))
@@ -22,4 +22,4 @@ class TemplateContentMaterializer(ContentTransform[None]):
22
22
 
23
23
  def visit_template_content(self, c: TemplateContent, ctx: None) -> Content:
24
24
  s = c.t.render(check.not_none(self._templater_context))
25
- return TextContent(s).update_metadata(ContentOriginal(c))
25
+ return TextContent(s).with_metadata(ContentOriginal(c))
@@ -25,9 +25,9 @@ MetadataT = ta.TypeVar('MetadataT', bound=Metadata)
25
25
 
26
26
 
27
27
  class MetadataContainer(
28
- tv.TypedValueGeneric[MetadataT],
29
28
  lang.Abstract,
30
29
  lang.PackageSealed,
30
+ ta.Generic[MetadataT],
31
31
  ):
32
32
  @property
33
33
  @abc.abstractmethod
@@ -35,7 +35,12 @@ class MetadataContainer(
35
35
  raise NotImplementedError
36
36
 
37
37
  @abc.abstractmethod
38
- def update_metadata(self, *mds: MetadataT, override: bool = False) -> ta.Self:
38
+ def with_metadata(
39
+ self,
40
+ *add: MetadataT,
41
+ discard: ta.Iterable[type] | None = None,
42
+ override: bool = False,
43
+ ) -> ta.Self:
39
44
  raise NotImplementedError
40
45
 
41
46
 
@@ -66,30 +71,22 @@ class MetadataContainerDataclass( # noqa
66
71
  def metadata(self) -> tv.TypedValues[MetadataT]:
67
72
  return check.isinstance(getattr(self, '_metadata'), tv.TypedValues)
68
73
 
69
- def discard_metadata(self, *tys: type) -> ta.Self:
70
- nmd = (md := self.metadata).discard(*tys)
71
-
72
- if nmd is md:
73
- return self
74
-
75
- return dc.replace(self, _metadata=nmd) # type: ignore[call-arg] # noqa
76
-
77
- def update_metadata(
74
+ def with_metadata(
78
75
  self,
79
- *mds: MetadataT,
76
+ *add: MetadataT,
80
77
  discard: ta.Iterable[type] | None = None,
81
78
  override: bool = False,
82
79
  ) -> ta.Self:
83
- nmd = (md := self.metadata).update(
84
- *mds,
80
+ new = (old := self.metadata).update(
81
+ *add,
85
82
  discard=discard,
86
83
  override=override,
87
84
  )
88
85
 
89
- if nmd is md:
86
+ if new is old:
90
87
  return self
91
88
 
92
- return dc.replace(self, _metadata=nmd) # type: ignore[call-arg] # noqa
89
+ return dc.replace(self, _metadata=new) # type: ignore[call-arg] # noqa
93
90
 
94
91
 
95
92
  ##
@@ -1,4 +1,153 @@
1
1
  # ruff: noqa: I001
2
+ """
3
+ The core service abstraction. In general, Services are intended to encapsulate non-trivial, resourceful, effectful
4
+ operations, which are likely to have various implementations, each having their own specific capabilities in addition
5
+ to their common interface, and where wrapping, adapting, and transforming them in a uniform manner is desirable.
6
+
7
+ For example:
8
+ - A ChatService is passed a Request with a Chat value and returns a Response with an AiChat value.
9
+ - A ChatChoicesService is passed a Request with a Chat and returns a Response with a list of AiChoices.
10
+ - A ChatChoicesServiceChatService is a simple adapter taking a ChatChoicesService which it will invoke with its given
11
+ Request, expect a single AiChoice value in that Response, and return that single AiChat as its Response - thus acting
12
+ as a ChatService.
13
+ - Thus, all chat backends that return choices can be adapted to code expecting a single chat as output.
14
+ - A ChatStreamService is passed a Request with a Chat value and returns a Response with a value from which AiDeltas
15
+ may be streamed.
16
+ - A ChatChoicesStreamService is passed a Request with a Chat value and returns a Response with a value from which
17
+ AiChoicesDeltas may be streamed.
18
+ - A ChatChoicesStreamServiceChatChoicesService is an adapter taking a ChatChoicesStreamService and aggregating the
19
+ AiChoicesDeltas into joined, non-delta AiChoices.
20
+ - This may then be wrapped in a ChatChoicesServiceChatService to act as a ChatService.
21
+ - In practice however there are usually dedicated streaming and non-streaming implementations if possible as
22
+ non-streaming will usually have less overhead.
23
+ - An OpenaiChatChoicesService can act as a ChatChoicesService, and will accept all generic ChatOptions, in addition to
24
+ any OpenaiChatOptions inapplicable to any other backend. It may also produce all generic ChatOutputs, in addition to
25
+ OpenaiChatOutputs that will not be produced by other backends.
26
+ - Beyond chat, a VectorSearchService is passed a Request with a VectorSearch value and returns a Response with a
27
+ VectorHits value.
28
+ - A RetryService wraps any other Service and will attempt to re-invoke it on failure.
29
+ - A FirstInWinsService wraps any number of other Services and will return the first non-error Response it receives.
30
+
31
+ The service abstraction consists of 3 interrelated generic types:
32
+ - Request, an immutable final generic class containing a single value and any number of options.
33
+ - Response, an immutable final generic class containing a single value and any number of outputs.
34
+ - Service, a generic protocol consisting of a single method `invoke`, taking a request and returning a response.
35
+
36
+ There are 2 related abstract base classes in the parent package:
37
+ - Option, a non-generic abstract class representing a service option.
38
+ - Output, a non-generic abstract class representing a service output.
39
+
40
+ The purpose of this arrangement is to provide the following:
41
+ - There is only one method - `Service.invoke` - to deal with.
42
+ - There is no base `Service` class - service types are distinguished only by the requests they accept and responses
43
+ they return.
44
+ - It facilitates a clear, introspectable, generally type-safe means for handling 'less-specific' and 'more-specific'
45
+ service types.
46
+ - It facilitates generic wrapper and transformation machinery.
47
+
48
+ The variance of the type parameters of the 3 classes is central:
49
+ - `Request[V_co, OptionT_co]`
50
+ - `Response[V_co, OutputT_contra]`
51
+ - `Service[RequestT_contra, ResponseT_co]`
52
+
53
+ And to understand this, it's important to understand how Option and Output subtypes are intended to be arranged:
54
+ - These types are *not* intended to form a deep type hierarchy:
55
+ - A RemoteChatOption is *not* intended to inherit from a ChatOption: a ChatOption (be it a base class or union alias)
56
+ represents an option that *any* ChatService can accept, whereas a RemoteChatOption represents an option that *only*
57
+ applies to a RemoteChatService.
58
+ - If RemoteChatOption inherited from a base ChatOption, then it would have to apply to *all* ChatService
59
+ implementations.
60
+ - For example: were ApiKey to inherit from ChatOption, then it would have to apply to all ChatServices, including
61
+ LocalChatService, which has no concept of an api key.
62
+ - Similarly, a RemoteChatOutput is *not* intended to inherit from a ChatOutput: a ChatOutput represents an output that
63
+ *any* ChatService can produce, whereas a RemoteChatOutput represents an output that *only* applies to a
64
+ RemoteChatService.
65
+ - These 2 types are intended to form flat, disjoint, unrelated families of subtypes, and Request and Response are
66
+ intended to be parameterized by the unions of all such families they may contain.
67
+ - Because of this, one's visual intuition regarding types and subtypes may be reversed: `int` is effectively a subtype
68
+ of `int | str` despite `int` being a visually shorter, less complex type.
69
+ - `int` is a *MORE SPECIFIC* / *STRICT SUBSET* subtype of `int | str`, the *LESS SPECIFIC* / *STRICT SUPERSET*
70
+ supertype.
71
+
72
+ Regarding type variance:
73
+ - Service has the classic setup of contravariant input and covariant output:
74
+ - A RemoteChatService *is a* ChatService.
75
+ - A RemoteChatService may accept less specific requests than a ChatService.
76
+ - A RemoteChatService may return more specific responses than a ChatService.
77
+ - Request is covariant on its options:
78
+ - Recall, a RemoteChatOption *is not a* ChatOption.
79
+ - A ChatRequest *is a* RemoteChatRequest as it will not contain options RemoteChatService cannot accept.
80
+ - Response is contravariant on its outputs:
81
+ - Recall, a RemoteChatOutput *is not a* ChatOutput.
82
+ - A RemoteChatResponse *is a* ChatResponse even though it may contain additional output variants not produced by
83
+ every ChatService.
84
+ - Code that calls a ChatService and is given a ChatResponse must be prepared to handle (usually by simply ignoring)
85
+ outputs not necessarily produced by a base ChatService.
86
+
87
+ Below is a representative illustration of these types and their relationships. Note how:
88
+ - There is no subclassing of Request, Response, or Service - just type aliasing.
89
+ - There is no deep, shared subclassing of Option or Output.
90
+ - The type args passed to Request and Response are unions of all the Option and Output subtypes they may contain.
91
+ - These unions are kept in pluralized type aliases for convenience.
92
+ - There is no base ChatOption or ChatOutput class - were there, it would not be included in the base classes of any
93
+ local or remote only option or output.
94
+ - The local and remote sections take different but equivalent approaches:
95
+ - There are no base LocalChatOption or LocalChatOutput classes, but there *are* base RemoteChatOption and
96
+ RemoteChatOutput classes.
97
+ - Without any common base classes (besides the lowest level Output and Option classes), the local section treats them
98
+ as each distinct and bespoke, and the pluralized LocalChatOptions and LocalChatOutputs type aliases aggregate them
99
+ by explicitly listing them.
100
+ - With the common RemoteChatOption and RemoteChatOutput base classes, the remote section treats them as a related
101
+ family that any 'RemoteChat'-like service should accept and produce.
102
+
103
+ ```
104
+ # Common chat
105
+
106
+ class MaxTokens(Option, tv.UniqueScalarTypedValue[int]): pass
107
+ class Temperature(Option, tv.UniqueScalarTypedValue[float]): pass
108
+
109
+ ChatOptions: ta.TypeAlias = MaxTokens | Temperature
110
+ ChatRequest: ta.TypeAlias = Request[Chat, ChatOptions]
111
+
112
+ class TokenUsage(Output, tv.UniqueScalarTypedValue[int]): pass
113
+ class ElapsedTime(Output, tv.UniqueScalarTypedValue[float]): pass
114
+
115
+ ChatOutput: ta.TypeAlias = TokenUsage | ElapsedTime
116
+ ChatResponse: ta.TypeAlias = Response[Message, ChatOutput]
117
+
118
+ ChatService: ta.TypeAlias = Service[ChatRequest, ChatResponse]
119
+
120
+ # Local chat
121
+
122
+ class ModelPath(Option, tv.ScalarTypedValue[str]): pass
123
+
124
+ LocalChatOptions: ta.TypeAlias = ChatOptions | ModelPath
125
+ LocalChatRequest: ta.TypeAlias = Request[Chat, LocalChatOptions]
126
+
127
+ class LogPath(Output, tv.ScalarTypedValue[str]): pass
128
+
129
+ LocalChatOutputs: ta.TypeAlias = ChatOutput | LogPath
130
+ LocalChatResponse: ta.TypeAlias = Response[Message, LocalChatOutputs]
131
+
132
+ LocalChatService: ta.TypeAlias = Service[LocalChatRequest, LocalChatResponse]
133
+
134
+ # Remote chat
135
+
136
+ class RemoteChatOption(Option, lang.Abstract): pass
137
+ class ApiKey(RemoteChatOption, tv.ScalarTypedValue[str]): pass
138
+
139
+ RemoteChatOptions: ta.TypeAlias = ChatOptions | RemoteChatOption
140
+ RemoteChatRequest: ta.TypeAlias = Request[Chat, RemoteChatOptions]
141
+
142
+ class RemoteChatOutput(Output, lang.Abstract): pass
143
+ class BilledCostInUsd(RemoteChatOutput, tv.UniqueScalarTypedValue[float]): pass
144
+
145
+ RemoteChatOutputs: ta.TypeAlias = ChatOutput | RemoteChatOutput
146
+ RemoteChatResponse: ta.TypeAlias = Response[Message, RemoteChatOutputs]
147
+
148
+ RemoteChatService: ta.TypeAlias = Service[RemoteChatRequest, RemoteChatResponse]
149
+ ```
150
+ """
2
151
 
3
152
 
4
153
  from .facades import ( # noqa
@@ -8,10 +157,16 @@ from .facades import ( # noqa
8
157
  )
9
158
 
10
159
  from .requests import ( # noqa
160
+ RequestMetadata,
161
+ RequestMetadatas,
162
+
11
163
  Request,
12
164
  )
13
165
 
14
166
  from .responses import ( # noqa
167
+ ResponseMetadata,
168
+ ResponseMetadatas,
169
+
15
170
  Response,
16
171
  )
17
172
 
@@ -1,3 +1,8 @@
1
+ """
2
+ FIXME:
3
+ - everything lol
4
+ - can this just do what metadata does
5
+ """
1
6
  import typing as ta
2
7
 
3
8
  from omlish import check
@@ -22,10 +27,21 @@ def _is_rr_rty(rty: rfl.Type) -> bool:
22
27
  )
23
28
 
24
29
 
25
- def _get_tv_fld(rty: rfl.Type) -> dc.Field:
30
+ class _RrFlds(ta.NamedTuple):
31
+ v: dc.Field
32
+ tv: dc.Field
33
+ md: dc.Field
34
+
35
+
36
+ def _get_rr_flds(rty: rfl.Type) -> _RrFlds:
26
37
  flds = col.make_map_by(lambda f: f.name, dc.fields(check.not_none(rfl.get_concrete_type(rty))), strict=True)
27
- flds.pop('v')
28
- return check.single(flds.values())
38
+ v_fld = flds.pop('v')
39
+ md_fld = flds.pop('_metadata')
40
+ return _RrFlds(
41
+ v=v_fld,
42
+ tv=check.single(flds.values()),
43
+ md=md_fld,
44
+ )
29
45
 
30
46
 
31
47
  ##
@@ -34,7 +50,7 @@ def _get_tv_fld(rty: rfl.Type) -> dc.Field:
34
50
  @dc.dataclass(frozen=True)
35
51
  class _RequestResponseMarshaler(msh.Marshaler):
36
52
  rty: rfl.Type
37
- tv_fld: dc.Field
53
+ rr_flds: _RrFlds
38
54
  v_m: msh.Marshaler | None
39
55
 
40
56
  def marshal(self, ctx: msh.MarshalContext, o: ta.Any) -> msh.Value:
@@ -51,9 +67,14 @@ class _RequestResponseMarshaler(msh.Marshaler):
51
67
  else:
52
68
  v_v = self.v_m.marshal(ctx, o.v)
53
69
 
70
+ md_fmd = self.rr_flds.md.metadata[msh.FieldMetadata]
71
+ md_m = md_fmd.marshaler_factory.make_marshaler(ctx.marshal_factory_context, self.rr_flds.md.type)() # FIXME
72
+ md_v = md_m.marshal(ctx, o._metadata) # noqa
73
+
54
74
  return {
55
75
  'v': v_v,
56
- **({lang.strip_prefix(self.tv_fld.name, '_'): tv_v} if tv_v else {}),
76
+ **({lang.strip_prefix(self.rr_flds.tv.name, '_'): tv_v} if tv_v else {}),
77
+ **({'metadata': md_v} if md_v else {}),
57
78
  }
58
79
 
59
80
 
@@ -76,7 +97,7 @@ class _RequestResponseMarshalerFactory(msh.MarshalerFactory):
76
97
  v_m = ctx.make_marshaler(v_rty)
77
98
  return _RequestResponseMarshaler(
78
99
  rty,
79
- _get_tv_fld(rty),
100
+ _get_rr_flds(rty),
80
101
  v_m,
81
102
  )
82
103
 
@@ -89,9 +110,10 @@ class _RequestResponseMarshalerFactory(msh.MarshalerFactory):
89
110
  @dc.dataclass(frozen=True)
90
111
  class _RequestResponseUnmarshaler(msh.Unmarshaler):
91
112
  rty: rfl.Type
92
- tv_fld: dc.Field
113
+ rr_flds: _RrFlds
93
114
  v_u: msh.Unmarshaler
94
115
  tv_u: msh.Unmarshaler
116
+ md_u: msh.Unmarshaler
95
117
 
96
118
  def unmarshal(self, ctx: msh.UnmarshalContext, v: msh.Value) -> ta.Any:
97
119
  dct = dict(check.isinstance(v, ta.Mapping))
@@ -99,9 +121,14 @@ class _RequestResponseUnmarshaler(msh.Unmarshaler):
99
121
  v_v = dct.pop('v')
100
122
  v = self.v_u.unmarshal(ctx, v_v)
101
123
 
124
+ if md_v := dct.pop('metadata', None):
125
+ md = self.md_u.unmarshal(ctx, md_v)
126
+ else:
127
+ md = []
128
+
102
129
  tvs: ta.Any
103
130
  if dct:
104
- tv_vs = dct.pop(lang.strip_prefix(self.tv_fld.name, '_'))
131
+ tv_vs = dct.pop(lang.strip_prefix(self.rr_flds.tv.name, '_'))
105
132
  tvs = self.tv_u.unmarshal(ctx, tv_vs)
106
133
  else:
107
134
  tvs = []
@@ -109,7 +136,7 @@ class _RequestResponseUnmarshaler(msh.Unmarshaler):
109
136
  check.empty(dct)
110
137
 
111
138
  cty = rfl.get_concrete_type(self.rty)
112
- return cty(v, tvs) # type: ignore
139
+ return cty(v, tvs, _metadata=md) # type: ignore
113
140
 
114
141
 
115
142
  class _RequestResponseUnmarshalerFactory(msh.UnmarshalerFactory):
@@ -123,15 +150,24 @@ class _RequestResponseUnmarshalerFactory(msh.UnmarshalerFactory):
123
150
  else:
124
151
  # FIXME: ...
125
152
  raise TypeError(rty)
153
+
154
+ rr_flds = _get_rr_flds(rty)
155
+
126
156
  tv_types_set = check.isinstance(tv_rty, rfl.Union).args
127
157
  tv_ta = tv.TypedValues[ta.Union[*tv_types_set]] # type: ignore
128
158
  tv_u = ctx.make_unmarshaler(tv_ta)
159
+
129
160
  v_u = ctx.make_unmarshaler(v_rty)
161
+
162
+ md_fmd = rr_flds.md.metadata[msh.FieldMetadata]
163
+ md_u = md_fmd.unmarshaler_factory.make_unmarshaler(ctx, rr_flds.md.type)() # FIXME
164
+
130
165
  return _RequestResponseUnmarshaler(
131
166
  rty,
132
- _get_tv_fld(rty),
167
+ _get_rr_flds(rty),
133
168
  v_u,
134
169
  tv_u,
170
+ md_u,
135
171
  )
136
172
 
137
173
  return inner
@@ -43,3 +43,14 @@ class _OrigClassCapture:
43
43
  """Enforces that __orig_class__ has only been set once."""
44
44
 
45
45
  return check.single(object.__getattribute__(self, '__captured_orig_classes__'))
46
+
47
+
48
+ def confer_orig_class(src, dst):
49
+ if src is not dst:
50
+ try:
51
+ oc = src.__orig_class__
52
+ except AttributeError:
53
+ pass
54
+ else:
55
+ dst.__orig_class__ = oc
56
+ return dst
@@ -45,14 +45,19 @@ class _TypedValues(
45
45
  lang.Abstract,
46
46
  ta.Generic[TypedValueT],
47
47
  ):
48
- __typed_values_class__: ta.ClassVar[type[tv.TypedValue]]
48
+ """
49
+ The reason this is so complicated compared to any other TypedValues field (like metadata) is that the real set of
50
+ TypedValue types it accepts is known only via __orig_class__.
51
+ """
52
+
53
+ __typed_values_base__: ta.ClassVar[type[tv.TypedValue]]
49
54
 
50
55
  def __init_subclass__(cls, **kwargs: ta.Any) -> None:
51
56
  super().__init_subclass__(**kwargs)
52
57
 
53
58
  tvt = _get_typed_values_type_arg(cls)
54
59
  tvct = rfl.get_concrete_type(tvt, use_type_var_bound=True)
55
- cls.__typed_values_class__ = check.issubclass(check.isinstance(tvct, type), tv.TypedValue)
60
+ cls.__typed_values_base__ = check.issubclass(check.isinstance(tvct, type), tv.TypedValue)
56
61
 
57
62
  #
58
63
 
@@ -77,7 +82,7 @@ class _TypedValues(
77
82
 
78
83
  tv_types_set = frozenset(tv.reflect_typed_values_impls(tvt))
79
84
  tv_types = tuple(sorted(
80
- [check.issubclass(c, self.__typed_values_class__) for c in tv_types_set],
85
+ [check.issubclass(c, self.__typed_values_base__) for c in tv_types_set],
81
86
  key=lambda c: c.__qualname__,
82
87
  ))
83
88