Compare commits
5 Commits
995af130cf
...
cb728e3bd6
Author | SHA1 | Date |
---|---|---|
Tyler Goodlet | cb728e3bd6 | |
Tyler Goodlet | fb8196e354 | |
Tyler Goodlet | b6ed26589a | |
Tyler Goodlet | 8ff18739be | |
Tyler Goodlet | 456979dd12 |
|
@ -1,5 +1,6 @@
|
||||||
'''
|
'''
|
||||||
Functional audits for our "capability based messaging (schema)" feats.
|
Low-level functional audits for our
|
||||||
|
"capability based messaging"-spec feats.
|
||||||
|
|
||||||
B~)
|
B~)
|
||||||
|
|
||||||
|
@ -22,6 +23,7 @@ from msgspec import (
|
||||||
Struct,
|
Struct,
|
||||||
ValidationError,
|
ValidationError,
|
||||||
)
|
)
|
||||||
|
import pytest
|
||||||
import tractor
|
import tractor
|
||||||
from tractor.msg import (
|
from tractor.msg import (
|
||||||
_def_msgspec_codec,
|
_def_msgspec_codec,
|
||||||
|
@ -33,14 +35,31 @@ from tractor.msg import (
|
||||||
apply_codec,
|
apply_codec,
|
||||||
current_msgspec_codec,
|
current_msgspec_codec,
|
||||||
)
|
)
|
||||||
|
from tractor.msg import types
|
||||||
from tractor.msg.types import (
|
from tractor.msg.types import (
|
||||||
PayloadT,
|
# PayloadT,
|
||||||
Msg,
|
Msg,
|
||||||
# Started,
|
# Started,
|
||||||
mk_msg_spec,
|
mk_msg_spec,
|
||||||
)
|
)
|
||||||
import trio
|
import trio
|
||||||
|
|
||||||
|
|
||||||
|
def test_msg_spec_xor_pld_spec():
|
||||||
|
'''
|
||||||
|
If the `.msg.types.Msg`-set is overridden, we
|
||||||
|
can't also support a `Msg.pld` spec.
|
||||||
|
|
||||||
|
'''
|
||||||
|
# apply custom hooks and set a `Decoder` which only
|
||||||
|
# loads `NamespacePath` types.
|
||||||
|
with pytest.raises(RuntimeError):
|
||||||
|
mk_codec(
|
||||||
|
ipc_msg_spec=Any,
|
||||||
|
ipc_pld_spec=NamespacePath,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# TODO: wrap these into `._codec` such that user can just pass
|
# TODO: wrap these into `._codec` such that user can just pass
|
||||||
# a type table of some sort?
|
# a type table of some sort?
|
||||||
def enc_hook(obj: Any) -> Any:
|
def enc_hook(obj: Any) -> Any:
|
||||||
|
@ -66,11 +85,13 @@ def ex_func(*args):
|
||||||
print(f'ex_func({args})')
|
print(f'ex_func({args})')
|
||||||
|
|
||||||
|
|
||||||
def mk_custom_codec() -> MsgCodec:
|
def mk_custom_codec(
|
||||||
|
ipc_msg_spec: Type[Any] = Any,
|
||||||
|
) -> MsgCodec:
|
||||||
# apply custom hooks and set a `Decoder` which only
|
# apply custom hooks and set a `Decoder` which only
|
||||||
# loads `NamespacePath` types.
|
# loads `NamespacePath` types.
|
||||||
nsp_codec: MsgCodec = mk_codec(
|
nsp_codec: MsgCodec = mk_codec(
|
||||||
ipc_msg_spec=NamespacePath,
|
ipc_msg_spec=ipc_msg_spec,
|
||||||
enc_hook=enc_hook,
|
enc_hook=enc_hook,
|
||||||
dec_hook=dec_hook,
|
dec_hook=dec_hook,
|
||||||
)
|
)
|
||||||
|
@ -215,113 +236,152 @@ def test_codec_hooks_mod():
|
||||||
|
|
||||||
|
|
||||||
def chk_pld_type(
|
def chk_pld_type(
|
||||||
generic: Msg|_GenericAlias,
|
payload_spec: Type[Struct]|Any,
|
||||||
payload_type: Type[Struct]|Any,
|
|
||||||
pld: Any,
|
pld: Any,
|
||||||
|
|
||||||
|
expect_roundtrip: bool|None = None,
|
||||||
|
|
||||||
) -> bool:
|
) -> bool:
|
||||||
|
|
||||||
roundtrip: bool = False
|
|
||||||
pld_val_type: Type = type(pld)
|
pld_val_type: Type = type(pld)
|
||||||
|
|
||||||
# gen_paramed: _GenericAlias = generic[payload_type]
|
|
||||||
# TODO: verify that the overridden subtypes
|
# TODO: verify that the overridden subtypes
|
||||||
# DO NOT have modified type-annots from original!
|
# DO NOT have modified type-annots from original!
|
||||||
# 'Start', .pld: FuncSpec
|
# 'Start', .pld: FuncSpec
|
||||||
# 'StartAck', .pld: IpcCtxSpec
|
# 'StartAck', .pld: IpcCtxSpec
|
||||||
# 'Stop', .pld: UNSEt
|
# 'Stop', .pld: UNSEt
|
||||||
# 'Error', .pld: ErrorData
|
# 'Error', .pld: ErrorData
|
||||||
# for typedef in (
|
|
||||||
# [gen_paramed]
|
|
||||||
# +
|
|
||||||
|
|
||||||
# # type-var should always be set for these sub-types
|
codec: MsgCodec = mk_codec(
|
||||||
# # as well!
|
# NOTE: this ONLY accepts `Msg.pld` fields of a specified
|
||||||
# Msg.__subclasses__()
|
# type union.
|
||||||
# ):
|
ipc_pld_spec=payload_spec,
|
||||||
# if typedef.__name__ not in [
|
)
|
||||||
# 'Msg',
|
|
||||||
# 'Started',
|
|
||||||
# 'Yield',
|
|
||||||
# 'Return',
|
|
||||||
# ]:
|
|
||||||
# continue
|
|
||||||
# payload_type: Type[Struct] = CustomPayload
|
|
||||||
|
|
||||||
# TODO: can remove all this right!?
|
# make a one-off dec to compare with our `MsgCodec` instance
|
||||||
#
|
# which does the below `mk_msg_spec()` call internally
|
||||||
# when parameterized (like `Msg[Any]`) then
|
ipc_msg_spec: Union[Type[Struct]]
|
||||||
# we expect an alias as input.
|
msg_types: list[Msg[payload_spec]]
|
||||||
# if isinstance(generic, _GenericAlias):
|
|
||||||
# assert payload_type in generic.__args__
|
|
||||||
# else:
|
|
||||||
# assert PayloadType in generic.__parameters__
|
|
||||||
# pld_param: Parameter = generic.__signature__.parameters['pld']
|
|
||||||
# assert pld_param.annotation is PayloadType
|
|
||||||
|
|
||||||
type_spec: Union[Type[Struct]]
|
|
||||||
msg_types: list[Msg[payload_type]]
|
|
||||||
(
|
(
|
||||||
type_spec,
|
ipc_msg_spec,
|
||||||
msg_types,
|
msg_types,
|
||||||
) = mk_msg_spec(
|
) = mk_msg_spec(
|
||||||
payload_type=payload_type,
|
payload_type_union=payload_spec,
|
||||||
)
|
)
|
||||||
enc = msgpack.Encoder()
|
_enc = msgpack.Encoder()
|
||||||
dec = msgpack.Decoder(
|
_dec = msgpack.Decoder(
|
||||||
type=type_spec, # like `Msg[Any]`
|
type=ipc_msg_spec or Any, # like `Msg[Any]`
|
||||||
|
)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
payload_spec
|
||||||
|
==
|
||||||
|
codec.pld_spec
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert codec.dec == dec
|
||||||
|
#
|
||||||
|
# ^-XXX-^ not sure why these aren't "equal" but when cast
|
||||||
|
# to `str` they seem to match ?? .. kk
|
||||||
|
|
||||||
|
assert (
|
||||||
|
str(ipc_msg_spec)
|
||||||
|
==
|
||||||
|
str(codec.msg_spec)
|
||||||
|
==
|
||||||
|
str(_dec.type)
|
||||||
|
==
|
||||||
|
str(codec.dec.type)
|
||||||
)
|
)
|
||||||
|
|
||||||
# verify the boxed-type for all variable payload-type msgs.
|
# verify the boxed-type for all variable payload-type msgs.
|
||||||
|
if not msg_types:
|
||||||
|
breakpoint()
|
||||||
|
|
||||||
|
roundtrip: bool|None = None
|
||||||
|
pld_spec_msg_names: list[str] = [
|
||||||
|
td.__name__ for td in types._payload_spec_msgs
|
||||||
|
]
|
||||||
for typedef in msg_types:
|
for typedef in msg_types:
|
||||||
|
skip_runtime_msg: bool = typedef.__name__ not in pld_spec_msg_names
|
||||||
|
if skip_runtime_msg:
|
||||||
|
continue
|
||||||
|
|
||||||
pld_field = structs.fields(typedef)[1]
|
pld_field = structs.fields(typedef)[1]
|
||||||
assert pld_field.type in {payload_type, PayloadT}
|
assert pld_field.type is payload_spec # TODO-^ does this need to work to get all subtypes to adhere?
|
||||||
# TODO: does this need to work to get all subtypes to
|
|
||||||
# adhere?
|
|
||||||
assert pld_field.type is payload_type
|
|
||||||
|
|
||||||
kwargs: dict[str, Any] = {
|
kwargs: dict[str, Any] = {
|
||||||
'cid': '666',
|
'cid': '666',
|
||||||
'pld': pld,
|
'pld': pld,
|
||||||
}
|
}
|
||||||
enc_msg = typedef(**kwargs)
|
enc_msg: Msg = typedef(**kwargs)
|
||||||
|
|
||||||
wire_bytes: bytes = enc.encode(enc_msg)
|
_wire_bytes: bytes = _enc.encode(enc_msg)
|
||||||
|
wire_bytes: bytes = codec.enc.encode(enc_msg)
|
||||||
|
assert _wire_bytes == wire_bytes
|
||||||
|
|
||||||
|
ve: ValidationError|None = None
|
||||||
try:
|
try:
|
||||||
dec_msg = dec.decode(wire_bytes)
|
dec_msg = codec.dec.decode(wire_bytes)
|
||||||
assert dec_msg.pld == pld
|
_dec_msg = _dec.decode(wire_bytes)
|
||||||
assert (roundtrip := (dec_msg == enc_msg))
|
|
||||||
|
|
||||||
except ValidationError as ve:
|
# decoded msg and thus payload should be exactly same!
|
||||||
# breakpoint()
|
assert (roundtrip := (
|
||||||
if pld_val_type is payload_type:
|
_dec_msg
|
||||||
|
==
|
||||||
|
dec_msg
|
||||||
|
==
|
||||||
|
enc_msg
|
||||||
|
))
|
||||||
|
|
||||||
|
if (
|
||||||
|
expect_roundtrip is not None
|
||||||
|
and expect_roundtrip != roundtrip
|
||||||
|
):
|
||||||
|
breakpoint()
|
||||||
|
|
||||||
|
assert (
|
||||||
|
pld
|
||||||
|
==
|
||||||
|
dec_msg.pld
|
||||||
|
==
|
||||||
|
enc_msg.pld
|
||||||
|
)
|
||||||
|
# assert (roundtrip := (_dec_msg == enc_msg))
|
||||||
|
|
||||||
|
except ValidationError as _ve:
|
||||||
|
ve = _ve
|
||||||
|
roundtrip: bool = False
|
||||||
|
if pld_val_type is payload_spec:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'Got `ValidationError` despite type-var match!?\n'
|
'Got `ValidationError` despite type-var match!?\n'
|
||||||
f'pld_val_type: {pld_val_type}\n'
|
f'pld_val_type: {pld_val_type}\n'
|
||||||
f'payload_type: {payload_type}\n'
|
f'payload_type: {payload_spec}\n'
|
||||||
) from ve
|
) from ve
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# ow we good cuz the pld spec mismatched.
|
# ow we good cuz the pld spec mismatched.
|
||||||
print(
|
print(
|
||||||
'Got expected `ValidationError` since,\n'
|
'Got expected `ValidationError` since,\n'
|
||||||
f'{pld_val_type} is not {payload_type}\n'
|
f'{pld_val_type} is not {payload_spec}\n'
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
if (
|
if (
|
||||||
pld_val_type is not payload_type
|
payload_spec is not Any
|
||||||
and payload_type is not Any
|
and
|
||||||
|
pld_val_type is not payload_spec
|
||||||
):
|
):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'DID NOT `ValidationError` despite expected type match!?\n'
|
'DID NOT `ValidationError` despite expected type match!?\n'
|
||||||
f'pld_val_type: {pld_val_type}\n'
|
f'pld_val_type: {pld_val_type}\n'
|
||||||
f'payload_type: {payload_type}\n'
|
f'payload_type: {payload_spec}\n'
|
||||||
)
|
)
|
||||||
|
|
||||||
return roundtrip
|
# full code decode should always be attempted!
|
||||||
|
if roundtrip is None:
|
||||||
|
breakpoint()
|
||||||
|
|
||||||
|
return roundtrip
|
||||||
|
|
||||||
|
|
||||||
def test_limit_msgspec():
|
def test_limit_msgspec():
|
||||||
|
@ -333,9 +393,10 @@ def test_limit_msgspec():
|
||||||
|
|
||||||
# ensure we can round-trip a boxing `Msg`
|
# ensure we can round-trip a boxing `Msg`
|
||||||
assert chk_pld_type(
|
assert chk_pld_type(
|
||||||
Msg,
|
# Msg,
|
||||||
Any,
|
Any,
|
||||||
None,
|
None,
|
||||||
|
expect_roundtrip=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: don't need this any more right since
|
# TODO: don't need this any more right since
|
||||||
|
@ -347,7 +408,7 @@ def test_limit_msgspec():
|
||||||
|
|
||||||
# verify that a mis-typed payload value won't decode
|
# verify that a mis-typed payload value won't decode
|
||||||
assert not chk_pld_type(
|
assert not chk_pld_type(
|
||||||
Msg,
|
# Msg,
|
||||||
int,
|
int,
|
||||||
pld='doggy',
|
pld='doggy',
|
||||||
)
|
)
|
||||||
|
@ -360,13 +421,13 @@ def test_limit_msgspec():
|
||||||
value: Any
|
value: Any
|
||||||
|
|
||||||
assert not chk_pld_type(
|
assert not chk_pld_type(
|
||||||
Msg,
|
# Msg,
|
||||||
CustomPayload,
|
CustomPayload,
|
||||||
pld='doggy',
|
pld='doggy',
|
||||||
)
|
)
|
||||||
|
|
||||||
assert chk_pld_type(
|
assert chk_pld_type(
|
||||||
Msg,
|
# Msg,
|
||||||
CustomPayload,
|
CustomPayload,
|
||||||
pld=CustomPayload(name='doggy', value='urmom')
|
pld=CustomPayload(name='doggy', value='urmom')
|
||||||
)
|
)
|
|
@ -536,7 +536,9 @@ def pack_error(
|
||||||
# content's `.msgdata`).
|
# content's `.msgdata`).
|
||||||
error_msg['tb_str'] = tb_str
|
error_msg['tb_str'] = tb_str
|
||||||
|
|
||||||
pkt: dict = {'error': error_msg}
|
pkt: dict = {
|
||||||
|
'error': error_msg,
|
||||||
|
}
|
||||||
if cid:
|
if cid:
|
||||||
pkt['cid'] = cid
|
pkt['cid'] = cid
|
||||||
|
|
||||||
|
|
|
@ -48,6 +48,7 @@ from tractor._exceptions import TransportClosed
|
||||||
from tractor.msg import (
|
from tractor.msg import (
|
||||||
_ctxvar_MsgCodec,
|
_ctxvar_MsgCodec,
|
||||||
MsgCodec,
|
MsgCodec,
|
||||||
|
mk_codec,
|
||||||
)
|
)
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
@ -162,7 +163,7 @@ class MsgpackTCPStream(MsgTransport):
|
||||||
|
|
||||||
# allow for custom IPC msg interchange format
|
# allow for custom IPC msg interchange format
|
||||||
# dynamic override Bo
|
# dynamic override Bo
|
||||||
self.codec: MsgCodec = codec or MsgCodec()
|
self.codec: MsgCodec = codec or mk_codec()
|
||||||
|
|
||||||
async def _iter_packets(self) -> AsyncGenerator[dict, None]:
|
async def _iter_packets(self) -> AsyncGenerator[dict, None]:
|
||||||
'''
|
'''
|
||||||
|
|
|
@ -89,7 +89,10 @@ async def _invoke_non_context(
|
||||||
|
|
||||||
# TODO: can we unify this with the `context=True` impl below?
|
# TODO: can we unify this with the `context=True` impl below?
|
||||||
if inspect.isasyncgen(coro):
|
if inspect.isasyncgen(coro):
|
||||||
await chan.send({'functype': 'asyncgen', 'cid': cid})
|
await chan.send({
|
||||||
|
'cid': cid,
|
||||||
|
'functype': 'asyncgen',
|
||||||
|
})
|
||||||
# XXX: massive gotcha! If the containing scope
|
# XXX: massive gotcha! If the containing scope
|
||||||
# is cancelled and we execute the below line,
|
# is cancelled and we execute the below line,
|
||||||
# any ``ActorNursery.__aexit__()`` WON'T be
|
# any ``ActorNursery.__aexit__()`` WON'T be
|
||||||
|
@ -109,18 +112,27 @@ async def _invoke_non_context(
|
||||||
# to_send = await chan.recv_nowait()
|
# to_send = await chan.recv_nowait()
|
||||||
# if to_send is not None:
|
# if to_send is not None:
|
||||||
# to_yield = await coro.asend(to_send)
|
# to_yield = await coro.asend(to_send)
|
||||||
await chan.send({'yield': item, 'cid': cid})
|
await chan.send({
|
||||||
|
'yield': item,
|
||||||
|
'cid': cid,
|
||||||
|
})
|
||||||
|
|
||||||
log.runtime(f"Finished iterating {coro}")
|
log.runtime(f"Finished iterating {coro}")
|
||||||
# TODO: we should really support a proper
|
# TODO: we should really support a proper
|
||||||
# `StopAsyncIteration` system here for returning a final
|
# `StopAsyncIteration` system here for returning a final
|
||||||
# value if desired
|
# value if desired
|
||||||
await chan.send({'stop': True, 'cid': cid})
|
await chan.send({
|
||||||
|
'stop': True,
|
||||||
|
'cid': cid,
|
||||||
|
})
|
||||||
|
|
||||||
# one way @stream func that gets treated like an async gen
|
# one way @stream func that gets treated like an async gen
|
||||||
# TODO: can we unify this with the `context=True` impl below?
|
# TODO: can we unify this with the `context=True` impl below?
|
||||||
elif treat_as_gen:
|
elif treat_as_gen:
|
||||||
await chan.send({'functype': 'asyncgen', 'cid': cid})
|
await chan.send({
|
||||||
|
'cid': cid,
|
||||||
|
'functype': 'asyncgen',
|
||||||
|
})
|
||||||
# XXX: the async-func may spawn further tasks which push
|
# XXX: the async-func may spawn further tasks which push
|
||||||
# back values like an async-generator would but must
|
# back values like an async-generator would but must
|
||||||
# manualy construct the response dict-packet-responses as
|
# manualy construct the response dict-packet-responses as
|
||||||
|
@ -133,7 +145,10 @@ async def _invoke_non_context(
|
||||||
if not cs.cancelled_caught:
|
if not cs.cancelled_caught:
|
||||||
# task was not cancelled so we can instruct the
|
# task was not cancelled so we can instruct the
|
||||||
# far end async gen to tear down
|
# far end async gen to tear down
|
||||||
await chan.send({'stop': True, 'cid': cid})
|
await chan.send({
|
||||||
|
'stop': True,
|
||||||
|
'cid': cid
|
||||||
|
})
|
||||||
else:
|
else:
|
||||||
# regular async function/method
|
# regular async function/method
|
||||||
# XXX: possibly just a scheduled `Actor._cancel_task()`
|
# XXX: possibly just a scheduled `Actor._cancel_task()`
|
||||||
|
@ -182,10 +197,10 @@ async def _invoke_non_context(
|
||||||
and chan.connected()
|
and chan.connected()
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
await chan.send(
|
await chan.send({
|
||||||
{'return': result,
|
'return': result,
|
||||||
'cid': cid}
|
'cid': cid,
|
||||||
)
|
})
|
||||||
except (
|
except (
|
||||||
BrokenPipeError,
|
BrokenPipeError,
|
||||||
trio.BrokenResourceError,
|
trio.BrokenResourceError,
|
||||||
|
@ -479,8 +494,8 @@ async def _invoke(
|
||||||
# "least sugary" type of RPC ep with support for
|
# "least sugary" type of RPC ep with support for
|
||||||
# bi-dir streaming B)
|
# bi-dir streaming B)
|
||||||
await chan.send({
|
await chan.send({
|
||||||
|
'cid': cid,
|
||||||
'functype': 'context',
|
'functype': 'context',
|
||||||
'cid': cid
|
|
||||||
})
|
})
|
||||||
|
|
||||||
# TODO: should we also use an `.open_context()` equiv
|
# TODO: should we also use an `.open_context()` equiv
|
||||||
|
|
|
@ -33,3 +33,24 @@ from ._codec import (
|
||||||
MsgCodec as MsgCodec,
|
MsgCodec as MsgCodec,
|
||||||
current_msgspec_codec as current_msgspec_codec,
|
current_msgspec_codec as current_msgspec_codec,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from .types import (
|
||||||
|
Msg as Msg,
|
||||||
|
|
||||||
|
Start as Start, # with pld
|
||||||
|
FuncSpec as FuncSpec,
|
||||||
|
|
||||||
|
StartAck as StartAck, # with pld
|
||||||
|
IpcCtxSpec as IpcCtxSpec,
|
||||||
|
|
||||||
|
Started as Started,
|
||||||
|
Yield as Yield,
|
||||||
|
Stop as Stop,
|
||||||
|
Return as Return,
|
||||||
|
|
||||||
|
Error as Error, # with pld
|
||||||
|
ErrorData as ErrorData,
|
||||||
|
|
||||||
|
# full msg spec set
|
||||||
|
__spec__ as __spec__,
|
||||||
|
)
|
||||||
|
|
|
@ -29,6 +29,7 @@ ToDo: backends we prolly should offer:
|
||||||
- https://capnproto.org/language.html#language-reference
|
- https://capnproto.org/language.html#language-reference
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
from __future__ import annotations
|
||||||
from contextvars import (
|
from contextvars import (
|
||||||
ContextVar,
|
ContextVar,
|
||||||
Token,
|
Token,
|
||||||
|
@ -54,18 +55,35 @@ from tractor.msg.types import (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# TODO: API changes towards being interchange lib agnostic!
|
# TODO: overall IPC msg-spec features (i.e. in this mod)!
|
||||||
#
|
#
|
||||||
# -[ ] capnproto has pre-compiled schema for eg..
|
# -[ ] API changes towards being interchange lib agnostic!
|
||||||
# * https://capnproto.org/language.html
|
# -[ ] capnproto has pre-compiled schema for eg..
|
||||||
# * http://capnproto.github.io/pycapnp/quickstart.html
|
# * https://capnproto.org/language.html
|
||||||
# * https://github.com/capnproto/pycapnp/blob/master/examples/addressbook.capnp
|
# * http://capnproto.github.io/pycapnp/quickstart.html
|
||||||
|
# * https://github.com/capnproto/pycapnp/blob/master/examples/addressbook.capnp
|
||||||
|
#
|
||||||
|
# -[ ] struct aware messaging coders as per:
|
||||||
|
# -[x] https://github.com/goodboy/tractor/issues/36
|
||||||
|
# -[ ] https://github.com/goodboy/tractor/issues/196
|
||||||
|
# -[ ] https://github.com/goodboy/tractor/issues/365
|
||||||
#
|
#
|
||||||
class MsgCodec(Struct):
|
class MsgCodec(Struct):
|
||||||
'''
|
'''
|
||||||
A IPC msg interchange format lib's encoder + decoder pair.
|
A IPC msg interchange format lib's encoder + decoder pair.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
_enc: msgpack.Encoder
|
||||||
|
_dec: msgpack.Decoder
|
||||||
|
|
||||||
|
pld_spec: Union[Type[Struct]]|None
|
||||||
|
|
||||||
|
# struct type unions
|
||||||
|
# https://jcristharif.com/msgspec/structs.html#tagged-unions
|
||||||
|
@property
|
||||||
|
def msg_spec(self) -> Union[Type[Struct]]:
|
||||||
|
return self._dec.type
|
||||||
|
|
||||||
lib: ModuleType = msgspec
|
lib: ModuleType = msgspec
|
||||||
|
|
||||||
# ad-hoc type extensions
|
# ad-hoc type extensions
|
||||||
|
@ -73,16 +91,8 @@ class MsgCodec(Struct):
|
||||||
enc_hook: Callable[[Any], Any]|None = None # coder
|
enc_hook: Callable[[Any], Any]|None = None # coder
|
||||||
dec_hook: Callable[[type, Any], Any]|None = None # decoder
|
dec_hook: Callable[[type, Any], Any]|None = None # decoder
|
||||||
|
|
||||||
# struct type unions
|
|
||||||
# https://jcristharif.com/msgspec/structs.html#tagged-unions
|
|
||||||
ipc_msg_spec: Union[Type[Struct]]|Any = Any
|
|
||||||
payload_msg_spec: Union[Type[Struct]] = Any
|
|
||||||
|
|
||||||
# post-configure cached props
|
|
||||||
_enc: msgpack.Encoder|None = None
|
|
||||||
_dec: msgpack.Decoder|None = None
|
|
||||||
|
|
||||||
# TODO: a sub-decoder system as well?
|
# TODO: a sub-decoder system as well?
|
||||||
|
# payload_msg_specs: Union[Type[Struct]] = Any
|
||||||
# see related comments in `.msg.types`
|
# see related comments in `.msg.types`
|
||||||
# _payload_decs: (
|
# _payload_decs: (
|
||||||
# dict[
|
# dict[
|
||||||
|
@ -91,42 +101,18 @@ class MsgCodec(Struct):
|
||||||
# ]
|
# ]
|
||||||
# |None
|
# |None
|
||||||
# ) = None
|
# ) = None
|
||||||
|
# OR
|
||||||
|
# ) = {
|
||||||
|
# # pre-seed decoders for std-py-type-set for use when
|
||||||
|
# # `Msg.pld == None|Any`.
|
||||||
|
# None: msgpack.Decoder(Any),
|
||||||
|
# Any: msgpack.Decoder(Any),
|
||||||
|
# }
|
||||||
|
|
||||||
# TODO: use `functools.cached_property` for these ?
|
# TODO: use `functools.cached_property` for these ?
|
||||||
# https://docs.python.org/3/library/functools.html#functools.cached_property
|
# https://docs.python.org/3/library/functools.html#functools.cached_property
|
||||||
@property
|
@property
|
||||||
def enc(self) -> msgpack.Encoder:
|
def enc(self) -> msgpack.Encoder:
|
||||||
return self._enc or self.encoder()
|
|
||||||
|
|
||||||
def encoder(
|
|
||||||
self,
|
|
||||||
enc_hook: Callable|None = None,
|
|
||||||
reset: bool = False,
|
|
||||||
|
|
||||||
# TODO: what's the default for this, and do we care?
|
|
||||||
# write_buffer_size: int
|
|
||||||
#
|
|
||||||
**kwargs,
|
|
||||||
|
|
||||||
) -> msgpack.Encoder:
|
|
||||||
'''
|
|
||||||
Set or get the maybe-cached `msgspec.msgpack.Encoder`
|
|
||||||
instance configured for this codec.
|
|
||||||
|
|
||||||
When `reset=True` any previously configured encoder will
|
|
||||||
be recreated and then cached with the new settings passed
|
|
||||||
as input.
|
|
||||||
|
|
||||||
'''
|
|
||||||
if (
|
|
||||||
self._enc is None
|
|
||||||
or reset
|
|
||||||
):
|
|
||||||
self._enc = self.lib.msgpack.Encoder(
|
|
||||||
enc_hook=enc_hook or self.enc_hook,
|
|
||||||
# write_buffer_size=write_buffer_size,
|
|
||||||
)
|
|
||||||
|
|
||||||
return self._enc
|
return self._enc
|
||||||
|
|
||||||
def encode(
|
def encode(
|
||||||
|
@ -139,40 +125,10 @@ class MsgCodec(Struct):
|
||||||
on a tranport protocol connection.
|
on a tranport protocol connection.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
return self.enc.encode(py_obj)
|
return self._enc.encode(py_obj)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dec(self) -> msgpack.Decoder:
|
def dec(self) -> msgpack.Decoder:
|
||||||
return self._dec or self.decoder()
|
|
||||||
|
|
||||||
def decoder(
|
|
||||||
self,
|
|
||||||
ipc_msg_spec: Union[Type[Struct]]|None = None,
|
|
||||||
dec_hook: Callable|None = None,
|
|
||||||
reset: bool = False,
|
|
||||||
**kwargs,
|
|
||||||
# ext_hook: ext_hook_sig
|
|
||||||
|
|
||||||
) -> msgpack.Decoder:
|
|
||||||
'''
|
|
||||||
Set or get the maybe-cached `msgspec.msgpack.Decoder`
|
|
||||||
instance configured for this codec.
|
|
||||||
|
|
||||||
When `reset=True` any previously configured decoder will
|
|
||||||
be recreated and then cached with the new settings passed
|
|
||||||
as input.
|
|
||||||
|
|
||||||
'''
|
|
||||||
if (
|
|
||||||
self._dec is None
|
|
||||||
or reset
|
|
||||||
):
|
|
||||||
self._dec = self.lib.msgpack.Decoder(
|
|
||||||
type=ipc_msg_spec or self.ipc_msg_spec,
|
|
||||||
dec_hook=dec_hook or self.dec_hook,
|
|
||||||
**kwargs,
|
|
||||||
)
|
|
||||||
|
|
||||||
return self._dec
|
return self._dec
|
||||||
|
|
||||||
def decode(
|
def decode(
|
||||||
|
@ -185,60 +141,181 @@ class MsgCodec(Struct):
|
||||||
determined by the
|
determined by the
|
||||||
|
|
||||||
'''
|
'''
|
||||||
return self.dec.decode(msg)
|
# https://jcristharif.com/msgspec/usage.html#typed-decoding
|
||||||
|
return self._dec.decode(msg)
|
||||||
|
|
||||||
|
# TODO: do we still want to try and support the sub-decoder with
|
||||||
|
# `.Raw` technique in the case that the `Generic` approach gives
|
||||||
|
# future grief?
|
||||||
|
#
|
||||||
|
# -[ ] <NEW-ISSUE-FOR-ThIS-HERE>
|
||||||
|
# -> https://jcristharif.com/msgspec/api.html#raw
|
||||||
|
#
|
||||||
|
#def mk_pld_subdec(
|
||||||
|
# self,
|
||||||
|
# payload_types: Union[Type[Struct]],
|
||||||
|
|
||||||
|
#) -> msgpack.Decoder:
|
||||||
|
# # TODO: sub-decoder suppor for `.pld: Raw`?
|
||||||
|
# # => see similar notes inside `.msg.types`..
|
||||||
|
# #
|
||||||
|
# # not sure we'll end up needing this though it might have
|
||||||
|
# # unforeseen advantages in terms of enabling encrypted
|
||||||
|
# # appliciation layer (only) payloads?
|
||||||
|
# #
|
||||||
|
# # register sub-payload decoders to load `.pld: Raw`
|
||||||
|
# # decoded `Msg`-packets using a dynamic lookup (table)
|
||||||
|
# # instead of a pre-defined msg-spec via `Generic`
|
||||||
|
# # parameterization.
|
||||||
|
# #
|
||||||
|
# (
|
||||||
|
# tags,
|
||||||
|
# payload_dec,
|
||||||
|
# ) = mk_tagged_union_dec(
|
||||||
|
# tagged_structs=list(payload_types.__args__),
|
||||||
|
# )
|
||||||
|
# # register sub-decoders by tag
|
||||||
|
# subdecs: dict[str, msgpack.Decoder]|None = self._payload_decs
|
||||||
|
# for name in tags:
|
||||||
|
# subdecs.setdefault(
|
||||||
|
# name,
|
||||||
|
# payload_dec,
|
||||||
|
# )
|
||||||
|
|
||||||
|
# return payload_dec
|
||||||
|
|
||||||
|
# sub-decoders for retreiving embedded
|
||||||
|
# payload data and decoding to a sender
|
||||||
|
# side defined (struct) type.
|
||||||
|
# def dec_payload(
|
||||||
|
# codec: MsgCodec,
|
||||||
|
# msg: Msg,
|
||||||
|
|
||||||
|
# ) -> Any|Struct:
|
||||||
|
|
||||||
|
# msg: Msg = codec.dec.decode(msg)
|
||||||
|
# payload_tag: str = msg.header.payload_tag
|
||||||
|
# payload_dec: msgpack.Decoder = codec._payload_decs[payload_tag]
|
||||||
|
# return payload_dec.decode(msg.pld)
|
||||||
|
|
||||||
|
# def enc_payload(
|
||||||
|
# codec: MsgCodec,
|
||||||
|
# payload: Any,
|
||||||
|
# cid: str,
|
||||||
|
|
||||||
|
# ) -> bytes:
|
||||||
|
|
||||||
|
# # tag_field: str|None = None
|
||||||
|
|
||||||
|
# plbytes = codec.enc.encode(payload)
|
||||||
|
# if b'msg_type' in plbytes:
|
||||||
|
# assert isinstance(payload, Struct)
|
||||||
|
|
||||||
|
# # tag_field: str = type(payload).__name__
|
||||||
|
# payload = msgspec.Raw(plbytes)
|
||||||
|
|
||||||
|
# msg = Msg(
|
||||||
|
# cid=cid,
|
||||||
|
# pld=payload,
|
||||||
|
# # Header(
|
||||||
|
# # payload_tag=tag_field,
|
||||||
|
# # # dialog_id,
|
||||||
|
# # ),
|
||||||
|
# )
|
||||||
|
# return codec.enc.encode(msg)
|
||||||
|
|
||||||
|
|
||||||
def mk_tagged_union_dec(
|
|
||||||
tagged_structs: list[Struct],
|
|
||||||
|
|
||||||
) -> tuple[
|
# TODO: sub-decoded `Raw` fields?
|
||||||
list[str],
|
# -[ ] see `MsgCodec._payload_decs` notes
|
||||||
msgpack.Decoder,
|
#
|
||||||
]:
|
# XXX if we wanted something more complex then field name str-keys
|
||||||
# See "tagged unions" docs:
|
# we might need a header field type to describe the lookup sys?
|
||||||
# https://jcristharif.com/msgspec/structs.html#tagged-unions
|
# class Header(Struct, tag=True):
|
||||||
|
# '''
|
||||||
|
# A msg header which defines payload properties
|
||||||
|
|
||||||
# "The quickest way to enable tagged unions is to set tag=True when
|
# '''
|
||||||
# defining every struct type in the union. In this case tag_field
|
# payload_tag: str|None = None
|
||||||
# defaults to "type", and tag defaults to the struct class name
|
|
||||||
# (e.g. "Get")."
|
|
||||||
first: Struct = tagged_structs[0]
|
|
||||||
types_union: Union[Type[Struct]] = Union[
|
|
||||||
first
|
|
||||||
]|Any
|
|
||||||
tags: list[str] = [first.__name__]
|
|
||||||
|
|
||||||
for struct in tagged_structs[1:]:
|
|
||||||
types_union |= struct
|
|
||||||
tags.append(struct.__name__)
|
|
||||||
|
|
||||||
dec = msgpack.Decoder(types_union)
|
#def mk_tagged_union_dec(
|
||||||
return (
|
# tagged_structs: list[Struct],
|
||||||
tags,
|
|
||||||
dec,
|
#) -> tuple[
|
||||||
)
|
# list[str],
|
||||||
|
# msgpack.Decoder,
|
||||||
|
#]:
|
||||||
|
# '''
|
||||||
|
# Create a `msgpack.Decoder` for an input `list[msgspec.Struct]`
|
||||||
|
# and return a `list[str]` of each struct's `tag_field: str` value
|
||||||
|
# which can be used to "map to" the initialized dec.
|
||||||
|
|
||||||
|
# '''
|
||||||
|
# # See "tagged unions" docs:
|
||||||
|
# # https://jcristharif.com/msgspec/structs.html#tagged-unions
|
||||||
|
|
||||||
|
# # "The quickest way to enable tagged unions is to set tag=True when
|
||||||
|
# # defining every struct type in the union. In this case tag_field
|
||||||
|
# # defaults to "type", and tag defaults to the struct class name
|
||||||
|
# # (e.g. "Get")."
|
||||||
|
# first: Struct = tagged_structs[0]
|
||||||
|
# types_union: Union[Type[Struct]] = Union[
|
||||||
|
# first
|
||||||
|
# ]|Any
|
||||||
|
# tags: list[str] = [first.__name__]
|
||||||
|
|
||||||
|
# for struct in tagged_structs[1:]:
|
||||||
|
# types_union |= struct
|
||||||
|
# tags.append(
|
||||||
|
# getattr(
|
||||||
|
# struct,
|
||||||
|
# struct.__struct_config__.tag_field,
|
||||||
|
# struct.__name__,
|
||||||
|
# )
|
||||||
|
# )
|
||||||
|
|
||||||
|
# dec = msgpack.Decoder(types_union)
|
||||||
|
# return (
|
||||||
|
# tags,
|
||||||
|
# dec,
|
||||||
|
# )
|
||||||
|
|
||||||
# TODO: struct aware messaging coders as per:
|
|
||||||
# - https://github.com/goodboy/tractor/issues/36
|
|
||||||
# - https://github.com/goodboy/tractor/issues/196
|
|
||||||
# - https://github.com/goodboy/tractor/issues/365
|
|
||||||
|
|
||||||
def mk_codec(
|
def mk_codec(
|
||||||
libname: str = 'msgspec',
|
ipc_msg_spec: Union[Type[Struct]]|Any|None = None,
|
||||||
|
|
||||||
# for codec-ing boxed `Msg`-with-payload msgs
|
|
||||||
payload_types: Union[Type[Struct]]|None = None,
|
|
||||||
|
|
||||||
# TODO: do we want to allow NOT/using a diff `Msg`-set?
|
|
||||||
#
|
#
|
||||||
|
# ^TODO^: in the long run, do we want to allow using a diff IPC `Msg`-set?
|
||||||
|
# it would break the runtime, but maybe say if you wanted
|
||||||
|
# to add some kinda field-specific or wholesale `.pld` ecryption?
|
||||||
|
|
||||||
# struct type unions set for `Decoder`
|
# struct type unions set for `Decoder`
|
||||||
# https://jcristharif.com/msgspec/structs.html#tagged-unions
|
# https://jcristharif.com/msgspec/structs.html#tagged-unions
|
||||||
ipc_msg_spec: Union[Type[Struct]]|Any = Any,
|
ipc_pld_spec: Union[Type[Struct]]|Any|None = None,
|
||||||
|
|
||||||
cache_now: bool = True,
|
# TODO: offering a per-msg(-field) type-spec such that
|
||||||
|
# the fields can be dynamically NOT decoded and left as `Raw`
|
||||||
|
# values which are later loaded by a sub-decoder specified
|
||||||
|
# by `tag_field: str` value key?
|
||||||
|
# payload_msg_specs: dict[
|
||||||
|
# str, # tag_field value as sub-decoder key
|
||||||
|
# Union[Type[Struct]] # `Msg.pld` type spec
|
||||||
|
# ]|None = None,
|
||||||
|
|
||||||
|
libname: str = 'msgspec',
|
||||||
|
|
||||||
# proxy as `Struct(**kwargs)`
|
# proxy as `Struct(**kwargs)`
|
||||||
|
# ------ - ------
|
||||||
|
dec_hook: Callable|None = None,
|
||||||
|
enc_hook: Callable|None = None,
|
||||||
|
# ------ - ------
|
||||||
**kwargs,
|
**kwargs,
|
||||||
|
#
|
||||||
|
# Encoder:
|
||||||
|
# write_buffer_size=write_buffer_size,
|
||||||
|
#
|
||||||
|
# Decoder:
|
||||||
|
# ext_hook: ext_hook_sig
|
||||||
|
|
||||||
) -> MsgCodec:
|
) -> MsgCodec:
|
||||||
'''
|
'''
|
||||||
|
@ -247,75 +324,78 @@ def mk_codec(
|
||||||
`msgspec` ;).
|
`msgspec` ;).
|
||||||
|
|
||||||
'''
|
'''
|
||||||
# (manually) generate a msg-payload-spec for all relevant
|
if (
|
||||||
# god-boxing-msg subtypes, parameterizing the `Msg.pld: PayloadT`
|
ipc_msg_spec is not None
|
||||||
# for the decoder such that all sub-type msgs in our SCIPP
|
and ipc_pld_spec
|
||||||
# will automatically decode to a type-"limited" payload (`Struct`)
|
):
|
||||||
# object (set).
|
raise RuntimeError(
|
||||||
payload_type_spec: Union[Type[Msg]]|None = None
|
f'If a payload spec is provided,\n'
|
||||||
if payload_types:
|
"the builtin SC-shuttle-protocol's msg set\n"
|
||||||
|
f'(i.e. `{Msg}`) MUST be used!\n\n'
|
||||||
|
f'However both values were passed as => mk_codec(\n'
|
||||||
|
f' ipc_msg_spec={ipc_msg_spec}`\n'
|
||||||
|
f' ipc_pld_spec={ipc_pld_spec}`\n)\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
elif (
|
||||||
|
ipc_pld_spec
|
||||||
|
and
|
||||||
|
|
||||||
|
# XXX required for now (or maybe forever?) until
|
||||||
|
# we can dream up a way to allow parameterizing and/or
|
||||||
|
# custom overrides to the `Msg`-spec protocol itself?
|
||||||
|
ipc_msg_spec is None
|
||||||
|
):
|
||||||
|
# (manually) generate a msg-payload-spec for all relevant
|
||||||
|
# god-boxing-msg subtypes, parameterizing the `Msg.pld: PayloadT`
|
||||||
|
# for the decoder such that all sub-type msgs in our SCIPP
|
||||||
|
# will automatically decode to a type-"limited" payload (`Struct`)
|
||||||
|
# object (set).
|
||||||
(
|
(
|
||||||
payload_type_spec,
|
ipc_msg_spec,
|
||||||
msg_types,
|
msg_types,
|
||||||
) = mk_msg_spec(
|
) = mk_msg_spec(
|
||||||
payload_type=payload_types,
|
payload_type_union=ipc_pld_spec,
|
||||||
)
|
)
|
||||||
assert len(payload_type_spec.__args__) == len(msg_types)
|
assert len(ipc_msg_spec.__args__) == len(msg_types)
|
||||||
|
assert ipc_msg_spec
|
||||||
|
|
||||||
# TODO: sub-decode `.pld: Raw`?
|
else:
|
||||||
# see similar notes inside `.msg.types`..
|
ipc_msg_spec = ipc_msg_spec or Any
|
||||||
#
|
|
||||||
# not sure we'll end up wanting/needing this
|
enc = msgpack.Encoder(
|
||||||
# though it might have unforeseen advantages in terms
|
enc_hook=enc_hook,
|
||||||
# of enabling encrypted appliciation layer (only)
|
)
|
||||||
# payloads?
|
dec = msgpack.Decoder(
|
||||||
#
|
type=ipc_msg_spec, # like `Msg[Any]`
|
||||||
# register sub-payload decoders to load `.pld: Raw`
|
dec_hook=dec_hook,
|
||||||
# decoded `Msg`-packets using a dynamic lookup (table)
|
)
|
||||||
# instead of a pre-defined msg-spec via `Generic`
|
|
||||||
# parameterization.
|
|
||||||
#
|
|
||||||
# (
|
|
||||||
# tags,
|
|
||||||
# payload_dec,
|
|
||||||
# ) = mk_tagged_union_dec(
|
|
||||||
# tagged_structs=list(payload_types.__args__),
|
|
||||||
# )
|
|
||||||
# _payload_decs: (
|
|
||||||
# dict[str, msgpack.Decoder]|None
|
|
||||||
# ) = {
|
|
||||||
# # pre-seed decoders for std-py-type-set for use when
|
|
||||||
# # `Msg.pld == None|Any`.
|
|
||||||
# None: msgpack.Decoder(Any),
|
|
||||||
# Any: msgpack.Decoder(Any),
|
|
||||||
# }
|
|
||||||
# for name in tags:
|
|
||||||
# _payload_decs[name] = payload_dec
|
|
||||||
|
|
||||||
codec = MsgCodec(
|
codec = MsgCodec(
|
||||||
ipc_msg_spec=ipc_msg_spec,
|
_enc=enc,
|
||||||
payload_msg_spec=payload_type_spec,
|
_dec=dec,
|
||||||
**kwargs,
|
pld_spec=ipc_pld_spec,
|
||||||
|
# payload_msg_specs=payload_msg_specs,
|
||||||
|
# **kwargs,
|
||||||
)
|
)
|
||||||
assert codec.lib.__name__ == libname
|
|
||||||
|
|
||||||
# by default, config-n-cache the codec pair from input settings.
|
# sanity on expected backend support
|
||||||
if cache_now:
|
assert codec.lib.__name__ == libname
|
||||||
assert codec.enc
|
|
||||||
assert codec.dec
|
|
||||||
|
|
||||||
return codec
|
return codec
|
||||||
|
|
||||||
|
|
||||||
# instance of the default `msgspec.msgpack` codec settings, i.e.
|
# instance of the default `msgspec.msgpack` codec settings, i.e.
|
||||||
# no custom structs, hooks or other special types.
|
# no custom structs, hooks or other special types.
|
||||||
_def_msgspec_codec: MsgCodec = mk_codec()
|
_def_msgspec_codec: MsgCodec = mk_codec(ipc_msg_spec=Any)
|
||||||
|
|
||||||
# NOTE: provides for per-`trio.Task` specificity of the
|
# NOTE: provides for per-`trio.Task` specificity of the
|
||||||
# IPC msging codec used by the transport layer when doing
|
# IPC msging codec used by the transport layer when doing
|
||||||
# `Channel.send()/.recv()` of wire data.
|
# `Channel.send()/.recv()` of wire data.
|
||||||
_ctxvar_MsgCodec: ContextVar[MsgCodec] = ContextVar(
|
_ctxvar_MsgCodec: ContextVar[MsgCodec] = ContextVar(
|
||||||
'msgspec_codec',
|
'msgspec_codec',
|
||||||
|
|
||||||
|
# TODO: move this to our new `Msg`-spec!
|
||||||
default=_def_msgspec_codec,
|
default=_def_msgspec_codec,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -353,7 +433,7 @@ def limit_msg_spec(
|
||||||
payload_types: Union[Type[Struct]],
|
payload_types: Union[Type[Struct]],
|
||||||
|
|
||||||
# TODO: don't need this approach right?
|
# TODO: don't need this approach right?
|
||||||
#
|
# -> related to the `MsgCodec._payload_decs` stuff above..
|
||||||
# tagged_structs: list[Struct]|None = None,
|
# tagged_structs: list[Struct]|None = None,
|
||||||
|
|
||||||
**codec_kwargs,
|
**codec_kwargs,
|
||||||
|
|
|
@ -22,9 +22,7 @@ that is,
|
||||||
the "Structurred-Concurrency-Inter-Process-(dialog)-(un)Protocol".
|
the "Structurred-Concurrency-Inter-Process-(dialog)-(un)Protocol".
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
# from contextlib import contextmanager as cm
|
|
||||||
import types
|
import types
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
Any,
|
||||||
|
@ -36,22 +34,13 @@ from typing import (
|
||||||
)
|
)
|
||||||
|
|
||||||
from msgspec import (
|
from msgspec import (
|
||||||
msgpack,
|
defstruct,
|
||||||
Raw,
|
# field,
|
||||||
Struct,
|
Struct,
|
||||||
UNSET,
|
UNSET,
|
||||||
|
UnsetType,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# TODO: can also remove yah?
|
|
||||||
#
|
|
||||||
# class Header(Struct, tag=True):
|
|
||||||
# '''
|
|
||||||
# A msg header which defines payload properties
|
|
||||||
|
|
||||||
# '''
|
|
||||||
# payload_tag: str|None = None
|
|
||||||
|
|
||||||
# type variable for the boxed payload field `.pld`
|
# type variable for the boxed payload field `.pld`
|
||||||
PayloadT = TypeVar('PayloadT')
|
PayloadT = TypeVar('PayloadT')
|
||||||
|
|
||||||
|
@ -61,6 +50,9 @@ class Msg(
|
||||||
Generic[PayloadT],
|
Generic[PayloadT],
|
||||||
tag=True,
|
tag=True,
|
||||||
tag_field='msg_type',
|
tag_field='msg_type',
|
||||||
|
|
||||||
|
# eq=True,
|
||||||
|
# order=True,
|
||||||
):
|
):
|
||||||
'''
|
'''
|
||||||
The "god" boxing msg type.
|
The "god" boxing msg type.
|
||||||
|
@ -70,9 +62,13 @@ class Msg(
|
||||||
tree.
|
tree.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
# header: Header
|
|
||||||
# TODO: use UNSET here?
|
|
||||||
cid: str|None # call/context-id
|
cid: str|None # call/context-id
|
||||||
|
# ^-TODO-^: more explicit type?
|
||||||
|
# -[ ] use UNSET here?
|
||||||
|
# https://jcristharif.com/msgspec/supported-types.html#unset
|
||||||
|
#
|
||||||
|
# -[ ] `uuid.UUID` which has multi-protocol support
|
||||||
|
# https://jcristharif.com/msgspec/supported-types.html#uuid
|
||||||
|
|
||||||
# The msgs "payload" (spelled without vowels):
|
# The msgs "payload" (spelled without vowels):
|
||||||
# https://en.wikipedia.org/wiki/Payload_(computing)
|
# https://en.wikipedia.org/wiki/Payload_(computing)
|
||||||
|
@ -94,9 +90,24 @@ class Msg(
|
||||||
pld: PayloadT
|
pld: PayloadT
|
||||||
|
|
||||||
|
|
||||||
# TODO: better name, like `Call/TaskInput`?
|
# TODO: caps based RPC support in the payload?
|
||||||
|
#
|
||||||
|
# -[ ] integration with our ``enable_modules: list[str]`` caps sys.
|
||||||
|
# ``pkgutil.resolve_name()`` internally uses
|
||||||
|
# ``importlib.import_module()`` which can be filtered by
|
||||||
|
# inserting a ``MetaPathFinder`` into ``sys.meta_path`` (which
|
||||||
|
# we could do before entering the ``Actor._process_messages()``
|
||||||
|
# loop)?
|
||||||
|
# - https://github.com/python/cpython/blob/main/Lib/pkgutil.py#L645
|
||||||
|
# - https://stackoverflow.com/questions/1350466/preventing-python-code-from-importing-certain-modules
|
||||||
|
# - https://stackoverflow.com/a/63320902
|
||||||
|
# - https://docs.python.org/3/library/sys.html#sys.meta_path
|
||||||
|
#
|
||||||
|
# -[ ] can we combine .ns + .func into a native `NamespacePath` field?
|
||||||
|
#
|
||||||
|
# -[ ]better name, like `Call/TaskInput`?
|
||||||
|
#
|
||||||
class FuncSpec(Struct):
|
class FuncSpec(Struct):
|
||||||
# TODO: can we combine these 2 into a `NamespacePath` field?
|
|
||||||
ns: str
|
ns: str
|
||||||
func: str
|
func: str
|
||||||
|
|
||||||
|
@ -126,19 +137,18 @@ class Start(
|
||||||
pld: FuncSpec
|
pld: FuncSpec
|
||||||
|
|
||||||
|
|
||||||
FuncType: Literal[
|
|
||||||
'asyncfunc',
|
|
||||||
'asyncgen',
|
|
||||||
'context', # TODO: the only one eventually?
|
|
||||||
] = 'context'
|
|
||||||
|
|
||||||
|
|
||||||
class IpcCtxSpec(Struct):
|
class IpcCtxSpec(Struct):
|
||||||
'''
|
'''
|
||||||
An inter-actor-`trio.Task`-comms `Context` spec.
|
An inter-actor-`trio.Task`-comms `Context` spec.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
functype: FuncType
|
# TODO: maybe better names for all these?
|
||||||
|
# -[ ] obvi ^ would need sync with `._rpc`
|
||||||
|
functype: Literal[
|
||||||
|
'asyncfunc',
|
||||||
|
'asyncgen',
|
||||||
|
'context', # TODO: the only one eventually?
|
||||||
|
]
|
||||||
|
|
||||||
# TODO: as part of the reponse we should report our allowed
|
# TODO: as part of the reponse we should report our allowed
|
||||||
# msg spec which should be generated from the type-annots as
|
# msg spec which should be generated from the type-annots as
|
||||||
|
@ -172,6 +182,7 @@ class Started(
|
||||||
decorated IPC endpoint.
|
decorated IPC endpoint.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
pld: PayloadT
|
||||||
|
|
||||||
|
|
||||||
# TODO: instead of using our existing `Start`
|
# TODO: instead of using our existing `Start`
|
||||||
|
@ -188,6 +199,7 @@ class Yield(
|
||||||
Per IPC transmission of a value from `await MsgStream.send(<value>)`.
|
Per IPC transmission of a value from `await MsgStream.send(<value>)`.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
pld: PayloadT
|
||||||
|
|
||||||
|
|
||||||
class Stop(Msg):
|
class Stop(Msg):
|
||||||
|
@ -196,7 +208,7 @@ class Stop(Msg):
|
||||||
of `StopAsyncIteration`.
|
of `StopAsyncIteration`.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
pld: UNSET
|
pld: UnsetType = UNSET
|
||||||
|
|
||||||
|
|
||||||
class Return(
|
class Return(
|
||||||
|
@ -208,6 +220,7 @@ class Return(
|
||||||
func-as-`trio.Task`.
|
func-as-`trio.Task`.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
pld: PayloadT
|
||||||
|
|
||||||
|
|
||||||
class ErrorData(Struct):
|
class ErrorData(Struct):
|
||||||
|
@ -248,46 +261,115 @@ class Error(Msg):
|
||||||
# cid: str
|
# cid: str
|
||||||
|
|
||||||
|
|
||||||
|
# built-in SC shuttle protocol msg type set in
|
||||||
|
# approx order of the IPC txn-state spaces.
|
||||||
|
__spec__: list[Msg] = [
|
||||||
|
|
||||||
|
# inter-actor RPC initiation
|
||||||
|
Start,
|
||||||
|
StartAck,
|
||||||
|
|
||||||
|
# no-outcome-yet IAC (inter-actor-communication)
|
||||||
|
Started,
|
||||||
|
Yield,
|
||||||
|
Stop,
|
||||||
|
|
||||||
|
# termination outcomes
|
||||||
|
Return,
|
||||||
|
Error,
|
||||||
|
]
|
||||||
|
|
||||||
|
_runtime_spec_msgs: list[Msg] = [
|
||||||
|
Start,
|
||||||
|
StartAck,
|
||||||
|
Stop,
|
||||||
|
Error,
|
||||||
|
]
|
||||||
|
_payload_spec_msgs: list[Msg] = [
|
||||||
|
Started,
|
||||||
|
Yield,
|
||||||
|
Return,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def mk_msg_spec(
|
def mk_msg_spec(
|
||||||
payload_type: Union[Type] = Any,
|
payload_type_union: Union[Type] = Any,
|
||||||
boxing_msg_set: set[Msg] = {
|
|
||||||
Started,
|
# boxing_msg_set: list[Msg] = _payload_spec_msgs,
|
||||||
Yield,
|
spec_build_method: Literal[
|
||||||
Return,
|
'indexed_generics', # works
|
||||||
},
|
'defstruct',
|
||||||
|
'types_new_class',
|
||||||
|
|
||||||
|
] = 'indexed_generics',
|
||||||
|
|
||||||
) -> tuple[
|
) -> tuple[
|
||||||
Union[Type[Msg]],
|
Union[Type[Msg]],
|
||||||
list[Type[Msg]],
|
list[Type[Msg]],
|
||||||
]:
|
]:
|
||||||
'''
|
'''
|
||||||
Generate a payload-type-parameterized `Msg` specification such
|
Create a payload-(data-)type-parameterized IPC message specification.
|
||||||
that IPC msgs which can be `Msg.pld` (payload) type
|
|
||||||
limited/filterd are specified given an input `payload_type:
|
Allows generating IPC msg types from the above builtin set
|
||||||
Union[Type]`.
|
with a payload (field) restricted data-type via the `Msg.pld:
|
||||||
|
PayloadT` type var. This allows runtime-task contexts to use
|
||||||
|
the python type system to limit/filter payload values as
|
||||||
|
determined by the input `payload_type_union: Union[Type]`.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
submsg_types: list[Type[Msg]] = Msg.__subclasses__()
|
submsg_types: list[Type[Msg]] = Msg.__subclasses__()
|
||||||
|
bases: tuple = (
|
||||||
|
# XXX NOTE XXX the below generic-parameterization seems to
|
||||||
|
# be THE ONLY way to get this to work correctly in terms
|
||||||
|
# of getting ValidationError on a roundtrip?
|
||||||
|
Msg[payload_type_union],
|
||||||
|
Generic[PayloadT],
|
||||||
|
)
|
||||||
|
defstruct_bases: tuple = (
|
||||||
|
Msg, # [payload_type_union],
|
||||||
|
# Generic[PayloadT],
|
||||||
|
# ^-XXX-^: not allowed? lul..
|
||||||
|
)
|
||||||
|
ipc_msg_types: list[Msg] = []
|
||||||
|
|
||||||
# TODO: see below as well,
|
idx_msg_types: list[Msg] = []
|
||||||
# => union building approach with `.__class_getitem__()`
|
defs_msg_types: list[Msg] = []
|
||||||
# doesn't seem to work..?
|
nc_msg_types: list[Msg] = []
|
||||||
#
|
|
||||||
# payload_type_spec: Union[Type[Msg]]
|
for msgtype in __spec__:
|
||||||
#
|
|
||||||
msg_types: list[Msg] = []
|
# for the NON-payload (user api) type specify-able
|
||||||
for msgtype in boxing_msg_set:
|
# msgs types, we simply aggregate the def as is
|
||||||
|
# for inclusion in the output type `Union`.
|
||||||
|
if msgtype not in _payload_spec_msgs:
|
||||||
|
ipc_msg_types.append(msgtype)
|
||||||
|
continue
|
||||||
|
|
||||||
# check inheritance sanity
|
# check inheritance sanity
|
||||||
assert msgtype in submsg_types
|
assert msgtype in submsg_types
|
||||||
|
|
||||||
# TODO: wait why do we need the dynamic version here?
|
# TODO: wait why do we need the dynamic version here?
|
||||||
# -[ ] paraming the `PayloadT` values via `Generic[T]`
|
# XXX ANSWER XXX -> BC INHERITANCE.. don't work w generics..
|
||||||
# doesn't seem to work at all?
|
|
||||||
# -[ ] is there a way to get it to work at module level
|
|
||||||
# just using inheritance or maybe a metaclass?
|
|
||||||
#
|
#
|
||||||
# index_paramed_msg_type: Msg = msgtype[payload_type]
|
# NOTE previously bc msgtypes WERE NOT inheritting
|
||||||
|
# directly the `Generic[PayloadT]` type, the manual method
|
||||||
|
# of generic-paraming with `.__class_getitem__()` wasn't
|
||||||
|
# working..
|
||||||
|
#
|
||||||
|
# XXX but bc i changed that to make every subtype inherit
|
||||||
|
# it, this manual "indexed parameterization" method seems
|
||||||
|
# to work?
|
||||||
|
#
|
||||||
|
# -[x] paraming the `PayloadT` values via `Generic[T]`
|
||||||
|
# does work it seems but WITHOUT inheritance of generics
|
||||||
|
#
|
||||||
|
# -[-] is there a way to get it to work at module level
|
||||||
|
# just using inheritance or maybe a metaclass?
|
||||||
|
# => thot that `defstruct` might work, but NOPE, see
|
||||||
|
# below..
|
||||||
|
#
|
||||||
|
idxed_msg_type: Msg = msgtype[payload_type_union]
|
||||||
|
idx_msg_types.append(idxed_msg_type)
|
||||||
|
|
||||||
# TODO: WHY do we need to dynamically generate the
|
# TODO: WHY do we need to dynamically generate the
|
||||||
# subtype-msgs here to ensure the `.pld` parameterization
|
# subtype-msgs here to ensure the `.pld` parameterization
|
||||||
|
@ -295,98 +377,69 @@ def mk_msg_spec(
|
||||||
# `msgpack.Decoder()`..?
|
# `msgpack.Decoder()`..?
|
||||||
#
|
#
|
||||||
# dynamically create the payload type-spec-limited msg set.
|
# dynamically create the payload type-spec-limited msg set.
|
||||||
manual_paramed_msg_subtype: Type = types.new_class(
|
newclass_msgtype: Type = types.new_class(
|
||||||
msgtype.__name__,
|
name=msgtype.__name__,
|
||||||
(
|
bases=bases,
|
||||||
# XXX NOTE XXX this seems to be THE ONLY
|
kwds={},
|
||||||
# way to get this to work correctly!?!
|
)
|
||||||
Msg[payload_type],
|
nc_msg_types.append(
|
||||||
Generic[PayloadT],
|
newclass_msgtype[payload_type_union]
|
||||||
),
|
|
||||||
{},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: grok the diff here better..
|
# with `msgspec.structs.defstruct`
|
||||||
|
# XXX ALSO DOESN'T WORK
|
||||||
|
defstruct_msgtype = defstruct(
|
||||||
|
name=msgtype.__name__,
|
||||||
|
fields=[
|
||||||
|
('cid', str),
|
||||||
|
|
||||||
|
# XXX doesn't seem to work..
|
||||||
|
# ('pld', PayloadT),
|
||||||
|
|
||||||
|
('pld', payload_type_union),
|
||||||
|
],
|
||||||
|
bases=defstruct_bases,
|
||||||
|
)
|
||||||
|
defs_msg_types.append(defstruct_msgtype)
|
||||||
|
|
||||||
# assert index_paramed_msg_type == manual_paramed_msg_subtype
|
# assert index_paramed_msg_type == manual_paramed_msg_subtype
|
||||||
|
|
||||||
# XXX TODO: why does the manual method work but not the
|
# paramed_msg_type = manual_paramed_msg_subtype
|
||||||
# `.__class_getitem__()` one!?!
|
|
||||||
paramed_msg_type = manual_paramed_msg_subtype
|
|
||||||
|
|
||||||
# payload_type_spec |= paramed_msg_type
|
# ipc_payload_msgs_type_union |= index_paramed_msg_type
|
||||||
msg_types.append(paramed_msg_type)
|
|
||||||
|
|
||||||
|
idx_spec: Union[Type[Msg]] = Union[*idx_msg_types]
|
||||||
|
def_spec: Union[Type[Msg]] = Union[*defs_msg_types]
|
||||||
|
nc_spec: Union[Type[Msg]] = Union[*nc_msg_types]
|
||||||
|
|
||||||
|
specs: dict[str, Union[Type[Msg]]] = {
|
||||||
|
'indexed_generics': idx_spec,
|
||||||
|
'defstruct': def_spec,
|
||||||
|
'types_new_class': nc_spec,
|
||||||
|
}
|
||||||
|
msgtypes_table: dict[str, list[Msg]] = {
|
||||||
|
'indexed_generics': idx_msg_types,
|
||||||
|
'defstruct': defs_msg_types,
|
||||||
|
'types_new_class': nc_msg_types,
|
||||||
|
}
|
||||||
|
|
||||||
|
# XXX lol apparently type unions can't ever
|
||||||
|
# be equal eh?
|
||||||
|
# TODO: grok the diff here better..
|
||||||
|
#
|
||||||
|
# assert (
|
||||||
|
# idx_spec
|
||||||
|
# ==
|
||||||
|
# nc_spec
|
||||||
|
# ==
|
||||||
|
# def_spec
|
||||||
|
# )
|
||||||
|
# breakpoint()
|
||||||
|
|
||||||
|
pld_spec: Union[Type] = specs[spec_build_method]
|
||||||
|
runtime_spec: Union[Type] = Union[*ipc_msg_types]
|
||||||
|
|
||||||
payload_type_spec: Union[Type[Msg]] = Union[*msg_types]
|
|
||||||
return (
|
return (
|
||||||
payload_type_spec,
|
pld_spec | runtime_spec,
|
||||||
msg_types,
|
msgtypes_table[spec_build_method] + ipc_msg_types,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# TODO: integration with our ``enable_modules: list[str]`` caps sys.
|
|
||||||
#
|
|
||||||
# ``pkgutil.resolve_name()`` internally uses
|
|
||||||
# ``importlib.import_module()`` which can be filtered by inserting
|
|
||||||
# a ``MetaPathFinder`` into ``sys.meta_path`` (which we could do before
|
|
||||||
# entering the ``Actor._process_messages()`` loop).
|
|
||||||
# https://github.com/python/cpython/blob/main/Lib/pkgutil.py#L645
|
|
||||||
# https://stackoverflow.com/questions/1350466/preventing-python-code-from-importing-certain-modules
|
|
||||||
# - https://stackoverflow.com/a/63320902
|
|
||||||
# - https://docs.python.org/3/library/sys.html#sys.meta_path
|
|
||||||
|
|
||||||
# TODO: do we still want to try and support the sub-decoder with
|
|
||||||
# `Raw` technique in the case that the `Generic` approach gives
|
|
||||||
# future grief?
|
|
||||||
#
|
|
||||||
# sub-decoders for retreiving embedded
|
|
||||||
# payload data and decoding to a sender
|
|
||||||
# side defined (struct) type.
|
|
||||||
_payload_decs: dict[
|
|
||||||
str|None,
|
|
||||||
msgpack.Decoder,
|
|
||||||
] = {
|
|
||||||
# default decoder is used when `Header.payload_tag == None`
|
|
||||||
None: msgpack.Decoder(Any),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def dec_payload(
|
|
||||||
msg: Msg,
|
|
||||||
msg_dec: msgpack.Decoder = msgpack.Decoder(
|
|
||||||
type=Msg[Any]
|
|
||||||
),
|
|
||||||
|
|
||||||
) -> Any|Struct:
|
|
||||||
|
|
||||||
msg: Msg = msg_dec.decode(msg)
|
|
||||||
payload_tag: str = msg.header.payload_tag
|
|
||||||
payload_dec: msgpack.Decoder = _payload_decs[payload_tag]
|
|
||||||
return payload_dec.decode(msg.pld)
|
|
||||||
|
|
||||||
|
|
||||||
def enc_payload(
|
|
||||||
enc: msgpack.Encoder,
|
|
||||||
payload: Any,
|
|
||||||
cid: str,
|
|
||||||
|
|
||||||
) -> bytes:
|
|
||||||
|
|
||||||
# tag_field: str|None = None
|
|
||||||
|
|
||||||
plbytes = enc.encode(payload)
|
|
||||||
if b'msg_type' in plbytes:
|
|
||||||
assert isinstance(payload, Struct)
|
|
||||||
|
|
||||||
# tag_field: str = type(payload).__name__
|
|
||||||
payload = Raw(plbytes)
|
|
||||||
|
|
||||||
msg = Msg(
|
|
||||||
cid=cid,
|
|
||||||
pld=payload,
|
|
||||||
# Header(
|
|
||||||
# payload_tag=tag_field,
|
|
||||||
# # dialog_id,
|
|
||||||
# ),
|
|
||||||
)
|
|
||||||
return enc.encode(msg)
|
|
||||||
|
|
Loading…
Reference in New Issue