Compare commits
47 Commits
bc1de90150
...
6bd685adb1
| Author | SHA1 | Date |
|---|---|---|
|
|
6bd685adb1 | |
|
|
c975c30e08 | |
|
|
ff4b81742e | |
|
|
8c84d35526 | |
|
|
322a6deb6b | |
|
|
31c549808e | |
|
|
ffa513b91f | |
|
|
49b1f4f43e | |
|
|
4d0ea26637 | |
|
|
7a8e50ae59 | |
|
|
a70d6ed8b1 | |
|
|
bbf01d5161 | |
|
|
ec8e8a2786 | |
|
|
c3d1ec22eb | |
|
|
8f44efa327 | |
|
|
5968a3c773 | |
|
|
80597b80bf | |
|
|
a41c6d5c70 | |
|
|
9c37b3f956 | |
|
|
8f6bc56174 | |
|
|
b14dbde77b | |
|
|
cd6509b724 | |
|
|
93d99ed2eb | |
|
|
6215e3b2dd | |
|
|
be5d8da8c0 | |
|
|
21ed181835 | |
|
|
9ec2749ab7 | |
|
|
f3441a6790 | |
|
|
cc42d38284 | |
|
|
6827ceba12 | |
|
|
94458807ce | |
|
|
be5e7e446b | |
|
|
571b2b320e | |
|
|
c7b5d00f19 | |
|
|
1049f7bf38 | |
|
|
cc3bfac741 | |
|
|
e71eec07de | |
|
|
b557ec20a7 | |
|
|
85457cb839 | |
|
|
850219f60c | |
|
|
d929fb75b5 | |
|
|
403c2174a1 | |
|
|
528012f35f | |
|
|
0dfa6f4a8a | |
|
|
a0d3741fac | |
|
|
149b800c9f | |
|
|
03f458a45c |
|
|
@ -20,7 +20,7 @@ async def sleep(
|
|||
|
||||
|
||||
async def open_ctx(
|
||||
n: tractor._supervise.ActorNursery
|
||||
n: tractor.runtime._supervise.ActorNursery
|
||||
):
|
||||
|
||||
# spawn both actors
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ async def main(service_name):
|
|||
await an.start_actor(service_name)
|
||||
|
||||
async with tractor.get_registry() as portal:
|
||||
print(f"Arbiter is listening on {portal.channel}")
|
||||
print(f"Registrar is listening on {portal.channel}")
|
||||
|
||||
async with tractor.wait_for_actor(service_name) as sockaddr:
|
||||
print(f"my_service is found at {sockaddr}")
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ dependencies = [
|
|||
"cffi>=1.17.1",
|
||||
"bidict>=0.23.1",
|
||||
"platformdirs>=4.4.0",
|
||||
"multiaddr>=0.1.1",
|
||||
]
|
||||
|
||||
# ------ project ------
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import os
|
|||
import signal
|
||||
import platform
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
import pytest
|
||||
import tractor
|
||||
|
|
@ -52,6 +54,76 @@ no_macos = pytest.mark.skipif(
|
|||
)
|
||||
|
||||
|
||||
def get_cpu_state(
|
||||
icpu: int = 0,
|
||||
setting: Literal[
|
||||
'scaling_governor',
|
||||
'*_pstate_max_freq',
|
||||
'scaling_max_freq',
|
||||
# 'scaling_cur_freq',
|
||||
] = '*_pstate_max_freq',
|
||||
) -> tuple[
|
||||
Path,
|
||||
str|int,
|
||||
]|None:
|
||||
'''
|
||||
Attempt to read the (first) CPU's setting according
|
||||
to the set `setting` from under the file-sys,
|
||||
|
||||
/sys/devices/system/cpu/cpu0/cpufreq/{setting}
|
||||
|
||||
Useful to determine latency headroom for various perf affected
|
||||
test suites.
|
||||
|
||||
'''
|
||||
try:
|
||||
# Read governor for core 0 (usually same for all)
|
||||
setting_path: Path = list(
|
||||
Path(f'/sys/devices/system/cpu/cpu{icpu}/cpufreq/')
|
||||
.glob(f'{setting}')
|
||||
)[0] # <- XXX must be single match!
|
||||
with open(
|
||||
setting_path,
|
||||
'r',
|
||||
) as f:
|
||||
return (
|
||||
setting_path,
|
||||
f.read().strip(),
|
||||
)
|
||||
except (FileNotFoundError, IndexError):
|
||||
return None
|
||||
|
||||
|
||||
def cpu_scaling_factor() -> float:
|
||||
'''
|
||||
Return a latency-headroom multiplier (>= 1.0) reflecting how
|
||||
much to inflate time-limits when CPU-freq scaling is active on
|
||||
linux.
|
||||
|
||||
When no scaling info is available (non-linux, missing sysfs),
|
||||
returns 1.0 (i.e. no headroom adjustment needed).
|
||||
|
||||
'''
|
||||
if _non_linux:
|
||||
return 1.
|
||||
|
||||
mx = get_cpu_state()
|
||||
cur = get_cpu_state(setting='scaling_max_freq')
|
||||
if mx is None or cur is None:
|
||||
return 1.
|
||||
|
||||
_mx_pth, max_freq = mx
|
||||
_cur_pth, cur_freq = cur
|
||||
cpu_scaled: float = int(cur_freq) / int(max_freq)
|
||||
|
||||
if cpu_scaled != 1.:
|
||||
return 1. / (
|
||||
cpu_scaled * 2 # <- bc likely "dual threaded"
|
||||
)
|
||||
|
||||
return 1.
|
||||
|
||||
|
||||
def pytest_addoption(
|
||||
parser: pytest.Parser,
|
||||
):
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ def test_shield_pause(
|
|||
child.pid,
|
||||
signal.SIGINT,
|
||||
)
|
||||
from tractor._supervise import _shutdown_msg
|
||||
from tractor.runtime._supervise import _shutdown_msg
|
||||
expect(
|
||||
child,
|
||||
# 'Shutting down actor runtime',
|
||||
|
|
|
|||
|
|
@ -8,17 +8,16 @@ from pathlib import Path
|
|||
import pytest
|
||||
import trio
|
||||
import tractor
|
||||
from tractor import (
|
||||
Actor,
|
||||
_state,
|
||||
_addr,
|
||||
)
|
||||
from tractor import Actor
|
||||
from tractor.runtime import _state
|
||||
from tractor.discovery import _addr
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bindspace_dir_str() -> str:
|
||||
|
||||
rt_dir: Path = tractor._state.get_rt_dir()
|
||||
from tractor.runtime._state import get_rt_dir
|
||||
rt_dir: Path = get_rt_dir()
|
||||
bs_dir: Path = rt_dir / 'doggy'
|
||||
bs_dir_str: str = str(bs_dir)
|
||||
assert not bs_dir.is_dir()
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ from tractor import (
|
|||
Portal,
|
||||
ipc,
|
||||
msg,
|
||||
_state,
|
||||
_addr,
|
||||
)
|
||||
from tractor.runtime import _state
|
||||
from tractor.discovery import _addr
|
||||
|
||||
@tractor.context
|
||||
async def chk_tpts(
|
||||
|
|
|
|||
|
|
@ -454,7 +454,7 @@ async def send_back_values(
|
|||
with (
|
||||
maybe_apply_codec(nsp_codec) as codec,
|
||||
limit_plds(
|
||||
rent_pld_spec,
|
||||
spec=rent_pld_spec,
|
||||
dec_hook=dec_nsp if add_hooks else None,
|
||||
ext_types=[NamespacePath] if add_hooks else None,
|
||||
) as pld_dec,
|
||||
|
|
@ -665,7 +665,9 @@ def test_ext_types_over_ipc(
|
|||
expect_codec=nsp_codec,
|
||||
enter_value=codec,
|
||||
)
|
||||
rent_pld_spec_type_strs: list[str] = _exts.enc_type_union(pld_spec)
|
||||
rent_pld_spec_type_strs: list[str] = _exts.enc_type_union(
|
||||
pld_spec
|
||||
)
|
||||
|
||||
# XXX should raise an mte (`MsgTypeError`)
|
||||
# when `add_hooks == False` bc the input
|
||||
|
|
@ -695,7 +697,7 @@ def test_ext_types_over_ipc(
|
|||
limit_plds(
|
||||
pld_spec,
|
||||
dec_hook=dec_nsp if add_hooks else None,
|
||||
ext_types=[NamespacePath] if add_hooks else None,
|
||||
ext_types=[NamespacePath] if add_hooks else None,
|
||||
) as pld_dec,
|
||||
):
|
||||
ctx_pld_dec: MsgDec = ctx._pld_rx._pld_dec
|
||||
|
|
@ -704,7 +706,7 @@ def test_ext_types_over_ipc(
|
|||
# if (
|
||||
# not add_hooks
|
||||
# and
|
||||
# NamespacePath in
|
||||
# NamespacePath in
|
||||
# ):
|
||||
# pytest.fail('ctx should fail to open without custom enc_hook!?')
|
||||
|
||||
|
|
@ -743,204 +745,10 @@ def test_ext_types_over_ipc(
|
|||
assert exc.boxed_type is TypeError
|
||||
|
||||
|
||||
# def chk_pld_type(
|
||||
# payload_spec: Type[Struct]|Any,
|
||||
# pld: Any,
|
||||
|
||||
# expect_roundtrip: bool|None = None,
|
||||
|
||||
# ) -> bool:
|
||||
|
||||
# pld_val_type: Type = type(pld)
|
||||
|
||||
# # TODO: verify that the overridden subtypes
|
||||
# # DO NOT have modified type-annots from original!
|
||||
# # 'Start', .pld: FuncSpec
|
||||
# # 'StartAck', .pld: IpcCtxSpec
|
||||
# # 'Stop', .pld: UNSEt
|
||||
# # 'Error', .pld: ErrorData
|
||||
|
||||
# codec: MsgCodec = mk_codec(
|
||||
# # NOTE: this ONLY accepts `PayloadMsg.pld` fields of a specified
|
||||
# # type union.
|
||||
# ipc_pld_spec=payload_spec,
|
||||
# )
|
||||
|
||||
# # make a one-off dec to compare with our `MsgCodec` instance
|
||||
# # which does the below `mk_msg_spec()` call internally
|
||||
# ipc_msg_spec: Union[Type[Struct]]
|
||||
# msg_types: list[PayloadMsg[payload_spec]]
|
||||
# (
|
||||
# ipc_msg_spec,
|
||||
# msg_types,
|
||||
# ) = mk_msg_spec(
|
||||
# payload_type_union=payload_spec,
|
||||
# )
|
||||
# _enc = msgpack.Encoder()
|
||||
# _dec = msgpack.Decoder(
|
||||
# type=ipc_msg_spec or Any, # like `PayloadMsg[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.
|
||||
# if not msg_types:
|
||||
# breakpoint()
|
||||
|
||||
# roundtrip: bool|None = None
|
||||
# pld_spec_msg_names: list[str] = [
|
||||
# td.__name__ for td in _payload_msgs
|
||||
# ]
|
||||
# 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]
|
||||
# assert pld_field.type is payload_spec # TODO-^ does this need to work to get all subtypes to adhere?
|
||||
|
||||
# kwargs: dict[str, Any] = {
|
||||
# 'cid': '666',
|
||||
# 'pld': pld,
|
||||
# }
|
||||
# enc_msg: PayloadMsg = typedef(**kwargs)
|
||||
|
||||
# _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:
|
||||
# dec_msg = codec.dec.decode(wire_bytes)
|
||||
# _dec_msg = _dec.decode(wire_bytes)
|
||||
|
||||
# # decoded msg and thus payload should be exactly same!
|
||||
# assert (roundtrip := (
|
||||
# _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(
|
||||
# 'Got `ValidationError` despite type-var match!?\n'
|
||||
# f'pld_val_type: {pld_val_type}\n'
|
||||
# f'payload_type: {payload_spec}\n'
|
||||
# ) from ve
|
||||
|
||||
# else:
|
||||
# # ow we good cuz the pld spec mismatched.
|
||||
# print(
|
||||
# 'Got expected `ValidationError` since,\n'
|
||||
# f'{pld_val_type} is not {payload_spec}\n'
|
||||
# )
|
||||
# else:
|
||||
# if (
|
||||
# payload_spec is not Any
|
||||
# and
|
||||
# pld_val_type is not payload_spec
|
||||
# ):
|
||||
# raise ValueError(
|
||||
# 'DID NOT `ValidationError` despite expected type match!?\n'
|
||||
# f'pld_val_type: {pld_val_type}\n'
|
||||
# f'payload_type: {payload_spec}\n'
|
||||
# )
|
||||
|
||||
# # full code decode should always be attempted!
|
||||
# if roundtrip is None:
|
||||
# breakpoint()
|
||||
|
||||
# return roundtrip
|
||||
|
||||
|
||||
# ?TODO? maybe remove since covered in the newer `test_pldrx_limiting`
|
||||
# via end-2-end testing of all this?
|
||||
# -[ ] IOW do we really NEED this lowlevel unit testing?
|
||||
#
|
||||
# def test_limit_msgspec(
|
||||
# debug_mode: bool,
|
||||
# ):
|
||||
# '''
|
||||
# Internals unit testing to verify that type-limiting an IPC ctx's
|
||||
# msg spec with `Pldrx.limit_plds()` results in various
|
||||
# encapsulated `msgspec` object settings and state.
|
||||
|
||||
# '''
|
||||
# async def main():
|
||||
# async with tractor.open_root_actor(
|
||||
# debug_mode=debug_mode,
|
||||
# ):
|
||||
# # ensure we can round-trip a boxing `PayloadMsg`
|
||||
# assert chk_pld_type(
|
||||
# payload_spec=Any,
|
||||
# pld=None,
|
||||
# expect_roundtrip=True,
|
||||
# )
|
||||
|
||||
# # verify that a mis-typed payload value won't decode
|
||||
# assert not chk_pld_type(
|
||||
# payload_spec=int,
|
||||
# pld='doggy',
|
||||
# )
|
||||
|
||||
# # parametrize the boxed `.pld` type as a custom-struct
|
||||
# # and ensure that parametrization propagates
|
||||
# # to all payload-msg-spec-able subtypes!
|
||||
# class CustomPayload(Struct):
|
||||
# name: str
|
||||
# value: Any
|
||||
|
||||
# assert not chk_pld_type(
|
||||
# payload_spec=CustomPayload,
|
||||
# pld='doggy',
|
||||
# )
|
||||
|
||||
# assert chk_pld_type(
|
||||
# payload_spec=CustomPayload,
|
||||
# pld=CustomPayload(name='doggy', value='urmom')
|
||||
# )
|
||||
|
||||
# # yah, we can `.pause_from_sync()` now!
|
||||
# # breakpoint()
|
||||
|
||||
# trio.run(main)
|
||||
# TODO: further SC-msg-specific verification that the overridden
|
||||
# subtypes DO NOT have modified type-annots from original!
|
||||
# 'Start', .pld: FuncSpec
|
||||
# 'StartAck', .pld: IpcCtxSpec
|
||||
# 'Stop', .pld: UNSEt
|
||||
# 'Error', .pld: ErrorData
|
||||
# def test_per_msg_payload_spec_limits():
|
||||
|
|
|
|||
|
|
@ -7,7 +7,15 @@ related settings around IPC contexts.
|
|||
from contextlib import (
|
||||
asynccontextmanager as acm,
|
||||
)
|
||||
import sys
|
||||
import types
|
||||
from typing import (
|
||||
Any,
|
||||
Union,
|
||||
Type,
|
||||
)
|
||||
|
||||
import msgspec
|
||||
from msgspec import (
|
||||
Struct,
|
||||
)
|
||||
|
|
@ -22,11 +30,10 @@ from tractor import (
|
|||
Portal,
|
||||
)
|
||||
from tractor.msg import (
|
||||
_codec,
|
||||
_ops as msgops,
|
||||
Return,
|
||||
)
|
||||
from tractor.msg import (
|
||||
_codec,
|
||||
_exts,
|
||||
)
|
||||
from tractor.msg.types import (
|
||||
log,
|
||||
|
|
@ -41,13 +48,22 @@ class PldMsg(
|
|||
# case of these details?
|
||||
#
|
||||
# https://jcristharif.com/msgspec/structs.html#tagged-unions
|
||||
# tag=True,
|
||||
# tag_field='msg_type',
|
||||
tag=True,
|
||||
tag_field='msg_type',
|
||||
):
|
||||
field: str
|
||||
|
||||
|
||||
maybe_msg_spec = PldMsg|None
|
||||
class Msg1(PldMsg):
|
||||
field: str
|
||||
|
||||
|
||||
class Msg2(PldMsg):
|
||||
field: int
|
||||
|
||||
|
||||
class AnyFieldMsg(PldMsg):
|
||||
field: Any
|
||||
|
||||
|
||||
@acm
|
||||
|
|
@ -61,7 +77,7 @@ async def maybe_expect_raises(
|
|||
Async wrapper for ensuring errors propagate from the inner scope.
|
||||
|
||||
'''
|
||||
if tractor._state.debug_mode():
|
||||
if tractor.debug_mode():
|
||||
timeout += 999
|
||||
|
||||
with trio.fail_after(timeout):
|
||||
|
|
@ -104,9 +120,15 @@ async def maybe_expect_raises(
|
|||
)
|
||||
|
||||
|
||||
@tractor.context(
|
||||
pld_spec=maybe_msg_spec,
|
||||
)
|
||||
# NOTE, this decorator is applied dynamically by both the root and
|
||||
# 'sub' actor such that we can dynamically apply various cases from
|
||||
# a parametrized test.
|
||||
#
|
||||
# maybe_msg_spec = PldMsg|None
|
||||
#
|
||||
# @tractor.context(
|
||||
# pld_spec=maybe_msg_spec,
|
||||
# )
|
||||
async def child(
|
||||
ctx: Context,
|
||||
started_value: int|PldMsg|None,
|
||||
|
|
@ -114,13 +136,13 @@ async def child(
|
|||
validate_pld_spec: bool,
|
||||
raise_on_started_mte: bool = True,
|
||||
|
||||
pack_any_field: bool = False,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
Call ``Context.started()`` more then once (an error).
|
||||
|
||||
'''
|
||||
expect_started_mte: bool = started_value == 10
|
||||
|
||||
# sanaity check that child RPC context is the current one
|
||||
curr_ctx: Context = current_ipc_ctx()
|
||||
assert ctx is curr_ctx
|
||||
|
|
@ -128,6 +150,7 @@ async def child(
|
|||
rx: msgops.PldRx = ctx._pld_rx
|
||||
curr_pldec: _codec.MsgDec = rx.pld_dec
|
||||
|
||||
|
||||
ctx_meta: dict = getattr(
|
||||
child,
|
||||
'_tractor_context_meta',
|
||||
|
|
@ -136,10 +159,28 @@ async def child(
|
|||
if ctx_meta:
|
||||
assert (
|
||||
ctx_meta['pld_spec']
|
||||
is curr_pldec.spec
|
||||
is curr_pldec.pld_spec
|
||||
is
|
||||
curr_pldec.spec
|
||||
is
|
||||
curr_pldec.pld_spec
|
||||
)
|
||||
|
||||
pld_types: set[Type] = _codec.unpack_spec_types(
|
||||
curr_pldec.pld_spec,
|
||||
)
|
||||
if (
|
||||
AnyFieldMsg in pld_types
|
||||
and
|
||||
pack_any_field
|
||||
):
|
||||
started_value = AnyFieldMsg(field=started_value)
|
||||
|
||||
expect_started_mte: bool = (
|
||||
started_value == 10
|
||||
and
|
||||
not pack_any_field
|
||||
)
|
||||
|
||||
# 2 cases: hdndle send-side and recv-only validation
|
||||
# - when `raise_on_started_mte == True`, send validate
|
||||
# - else, parent-recv-side only validation
|
||||
|
|
@ -219,16 +260,65 @@ async def child(
|
|||
# msg-type-error from this RPC task ;)
|
||||
return return_value
|
||||
|
||||
def decorate_child_ep(
|
||||
pld_spec: Union[Type],
|
||||
) -> types.ModuleType:
|
||||
'''
|
||||
Apply parametrized pld_spec to ctx ep like,
|
||||
|
||||
@tractor.context(
|
||||
pld_spec=maybe_msg_spec,
|
||||
)(child)
|
||||
|
||||
'''
|
||||
this_mod = sys.modules[__name__]
|
||||
global child # a mod-fn defined above
|
||||
assert this_mod.child is child
|
||||
this_mod.child = tractor.context(
|
||||
pld_spec=pld_spec,
|
||||
)(child)
|
||||
return this_mod
|
||||
|
||||
|
||||
@tractor.context
|
||||
async def set_chld_pldspec(
|
||||
ctx: tractor.Context,
|
||||
pld_spec_strs: list[str],
|
||||
):
|
||||
'''
|
||||
Dynamically apply the `@context(pld_spec=pld_spec)` deco to the
|
||||
current actor's in-mem instance of this test module.
|
||||
|
||||
Allows dynamically applying the "payload-spec" in both a parent
|
||||
and child actor after spawn.
|
||||
|
||||
'''
|
||||
this_mod = sys.modules[__name__]
|
||||
pld_spec: list[str] = _exts.dec_type_union(
|
||||
pld_spec_strs,
|
||||
mods=[
|
||||
this_mod,
|
||||
msgspec.inspect,
|
||||
],
|
||||
)
|
||||
decorate_child_ep(pld_spec)
|
||||
await ctx.started()
|
||||
await trio.sleep_forever()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'return_value',
|
||||
[
|
||||
'yo',
|
||||
None,
|
||||
Msg2(field=10),
|
||||
AnyFieldMsg(field='yo'),
|
||||
],
|
||||
ids=[
|
||||
'return[invalid-"yo"]',
|
||||
'return[valid-None]',
|
||||
'return[maybe-valid-None]',
|
||||
'return[maybe-valid-Msg2]',
|
||||
'return[maybe-valid-any-packed-yo]',
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
|
|
@ -236,10 +326,14 @@ async def child(
|
|||
[
|
||||
10,
|
||||
PldMsg(field='yo'),
|
||||
Msg1(field='yo'),
|
||||
AnyFieldMsg(field=10),
|
||||
],
|
||||
ids=[
|
||||
'Started[invalid-10]',
|
||||
'Started[valid-PldMsg]',
|
||||
'Started[maybe-valid-PldMsg]',
|
||||
'Started[maybe-valid-Msg1]',
|
||||
'Started[maybe-valid-any-packed-10]',
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
|
|
@ -253,12 +347,31 @@ async def child(
|
|||
'no-started-pld-validate',
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
'pld_spec',
|
||||
[
|
||||
PldMsg|None,
|
||||
|
||||
# demo how to have strict msgs alongside all other supported
|
||||
# py-types by embedding the any-types inside a shuttle msg.
|
||||
Msg1|Msg2|AnyFieldMsg,
|
||||
|
||||
# XXX, will never work since Struct overrides dict.
|
||||
# https://jcristharif.com/msgspec/usage.html#typed-decoding
|
||||
# Msg1|Msg2|msgspec.inspect.AnyType,
|
||||
],
|
||||
ids=[
|
||||
'maybe_PldMsg_spec',
|
||||
'Msg1_or_Msg2_or_AnyFieldMsg_spec',
|
||||
]
|
||||
)
|
||||
def test_basic_payload_spec(
|
||||
debug_mode: bool,
|
||||
loglevel: str,
|
||||
return_value: str|None,
|
||||
started_value: int|PldMsg,
|
||||
pld_check_started_value: bool,
|
||||
pld_spec: Union[Type],
|
||||
):
|
||||
'''
|
||||
Validate the most basic `PldRx` msg-type-spec semantics around
|
||||
|
|
@ -267,16 +380,33 @@ def test_basic_payload_spec(
|
|||
pld-spec.
|
||||
|
||||
'''
|
||||
invalid_return: bool = return_value == 'yo'
|
||||
invalid_started: bool = started_value == 10
|
||||
pld_types: set[Type] = _codec.unpack_spec_types(pld_spec)
|
||||
invalid_return: bool = (
|
||||
return_value == 'yo'
|
||||
)
|
||||
invalid_started: bool = (
|
||||
started_value == 10
|
||||
)
|
||||
|
||||
# dynamically apply ep's pld-spec in 'root'.
|
||||
decorate_child_ep(pld_spec)
|
||||
assert (
|
||||
child._tractor_context_meta['pld_spec'] == pld_spec
|
||||
)
|
||||
pld_spec_strs: list[str] = _exts.enc_type_union(
|
||||
pld_spec,
|
||||
)
|
||||
assert len(pld_types) > 1
|
||||
|
||||
async def main():
|
||||
nonlocal pld_spec
|
||||
|
||||
async with tractor.open_nursery(
|
||||
debug_mode=debug_mode,
|
||||
loglevel=loglevel,
|
||||
) as an:
|
||||
p: Portal = await an.start_actor(
|
||||
'child',
|
||||
'sub',
|
||||
enable_modules=[__name__],
|
||||
)
|
||||
|
||||
|
|
@ -286,9 +416,11 @@ def test_basic_payload_spec(
|
|||
if invalid_started:
|
||||
msg_type_str: str = 'Started'
|
||||
bad_value: int = 10
|
||||
|
||||
elif invalid_return:
|
||||
msg_type_str: str = 'Return'
|
||||
bad_value: str = 'yo'
|
||||
|
||||
else:
|
||||
# XXX but should never be used below then..
|
||||
msg_type_str: str = ''
|
||||
|
|
@ -302,6 +434,7 @@ def test_basic_payload_spec(
|
|||
invalid_started
|
||||
) else None
|
||||
)
|
||||
|
||||
async with (
|
||||
maybe_expect_raises(
|
||||
raises=should_raise,
|
||||
|
|
@ -315,6 +448,11 @@ def test_basic_payload_spec(
|
|||
# only for debug
|
||||
# post_mortem=True,
|
||||
),
|
||||
p.open_context(
|
||||
set_chld_pldspec,
|
||||
pld_spec_strs=pld_spec_strs,
|
||||
) as (deco_ctx, _),
|
||||
|
||||
p.open_context(
|
||||
child,
|
||||
return_value=return_value,
|
||||
|
|
@ -325,12 +463,18 @@ def test_basic_payload_spec(
|
|||
# now opened with 'child' sub
|
||||
assert current_ipc_ctx() is ctx
|
||||
|
||||
assert type(first) is PldMsg
|
||||
# assert type(first) is PldMsg
|
||||
assert isinstance(first, PldMsg)
|
||||
assert first.field == 'yo'
|
||||
|
||||
try:
|
||||
res: None|PldMsg = await ctx.result(hide_tb=False)
|
||||
assert res is None
|
||||
assert res == return_value
|
||||
if res is None:
|
||||
await tractor.pause()
|
||||
if isinstance(res, PldMsg):
|
||||
assert res.field == 10
|
||||
|
||||
except MsgTypeError as mte:
|
||||
maybe_mte = mte
|
||||
if not invalid_return:
|
||||
|
|
@ -356,6 +500,9 @@ def test_basic_payload_spec(
|
|||
ctx.outcome
|
||||
)
|
||||
|
||||
if should_raise is None:
|
||||
await deco_ctx.cancel()
|
||||
|
||||
if should_raise is None:
|
||||
assert maybe_mte is None
|
||||
|
||||
|
|
|
|||
|
|
@ -490,7 +490,7 @@ def test_cancel_via_SIGINT(
|
|||
"""Ensure that a control-C (SIGINT) signal cancels both the parent and
|
||||
child processes in trionic fashion
|
||||
"""
|
||||
pid = os.getpid()
|
||||
pid: int = os.getpid()
|
||||
|
||||
async def main():
|
||||
with trio.fail_after(2):
|
||||
|
|
@ -517,6 +517,8 @@ def test_cancel_via_SIGINT_other_task(
|
|||
started from a seperate ``trio`` child task.
|
||||
|
||||
'''
|
||||
from .conftest import cpu_scaling_factor
|
||||
|
||||
pid: int = os.getpid()
|
||||
timeout: float = (
|
||||
4 if _non_linux
|
||||
|
|
@ -525,6 +527,11 @@ def test_cancel_via_SIGINT_other_task(
|
|||
if _friggin_windows: # smh
|
||||
timeout += 1
|
||||
|
||||
# add latency headroom for CPU freq scaling (auto-cpufreq et al.)
|
||||
headroom: float = cpu_scaling_factor()
|
||||
if headroom != 1.:
|
||||
timeout *= headroom
|
||||
|
||||
async def spawn_and_sleep_forever(
|
||||
task_status=trio.TASK_STATUS_IGNORED
|
||||
):
|
||||
|
|
|
|||
|
|
@ -10,7 +10,20 @@ from tractor._testing import tractor_test
|
|||
MESSAGE = 'tractoring at full speed'
|
||||
|
||||
|
||||
def test_empty_mngrs_input_raises() -> None:
|
||||
def test_empty_mngrs_input_raises(
|
||||
tpt_proto: str,
|
||||
) -> None:
|
||||
# TODO, the `open_actor_cluster()` teardown hangs
|
||||
# intermittently on UDS when `gather_contexts(mngrs=())`
|
||||
# raises `ValueError` mid-setup; likely a race in the
|
||||
# actor-nursery cleanup vs UDS socket shutdown. Needs
|
||||
# a deeper look at `._clustering`/`._supervise` teardown
|
||||
# paths with the UDS transport.
|
||||
if tpt_proto == 'uds':
|
||||
pytest.skip(
|
||||
'actor-cluster teardown hangs intermittently on UDS'
|
||||
)
|
||||
|
||||
async def main():
|
||||
with trio.fail_after(3):
|
||||
async with (
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ from tractor._exceptions import (
|
|||
StreamOverrun,
|
||||
ContextCancelled,
|
||||
)
|
||||
from tractor._state import current_ipc_ctx
|
||||
from tractor.runtime._state import current_ipc_ctx
|
||||
|
||||
from tractor._testing import (
|
||||
tractor_test,
|
||||
|
|
@ -939,7 +939,7 @@ def test_one_end_stream_not_opened(
|
|||
|
||||
'''
|
||||
overrunner, buf_size_increase, entrypoint = overrun_by
|
||||
from tractor._runtime import Actor
|
||||
from tractor.runtime._runtime import Actor
|
||||
buf_size = buf_size_increase + Actor.msg_buffer_size
|
||||
|
||||
timeout: float = (
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"""
|
||||
Discovery subsys.
|
||||
'''
|
||||
Discovery subsystem via a "registrar" actor scenarios.
|
||||
|
||||
"""
|
||||
'''
|
||||
import os
|
||||
import signal
|
||||
import platform
|
||||
|
|
@ -24,7 +24,7 @@ async def test_reg_then_unreg(
|
|||
reg_addr: tuple,
|
||||
):
|
||||
actor = tractor.current_actor()
|
||||
assert actor.is_arbiter
|
||||
assert actor.is_registrar
|
||||
assert len(actor._registry) == 1 # only self is registered
|
||||
|
||||
async with tractor.open_nursery(
|
||||
|
|
@ -35,7 +35,7 @@ async def test_reg_then_unreg(
|
|||
uid = portal.channel.aid.uid
|
||||
|
||||
async with tractor.get_registry(reg_addr) as aportal:
|
||||
# this local actor should be the arbiter
|
||||
# this local actor should be the registrar
|
||||
assert actor is aportal.actor
|
||||
|
||||
async with tractor.wait_for_actor('actor'):
|
||||
|
|
@ -154,7 +154,7 @@ async def unpack_reg(
|
|||
actor_or_portal: tractor.Portal|tractor.Actor,
|
||||
):
|
||||
'''
|
||||
Get and unpack a "registry" RPC request from the "arbiter" registry
|
||||
Get and unpack a "registry" RPC request from the registrar
|
||||
system.
|
||||
|
||||
'''
|
||||
|
|
@ -163,7 +163,10 @@ async def unpack_reg(
|
|||
else:
|
||||
msg = await actor_or_portal.run_from_ns('self', 'get_registry')
|
||||
|
||||
return {tuple(key.split('.')): val for key, val in msg.items()}
|
||||
return {
|
||||
tuple(key.split('.')): val
|
||||
for key, val in msg.items()
|
||||
}
|
||||
|
||||
|
||||
async def spawn_and_check_registry(
|
||||
|
|
@ -194,15 +197,15 @@ async def spawn_and_check_registry(
|
|||
actor = tractor.current_actor()
|
||||
|
||||
if remote_arbiter:
|
||||
assert not actor.is_arbiter
|
||||
assert not actor.is_registrar
|
||||
|
||||
if actor.is_arbiter:
|
||||
extra = 1 # arbiter is local root actor
|
||||
if actor.is_registrar:
|
||||
extra = 1 # registrar is local root actor
|
||||
get_reg = partial(unpack_reg, actor)
|
||||
|
||||
else:
|
||||
get_reg = partial(unpack_reg, portal)
|
||||
extra = 2 # local root actor + remote arbiter
|
||||
extra = 2 # local root actor + remote registrar
|
||||
|
||||
# ensure current actor is registered
|
||||
registry: dict = await get_reg()
|
||||
|
|
@ -282,7 +285,7 @@ def test_subactors_unregister_on_cancel(
|
|||
):
|
||||
'''
|
||||
Verify that cancelling a nursery results in all subactors
|
||||
deregistering themselves with the arbiter.
|
||||
deregistering themselves with the registrar.
|
||||
|
||||
'''
|
||||
with pytest.raises(KeyboardInterrupt):
|
||||
|
|
@ -311,7 +314,7 @@ def test_subactors_unregister_on_cancel_remote_daemon(
|
|||
'''
|
||||
Verify that cancelling a nursery results in all subactors
|
||||
deregistering themselves with a **remote** (not in the local
|
||||
process tree) arbiter.
|
||||
process tree) registrar.
|
||||
|
||||
'''
|
||||
with pytest.raises(KeyboardInterrupt):
|
||||
|
|
@ -356,20 +359,24 @@ async def close_chans_before_nursery(
|
|||
try:
|
||||
get_reg = partial(unpack_reg, aportal)
|
||||
|
||||
async with tractor.open_nursery() as tn:
|
||||
portal1 = await tn.start_actor(
|
||||
name='consumer1', enable_modules=[__name__])
|
||||
portal2 = await tn.start_actor(
|
||||
'consumer2', enable_modules=[__name__])
|
||||
async with tractor.open_nursery() as an:
|
||||
portal1 = await an.start_actor(
|
||||
name='consumer1',
|
||||
enable_modules=[__name__],
|
||||
)
|
||||
portal2 = await an.start_actor(
|
||||
'consumer2',
|
||||
enable_modules=[__name__],
|
||||
)
|
||||
|
||||
# TODO: compact this back as was in last commit once
|
||||
# 3.9+, see https://github.com/goodboy/tractor/issues/207
|
||||
async with portal1.open_stream_from(
|
||||
stream_forever
|
||||
) as agen1:
|
||||
async with portal2.open_stream_from(
|
||||
async with (
|
||||
portal1.open_stream_from(
|
||||
stream_forever
|
||||
) as agen2:
|
||||
) as agen1,
|
||||
portal2.open_stream_from(
|
||||
stream_forever
|
||||
) as agen2,
|
||||
):
|
||||
async with (
|
||||
collapse_eg(),
|
||||
trio.open_nursery() as tn,
|
||||
|
|
@ -380,7 +387,7 @@ async def close_chans_before_nursery(
|
|||
await streamer(agen2)
|
||||
finally:
|
||||
# Kill the root nursery thus resulting in
|
||||
# normal arbiter channel ops to fail during
|
||||
# normal registrar channel ops to fail during
|
||||
# teardown. It doesn't seem like this is
|
||||
# reliably triggered by an external SIGINT.
|
||||
# tractor.current_actor()._root_nursery.cancel_scope.cancel()
|
||||
|
|
@ -392,6 +399,7 @@ async def close_chans_before_nursery(
|
|||
# also kill off channels cuz why not
|
||||
await agen1.aclose()
|
||||
await agen2.aclose()
|
||||
|
||||
finally:
|
||||
with trio.CancelScope(shield=True):
|
||||
await trio.sleep(1)
|
||||
|
|
@ -412,7 +420,7 @@ def test_close_channel_explicit(
|
|||
'''
|
||||
Verify that closing a stream explicitly and killing the actor's
|
||||
"root nursery" **before** the containing nursery tears down also
|
||||
results in subactor(s) deregistering from the arbiter.
|
||||
results in subactor(s) deregistering from the registrar.
|
||||
|
||||
'''
|
||||
with pytest.raises(KeyboardInterrupt):
|
||||
|
|
@ -427,7 +435,7 @@ def test_close_channel_explicit(
|
|||
|
||||
|
||||
@pytest.mark.parametrize('use_signal', [False, True])
|
||||
def test_close_channel_explicit_remote_arbiter(
|
||||
def test_close_channel_explicit_remote_registrar(
|
||||
daemon: subprocess.Popen,
|
||||
start_method: str,
|
||||
use_signal: bool,
|
||||
|
|
@ -436,7 +444,7 @@ def test_close_channel_explicit_remote_arbiter(
|
|||
'''
|
||||
Verify that closing a stream explicitly and killing the actor's
|
||||
"root nursery" **before** the containing nursery tears down also
|
||||
results in subactor(s) deregistering from the arbiter.
|
||||
results in subactor(s) deregistering from the registrar.
|
||||
|
||||
'''
|
||||
with pytest.raises(KeyboardInterrupt):
|
||||
|
|
@ -448,3 +456,65 @@ def test_close_channel_explicit_remote_arbiter(
|
|||
remote_arbiter=True,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@tractor.context
|
||||
async def kill_transport(
|
||||
ctx: tractor.Context,
|
||||
) -> None:
|
||||
|
||||
await ctx.started()
|
||||
actor: tractor.Actor = tractor.current_actor()
|
||||
actor.ipc_server.cancel()
|
||||
await trio.sleep_forever()
|
||||
|
||||
|
||||
|
||||
# @pytest.mark.parametrize('use_signal', [False, True])
|
||||
def test_stale_entry_is_deleted(
|
||||
debug_mode: bool,
|
||||
daemon: subprocess.Popen,
|
||||
start_method: str,
|
||||
reg_addr: tuple,
|
||||
):
|
||||
'''
|
||||
Ensure that when a stale entry is detected in the registrar's
|
||||
table that the `find_actor()` API takes care of deleting the
|
||||
stale entry and not delivering a bad portal.
|
||||
|
||||
'''
|
||||
async def main():
|
||||
|
||||
name: str = 'transport_fails_actor'
|
||||
_reg_ptl: tractor.Portal
|
||||
an: tractor.ActorNursery
|
||||
async with (
|
||||
tractor.open_nursery(
|
||||
debug_mode=debug_mode,
|
||||
registry_addrs=[reg_addr],
|
||||
) as an,
|
||||
tractor.get_registry(reg_addr) as _reg_ptl,
|
||||
):
|
||||
ptl: tractor.Portal = await an.start_actor(
|
||||
name,
|
||||
enable_modules=[__name__],
|
||||
)
|
||||
async with ptl.open_context(
|
||||
kill_transport,
|
||||
) as (first, ctx):
|
||||
async with tractor.find_actor(
|
||||
name,
|
||||
registry_addrs=[reg_addr],
|
||||
) as maybe_portal:
|
||||
# because the transitive
|
||||
# `._discovery.maybe_open_portal()` call should
|
||||
# fail and implicitly call `.delete_addr()`
|
||||
assert maybe_portal is None
|
||||
registry: dict = await unpack_reg(_reg_ptl)
|
||||
assert ptl.chan.aid.uid not in registry
|
||||
|
||||
# should fail since we knocked out the IPC tpt XD
|
||||
await ptl.cancel_actor()
|
||||
await an.cancel()
|
||||
|
||||
trio.run(main)
|
||||
|
|
|
|||
|
|
@ -94,8 +94,10 @@ def run_example_in_subproc(
|
|||
for f in p[2]
|
||||
|
||||
if (
|
||||
'__' not in f
|
||||
and f[0] != '_'
|
||||
'__' not in f # ignore any pkg-mods
|
||||
# ignore any `__pycache__` subdir
|
||||
and '__pycache__' not in str(p[0])
|
||||
and f[0] != '_' # ignore any WIP "examplel mods"
|
||||
and 'debugging' not in p[0]
|
||||
and 'integration' not in p[0]
|
||||
and 'advanced_faults' not in p[0]
|
||||
|
|
@ -143,12 +145,19 @@ def test_example(
|
|||
'This test does run just fine "in person" however..'
|
||||
)
|
||||
|
||||
from .conftest import cpu_scaling_factor
|
||||
|
||||
timeout: float = (
|
||||
60
|
||||
if ci_env and _non_linux
|
||||
else 16
|
||||
)
|
||||
|
||||
# add latency headroom for CPU freq scaling (auto-cpufreq et al.)
|
||||
headroom: float = cpu_scaling_factor()
|
||||
if headroom != 1.:
|
||||
timeout *= headroom
|
||||
|
||||
with open(ex_file, 'r') as ex:
|
||||
code = ex.read()
|
||||
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@ from tractor import (
|
|||
to_asyncio,
|
||||
RemoteActorError,
|
||||
ContextCancelled,
|
||||
_state,
|
||||
)
|
||||
from tractor.runtime import _state
|
||||
from tractor.trionics import BroadcastReceiver
|
||||
from tractor._testing import expect_ctxc
|
||||
|
||||
|
|
|
|||
|
|
@ -201,7 +201,7 @@ async def stream_from_peer(
|
|||
) -> None:
|
||||
|
||||
# sanity
|
||||
assert tractor._state.debug_mode() == debug_mode
|
||||
assert tractor.debug_mode() == debug_mode
|
||||
|
||||
peer: Portal
|
||||
try:
|
||||
|
|
@ -841,7 +841,7 @@ async def serve_subactors(
|
|||
async with open_nursery() as an:
|
||||
|
||||
# sanity
|
||||
assert tractor._state.debug_mode() == debug_mode
|
||||
assert tractor.debug_mode() == debug_mode
|
||||
|
||||
await ctx.started(peer_name)
|
||||
async with ctx.open_stream() as ipc:
|
||||
|
|
@ -880,7 +880,7 @@ async def client_req_subactor(
|
|||
) -> None:
|
||||
# sanity
|
||||
if debug_mode:
|
||||
assert tractor._state.debug_mode()
|
||||
assert tractor.debug_mode()
|
||||
|
||||
# TODO: other cases to do with sub lifetimes:
|
||||
# -[ ] test that we can have the server spawn a sub
|
||||
|
|
|
|||
|
|
@ -300,19 +300,43 @@ def test_a_quadruple_example(
|
|||
time_quad_ex: tuple[list[int], float],
|
||||
ci_env: bool,
|
||||
spawn_backend: str,
|
||||
test_log: tractor.log.StackLevelAdapter,
|
||||
):
|
||||
'''
|
||||
This also serves as a kind of "we'd like to be this fast test".
|
||||
This also serves as a "we'd like to be this fast" smoke test
|
||||
given past empirical eval of this suite.
|
||||
|
||||
'''
|
||||
non_linux: bool = (_sys := platform.system()) != 'Linux'
|
||||
|
||||
results, diff = time_quad_ex
|
||||
assert results
|
||||
this_fast_on_linux: float = 3
|
||||
this_fast = (
|
||||
6 if non_linux
|
||||
else 3
|
||||
else this_fast_on_linux
|
||||
)
|
||||
# ^ XXX NOTE,
|
||||
# i've noticed that tweaking the CPU governor setting
|
||||
# to not "always" enable "turbo" mode can result in latency
|
||||
# which causes this limit to be too little. Not sure if it'd
|
||||
# be worth it to adjust the linux value based on reading the
|
||||
# CPU conf from the sys?
|
||||
#
|
||||
# For ex, see the `auto-cpufreq` docs on such settings,
|
||||
# https://github.com/AdnanHodzic/auto-cpufreq?tab=readme-ov-file#example-config-file-contents
|
||||
#
|
||||
# HENCE this below latency-headroom compensation logic..
|
||||
from .conftest import cpu_scaling_factor
|
||||
headroom: float = cpu_scaling_factor()
|
||||
if headroom != 1.:
|
||||
this_fast = this_fast_on_linux * headroom
|
||||
test_log.warning(
|
||||
f'Adding latency headroom on linux bc CPU scaling,\n'
|
||||
f'headroom: {headroom}\n'
|
||||
f'this_fast_on_linux: {this_fast_on_linux} -> {this_fast}\n'
|
||||
)
|
||||
|
||||
results, diff = time_quad_ex
|
||||
assert results
|
||||
assert diff < this_fast
|
||||
|
||||
|
||||
|
|
@ -353,7 +377,7 @@ def test_not_fast_enough_quad(
|
|||
assert results is None
|
||||
|
||||
|
||||
@tractor_test
|
||||
@tractor_test(timeout=20)
|
||||
async def test_respawn_consumer_task(
|
||||
reg_addr: tuple,
|
||||
spawn_backend: str,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
"""
|
||||
Arbiter and "local" actor api
|
||||
Registrar and "local" actor api
|
||||
"""
|
||||
import time
|
||||
|
||||
|
|
@ -12,11 +12,11 @@ from tractor._testing import tractor_test
|
|||
|
||||
@pytest.mark.trio
|
||||
async def test_no_runtime():
|
||||
"""An arbitter must be established before any nurseries
|
||||
"""A registrar must be established before any nurseries
|
||||
can be created.
|
||||
|
||||
(In other words ``tractor.open_root_actor()`` must be engaged at
|
||||
some point?)
|
||||
(In other words ``tractor.open_root_actor()`` must be
|
||||
engaged at some point?)
|
||||
"""
|
||||
with pytest.raises(RuntimeError) :
|
||||
async with tractor.find_actor('doggy'):
|
||||
|
|
@ -25,9 +25,9 @@ async def test_no_runtime():
|
|||
|
||||
@tractor_test
|
||||
async def test_self_is_registered(reg_addr):
|
||||
"Verify waiting on the arbiter to register itself using the standard api."
|
||||
"Verify waiting on the registrar to register itself using the standard api."
|
||||
actor = tractor.current_actor()
|
||||
assert actor.is_arbiter
|
||||
assert actor.is_registrar
|
||||
with trio.fail_after(0.2):
|
||||
async with tractor.wait_for_actor('root') as portal:
|
||||
assert portal.channel.uid[0] == 'root'
|
||||
|
|
@ -35,11 +35,11 @@ async def test_self_is_registered(reg_addr):
|
|||
|
||||
@tractor_test
|
||||
async def test_self_is_registered_localportal(reg_addr):
|
||||
"Verify waiting on the arbiter to register itself using a local portal."
|
||||
"Verify waiting on the registrar to register itself using a local portal."
|
||||
actor = tractor.current_actor()
|
||||
assert actor.is_arbiter
|
||||
assert actor.is_registrar
|
||||
async with tractor.get_registry(reg_addr) as portal:
|
||||
assert isinstance(portal, tractor._portal.LocalPortal)
|
||||
assert isinstance(portal, tractor.runtime._portal.LocalPortal)
|
||||
|
||||
with trio.fail_after(0.2):
|
||||
sockaddr = await portal.run_from_ns(
|
||||
|
|
@ -57,8 +57,8 @@ def test_local_actor_async_func(reg_addr):
|
|||
async with tractor.open_root_actor(
|
||||
registry_addrs=[reg_addr],
|
||||
):
|
||||
# arbiter is started in-proc if dne
|
||||
assert tractor.current_actor().is_arbiter
|
||||
# registrar is started in-proc if dne
|
||||
assert tractor.current_actor().is_registrar
|
||||
|
||||
for i in range(10):
|
||||
nums.append(i)
|
||||
|
|
|
|||
|
|
@ -17,11 +17,11 @@ from tractor._testing import (
|
|||
)
|
||||
from tractor import (
|
||||
current_actor,
|
||||
_state,
|
||||
Actor,
|
||||
Context,
|
||||
Portal,
|
||||
)
|
||||
from tractor.runtime import _state
|
||||
from .conftest import (
|
||||
sig_prog,
|
||||
_INT_SIGNAL,
|
||||
|
|
@ -30,7 +30,7 @@ from .conftest import (
|
|||
|
||||
if TYPE_CHECKING:
|
||||
from tractor.msg import Aid
|
||||
from tractor._addr import (
|
||||
from tractor.discovery._addr import (
|
||||
UnwrappedAddress,
|
||||
)
|
||||
|
||||
|
|
@ -53,19 +53,19 @@ def test_abort_on_sigint(
|
|||
|
||||
|
||||
@tractor_test
|
||||
async def test_cancel_remote_arbiter(
|
||||
async def test_cancel_remote_registrar(
|
||||
daemon: subprocess.Popen,
|
||||
reg_addr: UnwrappedAddress,
|
||||
):
|
||||
assert not current_actor().is_arbiter
|
||||
assert not current_actor().is_registrar
|
||||
async with tractor.get_registry(reg_addr) as portal:
|
||||
await portal.cancel_actor()
|
||||
|
||||
time.sleep(0.1)
|
||||
# the arbiter channel server is cancelled but not its main task
|
||||
# the registrar channel server is cancelled but not its main task
|
||||
assert daemon.returncode is None
|
||||
|
||||
# no arbiter socket should exist
|
||||
# no registrar socket should exist
|
||||
with pytest.raises(OSError):
|
||||
async with tractor.get_registry(reg_addr) as portal:
|
||||
pass
|
||||
|
|
@ -80,7 +80,7 @@ def test_register_duplicate_name(
|
|||
registry_addrs=[reg_addr],
|
||||
) as an:
|
||||
|
||||
assert not current_actor().is_arbiter
|
||||
assert not current_actor().is_registrar
|
||||
|
||||
p1 = await an.start_actor('doggy')
|
||||
p2 = await an.start_actor('doggy')
|
||||
|
|
@ -122,7 +122,7 @@ async def get_root_portal(
|
|||
|
||||
# connect back to our immediate parent which should also
|
||||
# be the actor-tree's root.
|
||||
from tractor._discovery import get_root
|
||||
from tractor.discovery._discovery import get_root
|
||||
ptl: Portal
|
||||
async with get_root() as ptl:
|
||||
root_aid: Aid = ptl.chan.aid
|
||||
|
|
|
|||
|
|
@ -0,0 +1,333 @@
|
|||
'''
|
||||
Verify that externally registered remote actor error
|
||||
types are correctly relayed, boxed, and re-raised across
|
||||
IPC actor hops via `reg_err_types()`.
|
||||
|
||||
Also ensure that when custom error types are NOT registered
|
||||
the framework indicates the lookup failure to the user.
|
||||
|
||||
'''
|
||||
import pytest
|
||||
import trio
|
||||
import tractor
|
||||
from tractor import (
|
||||
Context,
|
||||
Portal,
|
||||
RemoteActorError,
|
||||
)
|
||||
from tractor._exceptions import (
|
||||
get_err_type,
|
||||
reg_err_types,
|
||||
)
|
||||
|
||||
|
||||
# -- custom app-level errors for testing --
|
||||
class CustomAppError(Exception):
|
||||
'''
|
||||
A hypothetical user-app error that should be
|
||||
boxed+relayed by `tractor` IPC when registered.
|
||||
|
||||
'''
|
||||
|
||||
|
||||
class AnotherAppError(Exception):
|
||||
'''
|
||||
A second custom error for multi-type registration.
|
||||
|
||||
'''
|
||||
|
||||
|
||||
class UnregisteredAppError(Exception):
|
||||
'''
|
||||
A custom error that is intentionally NEVER
|
||||
registered via `reg_err_types()` so we can
|
||||
verify the framework's failure indication.
|
||||
|
||||
'''
|
||||
|
||||
|
||||
# -- remote-task endpoints --
|
||||
@tractor.context
|
||||
async def raise_custom_err(
|
||||
ctx: Context,
|
||||
) -> None:
|
||||
'''
|
||||
Remote ep that raises a `CustomAppError`
|
||||
after sync-ing with the caller.
|
||||
|
||||
'''
|
||||
await ctx.started()
|
||||
raise CustomAppError(
|
||||
'the app exploded remotely'
|
||||
)
|
||||
|
||||
|
||||
@tractor.context
|
||||
async def raise_another_err(
|
||||
ctx: Context,
|
||||
) -> None:
|
||||
'''
|
||||
Remote ep that raises `AnotherAppError`.
|
||||
|
||||
'''
|
||||
await ctx.started()
|
||||
raise AnotherAppError(
|
||||
'another app-level kaboom'
|
||||
)
|
||||
|
||||
|
||||
@tractor.context
|
||||
async def raise_unreg_err(
|
||||
ctx: Context,
|
||||
) -> None:
|
||||
'''
|
||||
Remote ep that raises an `UnregisteredAppError`
|
||||
which has NOT been `reg_err_types()`-registered.
|
||||
|
||||
'''
|
||||
await ctx.started()
|
||||
raise UnregisteredAppError(
|
||||
'this error type is unknown to tractor'
|
||||
)
|
||||
|
||||
|
||||
# -- unit tests for the type-registry plumbing --
|
||||
|
||||
class TestRegErrTypesPlumbing:
|
||||
'''
|
||||
Low-level checks on `reg_err_types()` and
|
||||
`get_err_type()` without requiring IPC.
|
||||
|
||||
'''
|
||||
|
||||
def test_unregistered_type_returns_none(self):
|
||||
'''
|
||||
An unregistered custom error name should yield
|
||||
`None` from `get_err_type()`.
|
||||
|
||||
'''
|
||||
result = get_err_type('CustomAppError')
|
||||
assert result is None
|
||||
|
||||
def test_register_and_lookup(self):
|
||||
'''
|
||||
After `reg_err_types()`, the custom type should
|
||||
be discoverable via `get_err_type()`.
|
||||
|
||||
'''
|
||||
reg_err_types([CustomAppError])
|
||||
result = get_err_type('CustomAppError')
|
||||
assert result is CustomAppError
|
||||
|
||||
def test_register_multiple_types(self):
|
||||
'''
|
||||
Registering a list of types should make each
|
||||
one individually resolvable.
|
||||
|
||||
'''
|
||||
reg_err_types([
|
||||
CustomAppError,
|
||||
AnotherAppError,
|
||||
])
|
||||
assert (
|
||||
get_err_type('CustomAppError')
|
||||
is CustomAppError
|
||||
)
|
||||
assert (
|
||||
get_err_type('AnotherAppError')
|
||||
is AnotherAppError
|
||||
)
|
||||
|
||||
def test_builtin_types_always_resolve(self):
|
||||
'''
|
||||
Builtin error types like `RuntimeError` and
|
||||
`ValueError` should always be found without
|
||||
any prior registration.
|
||||
|
||||
'''
|
||||
assert (
|
||||
get_err_type('RuntimeError')
|
||||
is RuntimeError
|
||||
)
|
||||
assert (
|
||||
get_err_type('ValueError')
|
||||
is ValueError
|
||||
)
|
||||
|
||||
def test_tractor_native_types_resolve(self):
|
||||
'''
|
||||
`tractor`-internal exc types (e.g.
|
||||
`ContextCancelled`) should always resolve.
|
||||
|
||||
'''
|
||||
assert (
|
||||
get_err_type('ContextCancelled')
|
||||
is tractor.ContextCancelled
|
||||
)
|
||||
|
||||
def test_boxed_type_str_without_ipc_msg(self):
|
||||
'''
|
||||
When a `RemoteActorError` is constructed
|
||||
without an IPC msg (and no resolvable type),
|
||||
`.boxed_type_str` should return `'<unknown>'`.
|
||||
|
||||
'''
|
||||
rae = RemoteActorError('test')
|
||||
assert rae.boxed_type_str == '<unknown>'
|
||||
|
||||
|
||||
# -- IPC-level integration tests --
|
||||
|
||||
def test_registered_custom_err_relayed(
|
||||
debug_mode: bool,
|
||||
tpt_proto: str,
|
||||
):
|
||||
'''
|
||||
When a custom error type is registered via
|
||||
`reg_err_types()` on BOTH sides of an IPC dialog,
|
||||
the parent should receive a `RemoteActorError`
|
||||
whose `.boxed_type` matches the original custom
|
||||
error type.
|
||||
|
||||
'''
|
||||
reg_err_types([CustomAppError])
|
||||
|
||||
async def main():
|
||||
async with tractor.open_nursery(
|
||||
debug_mode=debug_mode,
|
||||
enable_transports=[tpt_proto],
|
||||
) as an:
|
||||
ptl: Portal = await an.start_actor(
|
||||
'custom-err-raiser',
|
||||
enable_modules=[__name__],
|
||||
)
|
||||
async with ptl.open_context(
|
||||
raise_custom_err,
|
||||
) as (ctx, sent):
|
||||
assert not sent
|
||||
try:
|
||||
await ctx.wait_for_result()
|
||||
except RemoteActorError as rae:
|
||||
assert rae.boxed_type is CustomAppError
|
||||
assert rae.src_type is CustomAppError
|
||||
assert 'the app exploded remotely' in str(
|
||||
rae.tb_str
|
||||
)
|
||||
raise
|
||||
|
||||
with pytest.raises(RemoteActorError) as excinfo:
|
||||
trio.run(main)
|
||||
|
||||
rae = excinfo.value
|
||||
assert rae.boxed_type is CustomAppError
|
||||
|
||||
|
||||
def test_registered_another_err_relayed(
|
||||
debug_mode: bool,
|
||||
tpt_proto: str,
|
||||
):
|
||||
'''
|
||||
Same as above but for a different custom error
|
||||
type to verify multi-type registration works
|
||||
end-to-end over IPC.
|
||||
|
||||
'''
|
||||
reg_err_types([AnotherAppError])
|
||||
|
||||
async def main():
|
||||
async with tractor.open_nursery(
|
||||
debug_mode=debug_mode,
|
||||
enable_transports=[tpt_proto],
|
||||
) as an:
|
||||
ptl: Portal = await an.start_actor(
|
||||
'another-err-raiser',
|
||||
enable_modules=[__name__],
|
||||
)
|
||||
async with ptl.open_context(
|
||||
raise_another_err,
|
||||
) as (ctx, sent):
|
||||
assert not sent
|
||||
try:
|
||||
await ctx.wait_for_result()
|
||||
except RemoteActorError as rae:
|
||||
assert (
|
||||
rae.boxed_type
|
||||
is AnotherAppError
|
||||
)
|
||||
raise
|
||||
|
||||
await an.cancel()
|
||||
|
||||
with pytest.raises(RemoteActorError) as excinfo:
|
||||
trio.run(main)
|
||||
|
||||
rae = excinfo.value
|
||||
assert rae.boxed_type is AnotherAppError
|
||||
|
||||
|
||||
def test_unregistered_err_still_relayed(
|
||||
debug_mode: bool,
|
||||
tpt_proto: str,
|
||||
):
|
||||
'''
|
||||
Verify that even when a custom error type is NOT registered via
|
||||
`reg_err_types()`, the remote error is still relayed as
|
||||
a `RemoteActorError` with all string-level info preserved
|
||||
(traceback, type name, source actor uid).
|
||||
|
||||
The `.boxed_type` will be `None` (type obj can't be resolved) but
|
||||
`.boxed_type_str` and `.src_type_str` still report the original
|
||||
type name from the IPC msg.
|
||||
|
||||
This documents the expected limitation: without `reg_err_types()`
|
||||
the `.boxed_type` property can NOT resolve to the original Python
|
||||
type.
|
||||
|
||||
'''
|
||||
# NOTE: intentionally do NOT call
|
||||
# `reg_err_types([UnregisteredAppError])`
|
||||
|
||||
async def main():
|
||||
async with tractor.open_nursery(
|
||||
debug_mode=debug_mode,
|
||||
enable_transports=[tpt_proto],
|
||||
) as an:
|
||||
ptl: Portal = await an.start_actor(
|
||||
'unreg-err-raiser',
|
||||
enable_modules=[__name__],
|
||||
)
|
||||
async with ptl.open_context(
|
||||
raise_unreg_err,
|
||||
) as (ctx, sent):
|
||||
assert not sent
|
||||
await ctx.wait_for_result()
|
||||
|
||||
await an.cancel()
|
||||
|
||||
with pytest.raises(RemoteActorError) as excinfo:
|
||||
trio.run(main)
|
||||
|
||||
rae = excinfo.value
|
||||
|
||||
# the error IS relayed even without
|
||||
# registration; type obj is unresolvable but
|
||||
# all string-level info is preserved.
|
||||
assert rae.boxed_type is None # NOT `UnregisteredAppError`
|
||||
assert rae.src_type is None
|
||||
|
||||
# string names survive the IPC round-trip
|
||||
# via the `Error` msg fields.
|
||||
assert (
|
||||
rae.src_type_str
|
||||
==
|
||||
'UnregisteredAppError'
|
||||
)
|
||||
assert (
|
||||
rae.boxed_type_str
|
||||
==
|
||||
'UnregisteredAppError'
|
||||
)
|
||||
|
||||
# original traceback content is preserved
|
||||
assert 'this error type is unknown' in rae.tb_str
|
||||
assert 'UnregisteredAppError' in rae.tb_str
|
||||
|
|
@ -94,15 +94,15 @@ def test_runtime_vars_unset(
|
|||
after the root actor-runtime exits!
|
||||
|
||||
'''
|
||||
assert not tractor._state._runtime_vars['_debug_mode']
|
||||
assert not tractor.runtime._state._runtime_vars['_debug_mode']
|
||||
async def main():
|
||||
assert not tractor._state._runtime_vars['_debug_mode']
|
||||
assert not tractor.runtime._state._runtime_vars['_debug_mode']
|
||||
async with tractor.open_nursery(
|
||||
debug_mode=True,
|
||||
):
|
||||
assert tractor._state._runtime_vars['_debug_mode']
|
||||
assert tractor.runtime._state._runtime_vars['_debug_mode']
|
||||
|
||||
# after runtime closure, should be reverted!
|
||||
assert not tractor._state._runtime_vars['_debug_mode']
|
||||
assert not tractor.runtime._state._runtime_vars['_debug_mode']
|
||||
|
||||
trio.run(main)
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ def test_rpc_errors(
|
|||
) as n:
|
||||
|
||||
actor = tractor.current_actor()
|
||||
assert actor.is_arbiter
|
||||
assert actor.is_registrar
|
||||
await n.run_in_actor(
|
||||
sleep_back_actor,
|
||||
actor_name=subactor_requests_to,
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ async def spawn(
|
|||
):
|
||||
# now runtime exists
|
||||
actor: tractor.Actor = tractor.current_actor()
|
||||
assert actor.is_arbiter == should_be_root
|
||||
assert actor.is_registrar == should_be_root
|
||||
|
||||
# spawns subproc here
|
||||
portal: tractor.Portal = await an.run_in_actor(
|
||||
|
|
@ -68,7 +68,7 @@ async def spawn(
|
|||
assert result == 10
|
||||
return result
|
||||
else:
|
||||
assert actor.is_arbiter == should_be_root
|
||||
assert actor.is_registrar == should_be_root
|
||||
return 10
|
||||
|
||||
|
||||
|
|
@ -181,7 +181,7 @@ def test_loglevel_propagated_to_subactor(
|
|||
|
||||
async def main():
|
||||
async with tractor.open_nursery(
|
||||
name='arbiter',
|
||||
name='registrar',
|
||||
start_method=start_method,
|
||||
arbiter_addr=reg_addr,
|
||||
|
||||
|
|
|
|||
|
|
@ -30,21 +30,23 @@ from ._streaming import (
|
|||
MsgStream as MsgStream,
|
||||
stream as stream,
|
||||
)
|
||||
from ._discovery import (
|
||||
from .discovery._discovery import (
|
||||
get_registry as get_registry,
|
||||
find_actor as find_actor,
|
||||
wait_for_actor as wait_for_actor,
|
||||
query_actor as query_actor,
|
||||
)
|
||||
from ._supervise import (
|
||||
from .runtime._supervise import (
|
||||
open_nursery as open_nursery,
|
||||
ActorNursery as ActorNursery,
|
||||
)
|
||||
from ._state import (
|
||||
from .runtime._state import (
|
||||
RuntimeVars as RuntimeVars,
|
||||
current_actor as current_actor,
|
||||
is_root_process as is_root_process,
|
||||
current_ipc_ctx as current_ipc_ctx,
|
||||
debug_mode as debug_mode
|
||||
debug_mode as debug_mode,
|
||||
get_runtime_vars as get_runtime_vars,
|
||||
is_root_process as is_root_process,
|
||||
)
|
||||
from ._exceptions import (
|
||||
ContextCancelled as ContextCancelled,
|
||||
|
|
@ -65,6 +67,10 @@ from ._root import (
|
|||
open_root_actor as open_root_actor,
|
||||
)
|
||||
from .ipc import Channel as Channel
|
||||
from ._portal import Portal as Portal
|
||||
from ._runtime import Actor as Actor
|
||||
from .runtime._portal import Portal as Portal
|
||||
from .runtime._runtime import Actor as Actor
|
||||
from .discovery._registry import (
|
||||
Registrar as Registrar,
|
||||
Arbiter as Arbiter,
|
||||
)
|
||||
# from . import hilevel as hilevel
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ import argparse
|
|||
|
||||
from ast import literal_eval
|
||||
|
||||
from ._runtime import Actor
|
||||
from ._entry import _trio_main
|
||||
from .runtime._runtime import Actor
|
||||
from .spawn._entry import _trio_main
|
||||
|
||||
|
||||
def parse_uid(arg):
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ from ._streaming import (
|
|||
MsgStream,
|
||||
open_stream_from_ctx,
|
||||
)
|
||||
from ._state import (
|
||||
from .runtime._state import (
|
||||
current_actor,
|
||||
debug_mode,
|
||||
_ctxvar_Context,
|
||||
|
|
@ -107,8 +107,8 @@ from .trionics import (
|
|||
)
|
||||
# ------ - ------
|
||||
if TYPE_CHECKING:
|
||||
from ._portal import Portal
|
||||
from ._runtime import Actor
|
||||
from .runtime._portal import Portal
|
||||
from .runtime._runtime import Actor
|
||||
from .ipc._transport import MsgTransport
|
||||
from .devx._frame_stack import (
|
||||
CallerInfo,
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ from msgspec import (
|
|||
ValidationError,
|
||||
)
|
||||
|
||||
from tractor._state import current_actor
|
||||
from tractor.runtime._state import current_actor
|
||||
from tractor.log import get_logger
|
||||
from tractor.msg import (
|
||||
Error,
|
||||
|
|
@ -187,7 +187,31 @@ _body_fields: list[str] = list(
|
|||
)
|
||||
|
||||
|
||||
def get_err_type(type_name: str) -> BaseException|None:
|
||||
def reg_err_types(
|
||||
exc_types: list[Type[Exception]],
|
||||
) -> None:
|
||||
'''
|
||||
Register custom exception types for local lookup.
|
||||
|
||||
Such that error types can be registered by an external
|
||||
`tractor`-use-app code base which are expected to be raised
|
||||
remotely; enables them being re-raised on the receiver side of
|
||||
some inter-actor IPC dialog.
|
||||
|
||||
'''
|
||||
for exc_type in exc_types:
|
||||
log.debug(
|
||||
f'Register custom exception,\n'
|
||||
f'{exc_type!r}\n'
|
||||
)
|
||||
setattr(
|
||||
_this_mod,
|
||||
exc_type.__name__,
|
||||
exc_type,
|
||||
)
|
||||
|
||||
|
||||
def get_err_type(type_name: str) -> Type[BaseException]|None:
|
||||
'''
|
||||
Look up an exception type by name from the set of locally known
|
||||
namespaces:
|
||||
|
|
@ -301,7 +325,8 @@ class RemoteActorError(Exception):
|
|||
# also pertains to our long long oustanding issue XD
|
||||
# https://github.com/goodboy/tractor/issues/5
|
||||
self._boxed_type: BaseException = boxed_type
|
||||
self._src_type: BaseException|None = None
|
||||
self._src_type: Type[BaseException]|None = None
|
||||
self._src_type_resolved: bool = False
|
||||
self._ipc_msg: Error|None = ipc_msg
|
||||
self._extra_msgdata = extra_msgdata
|
||||
|
||||
|
|
@ -410,24 +435,41 @@ class RemoteActorError(Exception):
|
|||
return self._ipc_msg.src_type_str
|
||||
|
||||
@property
|
||||
def src_type(self) -> str:
|
||||
def src_type(self) -> Type[BaseException]|None:
|
||||
'''
|
||||
Error type raised by original remote faulting actor.
|
||||
Error type raised by original remote faulting
|
||||
actor.
|
||||
|
||||
When the error has only been relayed a single actor-hop
|
||||
this will be the same as the `.boxed_type`.
|
||||
When the error has only been relayed a single
|
||||
actor-hop this will be the same as
|
||||
`.boxed_type`.
|
||||
|
||||
If the type can not be resolved locally (i.e.
|
||||
it was not registered via `reg_err_types()`)
|
||||
a warning is logged and `None` is returned;
|
||||
all string-level error info (`.src_type_str`,
|
||||
`.tb_str`, etc.) remains available.
|
||||
|
||||
'''
|
||||
if self._src_type is None:
|
||||
if not self._src_type_resolved:
|
||||
self._src_type_resolved = True
|
||||
|
||||
if self._ipc_msg is None:
|
||||
return None
|
||||
|
||||
self._src_type = get_err_type(
|
||||
self._ipc_msg.src_type_str
|
||||
)
|
||||
|
||||
if not self._src_type:
|
||||
raise TypeError(
|
||||
f'Failed to lookup src error type with '
|
||||
f'`tractor._exceptions.get_err_type()` :\n'
|
||||
f'{self.src_type_str}'
|
||||
log.warning(
|
||||
f'Failed to lookup src error type via\n'
|
||||
f'`tractor._exceptions.get_err_type()`:\n'
|
||||
f'\n'
|
||||
f'`{self._ipc_msg.src_type_str}`'
|
||||
f' is not registered!\n'
|
||||
f'\n'
|
||||
f'Call `reg_err_types()` to enable'
|
||||
f' full type reconstruction.\n'
|
||||
)
|
||||
|
||||
return self._src_type
|
||||
|
|
@ -435,20 +477,30 @@ class RemoteActorError(Exception):
|
|||
@property
|
||||
def boxed_type_str(self) -> str:
|
||||
'''
|
||||
String-name of the (last hop's) boxed error type.
|
||||
String-name of the (last hop's) boxed error
|
||||
type.
|
||||
|
||||
Falls back to the IPC-msg-encoded type-name
|
||||
str when the type can not be resolved locally
|
||||
(e.g. unregistered custom errors).
|
||||
|
||||
'''
|
||||
# TODO, maybe support also serializing the
|
||||
# `ExceptionGroup.exeptions: list[BaseException]` set under
|
||||
# certain conditions?
|
||||
# `ExceptionGroup.exceptions: list[BaseException]`
|
||||
# set under certain conditions?
|
||||
bt: Type[BaseException] = self.boxed_type
|
||||
if bt:
|
||||
return str(bt.__name__)
|
||||
|
||||
return ''
|
||||
# fallback to the str name from the IPC msg
|
||||
# when the type obj can't be resolved.
|
||||
if self._ipc_msg:
|
||||
return self._ipc_msg.boxed_type_str
|
||||
|
||||
return '<unknown>'
|
||||
|
||||
@property
|
||||
def boxed_type(self) -> Type[BaseException]:
|
||||
def boxed_type(self) -> Type[BaseException]|None:
|
||||
'''
|
||||
Error type boxed by last actor IPC hop.
|
||||
|
||||
|
|
@ -677,10 +729,22 @@ class RemoteActorError(Exception):
|
|||
failing actor's remote env.
|
||||
|
||||
'''
|
||||
# TODO: better tb insertion and all the fancier dunder
|
||||
# metadata stuff as per `.__context__` etc. and friends:
|
||||
# TODO: better tb insertion and all the fancier
|
||||
# dunder metadata stuff as per `.__context__`
|
||||
# etc. and friends:
|
||||
# https://github.com/python-trio/trio/issues/611
|
||||
src_type_ref: Type[BaseException] = self.src_type
|
||||
src_type_ref: Type[BaseException]|None = (
|
||||
self.src_type
|
||||
)
|
||||
if src_type_ref is None:
|
||||
# unresolvable type: fall back to
|
||||
# a `RuntimeError` preserving original
|
||||
# traceback + type name.
|
||||
return RuntimeError(
|
||||
f'{self.src_type_str}: '
|
||||
f'{self.tb_str}'
|
||||
)
|
||||
|
||||
return src_type_ref(self.tb_str)
|
||||
|
||||
# TODO: local recontruction of nested inception for a given
|
||||
|
|
@ -1209,14 +1273,31 @@ def unpack_error(
|
|||
if not isinstance(msg, Error):
|
||||
return None
|
||||
|
||||
# try to lookup a suitable error type from the local runtime
|
||||
# env then use it to construct a local instance.
|
||||
# boxed_type_str: str = error_dict['boxed_type_str']
|
||||
# try to lookup a suitable error type from the
|
||||
# local runtime env then use it to construct a
|
||||
# local instance.
|
||||
boxed_type_str: str = msg.boxed_type_str
|
||||
boxed_type: Type[BaseException] = get_err_type(boxed_type_str)
|
||||
boxed_type: Type[BaseException]|None = get_err_type(
|
||||
boxed_type_str
|
||||
)
|
||||
|
||||
# retrieve the error's msg-encoded remotoe-env info
|
||||
message: str = f'remote task raised a {msg.boxed_type_str!r}\n'
|
||||
if boxed_type is None:
|
||||
log.warning(
|
||||
f'Failed to resolve remote error type\n'
|
||||
f'`{boxed_type_str}` - boxing as\n'
|
||||
f'`RemoteActorError` with original\n'
|
||||
f'traceback preserved.\n'
|
||||
f'\n'
|
||||
f'Call `reg_err_types()` to enable\n'
|
||||
f'full type reconstruction.\n'
|
||||
)
|
||||
|
||||
# retrieve the error's msg-encoded remote-env
|
||||
# info
|
||||
message: str = (
|
||||
f'remote task raised a '
|
||||
f'{msg.boxed_type_str!r}\n'
|
||||
)
|
||||
|
||||
# TODO: do we even really need these checks for RAEs?
|
||||
if boxed_type_str in [
|
||||
|
|
|
|||
|
|
@ -37,19 +37,20 @@ import warnings
|
|||
|
||||
import trio
|
||||
|
||||
from . import _runtime
|
||||
from .runtime import _runtime
|
||||
from .discovery._registry import Registrar
|
||||
from .devx import (
|
||||
debug,
|
||||
_frame_stack,
|
||||
pformat as _pformat,
|
||||
)
|
||||
from . import _spawn
|
||||
from . import _state
|
||||
from .spawn import _spawn
|
||||
from .runtime import _state
|
||||
from . import log
|
||||
from .ipc import (
|
||||
_connect_chan,
|
||||
)
|
||||
from ._addr import (
|
||||
from .discovery._addr import (
|
||||
Address,
|
||||
UnwrappedAddress,
|
||||
default_lo_addrs,
|
||||
|
|
@ -267,7 +268,6 @@ async def open_root_actor(
|
|||
if start_method is not None:
|
||||
_spawn.try_set_start_method(start_method)
|
||||
|
||||
# TODO! remove this ASAP!
|
||||
if arbiter_addr is not None:
|
||||
warnings.warn(
|
||||
'`arbiter_addr` is now deprecated\n'
|
||||
|
|
@ -400,7 +400,7 @@ async def open_root_actor(
|
|||
'registry socket(s) already bound'
|
||||
)
|
||||
|
||||
# we were able to connect to an arbiter
|
||||
# we were able to connect to a registrar
|
||||
logger.info(
|
||||
f'Registry(s) seem(s) to exist @ {ponged_addrs}'
|
||||
)
|
||||
|
|
@ -453,8 +453,7 @@ async def open_root_actor(
|
|||
# https://github.com/goodboy/tractor/pull/348
|
||||
# https://github.com/goodboy/tractor/issues/296
|
||||
|
||||
# TODO: rename as `RootActor` or is that even necessary?
|
||||
actor = _runtime.Arbiter(
|
||||
actor = Registrar(
|
||||
name=name or 'registrar',
|
||||
uuid=mk_uuid(),
|
||||
registry_addrs=registry_addrs,
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ from tractor.msg import (
|
|||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ._runtime import Actor
|
||||
from .runtime._runtime import Actor
|
||||
from ._context import Context
|
||||
from .ipc import Channel
|
||||
|
||||
|
|
|
|||
|
|
@ -26,9 +26,7 @@ import random
|
|||
from typing import (
|
||||
Type,
|
||||
)
|
||||
from tractor import (
|
||||
_addr,
|
||||
)
|
||||
from tractor.discovery import _addr
|
||||
|
||||
|
||||
def get_rando_addr(
|
||||
|
|
|
|||
|
|
@ -21,17 +21,27 @@ and applications.
|
|||
'''
|
||||
from functools import (
|
||||
partial,
|
||||
wraps,
|
||||
)
|
||||
import inspect
|
||||
import platform
|
||||
from typing import (
|
||||
Callable,
|
||||
Type,
|
||||
)
|
||||
|
||||
import pytest
|
||||
import tractor
|
||||
import trio
|
||||
import wrapt
|
||||
|
||||
|
||||
def tractor_test(fn):
|
||||
def tractor_test(
|
||||
wrapped: Callable|None = None,
|
||||
*,
|
||||
# @tractor_test(<deco-params>)
|
||||
timeout:float = 30,
|
||||
hide_tb: bool = True,
|
||||
):
|
||||
'''
|
||||
Decorator for async test fns to decorator-wrap them as "native"
|
||||
looking sync funcs runnable by `pytest` and auto invoked with
|
||||
|
|
@ -45,8 +55,18 @@ def tractor_test(fn):
|
|||
Basic deco use:
|
||||
---------------
|
||||
|
||||
@tractor_test
|
||||
async def test_whatever():
|
||||
@tractor_test(
|
||||
timeout=10,
|
||||
)
|
||||
async def test_whatever(
|
||||
# fixture param declarations
|
||||
loglevel: str,
|
||||
start_method: str,
|
||||
reg_addr: tuple,
|
||||
tpt_proto: str,
|
||||
debug_mode: bool,
|
||||
):
|
||||
# already inside a root-actor runtime `trio.Task`
|
||||
await ...
|
||||
|
||||
|
||||
|
|
@ -55,7 +75,7 @@ def tractor_test(fn):
|
|||
If any of the following fixture are requested by the wrapped test
|
||||
fn (via normal func-args declaration),
|
||||
|
||||
- `reg_addr` (a socket addr tuple where arbiter is listening)
|
||||
- `reg_addr` (a socket addr tuple where registrar is listening)
|
||||
- `loglevel` (logging level passed to tractor internals)
|
||||
- `start_method` (subprocess spawning backend)
|
||||
|
||||
|
|
@ -67,52 +87,69 @@ def tractor_test(fn):
|
|||
`tractor.open_root_actor()` funcargs.
|
||||
|
||||
'''
|
||||
@wraps(fn)
|
||||
__tracebackhide__: bool = hide_tb
|
||||
|
||||
# handle the decorator not called with () case.
|
||||
# i.e. in `wrapt` support a deco-with-optional-args,
|
||||
# https://wrapt.readthedocs.io/en/master/decorators.html#decorators-with-optional-arguments
|
||||
if wrapped is None:
|
||||
return wrapt.PartialCallableObjectProxy(
|
||||
tractor_test,
|
||||
timeout=timeout,
|
||||
hide_tb=hide_tb
|
||||
)
|
||||
|
||||
@wrapt.decorator
|
||||
def wrapper(
|
||||
*args,
|
||||
loglevel=None,
|
||||
reg_addr=None,
|
||||
start_method: str|None = None,
|
||||
debug_mode: bool = False,
|
||||
tpt_proto: str|None=None,
|
||||
**kwargs
|
||||
wrapped: Callable,
|
||||
instance: object|Type|None,
|
||||
args: tuple,
|
||||
kwargs: dict,
|
||||
):
|
||||
# __tracebackhide__ = True
|
||||
__tracebackhide__: bool = hide_tb
|
||||
|
||||
# NOTE: inject ant test func declared fixture
|
||||
# names by manually checking!
|
||||
if 'reg_addr' in inspect.signature(fn).parameters:
|
||||
# injects test suite fixture value to test as well
|
||||
# as `run()`
|
||||
kwargs['reg_addr'] = reg_addr
|
||||
# NOTE, ensure we inject any test-fn declared fixture names.
|
||||
for kw in [
|
||||
'reg_addr',
|
||||
'loglevel',
|
||||
'start_method',
|
||||
'debug_mode',
|
||||
'tpt_proto',
|
||||
'timeout',
|
||||
]:
|
||||
if kw in inspect.signature(wrapped).parameters:
|
||||
assert kw in kwargs
|
||||
|
||||
if 'loglevel' in inspect.signature(fn).parameters:
|
||||
# allows test suites to define a 'loglevel' fixture
|
||||
# that activates the internal logging
|
||||
kwargs['loglevel'] = loglevel
|
||||
start_method = kwargs.get('start_method')
|
||||
if platform.system() == "Windows":
|
||||
if start_method is None:
|
||||
kwargs['start_method'] = 'trio'
|
||||
elif start_method != 'trio':
|
||||
raise ValueError(
|
||||
'ONLY the `start_method="trio"` is supported on Windows.'
|
||||
)
|
||||
|
||||
if start_method is None:
|
||||
if platform.system() == "Windows":
|
||||
start_method = 'trio'
|
||||
# open a root-actor, passing certain
|
||||
# `tractor`-runtime-settings, then invoke the test-fn body as
|
||||
# the root-most task.
|
||||
#
|
||||
# https://wrapt.readthedocs.io/en/master/decorators.html#processing-function-arguments
|
||||
async def _main(
|
||||
*args,
|
||||
|
||||
if 'start_method' in inspect.signature(fn).parameters:
|
||||
# set of subprocess spawning backends
|
||||
kwargs['start_method'] = start_method
|
||||
# runtime-settings
|
||||
loglevel:str|None = None,
|
||||
reg_addr:tuple|None = None,
|
||||
start_method: str|None = None,
|
||||
debug_mode: bool = False,
|
||||
tpt_proto: str|None = None,
|
||||
|
||||
if 'debug_mode' in inspect.signature(fn).parameters:
|
||||
# set of subprocess spawning backends
|
||||
kwargs['debug_mode'] = debug_mode
|
||||
**kwargs,
|
||||
):
|
||||
__tracebackhide__: bool = hide_tb
|
||||
|
||||
if 'tpt_proto' in inspect.signature(fn).parameters:
|
||||
# set of subprocess spawning backends
|
||||
kwargs['tpt_proto'] = tpt_proto
|
||||
|
||||
if kwargs:
|
||||
|
||||
# use explicit root actor start
|
||||
async def _main():
|
||||
with trio.fail_after(timeout):
|
||||
async with tractor.open_root_actor(
|
||||
# **kwargs,
|
||||
registry_addrs=[reg_addr] if reg_addr else None,
|
||||
loglevel=loglevel,
|
||||
start_method=start_method,
|
||||
|
|
@ -121,17 +158,31 @@ def tractor_test(fn):
|
|||
debug_mode=debug_mode,
|
||||
|
||||
):
|
||||
await fn(*args, **kwargs)
|
||||
# invoke test-fn body IN THIS task
|
||||
await wrapped(
|
||||
*args,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
main = _main
|
||||
funcname = wrapped.__name__
|
||||
if not inspect.iscoroutinefunction(wrapped):
|
||||
raise TypeError(
|
||||
f"Test-fn {funcname!r} must be an async-function !!"
|
||||
)
|
||||
|
||||
else:
|
||||
# use implicit root actor start
|
||||
main = partial(fn, *args, **kwargs)
|
||||
# invoke runtime via a root task.
|
||||
return trio.run(
|
||||
partial(
|
||||
_main,
|
||||
*args,
|
||||
**kwargs,
|
||||
)
|
||||
)
|
||||
|
||||
return trio.run(main)
|
||||
|
||||
return wrapper
|
||||
return wrapper(
|
||||
wrapped,
|
||||
)
|
||||
|
||||
|
||||
def pytest_addoption(
|
||||
|
|
@ -179,7 +230,8 @@ def pytest_addoption(
|
|||
|
||||
def pytest_configure(config):
|
||||
backend = config.option.spawn_backend
|
||||
tractor._spawn.try_set_start_method(backend)
|
||||
from tractor.spawn._spawn import try_set_start_method
|
||||
try_set_start_method(backend)
|
||||
|
||||
# register custom marks to avoid warnings see,
|
||||
# https://docs.pytest.org/en/stable/how-to/writing_plugins.html#registering-custom-markers
|
||||
|
|
@ -225,7 +277,8 @@ def tpt_protos(request) -> list[str]:
|
|||
|
||||
# XXX ensure we support the protocol by name via lookup!
|
||||
for proto_key in proto_keys:
|
||||
addr_type = tractor._addr._address_types[proto_key]
|
||||
from tractor.discovery import _addr
|
||||
addr_type = _addr._address_types[proto_key]
|
||||
assert addr_type.proto_key == proto_key
|
||||
|
||||
yield proto_keys
|
||||
|
|
@ -256,7 +309,7 @@ def tpt_proto(
|
|||
# f'tpt-proto={proto_key!r}\n'
|
||||
# )
|
||||
|
||||
from tractor import _state
|
||||
from tractor.runtime import _state
|
||||
if _state._def_tpt_proto != proto_key:
|
||||
_state._def_tpt_proto = proto_key
|
||||
_state._runtime_vars['_enable_tpts'] = [
|
||||
|
|
|
|||
|
|
@ -45,17 +45,15 @@ from typing import (
|
|||
)
|
||||
|
||||
import trio
|
||||
from tractor import (
|
||||
_state,
|
||||
log as logmod,
|
||||
)
|
||||
from tractor.runtime import _state
|
||||
from tractor import log as logmod
|
||||
from tractor.devx import debug
|
||||
|
||||
log = logmod.get_logger()
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tractor._spawn import ProcessType
|
||||
from tractor.spawn._spawn import ProcessType
|
||||
from tractor import (
|
||||
Actor,
|
||||
ActorNursery,
|
||||
|
|
|
|||
|
|
@ -53,8 +53,8 @@ import trio
|
|||
from tractor._exceptions import (
|
||||
NoRuntime,
|
||||
)
|
||||
from tractor import _state
|
||||
from tractor._state import (
|
||||
from tractor.runtime import _state
|
||||
from tractor.runtime._state import (
|
||||
current_actor,
|
||||
debug_mode,
|
||||
)
|
||||
|
|
@ -76,7 +76,7 @@ from ._repl import (
|
|||
|
||||
if TYPE_CHECKING:
|
||||
from trio.lowlevel import Task
|
||||
from tractor._runtime import (
|
||||
from tractor.runtime._runtime import (
|
||||
Actor,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ from functools import (
|
|||
import os
|
||||
|
||||
import pdbp
|
||||
from tractor._state import (
|
||||
from tractor.runtime._state import (
|
||||
is_root_process,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ from typing import (
|
|||
)
|
||||
import trio
|
||||
from tractor.log import get_logger
|
||||
from tractor._state import (
|
||||
from tractor.runtime._state import (
|
||||
current_actor,
|
||||
is_root_process,
|
||||
)
|
||||
|
|
@ -44,7 +44,7 @@ if TYPE_CHECKING:
|
|||
from tractor.ipc import (
|
||||
Channel,
|
||||
)
|
||||
from tractor._runtime import (
|
||||
from tractor.runtime._runtime import (
|
||||
Actor,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ from trio.lowlevel import (
|
|||
Task,
|
||||
)
|
||||
from tractor._context import Context
|
||||
from tractor._state import (
|
||||
from tractor.runtime._state import (
|
||||
current_actor,
|
||||
debug_mode,
|
||||
is_root_process,
|
||||
|
|
|
|||
|
|
@ -55,12 +55,12 @@ import tractor
|
|||
from tractor.log import get_logger
|
||||
from tractor.to_asyncio import run_trio_task_in_future
|
||||
from tractor._context import Context
|
||||
from tractor import _state
|
||||
from tractor.runtime import _state
|
||||
from tractor._exceptions import (
|
||||
NoRuntime,
|
||||
InternalError,
|
||||
)
|
||||
from tractor._state import (
|
||||
from tractor.runtime._state import (
|
||||
current_actor,
|
||||
current_ipc_ctx,
|
||||
is_root_process,
|
||||
|
|
@ -87,7 +87,7 @@ from ..pformat import (
|
|||
if TYPE_CHECKING:
|
||||
from trio.lowlevel import Task
|
||||
from threading import Thread
|
||||
from tractor._runtime import (
|
||||
from tractor.runtime._runtime import (
|
||||
Actor,
|
||||
)
|
||||
# from ._post_mortem import BoxedMaybeException
|
||||
|
|
@ -561,6 +561,9 @@ async def _pause(
|
|||
return
|
||||
|
||||
elif isinstance(pause_err, trio.Cancelled):
|
||||
__tracebackhide__: bool = False
|
||||
# XXX, unmask to REPL it.
|
||||
# mk_pdb().set_trace(frame=inspect.currentframe())
|
||||
_repl_fail_report += (
|
||||
'You called `tractor.pause()` from an already cancelled scope!\n\n'
|
||||
'Consider `await tractor.pause(shield=True)` to make it work B)\n'
|
||||
|
|
|
|||
|
|
@ -55,12 +55,12 @@ import tractor
|
|||
from tractor.to_asyncio import run_trio_task_in_future
|
||||
from tractor.log import get_logger
|
||||
from tractor._context import Context
|
||||
from tractor import _state
|
||||
from tractor.runtime import _state
|
||||
from tractor._exceptions import (
|
||||
DebugRequestError,
|
||||
InternalError,
|
||||
)
|
||||
from tractor._state import (
|
||||
from tractor.runtime._state import (
|
||||
current_actor,
|
||||
is_root_process,
|
||||
)
|
||||
|
|
@ -71,7 +71,7 @@ if TYPE_CHECKING:
|
|||
from tractor.ipc import (
|
||||
IPCServer,
|
||||
)
|
||||
from tractor._runtime import (
|
||||
from tractor.runtime._runtime import (
|
||||
Actor,
|
||||
)
|
||||
from ._repl import (
|
||||
|
|
@ -1013,7 +1013,7 @@ async def request_root_stdio_lock(
|
|||
DebugStatus.req_task = current_task()
|
||||
req_err: BaseException|None = None
|
||||
try:
|
||||
from tractor._discovery import get_root
|
||||
from tractor.discovery._discovery import get_root
|
||||
# NOTE: we need this to ensure that this task exits
|
||||
# BEFORE the REPl instance raises an error like
|
||||
# `bdb.BdbQuit` directly, OW you get a trio cs stack
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
# tractor: structured concurrent "actors".
|
||||
# Copyright 2018-eternity Tyler Goodlet.
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
'''
|
||||
Discovery (protocols) API for automatic addressing
|
||||
and location management of (service) actors.
|
||||
|
||||
NOTE: to avoid circular imports, this ``__init__``
|
||||
does NOT eagerly import submodules. Use direct
|
||||
module paths like ``tractor.discovery._addr`` or
|
||||
``tractor.discovery._discovery`` instead.
|
||||
|
||||
'''
|
||||
|
|
@ -27,15 +27,15 @@ from trio import (
|
|||
SocketListener,
|
||||
)
|
||||
|
||||
from .log import get_logger
|
||||
from ._state import (
|
||||
from ..log import get_logger
|
||||
from ..runtime._state import (
|
||||
_def_tpt_proto,
|
||||
)
|
||||
from .ipc._tcp import TCPAddress
|
||||
from .ipc._uds import UDSAddress
|
||||
from ..ipc._tcp import TCPAddress
|
||||
from ..ipc._uds import UDSAddress
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ._runtime import Actor
|
||||
from ..runtime._runtime import Actor
|
||||
|
||||
log = get_logger()
|
||||
|
||||
|
|
@ -28,29 +28,29 @@ from typing import (
|
|||
from contextlib import asynccontextmanager as acm
|
||||
|
||||
from tractor.log import get_logger
|
||||
from .trionics import (
|
||||
from ..trionics import (
|
||||
gather_contexts,
|
||||
collapse_eg,
|
||||
)
|
||||
from .ipc import _connect_chan, Channel
|
||||
from ..ipc import _connect_chan, Channel
|
||||
from ._addr import (
|
||||
UnwrappedAddress,
|
||||
Address,
|
||||
wrap_address
|
||||
)
|
||||
from ._portal import (
|
||||
from ..runtime._portal import (
|
||||
Portal,
|
||||
open_portal,
|
||||
LocalPortal,
|
||||
)
|
||||
from ._state import (
|
||||
from ..runtime._state import (
|
||||
current_actor,
|
||||
_runtime_vars,
|
||||
_def_tpt_proto,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ._runtime import Actor
|
||||
from ..runtime._runtime import Actor
|
||||
|
||||
|
||||
log = get_logger()
|
||||
|
|
@ -60,7 +60,7 @@ log = get_logger()
|
|||
async def get_registry(
|
||||
addr: UnwrappedAddress|None = None,
|
||||
) -> AsyncGenerator[
|
||||
Portal | LocalPortal | None,
|
||||
Portal|LocalPortal|None,
|
||||
None,
|
||||
]:
|
||||
'''
|
||||
|
|
@ -72,8 +72,8 @@ async def get_registry(
|
|||
'''
|
||||
actor: Actor = current_actor()
|
||||
if actor.is_registrar:
|
||||
# we're already the arbiter
|
||||
# (likely a re-entrant call from the arbiter actor)
|
||||
# we're already the registrar
|
||||
# (likely a re-entrant call from the registrar actor)
|
||||
yield LocalPortal(
|
||||
actor,
|
||||
Channel(transport=None)
|
||||
|
|
@ -153,21 +153,27 @@ async def query_actor(
|
|||
regaddr: UnwrappedAddress|None = None,
|
||||
|
||||
) -> AsyncGenerator[
|
||||
UnwrappedAddress|None,
|
||||
tuple[UnwrappedAddress|None, Portal|LocalPortal|None],
|
||||
None,
|
||||
]:
|
||||
'''
|
||||
Lookup a transport address (by actor name) via querying a registrar
|
||||
listening @ `regaddr`.
|
||||
|
||||
Returns the transport protocol (socket) address or `None` if no
|
||||
entry under that name exists.
|
||||
Yields a `tuple` of `(addr, reg_portal)` where,
|
||||
- `addr` is the transport protocol (socket) address or `None` if
|
||||
no entry under that name exists,
|
||||
- `reg_portal` is the `Portal` (or `LocalPortal` when the
|
||||
current actor is the registrar) used for the lookup (or
|
||||
`None` when the peer was found locally via
|
||||
`get_peer_by_name()`).
|
||||
|
||||
'''
|
||||
actor: Actor = current_actor()
|
||||
if (
|
||||
name == 'registrar'
|
||||
and actor.is_registrar
|
||||
and
|
||||
actor.is_registrar
|
||||
):
|
||||
raise RuntimeError(
|
||||
'The current actor IS the registry!?'
|
||||
|
|
@ -175,10 +181,10 @@ async def query_actor(
|
|||
|
||||
maybe_peers: list[Channel]|None = get_peer_by_name(name)
|
||||
if maybe_peers:
|
||||
yield maybe_peers[0].raddr
|
||||
yield maybe_peers[0].raddr, None
|
||||
return
|
||||
|
||||
reg_portal: Portal
|
||||
reg_portal: Portal|LocalPortal
|
||||
regaddr: Address = wrap_address(regaddr) or actor.reg_addrs[0]
|
||||
async with get_registry(regaddr) as reg_portal:
|
||||
# TODO: return portals to all available actors - for now
|
||||
|
|
@ -188,8 +194,7 @@ async def query_actor(
|
|||
'find_actor',
|
||||
name=name,
|
||||
)
|
||||
yield addr
|
||||
|
||||
yield addr, reg_portal
|
||||
|
||||
@acm
|
||||
async def maybe_open_portal(
|
||||
|
|
@ -204,15 +209,49 @@ async def maybe_open_portal(
|
|||
async with query_actor(
|
||||
name=name,
|
||||
regaddr=addr,
|
||||
) as addr:
|
||||
pass
|
||||
) as (addr, reg_portal):
|
||||
if not addr:
|
||||
yield None
|
||||
return
|
||||
|
||||
if addr:
|
||||
async with _connect_chan(addr) as chan:
|
||||
async with open_portal(chan) as portal:
|
||||
yield portal
|
||||
else:
|
||||
yield None
|
||||
try:
|
||||
async with _connect_chan(addr) as chan:
|
||||
async with open_portal(chan) as portal:
|
||||
yield portal
|
||||
|
||||
# most likely we were unable to connect the
|
||||
# transport and there is likely a stale entry in
|
||||
# the registry actor's table, thus we need to
|
||||
# instruct it to clear that stale entry and then
|
||||
# more silently (pretend there was no reason but
|
||||
# to) indicate that the target actor can't be
|
||||
# contacted at that addr.
|
||||
except OSError:
|
||||
# NOTE: ensure we delete the stale entry
|
||||
# from the registrar actor when available.
|
||||
if reg_portal is not None:
|
||||
uid: tuple[str, str]|None = await reg_portal.run_from_ns(
|
||||
'self',
|
||||
'delete_addr',
|
||||
addr=addr,
|
||||
)
|
||||
if uid:
|
||||
log.warning(
|
||||
f'Deleted stale registry entry !\n'
|
||||
f'addr: {addr!r}\n'
|
||||
f'uid: {uid!r}\n'
|
||||
)
|
||||
else:
|
||||
log.warning(
|
||||
f'No registry entry found for addr: {addr!r}'
|
||||
)
|
||||
else:
|
||||
log.warning(
|
||||
f'Connection to {addr!r} failed'
|
||||
f' and no registry portal available'
|
||||
f' to delete stale entry.'
|
||||
)
|
||||
yield None
|
||||
|
||||
|
||||
@acm
|
||||
|
|
@ -229,10 +268,10 @@ async def find_actor(
|
|||
None,
|
||||
]:
|
||||
'''
|
||||
Ask the arbiter to find actor(s) by name.
|
||||
Ask the registrar to find actor(s) by name.
|
||||
|
||||
Returns a connected portal to the last registered matching actor
|
||||
known to the arbiter.
|
||||
Returns a connected portal to the last registered
|
||||
matching actor known to the registrar.
|
||||
|
||||
'''
|
||||
# optimization path, use any pre-existing peer channel
|
||||
|
|
@ -280,7 +319,7 @@ async def find_actor(
|
|||
if not any(portals):
|
||||
if raise_on_none:
|
||||
raise RuntimeError(
|
||||
f'No actor "{name}" found registered @ {registry_addrs}'
|
||||
f'No actor {name!r} found registered @ {registry_addrs!r}'
|
||||
)
|
||||
yield None
|
||||
return
|
||||
|
|
@ -38,6 +38,7 @@ prots: bidict[int, str] = {
|
|||
|
||||
'tcp': 4,
|
||||
'udp': 4,
|
||||
'uds': 4,
|
||||
|
||||
# TODO: support the next-gen shite Bo
|
||||
# 'quic': 4,
|
||||
|
|
@ -51,6 +52,7 @@ prot_params: dict[str, tuple[str]] = {
|
|||
|
||||
'tcp': ('port',),
|
||||
'udp': ('port',),
|
||||
'uds': ('path',),
|
||||
|
||||
# 'quic': ('port',),
|
||||
# 'ssh': ('port',),
|
||||
|
|
@ -75,7 +77,7 @@ def iter_prot_layers(
|
|||
assert not root # there is a root '/' on LHS
|
||||
itokens = iter(tokens)
|
||||
|
||||
prot: str | None = None
|
||||
prot: str|None = None
|
||||
params: list[str] = []
|
||||
for token in itokens:
|
||||
# every prot path should start with a known
|
||||
|
|
@ -98,7 +100,10 @@ def iter_prot_layers(
|
|||
|
||||
def parse_maddr(
|
||||
multiaddr: str,
|
||||
) -> dict[str, str | int | dict]:
|
||||
) -> dict[
|
||||
str,
|
||||
str|int|dict,
|
||||
]:
|
||||
'''
|
||||
Parse a libp2p style "multiaddress" into its distinct protocol
|
||||
segments where each segment is of the form:
|
||||
|
|
@ -122,14 +127,17 @@ def parse_maddr(
|
|||
`'/wg/1.1.1.1/51820/<pubkey>'`
|
||||
|
||||
'''
|
||||
layers: dict[str, str | int | dict] = {}
|
||||
layers: dict[str, str|int|dict] = {}
|
||||
for (
|
||||
prot_key,
|
||||
params,
|
||||
) in iter_prot_layers(multiaddr):
|
||||
|
||||
layer: int = prots[prot_key] # OSI layer used for sorting
|
||||
ep: dict[str, int | str] = {'layer': layer}
|
||||
ep: dict[str, int|str] = {
|
||||
'layer': layer,
|
||||
'proto': prot_key,
|
||||
}
|
||||
layers[prot_key] = ep
|
||||
|
||||
# TODO; validation and resolving of names:
|
||||
|
|
@ -139,7 +147,7 @@ def parse_maddr(
|
|||
# any loaded network.resolv: dict[str, str]
|
||||
rparams: list = list(reversed(params))
|
||||
for key in prot_params[prot_key]:
|
||||
val: str | int = rparams.pop()
|
||||
val: str|int = rparams.pop()
|
||||
|
||||
# TODO: UGHH, dunno what we should do for validation
|
||||
# here, put it in the params spec somehow?
|
||||
|
|
@ -0,0 +1,253 @@
|
|||
# tractor: structured concurrent "actors".
|
||||
# Copyright 2018-eternity Tyler Goodlet.
|
||||
|
||||
# This program is free software: you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Affero General Public
|
||||
# License as published by the Free Software Foundation, either
|
||||
# version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
|
||||
# This program is distributed in the hope that it will be
|
||||
# useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
# PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
|
||||
# You should have received a copy of the GNU Affero General
|
||||
# Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
|
||||
'''
|
||||
Actor-registry for process-tree service discovery.
|
||||
|
||||
The `Registrar` is a special `Actor` subtype that serves as
|
||||
the process-tree's name-registry, tracking actor
|
||||
name-to-address mappings so peers can discover each other.
|
||||
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
from bidict import bidict
|
||||
import trio
|
||||
|
||||
from ..runtime._runtime import Actor
|
||||
from ._addr import (
|
||||
UnwrappedAddress,
|
||||
Address,
|
||||
wrap_address,
|
||||
)
|
||||
from ..devx import debug
|
||||
from ..log import get_logger
|
||||
|
||||
|
||||
log = get_logger('tractor')
|
||||
|
||||
|
||||
class Registrar(Actor):
|
||||
'''
|
||||
A special registrar `Actor` who can contact all other
|
||||
actors within its immediate process tree and keeps
|
||||
a registry of others meant to be discoverable in
|
||||
a distributed application.
|
||||
|
||||
Normally the registrar is also the "root actor" and
|
||||
thus always has access to the top-most-level actor
|
||||
(process) nursery.
|
||||
|
||||
By default, the registrar is always initialized when
|
||||
and if no other registrar socket addrs have been
|
||||
specified to runtime init entry-points (such as
|
||||
`open_root_actor()` or `open_nursery()`). Any time
|
||||
a new main process is launched (and thus a new root
|
||||
actor created) and, no existing registrar can be
|
||||
contacted at the provided `registry_addr`, then
|
||||
a new one is always created; however, if one can be
|
||||
reached it is used.
|
||||
|
||||
Normally a distributed app requires at least one
|
||||
registrar per logical host where for that given
|
||||
"host space" (aka localhost IPC domain of addresses)
|
||||
it is responsible for making all other host (local
|
||||
address) bound actors *discoverable* to external
|
||||
actor trees running on remote hosts.
|
||||
|
||||
'''
|
||||
is_registrar = True
|
||||
|
||||
def is_registry(self) -> bool:
|
||||
return self.is_registrar
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
|
||||
self._registry: bidict[
|
||||
tuple[str, str],
|
||||
UnwrappedAddress,
|
||||
] = bidict({})
|
||||
self._waiters: dict[
|
||||
str,
|
||||
# either an event to sync to receiving an
|
||||
# actor uid (which is filled in once the actor
|
||||
# has sucessfully registered), or that uid
|
||||
# after registry is complete.
|
||||
list[trio.Event|tuple[str, str]]
|
||||
] = {}
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
async def find_actor(
|
||||
self,
|
||||
name: str,
|
||||
|
||||
) -> UnwrappedAddress|None:
|
||||
|
||||
for uid, addr in self._registry.items():
|
||||
if name in uid:
|
||||
return addr
|
||||
|
||||
return None
|
||||
|
||||
async def get_registry(
|
||||
self
|
||||
|
||||
) -> dict[str, UnwrappedAddress]:
|
||||
'''
|
||||
Return current name registry.
|
||||
|
||||
This method is async to allow for cross-actor
|
||||
invocation.
|
||||
|
||||
'''
|
||||
# NOTE: requires ``strict_map_key=False`` to the
|
||||
# msgpack unpacker since we have tuples as keys
|
||||
# (note this makes the registrar suscetible to
|
||||
# hashdos):
|
||||
# https://github.com/msgpack/msgpack-python#major-breaking-changes-in-msgpack-10
|
||||
return {
|
||||
'.'.join(key): val
|
||||
for key, val in self._registry.items()
|
||||
}
|
||||
|
||||
async def wait_for_actor(
|
||||
self,
|
||||
name: str,
|
||||
|
||||
) -> list[UnwrappedAddress]:
|
||||
'''
|
||||
Wait for a particular actor to register.
|
||||
|
||||
This is a blocking call if no actor by the
|
||||
provided name is currently registered.
|
||||
|
||||
'''
|
||||
addrs: list[UnwrappedAddress] = []
|
||||
addr: UnwrappedAddress
|
||||
|
||||
mailbox_info: str = (
|
||||
'Actor registry contact infos:\n'
|
||||
)
|
||||
for uid, addr in self._registry.items():
|
||||
mailbox_info += (
|
||||
f'|_uid: {uid}\n'
|
||||
f'|_addr: {addr}\n\n'
|
||||
)
|
||||
if name == uid[0]:
|
||||
addrs.append(addr)
|
||||
|
||||
if not addrs:
|
||||
waiter = trio.Event()
|
||||
self._waiters.setdefault(
|
||||
name, []
|
||||
).append(waiter)
|
||||
await waiter.wait()
|
||||
|
||||
for uid in self._waiters[name]:
|
||||
if not isinstance(uid, trio.Event):
|
||||
addrs.append(
|
||||
self._registry[uid]
|
||||
)
|
||||
|
||||
log.runtime(mailbox_info)
|
||||
return addrs
|
||||
|
||||
async def register_actor(
|
||||
self,
|
||||
uid: tuple[str, str],
|
||||
addr: UnwrappedAddress
|
||||
) -> None:
|
||||
uid = name, hash = (
|
||||
str(uid[0]),
|
||||
str(uid[1]),
|
||||
)
|
||||
waddr: Address = wrap_address(addr)
|
||||
if not waddr.is_valid:
|
||||
# should never be 0-dynamic-os-alloc
|
||||
await debug.pause()
|
||||
|
||||
# XXX NOTE, value must also be hashable AND since
|
||||
# `._registry` is a `bidict` values must be unique;
|
||||
# use `.forceput()` to replace any prior (stale)
|
||||
# entries that might map a different uid to the same
|
||||
# addr (e.g. after an unclean shutdown or
|
||||
# actor-restart reusing the same address).
|
||||
self._registry.forceput(uid, tuple(addr))
|
||||
|
||||
# pop and signal all waiter events
|
||||
events = self._waiters.pop(name, [])
|
||||
self._waiters.setdefault(
|
||||
name, []
|
||||
).append(uid)
|
||||
for event in events:
|
||||
if isinstance(event, trio.Event):
|
||||
event.set()
|
||||
|
||||
async def unregister_actor(
|
||||
self,
|
||||
uid: tuple[str, str]
|
||||
|
||||
) -> None:
|
||||
uid = (str(uid[0]), str(uid[1]))
|
||||
entry: tuple = self._registry.pop(
|
||||
uid, None
|
||||
)
|
||||
if entry is None:
|
||||
log.warning(
|
||||
f'Request to de-register'
|
||||
f' {uid!r} failed?'
|
||||
)
|
||||
|
||||
async def delete_addr(
|
||||
self,
|
||||
addr: tuple[str, int|str]|list[str|int],
|
||||
) -> tuple[str, str]|None:
|
||||
# NOTE: `addr` arrives as a `list` over IPC
|
||||
# (msgpack deserializes tuples -> lists) so
|
||||
# coerce to `tuple` for the bidict hash lookup.
|
||||
uid: tuple[str, str]|None = (
|
||||
self._registry.inverse.pop(
|
||||
tuple(addr),
|
||||
None,
|
||||
)
|
||||
)
|
||||
if uid:
|
||||
report: str = (
|
||||
'Deleting registry-entry for,\n'
|
||||
)
|
||||
else:
|
||||
report: str = (
|
||||
'No registry entry for,\n'
|
||||
)
|
||||
|
||||
log.warning(
|
||||
report
|
||||
+
|
||||
f'{addr!r}@{uid!r}'
|
||||
)
|
||||
return uid
|
||||
|
||||
|
||||
# Backward compat alias
|
||||
Arbiter = Registrar
|
||||
|
|
@ -146,7 +146,7 @@ _pubtask2lock: dict[str, trio.StrictFIFOLock] = {}
|
|||
|
||||
|
||||
def pub(
|
||||
wrapped: typing.Callable | None = None,
|
||||
wrapped: typing.Callable|None = None,
|
||||
*,
|
||||
tasks: set[str] = set(),
|
||||
):
|
||||
|
|
@ -244,8 +244,12 @@ def pub(
|
|||
task2lock[name] = trio.StrictFIFOLock()
|
||||
|
||||
@wrapt.decorator
|
||||
async def wrapper(agen, instance, args, kwargs):
|
||||
|
||||
async def wrapper(
|
||||
agen,
|
||||
instance,
|
||||
args,
|
||||
kwargs,
|
||||
):
|
||||
# XXX: this is used to extract arguments properly as per the
|
||||
# `wrapt` docs
|
||||
async def _execute(
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ from ._types import (
|
|||
transport_from_addr,
|
||||
transport_from_stream,
|
||||
)
|
||||
from tractor._addr import (
|
||||
from tractor.discovery._addr import (
|
||||
is_wrapped_addr,
|
||||
wrap_address,
|
||||
Address,
|
||||
|
|
|
|||
|
|
@ -50,26 +50,24 @@ from ..devx.pformat import (
|
|||
from .._exceptions import (
|
||||
TransportClosed,
|
||||
)
|
||||
from .. import _rpc
|
||||
from ..runtime import _rpc
|
||||
from ..msg import (
|
||||
MsgType,
|
||||
Struct,
|
||||
types as msgtypes,
|
||||
)
|
||||
from ..trionics import maybe_open_nursery
|
||||
from .. import (
|
||||
_state,
|
||||
log,
|
||||
)
|
||||
from .._addr import Address
|
||||
from ..runtime import _state
|
||||
from .. import log
|
||||
from ..discovery._addr import Address
|
||||
from ._chan import Channel
|
||||
from ._transport import MsgTransport
|
||||
from ._uds import UDSAddress
|
||||
from ._tcp import TCPAddress
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .._runtime import Actor
|
||||
from .._supervise import ActorNursery
|
||||
from ..runtime._runtime import Actor
|
||||
from ..runtime._supervise import ActorNursery
|
||||
|
||||
|
||||
log = log.get_logger()
|
||||
|
|
@ -357,7 +355,7 @@ async def handle_stream_from_peer(
|
|||
# and `MsgpackStream._inter_packets()` on a read from the
|
||||
# stream particularly when the runtime is first starting up
|
||||
# inside `open_root_actor()` where there is a check for
|
||||
# a bound listener on the "arbiter" addr. the reset will be
|
||||
# a bound listener on the registrar addr. the reset will be
|
||||
# because the handshake was never meant took place.
|
||||
log.runtime(
|
||||
con_status
|
||||
|
|
@ -970,7 +968,7 @@ class Server(Struct):
|
|||
in `accept_addrs`.
|
||||
|
||||
'''
|
||||
from .._addr import (
|
||||
from ..discovery._addr import (
|
||||
default_lo_addrs,
|
||||
wrap_address,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ from tractor.msg import (
|
|||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tractor._addr import Address
|
||||
from tractor.discovery._addr import Address
|
||||
|
||||
log = get_logger()
|
||||
|
||||
|
|
@ -225,7 +225,7 @@ class MsgpackTransport(MsgTransport):
|
|||
|
||||
# not sure entirely why we need this but without it we
|
||||
# seem to be getting racy failures here on
|
||||
# arbiter/registry name subs..
|
||||
# registrar name subs..
|
||||
trio.BrokenResourceError,
|
||||
|
||||
) as trans_err:
|
||||
|
|
|
|||
|
|
@ -53,14 +53,14 @@ from tractor.log import get_logger
|
|||
from tractor.ipc._transport import (
|
||||
MsgpackTransport,
|
||||
)
|
||||
from tractor._state import (
|
||||
from tractor.runtime._state import (
|
||||
get_rt_dir,
|
||||
current_actor,
|
||||
is_root_process,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ._runtime import Actor
|
||||
from tractor.runtime._runtime import Actor
|
||||
|
||||
|
||||
# Platform-specific credential passing constants
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ import colorlog # type: ignore
|
|||
# import colored_traceback.auto # ?TODO, need better config?
|
||||
import trio
|
||||
|
||||
from ._state import current_actor
|
||||
from .runtime._state import current_actor
|
||||
|
||||
|
||||
_default_loglevel: str = 'ERROR'
|
||||
|
|
|
|||
|
|
@ -181,7 +181,11 @@ class MsgDec(Struct):
|
|||
|
||||
|
||||
def mk_dec(
|
||||
spec: Union[Type[Struct]]|Type|None,
|
||||
spec: (
|
||||
Union[Type[Struct]]
|
||||
|Type # lone type
|
||||
|None # implying `Union[*ext_types]|None`
|
||||
),
|
||||
|
||||
# NOTE, required for ad-hoc type extensions to the underlying
|
||||
# serialization proto (which is default `msgpack`),
|
||||
|
|
@ -194,16 +198,18 @@ def mk_dec(
|
|||
Create an IPC msg decoder, a slightly higher level wrapper around
|
||||
a `msgspec.msgpack.Decoder` which provides,
|
||||
|
||||
- easier introspection of the underlying type spec via
|
||||
the `.spec` and `.spec_str` attrs,
|
||||
- easier introspection of the underlying type spec via the
|
||||
`.spec` and `.spec_str` attrs,
|
||||
- `.hook` access to the `Decoder.dec_hook()`,
|
||||
- automatic custom extension-types decode support when
|
||||
`dec_hook()` is provided such that any `PayloadMsg.pld` tagged
|
||||
as a type from from `ext_types` (presuming the `MsgCodec.encode()` also used
|
||||
a `.enc_hook()`) is processed and constructed by a `PldRx` implicitily.
|
||||
as a type from from `ext_types` (presuming the
|
||||
`MsgCodec.encode()` also used a `.enc_hook()`) is processed and
|
||||
constructed by a `PldRx` implicitily.
|
||||
|
||||
NOTE, as mentioned a `MsgDec` is normally used for `PayloadMsg.pld: PayloadT` field
|
||||
decoding inside an IPC-ctx-oriented `PldRx`.
|
||||
NOTE, as mentioned a `MsgDec` is normally used for
|
||||
`PayloadMsg.pld: PayloadT` field decoding inside an
|
||||
IPC-ctx-oriented `PldRx`.
|
||||
|
||||
'''
|
||||
if (
|
||||
|
|
@ -248,12 +254,16 @@ def mk_dec(
|
|||
# will work? kk B)
|
||||
#
|
||||
# maybe_box_struct = mk_boxed_ext_struct(ext_types)
|
||||
spec = Raw | Union[*ext_types]
|
||||
|
||||
spec = spec | Union[*ext_types]
|
||||
|
||||
return MsgDec(
|
||||
_dec=msgpack.Decoder(
|
||||
type=spec, # like `MsgType[Any]`
|
||||
type=spec,
|
||||
dec_hook=dec_hook,
|
||||
# ?TODO, support it?
|
||||
# https://jcristharif.com/msgspec/usage.html#strict-vs-lax-mode
|
||||
# strict=False,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -33,9 +33,7 @@ converters,
|
|||
|_ https://jcristharif.com/msgspec/changelog.html
|
||||
|
||||
'''
|
||||
from types import (
|
||||
ModuleType,
|
||||
)
|
||||
import types
|
||||
import typing
|
||||
from typing import (
|
||||
Type,
|
||||
|
|
@ -44,35 +42,51 @@ from typing import (
|
|||
|
||||
def dec_type_union(
|
||||
type_names: list[str],
|
||||
mods: list[ModuleType] = []
|
||||
mods: list[types.ModuleType] = []
|
||||
) -> Type|Union[Type]:
|
||||
'''
|
||||
Look up types by name, compile into a list and then create and
|
||||
return a `typing.Union` from the full set.
|
||||
|
||||
'''
|
||||
# import importlib
|
||||
types: list[Type] = []
|
||||
_types: list[Type] = []
|
||||
for type_name in type_names:
|
||||
for mod in [
|
||||
typing,
|
||||
# importlib.import_module(__name__),
|
||||
types,
|
||||
] + mods:
|
||||
if type_ref := getattr(
|
||||
mod,
|
||||
type_name,
|
||||
False,
|
||||
):
|
||||
types.append(type_ref)
|
||||
_types.append(type_ref)
|
||||
break
|
||||
|
||||
# special case handling only..
|
||||
# ipc_pld_spec: Union[Type] = eval(
|
||||
# pld_spec_str,
|
||||
# {}, # globals
|
||||
# {'typing': typing}, # locals
|
||||
# )
|
||||
report: str = ''
|
||||
if not _types:
|
||||
report: str = 'No type-instances could be resolved from `type_names` ??\n'
|
||||
|
||||
return Union[*types]
|
||||
elif len(type_names) != len(_types):
|
||||
report: str = (
|
||||
f'Some type-instances could not be resolved from `type_names` ??\n'
|
||||
f'_types: {_types!r}\n'
|
||||
)
|
||||
|
||||
if report:
|
||||
raise ValueError(
|
||||
report
|
||||
+
|
||||
f'type_names: {type_names!r}\n'
|
||||
)
|
||||
|
||||
if not _types:
|
||||
raise ValueError(
|
||||
f'No type-instance could be resolved from `type_names` ??\n'
|
||||
f'type_names: {type_names!r}\n'
|
||||
)
|
||||
|
||||
return Union[*_types]
|
||||
|
||||
|
||||
def enc_type_union(
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ from tractor._exceptions import (
|
|||
_mk_recv_mte,
|
||||
pack_error,
|
||||
)
|
||||
from tractor._state import (
|
||||
from tractor.runtime._state import (
|
||||
current_ipc_ctx,
|
||||
)
|
||||
from ._codec import (
|
||||
|
|
@ -119,7 +119,7 @@ class PldRx(Struct):
|
|||
def limit_plds(
|
||||
self,
|
||||
spec: Union[Type[Struct]],
|
||||
**dec_kwargs,
|
||||
**mk_dec_kwargs,
|
||||
|
||||
) -> MsgDec:
|
||||
'''
|
||||
|
|
@ -135,7 +135,7 @@ class PldRx(Struct):
|
|||
orig_dec: MsgDec = self._pld_dec
|
||||
limit_dec: MsgDec = mk_dec(
|
||||
spec=spec,
|
||||
**dec_kwargs,
|
||||
**mk_dec_kwargs,
|
||||
)
|
||||
try:
|
||||
self._pld_dec = limit_dec
|
||||
|
|
@ -582,6 +582,7 @@ async def drain_to_final_msg(
|
|||
even after ctx closure and the `.open_context()` block exit.
|
||||
|
||||
'''
|
||||
__tracebackhide__: bool = hide_tb
|
||||
raise_overrun: bool = not ctx._allow_overruns
|
||||
parent_never_opened_stream: bool = ctx._stream is None
|
||||
|
||||
|
|
@ -834,7 +835,8 @@ async def drain_to_final_msg(
|
|||
f'{ctx.outcome}\n'
|
||||
)
|
||||
|
||||
__tracebackhide__: bool = hide_tb
|
||||
# ?TODO? why was this here and not above?
|
||||
# __tracebackhide__: bool = hide_tb
|
||||
return (
|
||||
result_msg,
|
||||
pre_result_drained,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
# tractor: structured concurrent "actors".
|
||||
# Copyright 2018-eternity Tyler Goodlet.
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
'''
|
||||
The actor runtime: core machinery for the
|
||||
actor-model implemented on a `trio` task runtime.
|
||||
|
||||
NOTE: to avoid circular imports, this ``__init__``
|
||||
does NOT eagerly import submodules. Use direct
|
||||
module paths like ``tractor.runtime._state`` or
|
||||
``tractor.runtime._runtime`` instead.
|
||||
|
||||
'''
|
||||
|
|
@ -39,30 +39,30 @@ import warnings
|
|||
|
||||
import trio
|
||||
|
||||
from .trionics import (
|
||||
from ..trionics import (
|
||||
maybe_open_nursery,
|
||||
collapse_eg,
|
||||
)
|
||||
from ._state import (
|
||||
current_actor,
|
||||
)
|
||||
from .ipc import Channel
|
||||
from .log import get_logger
|
||||
from .msg import (
|
||||
from ..ipc import Channel
|
||||
from ..log import get_logger
|
||||
from ..msg import (
|
||||
# Error,
|
||||
PayloadMsg,
|
||||
NamespacePath,
|
||||
Return,
|
||||
)
|
||||
from ._exceptions import (
|
||||
from .._exceptions import (
|
||||
NoResult,
|
||||
TransportClosed,
|
||||
)
|
||||
from ._context import (
|
||||
from .._context import (
|
||||
Context,
|
||||
open_context_from_portal,
|
||||
)
|
||||
from ._streaming import (
|
||||
from .._streaming import (
|
||||
MsgStream,
|
||||
)
|
||||
|
||||
|
|
@ -43,11 +43,11 @@ from trio import (
|
|||
TaskStatus,
|
||||
)
|
||||
|
||||
from .ipc import Channel
|
||||
from ._context import (
|
||||
from ..ipc import Channel
|
||||
from .._context import (
|
||||
Context,
|
||||
)
|
||||
from ._exceptions import (
|
||||
from .._exceptions import (
|
||||
ContextCancelled,
|
||||
RemoteActorError,
|
||||
ModuleNotExposed,
|
||||
|
|
@ -56,19 +56,19 @@ from ._exceptions import (
|
|||
pack_error,
|
||||
unpack_error,
|
||||
)
|
||||
from .trionics import (
|
||||
from ..trionics import (
|
||||
collapse_eg,
|
||||
is_multi_cancelled,
|
||||
maybe_raise_from_masking_exc,
|
||||
)
|
||||
from .devx import (
|
||||
from ..devx import (
|
||||
debug,
|
||||
add_div,
|
||||
pformat as _pformat,
|
||||
)
|
||||
from . import _state
|
||||
from .log import get_logger
|
||||
from .msg import (
|
||||
from ..log import get_logger
|
||||
from ..msg import (
|
||||
current_codec,
|
||||
MsgCodec,
|
||||
PayloadT,
|
||||
|
|
@ -83,46 +83,46 @@ from tractor.msg import (
|
|||
pretty_struct,
|
||||
types as msgtypes,
|
||||
)
|
||||
from .trionics import (
|
||||
from ..trionics import (
|
||||
collapse_eg,
|
||||
maybe_open_nursery,
|
||||
)
|
||||
from .ipc import (
|
||||
from ..ipc import (
|
||||
Channel,
|
||||
# IPCServer, # causes cycles atm..
|
||||
_server,
|
||||
)
|
||||
from ._addr import (
|
||||
from ..discovery._addr import (
|
||||
UnwrappedAddress,
|
||||
Address,
|
||||
# default_lo_addrs,
|
||||
get_address_cls,
|
||||
wrap_address,
|
||||
)
|
||||
from ._context import (
|
||||
from .._context import (
|
||||
mk_context,
|
||||
Context,
|
||||
)
|
||||
from .log import get_logger
|
||||
from ._exceptions import (
|
||||
from ..log import get_logger
|
||||
from .._exceptions import (
|
||||
ContextCancelled,
|
||||
InternalError,
|
||||
ModuleNotExposed,
|
||||
MsgTypeError,
|
||||
unpack_error,
|
||||
)
|
||||
from .devx import (
|
||||
from ..devx import (
|
||||
debug,
|
||||
pformat as _pformat
|
||||
)
|
||||
from ._discovery import get_registry
|
||||
from ..discovery._discovery import get_registry
|
||||
from ._portal import Portal
|
||||
from . import _state
|
||||
from . import _mp_fixup_main
|
||||
from ..spawn import _mp_fixup_main
|
||||
from . import _rpc
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ._supervise import ActorNursery
|
||||
from ._supervise import ActorNursery # noqa
|
||||
from trio._channel import MemoryChannelState
|
||||
|
||||
|
||||
|
|
@ -175,13 +175,21 @@ class Actor:
|
|||
dialog.
|
||||
|
||||
'''
|
||||
# ugh, we need to get rid of this and replace with a "registry" sys
|
||||
# https://github.com/goodboy/tractor/issues/216
|
||||
is_arbiter: bool = False
|
||||
is_registrar: bool = False
|
||||
|
||||
@property
|
||||
def is_registrar(self) -> bool:
|
||||
return self.is_arbiter
|
||||
def is_arbiter(self) -> bool:
|
||||
'''
|
||||
Deprecated, use `.is_registrar`.
|
||||
|
||||
'''
|
||||
warnings.warn(
|
||||
'`Actor.is_arbiter` is deprecated.\n'
|
||||
'Use `.is_registrar` instead.',
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.is_registrar
|
||||
|
||||
@property
|
||||
def is_root(self) -> bool:
|
||||
|
|
@ -237,7 +245,6 @@ class Actor:
|
|||
registry_addrs: list[Address]|None = None,
|
||||
spawn_method: str|None = None,
|
||||
|
||||
# TODO: remove!
|
||||
arbiter_addr: UnwrappedAddress|None = None,
|
||||
|
||||
) -> None:
|
||||
|
|
@ -287,8 +294,8 @@ class Actor:
|
|||
]
|
||||
|
||||
# marked by the process spawning backend at startup
|
||||
# will be None for the parent most process started manually
|
||||
# by the user (currently called the "arbiter")
|
||||
# will be None for the parent most process started
|
||||
# manually by the user (the "registrar")
|
||||
self._spawn_method: str = spawn_method
|
||||
|
||||
# RPC state
|
||||
|
|
@ -907,7 +914,7 @@ class Actor:
|
|||
# TODO! -[ ] another `Struct` for rtvs..
|
||||
rvs: dict[str, Any] = spawnspec._runtime_vars
|
||||
if rvs['_debug_mode']:
|
||||
from .devx import (
|
||||
from ..devx import (
|
||||
enable_stack_on_sig,
|
||||
maybe_init_greenback,
|
||||
)
|
||||
|
|
@ -1656,7 +1663,7 @@ async def async_main(
|
|||
# TODO, just read direct from ipc_server?
|
||||
accept_addrs: list[UnwrappedAddress] = actor.accept_addrs
|
||||
|
||||
# Register with the arbiter if we're told its addr
|
||||
# Register with the registrar if we're told its addr
|
||||
log.runtime(
|
||||
f'Registering `{actor.name}` => {pformat(accept_addrs)}\n'
|
||||
# ^-TODO-^ we should instead show the maddr here^^
|
||||
|
|
@ -1880,153 +1887,8 @@ async def async_main(
|
|||
log.runtime(teardown_report)
|
||||
|
||||
|
||||
# TODO: rename to `Registry` and move to `.discovery._registry`!
|
||||
class Arbiter(Actor):
|
||||
'''
|
||||
A special registrar (and for now..) `Actor` who can contact all
|
||||
other actors within its immediate process tree and possibly keeps
|
||||
a registry of others meant to be discoverable in a distributed
|
||||
application. Normally the registrar is also the "root actor" and
|
||||
thus always has access to the top-most-level actor (process)
|
||||
nursery.
|
||||
|
||||
By default, the registrar is always initialized when and if no
|
||||
other registrar socket addrs have been specified to runtime
|
||||
init entry-points (such as `open_root_actor()` or
|
||||
`open_nursery()`). Any time a new main process is launched (and
|
||||
thus thus a new root actor created) and, no existing registrar
|
||||
can be contacted at the provided `registry_addr`, then a new
|
||||
one is always created; however, if one can be reached it is
|
||||
used.
|
||||
|
||||
Normally a distributed app requires at least registrar per
|
||||
logical host where for that given "host space" (aka localhost
|
||||
IPC domain of addresses) it is responsible for making all other
|
||||
host (local address) bound actors *discoverable* to external
|
||||
actor trees running on remote hosts.
|
||||
|
||||
'''
|
||||
is_arbiter = True
|
||||
|
||||
# TODO, implement this as a read on there existing a `._state` of
|
||||
# some sort setup by whenever we impl this all as
|
||||
# a `.discovery._registry.open_registry()` API
|
||||
def is_registry(self) -> bool:
|
||||
return self.is_arbiter
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
|
||||
self._registry: dict[
|
||||
tuple[str, str],
|
||||
UnwrappedAddress,
|
||||
] = {}
|
||||
self._waiters: dict[
|
||||
str,
|
||||
# either an event to sync to receiving an actor uid (which
|
||||
# is filled in once the actor has sucessfully registered),
|
||||
# or that uid after registry is complete.
|
||||
list[trio.Event | tuple[str, str]]
|
||||
] = {}
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
async def find_actor(
|
||||
self,
|
||||
name: str,
|
||||
|
||||
) -> UnwrappedAddress|None:
|
||||
|
||||
for uid, addr in self._registry.items():
|
||||
if name in uid:
|
||||
return addr
|
||||
|
||||
return None
|
||||
|
||||
async def get_registry(
|
||||
self
|
||||
|
||||
) -> dict[str, UnwrappedAddress]:
|
||||
'''
|
||||
Return current name registry.
|
||||
|
||||
This method is async to allow for cross-actor invocation.
|
||||
|
||||
'''
|
||||
# NOTE: requires ``strict_map_key=False`` to the msgpack
|
||||
# unpacker since we have tuples as keys (not this makes the
|
||||
# arbiter suscetible to hashdos):
|
||||
# https://github.com/msgpack/msgpack-python#major-breaking-changes-in-msgpack-10
|
||||
return {
|
||||
'.'.join(key): val
|
||||
for key, val in self._registry.items()
|
||||
}
|
||||
|
||||
async def wait_for_actor(
|
||||
self,
|
||||
name: str,
|
||||
|
||||
) -> list[UnwrappedAddress]:
|
||||
'''
|
||||
Wait for a particular actor to register.
|
||||
|
||||
This is a blocking call if no actor by the provided name is currently
|
||||
registered.
|
||||
|
||||
'''
|
||||
addrs: list[UnwrappedAddress] = []
|
||||
addr: UnwrappedAddress
|
||||
|
||||
mailbox_info: str = 'Actor registry contact infos:\n'
|
||||
for uid, addr in self._registry.items():
|
||||
mailbox_info += (
|
||||
f'|_uid: {uid}\n'
|
||||
f'|_addr: {addr}\n\n'
|
||||
)
|
||||
if name == uid[0]:
|
||||
addrs.append(addr)
|
||||
|
||||
if not addrs:
|
||||
waiter = trio.Event()
|
||||
self._waiters.setdefault(name, []).append(waiter)
|
||||
await waiter.wait()
|
||||
|
||||
for uid in self._waiters[name]:
|
||||
if not isinstance(uid, trio.Event):
|
||||
addrs.append(self._registry[uid])
|
||||
|
||||
log.runtime(mailbox_info)
|
||||
return addrs
|
||||
|
||||
async def register_actor(
|
||||
self,
|
||||
uid: tuple[str, str],
|
||||
addr: UnwrappedAddress
|
||||
) -> None:
|
||||
uid = name, hash = (str(uid[0]), str(uid[1]))
|
||||
waddr: Address = wrap_address(addr)
|
||||
if not waddr.is_valid:
|
||||
# should never be 0-dynamic-os-alloc
|
||||
await debug.pause()
|
||||
|
||||
self._registry[uid] = addr
|
||||
|
||||
# pop and signal all waiter events
|
||||
events = self._waiters.pop(name, [])
|
||||
self._waiters.setdefault(name, []).append(uid)
|
||||
for event in events:
|
||||
if isinstance(event, trio.Event):
|
||||
event.set()
|
||||
|
||||
async def unregister_actor(
|
||||
self,
|
||||
uid: tuple[str, str]
|
||||
|
||||
) -> None:
|
||||
uid = (str(uid[0]), str(uid[1]))
|
||||
entry: tuple = self._registry.pop(uid, None)
|
||||
if entry is None:
|
||||
log.warning(f'Request to de-register {uid} failed?')
|
||||
# Backward compat: class moved to discovery._registry
|
||||
from ..discovery._registry import (
|
||||
Registrar as Registrar,
|
||||
)
|
||||
Arbiter = Registrar
|
||||
|
|
@ -25,6 +25,7 @@ from contextvars import (
|
|||
from pathlib import Path
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Literal,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
|
@ -32,9 +33,14 @@ from typing import (
|
|||
import platformdirs
|
||||
from trio.lowlevel import current_task
|
||||
|
||||
from msgspec import (
|
||||
field,
|
||||
Struct,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ._runtime import Actor
|
||||
from ._context import Context
|
||||
from .._context import Context
|
||||
|
||||
|
||||
# default IPC transport protocol settings
|
||||
|
|
@ -47,9 +53,70 @@ _def_tpt_proto: TransportProtocolKey = 'tcp'
|
|||
_current_actor: Actor|None = None # type: ignore # noqa
|
||||
_last_actor_terminated: Actor|None = None
|
||||
|
||||
|
||||
# TODO: mk this a `msgspec.Struct`!
|
||||
# -[ ] type out all fields obvi!
|
||||
# -[x] type out all fields obvi!
|
||||
# -[ ] (eventually) mk wire-ready for monitoring?
|
||||
class RuntimeVars(Struct):
|
||||
'''
|
||||
Actor-(and thus process)-global runtime state.
|
||||
|
||||
This struct is relayed from parent to child during sub-actor
|
||||
spawning and is a singleton instance per process.
|
||||
|
||||
Generally contains,
|
||||
- root-actor indicator.
|
||||
- comms-info: addrs for both (public) process/service-discovery
|
||||
and in-tree contact with other actors.
|
||||
- transport-layer IPC protocol server(s) settings.
|
||||
- debug-mode settings for enabling sync breakpointing and any
|
||||
surrounding REPL-fixture hooking.
|
||||
- infected-`asyncio` via guest-mode toggle(s)/cohfig.
|
||||
|
||||
'''
|
||||
_is_root: bool = False # bool
|
||||
_root_mailbox: tuple[str, str|int] = (None, None) # tuple[str|None, str|None]
|
||||
_root_addrs: list[
|
||||
tuple[str, str|int],
|
||||
] = [] # tuple[str|None, str|None]
|
||||
|
||||
# parent->chld ipc protocol caps
|
||||
_enable_tpts: list[TransportProtocolKey] = field(
|
||||
default_factory=lambda: [_def_tpt_proto],
|
||||
)
|
||||
|
||||
# registrar info
|
||||
_registry_addrs: list[tuple] = []
|
||||
|
||||
# `debug_mode: bool` settings
|
||||
_debug_mode: bool = False # bool
|
||||
repl_fixture: bool|Callable = False # |AbstractContextManager[bool]
|
||||
# for `tractor.pause_from_sync()` & `breakpoint()` support
|
||||
use_greenback: bool = False
|
||||
|
||||
# infected-`asyncio`-mode: `trio` running as guest.
|
||||
_is_infected_aio: bool = False
|
||||
|
||||
def __setattr__(
|
||||
self,
|
||||
key,
|
||||
val,
|
||||
) -> None:
|
||||
breakpoint()
|
||||
super().__setattr__(key, val)
|
||||
|
||||
def update(
|
||||
self,
|
||||
from_dict: dict|Struct,
|
||||
) -> None:
|
||||
for attr, val in from_dict.items():
|
||||
setattr(
|
||||
self,
|
||||
attr,
|
||||
val,
|
||||
)
|
||||
|
||||
|
||||
_runtime_vars: dict[str, Any] = {
|
||||
# root of actor-process tree info
|
||||
'_is_root': False, # bool
|
||||
|
|
@ -73,6 +140,23 @@ _runtime_vars: dict[str, Any] = {
|
|||
}
|
||||
|
||||
|
||||
def get_runtime_vars(
|
||||
as_dict: bool = True,
|
||||
) -> dict:
|
||||
'''
|
||||
Deliver a **copy** of the current `Actor`'s "runtime variables".
|
||||
|
||||
By default, for historical impl reasons, this delivers the `dict`
|
||||
form, but the `RuntimeVars` struct should be utilized as possible
|
||||
for future calls.
|
||||
|
||||
'''
|
||||
if as_dict:
|
||||
return dict(_runtime_vars)
|
||||
|
||||
return RuntimeVars(**_runtime_vars)
|
||||
|
||||
|
||||
def last_actor() -> Actor|None:
|
||||
'''
|
||||
Try to return last active `Actor` singleton
|
||||
|
|
@ -98,7 +182,7 @@ def current_actor(
|
|||
_current_actor is None
|
||||
):
|
||||
msg: str = 'No local actor has been initialized yet?\n'
|
||||
from ._exceptions import NoRuntime
|
||||
from .._exceptions import NoRuntime
|
||||
|
||||
if last := last_actor():
|
||||
msg += (
|
||||
|
|
@ -164,7 +248,7 @@ def current_ipc_ctx(
|
|||
not ctx
|
||||
and error_on_not_set
|
||||
):
|
||||
from ._exceptions import InternalError
|
||||
from .._exceptions import InternalError
|
||||
raise InternalError(
|
||||
'No IPC context has been allocated for this task yet?\n'
|
||||
f'|_{current_task()}\n'
|
||||
|
|
@ -30,36 +30,36 @@ import warnings
|
|||
import trio
|
||||
|
||||
|
||||
from .devx import (
|
||||
from ..devx import (
|
||||
debug,
|
||||
pformat as _pformat,
|
||||
)
|
||||
from ._addr import (
|
||||
from ..discovery._addr import (
|
||||
UnwrappedAddress,
|
||||
mk_uuid,
|
||||
)
|
||||
from ._state import current_actor, is_main_process
|
||||
from .log import get_logger, get_loglevel
|
||||
from ..log import get_logger, get_loglevel
|
||||
from ._runtime import Actor
|
||||
from ._portal import Portal
|
||||
from .trionics import (
|
||||
from ..trionics import (
|
||||
is_multi_cancelled,
|
||||
collapse_eg,
|
||||
)
|
||||
from ._exceptions import (
|
||||
from .._exceptions import (
|
||||
ContextCancelled,
|
||||
)
|
||||
from ._root import (
|
||||
from .._root import (
|
||||
open_root_actor,
|
||||
)
|
||||
from . import _state
|
||||
from . import _spawn
|
||||
from ..spawn import _spawn
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import multiprocessing as mp
|
||||
# from .ipc._server import IPCServer
|
||||
from .ipc import IPCServer
|
||||
# from ..ipc._server import IPCServer
|
||||
from ..ipc import IPCServer
|
||||
|
||||
|
||||
log = get_logger()
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
# tractor: structured concurrent "actors".
|
||||
# Copyright 2018-eternity Tyler Goodlet.
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
'''
|
||||
Actor process spawning machinery using
|
||||
multiple backends (trio, multiprocessing).
|
||||
|
||||
NOTE: to avoid circular imports, this ``__init__``
|
||||
does NOT eagerly import submodules. Use direct
|
||||
module paths like ``tractor.spawn._spawn`` or
|
||||
``tractor.spawn._entry`` instead.
|
||||
|
||||
'''
|
||||
|
|
@ -29,19 +29,19 @@ from typing import (
|
|||
|
||||
import trio # type: ignore
|
||||
|
||||
from .log import (
|
||||
from ..log import (
|
||||
get_console_log,
|
||||
get_logger,
|
||||
)
|
||||
from . import _state
|
||||
from .devx import (
|
||||
from ..runtime import _state
|
||||
from ..devx import (
|
||||
_frame_stack,
|
||||
pformat,
|
||||
)
|
||||
# from .msg import pretty_struct
|
||||
from .to_asyncio import run_as_asyncio_guest
|
||||
from ._addr import UnwrappedAddress
|
||||
from ._runtime import (
|
||||
# from ..msg import pretty_struct
|
||||
from ..to_asyncio import run_as_asyncio_guest
|
||||
from ..discovery._addr import UnwrappedAddress
|
||||
from ..runtime._runtime import (
|
||||
async_main,
|
||||
Actor,
|
||||
)
|
||||
|
|
@ -125,7 +125,7 @@ class PatchedForkServer(ForkServer):
|
|||
self._forkserver_pid = None
|
||||
|
||||
# XXX only thing that changed!
|
||||
cmd = ('from tractor._forkserver_override import main; ' +
|
||||
cmd = ('from tractor.spawn._forkserver_override import main; ' +
|
||||
'main(%d, %d, %r, **%r)')
|
||||
|
||||
if self._preload_modules:
|
||||
|
|
@ -34,11 +34,11 @@ from typing import (
|
|||
import trio
|
||||
from trio import TaskStatus
|
||||
|
||||
from .devx import (
|
||||
from ..devx import (
|
||||
debug,
|
||||
pformat as _pformat
|
||||
)
|
||||
from tractor._state import (
|
||||
from tractor.runtime._state import (
|
||||
current_actor,
|
||||
is_main_process,
|
||||
is_root_process,
|
||||
|
|
@ -46,10 +46,10 @@ from tractor._state import (
|
|||
_runtime_vars,
|
||||
)
|
||||
from tractor.log import get_logger
|
||||
from tractor._addr import UnwrappedAddress
|
||||
from tractor._portal import Portal
|
||||
from tractor._runtime import Actor
|
||||
from tractor._entry import _mp_main
|
||||
from tractor.discovery._addr import UnwrappedAddress
|
||||
from tractor.runtime._portal import Portal
|
||||
from tractor.runtime._runtime import Actor
|
||||
from ._entry import _mp_main
|
||||
from tractor._exceptions import ActorFailure
|
||||
from tractor.msg import (
|
||||
types as msgtypes,
|
||||
|
|
@ -58,11 +58,11 @@ from tractor.msg import (
|
|||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ipc import (
|
||||
from tractor.ipc import (
|
||||
_server,
|
||||
Channel,
|
||||
)
|
||||
from ._supervise import ActorNursery
|
||||
from tractor.runtime._supervise import ActorNursery
|
||||
ProcessType = TypeVar('ProcessType', mp.Process, trio.Process)
|
||||
|
||||
|
||||
|
|
@ -43,7 +43,7 @@ from tractor._exceptions import (
|
|||
AsyncioTaskExited,
|
||||
AsyncioCancelled,
|
||||
)
|
||||
from tractor._state import (
|
||||
from tractor.runtime._state import (
|
||||
debug_mode,
|
||||
_runtime_vars,
|
||||
)
|
||||
|
|
@ -1730,7 +1730,7 @@ def run_as_asyncio_guest(
|
|||
# asyncio.CancelledError,
|
||||
# ^^XXX `.shield()` call above prevents this??
|
||||
|
||||
)as state_err:
|
||||
) as state_err:
|
||||
|
||||
# XXX be super dupere noisy about abandonment issues!
|
||||
aio_task: asyncio.Task = asyncio.current_task()
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ from typing import (
|
|||
)
|
||||
|
||||
import trio
|
||||
from tractor._state import current_actor
|
||||
from tractor.runtime._state import current_actor
|
||||
from tractor.log import get_logger
|
||||
# from ._beg import collapse_eg
|
||||
# from ._taskc import (
|
||||
|
|
|
|||
349
uv.lock
349
uv.lock
|
|
@ -2,6 +2,15 @@ version = 1
|
|||
revision = 3
|
||||
requires-python = ">=3.12, <3.14"
|
||||
|
||||
[[package]]
|
||||
name = "async-generator"
|
||||
version = "1.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ce/b6/6fa6b3b598a03cba5e80f829e0dadbb49d7645f523d209b2fb7ea0bbb02a/async_generator-1.10.tar.gz", hash = "sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144", size = 29870, upload-time = "2018-08-01T03:36:21.69Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/71/52/39d20e03abd0ac9159c162ec24b93fbcaa111e8400308f2465432495ca2b/async_generator-1.10-py3-none-any.whl", hash = "sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b", size = 18857, upload-time = "2018-08-01T03:36:20.029Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "attrs"
|
||||
version = "24.3.0"
|
||||
|
|
@ -11,6 +20,15 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/89/aa/ab0f7891a01eeb2d2e338ae8fecbe57fcebea1a24dbb64d45801bfab481d/attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308", size = 63397, upload-time = "2024-12-16T06:59:26.977Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base58"
|
||||
version = "2.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7f/45/8ae61209bb9015f516102fa559a2914178da1d5868428bd86a1b4421141d/base58-2.1.1.tar.gz", hash = "sha256:c5d0cb3f5b6e81e8e35da5754388ddcc6d0d14b6c6a132cb93d69ed580a7278c", size = 6528, upload-time = "2021-10-30T22:12:17.858Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/45/ec96b29162a402fc4c1c5512d114d7b3787b9d1c2ec241d9568b4816ee23/base58-2.1.1-py3-none-any.whl", hash = "sha256:11a36f4d3ce51dfc1043f3218591ac4eb1ceb172919cebe05b52a5bcc8d245c2", size = 5621, upload-time = "2021-10-30T22:12:16.658Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bidict"
|
||||
version = "0.23.1"
|
||||
|
|
@ -20,6 +38,74 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5", size = 32764, upload-time = "2024-02-18T19:09:04.156Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blake3"
|
||||
version = "1.0.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/75/aa/abcd75e9600987a0bc6cfe9b6b2ff3f0e2cb08c170addc6e76035b5c4cb3/blake3-1.0.8.tar.gz", hash = "sha256:513cc7f0f5a7c035812604c2c852a0c1468311345573de647e310aca4ab165ba", size = 117308, upload-time = "2025-10-14T06:47:48.83Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/a0/b7b6dff04012cfd6e665c09ee446f749bd8ea161b00f730fe1bdecd0f033/blake3-1.0.8-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:d8da4233984d51471bd4e4366feda1d90d781e712e0a504ea54b1f2b3577557b", size = 347983, upload-time = "2025-10-14T06:45:47.214Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/a2/264091cac31d7ae913f1f296abc20b8da578b958ffb86100a7ce80e8bf5c/blake3-1.0.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1257be19f2d381c868a34cc822fc7f12f817ddc49681b6d1a2790bfbda1a9865", size = 325415, upload-time = "2025-10-14T06:45:48.482Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/7d/85a4c0782f613de23d114a7a78fcce270f75b193b3ff3493a0de24ba104a/blake3-1.0.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:269f255b110840e52b6ce9db02217e39660ebad3e34ddd5bca8b8d378a77e4e1", size = 371296, upload-time = "2025-10-14T06:45:49.674Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/20/488475254976ed93fab57c67aa80d3b40df77f7d9db6528c9274bff53e08/blake3-1.0.8-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:66ca28a673025c40db3eba21a9cac52f559f83637efa675b3f6bd8683f0415f3", size = 374516, upload-time = "2025-10-14T06:45:51.23Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/21/2a1c47fedb77fb396512677ec6d46caf42ac6e9a897db77edd0a2a46f7bb/blake3-1.0.8-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcb04966537777af56c1f399b35525aa70a1225816e121ff95071c33c0f7abca", size = 447911, upload-time = "2025-10-14T06:45:52.637Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/7d/db0626df16029713e7e61b67314c4835e85c296d82bd907c21c6ea271da2/blake3-1.0.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5b5da177d62cc4b7edf0cea08fe4dec960c9ac27f916131efa890a01f747b93", size = 505420, upload-time = "2025-10-14T06:45:54.445Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/55/6e737850c2d58a6d9de8a76dad2ae0f75b852a23eb4ecb07a0b165e6e436/blake3-1.0.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:38209b10482c97e151681ea3e91cc7141f56adbbf4820a7d701a923124b41e6a", size = 394189, upload-time = "2025-10-14T06:45:55.719Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/94/eafaa5cdddadc0c9c603a6a6d8339433475e1a9f60c8bb9c2eed2d8736b6/blake3-1.0.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:504d1399b7fb91dfe5c25722d2807990493185faa1917456455480c36867adb5", size = 388001, upload-time = "2025-10-14T06:45:57.067Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/81/735fa00d13de7f68b25e1b9cb36ff08c6f165e688d85d8ec2cbfcdedccc5/blake3-1.0.8-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c84af132aa09abeadf9a0118c8fb26f4528f3f42c10ef8be0fcf31c478774ec4", size = 550302, upload-time = "2025-10-14T06:45:58.657Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/c6/d1fe8bdea4a6088bd54b5a58bc40aed89a4e784cd796af7722a06f74bae7/blake3-1.0.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a25db3d36b55f5ed6a86470155cc749fc9c5b91c949b8d14f48658f9d960d9ec", size = 554211, upload-time = "2025-10-14T06:46:00.269Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/d1/ca74aa450cbe10e396e061f26f7a043891ffa1485537d6b30d3757e20995/blake3-1.0.8-cp312-cp312-win32.whl", hash = "sha256:e0fee93d5adcd44378b008c147e84f181f23715307a64f7b3db432394bbfce8b", size = 228343, upload-time = "2025-10-14T06:46:01.533Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/42/bbd02647169e3fbed27558555653ac2578c6f17ccacf7d1956c58ef1d214/blake3-1.0.8-cp312-cp312-win_amd64.whl", hash = "sha256:6a6eafc29e4f478d365a87d2f25782a521870c8514bb43734ac85ae9be71caf7", size = 215704, upload-time = "2025-10-14T06:46:02.79Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/b8/11de9528c257f7f1633f957ccaff253b706838d22c5d2908e4735798ec01/blake3-1.0.8-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:46dc20976bd6c235959ef0246ec73420d1063c3da2839a9c87ca395cf1fd7943", size = 347771, upload-time = "2025-10-14T06:46:04.248Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/26/f7668be55c909678b001ecacff11ad7016cd9b4e9c7cc87b5971d638c5a9/blake3-1.0.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d17eb6382634b3a5bc0c0e0454d5265b0becaeeadb6801ed25150b39a999d0cc", size = 325431, upload-time = "2025-10-14T06:46:06.136Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/57/e8a85fa261894bf7ce7af928ff3408aab60287ab8d58b55d13a3f700b619/blake3-1.0.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19fc6f2b7edab8acff6895fc6e38c19bd79f4c089e21153020c75dfc7397d52d", size = 370994, upload-time = "2025-10-14T06:46:07.398Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/cd/765b76bb48b8b294fea94c9008b0d82b4cfa0fa2f3c6008d840d01a597e4/blake3-1.0.8-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4f54cff7f15d91dc78a63a2dd02a3dccdc932946f271e2adb4130e0b4cf608ba", size = 374372, upload-time = "2025-10-14T06:46:08.698Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/7a/32084eadbb28592bb07298f0de316d2da586c62f31500a6b1339a7e7b29b/blake3-1.0.8-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7e12a777f6b798eb8d06f875d6e108e3008bd658d274d8c676dcf98e0f10537", size = 447627, upload-time = "2025-10-14T06:46:10.002Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/f4/3788a1d86e17425eea147e28d7195d7053565fc279236a9fd278c2ec495e/blake3-1.0.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddfc59b0176fb31168f08d5dd536e69b1f4f13b5a0f4b0c3be1003efd47f9308", size = 507536, upload-time = "2025-10-14T06:46:11.614Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/01/4639cba48513b94192681b4da472cdec843d3001c5344d7051ee5eaef606/blake3-1.0.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a2336d5b2a801a7256da21150348f41610a6c21dae885a3acb1ebbd7333d88d8", size = 394105, upload-time = "2025-10-14T06:46:12.808Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/ae/6e55c19c8460fada86cd1306a390a09b0c5a2e2e424f9317d2edacea439f/blake3-1.0.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4072196547484c95a5a09adbb952e9bb501949f03f9e2a85e7249ef85faaba8", size = 386928, upload-time = "2025-10-14T06:46:16.284Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/6c/05b7a5a907df1be53a8f19e7828986fc6b608a44119641ef9c0804fbef15/blake3-1.0.8-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:0eab3318ec02f8e16fe549244791ace2ada2c259332f0c77ab22cf94dfff7130", size = 550003, upload-time = "2025-10-14T06:46:17.791Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/03/f0ea4adfedc1717623be6460b3710fcb725ca38082c14274369803f727e1/blake3-1.0.8-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a33b9a1fb6d1d559a8e0d04b041e99419a6bb771311c774f6ff57ed7119c70ed", size = 553857, upload-time = "2025-10-14T06:46:19.088Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/6f/e5410d2e2a30c8aba8389ffc1c0061356916bf5ecd0a210344e7b69b62ab/blake3-1.0.8-cp313-cp313-win32.whl", hash = "sha256:e171b169cb7ea618e362a4dddb7a4d4c173bbc08b9ba41ea3086dd1265530d4f", size = 228315, upload-time = "2025-10-14T06:46:20.391Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/ef/d9c297956dfecd893f29f59e7b22445aba5b47b7f6815d9ba5dcd73fcae6/blake3-1.0.8-cp313-cp313-win_amd64.whl", hash = "sha256:3168c457255b5d2a2fc356ba696996fcaff5d38284f968210d54376312107662", size = 215477, upload-time = "2025-10-14T06:46:21.542Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/ba/eaa7723d66dd8ab762a3e85e139bb9c46167b751df6e950ad287adb8fb61/blake3-1.0.8-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:b4d672c24dc15ec617d212a338a4ca14b449829b6072d09c96c63b6e6b621aed", size = 347289, upload-time = "2025-10-14T06:46:22.772Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/b3/6957f6ee27f0d5b8c4efdfda68a1298926a88c099f4dd89c711049d16526/blake3-1.0.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:1af0e5a29aa56d4fba904452ae784740997440afd477a15e583c38338e641f41", size = 324444, upload-time = "2025-10-14T06:46:24.729Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/da/722cebca11238f3b24d3cefd2361c9c9ea47cfa0ad9288eeb4d1e0b7cf93/blake3-1.0.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef153c5860d5bf1cc71aece69b28097d2a392913eb323d6b52555c875d0439fc", size = 370441, upload-time = "2025-10-14T06:46:26.29Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/d5/2f7440c8e41c0af995bad3a159e042af0f4ed1994710af5b4766ca918f65/blake3-1.0.8-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e8ae3689f0c7bfa6ce6ae45cab110e4c3442125c4c23b28f1f097856de26e4d1", size = 374312, upload-time = "2025-10-14T06:46:27.451Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/6c/fb6a7812e60ce3e110bcbbb11f167caf3e975c589572c41e1271f35f2c41/blake3-1.0.8-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fb83532f7456ddeb68dae1b36e1f7c52f9cb72852ac01159bbcb1a12b0f8be0", size = 447007, upload-time = "2025-10-14T06:46:29.056Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/3b/c99b43fae5047276ea9d944077c190fc1e5f22f57528b9794e21f7adedc6/blake3-1.0.8-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ae7754c7d96e92a70a52e07c732d594cf9924d780f49fffd3a1e9235e0f5ba7", size = 507323, upload-time = "2025-10-14T06:46:30.661Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/bb/ba90eddd592f8c074a0694cb0a744b6bd76bfe67a14c2b490c8bdfca3119/blake3-1.0.8-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4bacaae75e98dee3b7da6c5ee3b81ee21a3352dd2477d6f1d1dbfd38cdbf158a", size = 393449, upload-time = "2025-10-14T06:46:31.805Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/ed/58a2acd0b9e14459cdaef4344db414d4a36e329b9720921b442a454dd443/blake3-1.0.8-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9456c829601d72852d8ba0af8dae0610f7def1d59f5942efde1e2ef93e8a8b57", size = 386844, upload-time = "2025-10-14T06:46:33.195Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/04/fed09845b18d90862100c8e48308261e2f663aab25d3c71a6a0bdda6618b/blake3-1.0.8-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:497ef8096ec4ac1ffba9a66152cee3992337cebf8ea434331d8fd9ce5423d227", size = 549550, upload-time = "2025-10-14T06:46:35.23Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/65/1859fddfabc1cc72548c2269d988819aad96d854e25eae00531517925901/blake3-1.0.8-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:511133bab85ff60ed143424ce484d08c60894ff7323f685d7a6095f43f0c85c3", size = 553805, upload-time = "2025-10-14T06:46:36.532Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/c7/2969352017f62378e388bb07bb2191bc9a953f818dc1cd6b9dd5c24916e1/blake3-1.0.8-cp313-cp313t-win32.whl", hash = "sha256:9c9fbdacfdeb68f7ca53bb5a7a5a593ec996eaf21155ad5b08d35e6f97e60877", size = 228068, upload-time = "2025-10-14T06:46:37.826Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/fc/923e25ac9cadfff1cd20038bcc0854d0f98061eb6bc78e42c43615f5982d/blake3-1.0.8-cp313-cp313t-win_amd64.whl", hash = "sha256:3cec94ed5676821cf371e9c9d25a41b4f3ebdb5724719b31b2749653b7cc1dfa", size = 215369, upload-time = "2025-10-14T06:46:39.054Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/2a/9f13ea01b03b1b4751a1cc2b6c1ef4b782e19433a59cf35b59cafb2a2696/blake3-1.0.8-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:2c33dac2c6112bc23f961a7ca305c7e34702c8177040eb98d0389d13a347b9e1", size = 347016, upload-time = "2025-10-14T06:46:40.318Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/8e/8458c4285fbc5de76414f243e4e0fcab795d71a8b75324e14959aee699da/blake3-1.0.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c445eff665d21c3b3b44f864f849a2225b1164c08654beb23224a02f087b7ff1", size = 324496, upload-time = "2025-10-14T06:46:42.355Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/fa/b913eb9cc4af708c03e01e6b88a8bb3a74833ba4ae4b16b87e2829198e06/blake3-1.0.8-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a47939f04b89c5c6ff1e51e883e5efab1ea1bf01a02f4d208d216dddd63d0dd8", size = 370654, upload-time = "2025-10-14T06:46:43.907Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/4f/245e0800c33b99c8f2b570d9a7199b51803694913ee4897f339648502933/blake3-1.0.8-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:73e0b4fa25f6e3078526a592fb38fca85ef204fd02eced6731e1cdd9396552d4", size = 374693, upload-time = "2025-10-14T06:46:45.186Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/a6/8cb182c8e482071dbdfcc6ec0048271fd48bcb78782d346119ff54993700/blake3-1.0.8-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b0543c57eb9d6dac9d4bced63e9f7f7b546886ac04cec8da3c3d9c8f30cbbb7", size = 447673, upload-time = "2025-10-14T06:46:46.358Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/b7/1cbbb5574d2a9436d1b15e7eb5b9d82e178adcaca71a97b0fddaca4bfe3a/blake3-1.0.8-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed972ebd553c0c25363459e9fc71a38c045d8419e365b59acd8cd791eff13981", size = 507233, upload-time = "2025-10-14T06:46:48.109Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/45/b55825d90af353b3e26c653bab278da9d6563afcf66736677f9397e465be/blake3-1.0.8-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3bafdec95dfffa3f6571e529644744e280337df15ddd9728f224ba70c5779b23", size = 393852, upload-time = "2025-10-14T06:46:49.511Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/73/9058a1a457dd20491d1b37de53d6876eff125e1520d9b2dd7d0acbc88de2/blake3-1.0.8-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d78f06f3fb838b34c330e2987090376145cbe5944d8608a0c4779c779618f7b", size = 386442, upload-time = "2025-10-14T06:46:51.205Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/6d/561d537ffc17985e276e08bf4513f1c106f1fdbef571e782604dc4e44070/blake3-1.0.8-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:dd03ff08d1b6e4fdda1cd03826f971ae8966ef6f683a8c68aa27fb21904b5aa9", size = 549929, upload-time = "2025-10-14T06:46:52.494Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/2f/dbe20d2c57f1a67c63be4ba310bcebc707b945c902a0bde075d2a8f5cd5c/blake3-1.0.8-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:4e02a3c499e35bf51fc15b2738aca1a76410804c877bcd914752cac4f71f052a", size = 553750, upload-time = "2025-10-14T06:46:54.194Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/da/c6cb712663c869b2814870c2798e57289c4268c5ac5fb12d467fce244860/blake3-1.0.8-cp314-cp314-win32.whl", hash = "sha256:a585357d5d8774aad9ffc12435de457f9e35cde55e0dc8bc43ab590a6929e59f", size = 228404, upload-time = "2025-10-14T06:46:56.807Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/b6/c7dcd8bc3094bba1c4274e432f9e77a7df703532ca000eaa550bd066b870/blake3-1.0.8-cp314-cp314-win_amd64.whl", hash = "sha256:9ab5998e2abd9754819753bc2f1cf3edf82d95402bff46aeef45ed392a5468bf", size = 215460, upload-time = "2025-10-14T06:46:58.15Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/3c/6c8afd856c353176836daa5cc33a7989e8f54569e9d53eb1c53fc8f80c34/blake3-1.0.8-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:e2df12f295f95a804338bd300e8fad4a6f54fd49bd4d9c5893855a230b5188a8", size = 347482, upload-time = "2025-10-14T06:47:00.189Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/35/92cd5501ce8e1f5cabdc0c3ac62d69fdb13ff0b60b62abbb2b6d0a53a790/blake3-1.0.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:63379be58438878eeb76ebe4f0efbeaabf42b79f2cff23b6126b7991588ced67", size = 324376, upload-time = "2025-10-14T06:47:01.413Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/33/503b37220a3e2e31917ef13722efd00055af51c5e88ae30974c733d7ece6/blake3-1.0.8-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88d527c247f9609dc1d45a08fd243e39f0d5300d54c57e048de24d4fa9240ebb", size = 370220, upload-time = "2025-10-14T06:47:02.573Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/df/fe817843adf59516c04d44387bd643b422a3b0400ea95c6ede6a49920737/blake3-1.0.8-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506a47897a11ebe8f3cdeb52f1365d6a2f83959e98ccb0c830f8f73277d4d358", size = 373454, upload-time = "2025-10-14T06:47:03.784Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/4d/90a2a623575373dfc9b683f1bad1bf017feafa5a6d65d94fb09543050740/blake3-1.0.8-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5122a61b3b004bbbd979bdf83a3aaab432da3e2a842d7ddf1c273f2503b4884", size = 447102, upload-time = "2025-10-14T06:47:04.958Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/ff/4e8ce314f60115c4c657b1fdbe9225b991da4f5bcc5d1c1f1d151e2f39d6/blake3-1.0.8-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0171e85d56dec1219abdae5f49a0ed12cb3f86a454c29160a64fd8a8166bba37", size = 506791, upload-time = "2025-10-14T06:47:06.82Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/88/2963a1f18aab52bdcf35379b2b48c34bbc462320c37e76960636b8602c36/blake3-1.0.8-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:003f61e8c41dd9931edddf1cc6a1bb680fb2ac0ad15493ef4a1df9adc59ce9df", size = 393717, upload-time = "2025-10-14T06:47:09.085Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/d1/a848ed8e8d4e236b9b16381768c9ae99d92890c24886bb4505aa9c3d2033/blake3-1.0.8-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2c3151955efb09ba58cd3e1263521e15e9e3866a40d6bd3556d86fc968e8f95", size = 386150, upload-time = "2025-10-14T06:47:10.363Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/09/e3eb5d60f97c01de23d9f434e6e1fc117efb466eaa1f6ddbbbcb62580d6e/blake3-1.0.8-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:5eb25bca3cee2e0dd746a214784fb36be6a43640c01c55b6b4e26196e72d076c", size = 549120, upload-time = "2025-10-14T06:47:11.713Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/ad/3d9661c710febb8957dd685fdb3e5a861aa0ac918eda3031365ce45789e2/blake3-1.0.8-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ab4e1dea4fa857944944db78e8f20d99ee2e16b2dea5a14f514fb0607753ac83", size = 553264, upload-time = "2025-10-14T06:47:13.317Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/55/e332a5b49edf377d0690e95951cca21a00c568f6e37315f9749efee52617/blake3-1.0.8-cp314-cp314t-win32.whl", hash = "sha256:67f1bc11bf59464ef092488c707b13dd4e872db36e25c453dfb6e0c7498df9f1", size = 228116, upload-time = "2025-10-14T06:47:14.516Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/5c/dbd00727a3dd165d7e0e8af40e630cd7e45d77b525a3218afaff8a87358e/blake3-1.0.8-cp314-cp314t-win_amd64.whl", hash = "sha256:421b99cdf1ff2d1bf703bc56c454f4b286fce68454dd8711abbcb5a0df90c19a", size = 215133, upload-time = "2025-10-14T06:47:16.069Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "1.17.1"
|
||||
|
|
@ -74,6 +160,15 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/e3/51/9b208e85196941db2f0654ad0357ca6388ab3ed67efdbfc799f35d1f83aa/colorlog-6.9.0-py3-none-any.whl", hash = "sha256:5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff", size = 11424, upload-time = "2024-10-29T18:34:49.815Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dnspython"
|
||||
version = "2.8.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "greenback"
|
||||
version = "1.2.1"
|
||||
|
|
@ -130,6 +225,18 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "importlib-metadata"
|
||||
version = "8.7.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "zipp" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.0.0"
|
||||
|
|
@ -139,6 +246,94 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892, upload-time = "2023-01-07T11:08:09.864Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mmh3"
|
||||
version = "5.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a7/af/f28c2c2f51f31abb4725f9a64bc7863d5f491f6539bd26aee2a1d21a649e/mmh3-5.2.0.tar.gz", hash = "sha256:1efc8fec8478e9243a78bb993422cf79f8ff85cb4cf6b79647480a31e0d950a8", size = 33582, upload-time = "2025-07-29T07:43:48.49Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/6a/d5aa7edb5c08e0bd24286c7d08341a0446f9a2fbbb97d96a8a6dd81935ee/mmh3-5.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:384eda9361a7bf83a85e09447e1feafe081034af9dd428893701b959230d84be", size = 56141, upload-time = "2025-07-29T07:42:13.456Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/49/131d0fae6447bc4a7299ebdb1a6fb9d08c9f8dcf97d75ea93e8152ddf7ab/mmh3-5.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c9da0d568569cc87315cb063486d761e38458b8ad513fedd3dc9263e1b81bcd", size = 40681, upload-time = "2025-07-29T07:42:14.306Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/6f/9221445a6bcc962b7f5ff3ba18ad55bba624bacdc7aa3fc0a518db7da8ec/mmh3-5.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86d1be5d63232e6eb93c50881aea55ff06eb86d8e08f9b5417c8c9b10db9db96", size = 40062, upload-time = "2025-07-29T07:42:15.08Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/d4/6bb2d0fef81401e0bb4c297d1eb568b767de4ce6fc00890bc14d7b51ecc4/mmh3-5.2.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bf7bee43e17e81671c447e9c83499f53d99bf440bc6d9dc26a841e21acfbe094", size = 97333, upload-time = "2025-07-29T07:42:16.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/e0/ccf0daff8134efbb4fbc10a945ab53302e358c4b016ada9bf97a6bdd50c1/mmh3-5.2.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7aa18cdb58983ee660c9c400b46272e14fa253c675ed963d3812487f8ca42037", size = 103310, upload-time = "2025-07-29T07:42:17.796Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/63/1965cb08a46533faca0e420e06aff8bbaf9690a6f0ac6ae6e5b2e4544687/mmh3-5.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9d032488fcec32d22be6542d1a836f00247f40f320844dbb361393b5b22773", size = 106178, upload-time = "2025-07-29T07:42:19.281Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/41/c883ad8e2c234013f27f92061200afc11554ea55edd1bcf5e1accd803a85/mmh3-5.2.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1861fb6b1d0453ed7293200139c0a9011eeb1376632e048e3766945b13313c5", size = 113035, upload-time = "2025-07-29T07:42:20.356Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/b5/1ccade8b1fa625d634a18bab7bf08a87457e09d5ec8cf83ca07cbea9d400/mmh3-5.2.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:99bb6a4d809aa4e528ddfe2c85dd5239b78b9dd14be62cca0329db78505e7b50", size = 120784, upload-time = "2025-07-29T07:42:21.377Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/1c/919d9171fcbdcdab242e06394464ccf546f7d0f3b31e0d1e3a630398782e/mmh3-5.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1f8d8b627799f4e2fcc7c034fed8f5f24dc7724ff52f69838a3d6d15f1ad4765", size = 99137, upload-time = "2025-07-29T07:42:22.344Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/8a/1eebef5bd6633d36281d9fc83cf2e9ba1ba0e1a77dff92aacab83001cee4/mmh3-5.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b5995088dd7023d2d9f310a0c67de5a2b2e06a570ecfd00f9ff4ab94a67cde43", size = 98664, upload-time = "2025-07-29T07:42:23.269Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/41/a5d981563e2ee682b21fb65e29cc0f517a6734a02b581359edd67f9d0360/mmh3-5.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1a5f4d2e59d6bba8ef01b013c472741835ad961e7c28f50c82b27c57748744a4", size = 106459, upload-time = "2025-07-29T07:42:24.238Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/31/342494cd6ab792d81e083680875a2c50fa0c5df475ebf0b67784f13e4647/mmh3-5.2.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fd6e6c3d90660d085f7e73710eab6f5545d4854b81b0135a3526e797009dbda3", size = 110038, upload-time = "2025-07-29T07:42:25.629Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/44/efda282170a46bb4f19c3e2b90536513b1d821c414c28469a227ca5a1789/mmh3-5.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c4a2f3d83879e3de2eb8cbf562e71563a8ed15ee9b9c2e77ca5d9f73072ac15c", size = 97545, upload-time = "2025-07-29T07:42:27.04Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/8f/534ae319c6e05d714f437e7206f78c17e66daca88164dff70286b0e8ea0c/mmh3-5.2.0-cp312-cp312-win32.whl", hash = "sha256:2421b9d665a0b1ad724ec7332fb5a98d075f50bc51a6ff854f3a1882bd650d49", size = 40805, upload-time = "2025-07-29T07:42:28.032Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/f6/f6abdcfefcedab3c964868048cfe472764ed358c2bf6819a70dd4ed4ed3a/mmh3-5.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:72d80005b7634a3a2220f81fbeb94775ebd12794623bb2e1451701ea732b4aa3", size = 41597, upload-time = "2025-07-29T07:42:28.894Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/fd/f7420e8cbce45c259c770cac5718badf907b302d3a99ec587ba5ce030237/mmh3-5.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:3d6bfd9662a20c054bc216f861fa330c2dac7c81e7fb8307b5e32ab5b9b4d2e0", size = 39350, upload-time = "2025-07-29T07:42:29.794Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/fa/27f6ab93995ef6ad9f940e96593c5dd24744d61a7389532b0fec03745607/mmh3-5.2.0-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:e79c00eba78f7258e5b354eccd4d7907d60317ced924ea4a5f2e9d83f5453065", size = 40874, upload-time = "2025-07-29T07:42:30.662Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/9c/03d13bcb6a03438bc8cac3d2e50f80908d159b31a4367c2e1a7a077ded32/mmh3-5.2.0-cp313-cp313-android_21_x86_64.whl", hash = "sha256:956127e663d05edbeec54df38885d943dfa27406594c411139690485128525de", size = 42012, upload-time = "2025-07-29T07:42:31.539Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/78/0865d9765408a7d504f1789944e678f74e0888b96a766d578cb80b040999/mmh3-5.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:c3dca4cb5b946ee91b3d6bb700d137b1cd85c20827f89fdf9c16258253489044", size = 39197, upload-time = "2025-07-29T07:42:32.374Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/12/76c3207bd186f98b908b6706c2317abb73756d23a4e68ea2bc94825b9015/mmh3-5.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:e651e17bfde5840e9e4174b01e9e080ce49277b70d424308b36a7969d0d1af73", size = 39840, upload-time = "2025-07-29T07:42:33.227Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/0d/574b6cce5555c9f2b31ea189ad44986755eb14e8862db28c8b834b8b64dc/mmh3-5.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:9f64bf06f4bf623325fda3a6d02d36cd69199b9ace99b04bb2d7fd9f89688504", size = 40644, upload-time = "2025-07-29T07:42:34.099Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/82/3731f8640b79c46707f53ed72034a58baad400be908c87b0088f1f89f986/mmh3-5.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ddc63328889bcaee77b743309e5c7d2d52cee0d7d577837c91b6e7cc9e755e0b", size = 56153, upload-time = "2025-07-29T07:42:35.031Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/34/e02dca1d4727fd9fdeaff9e2ad6983e1552804ce1d92cc796e5b052159bb/mmh3-5.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bb0fdc451fb6d86d81ab8f23d881b8d6e37fc373a2deae1c02d27002d2ad7a05", size = 40684, upload-time = "2025-07-29T07:42:35.914Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/36/3dee40767356e104967e6ed6d102ba47b0b1ce2a89432239b95a94de1b89/mmh3-5.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b29044e1ffdb84fe164d0a7ea05c7316afea93c00f8ed9449cf357c36fc4f814", size = 40057, upload-time = "2025-07-29T07:42:36.755Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/58/228c402fccf76eb39a0a01b8fc470fecf21965584e66453b477050ee0e99/mmh3-5.2.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:58981d6ea9646dbbf9e59a30890cbf9f610df0e4a57dbfe09215116fd90b0093", size = 97344, upload-time = "2025-07-29T07:42:37.675Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/82/fc5ce89006389a6426ef28e326fc065b0fbaaed230373b62d14c889f47ea/mmh3-5.2.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7e5634565367b6d98dc4aa2983703526ef556b3688ba3065edb4b9b90ede1c54", size = 103325, upload-time = "2025-07-29T07:42:38.591Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/8c/261e85777c6aee1ebd53f2f17e210e7481d5b0846cd0b4a5c45f1e3761b8/mmh3-5.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0271ac12415afd3171ab9a3c7cbfc71dee2c68760a7dc9d05bf8ed6ddfa3a7a", size = 106240, upload-time = "2025-07-29T07:42:39.563Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/73/2f76b3ad8a3d431824e9934403df36c0ddacc7831acf82114bce3c4309c8/mmh3-5.2.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:45b590e31bc552c6f8e2150ff1ad0c28dd151e9f87589e7eaf508fbdd8e8e908", size = 113060, upload-time = "2025-07-29T07:42:40.585Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/b9/7ea61a34e90e50a79a9d87aa1c0b8139a7eaf4125782b34b7d7383472633/mmh3-5.2.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bdde97310d59604f2a9119322f61b31546748499a21b44f6715e8ced9308a6c5", size = 120781, upload-time = "2025-07-29T07:42:41.618Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/5b/ae1a717db98c7894a37aeedbd94b3f99e6472a836488f36b6849d003485b/mmh3-5.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc9c5f280438cf1c1a8f9abb87dc8ce9630a964120cfb5dd50d1e7ce79690c7a", size = 99174, upload-time = "2025-07-29T07:42:42.587Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/de/000cce1d799fceebb6d4487ae29175dd8e81b48e314cba7b4da90bcf55d7/mmh3-5.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c903e71fd8debb35ad2a4184c1316b3cb22f64ce517b4e6747f25b0a34e41266", size = 98734, upload-time = "2025-07-29T07:42:43.996Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/19/0dc364391a792b72fbb22becfdeacc5add85cc043cd16986e82152141883/mmh3-5.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:eed4bba7ff8a0d37106ba931ab03bdd3915fbb025bcf4e1f0aa02bc8114960c5", size = 106493, upload-time = "2025-07-29T07:42:45.07Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/b1/bc8c28e4d6e807bbb051fefe78e1156d7f104b89948742ad310612ce240d/mmh3-5.2.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:1fdb36b940e9261aff0b5177c5b74a36936b902f473180f6c15bde26143681a9", size = 110089, upload-time = "2025-07-29T07:42:46.122Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/a2/d20f3f5c95e9c511806686c70d0a15479cc3941c5f322061697af1c1ff70/mmh3-5.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7303aab41e97adcf010a09efd8f1403e719e59b7705d5e3cfed3dd7571589290", size = 97571, upload-time = "2025-07-29T07:42:47.18Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/23/665296fce4f33488deec39a750ffd245cfc07aafb0e3ef37835f91775d14/mmh3-5.2.0-cp313-cp313-win32.whl", hash = "sha256:03e08c6ebaf666ec1e3d6ea657a2d363bb01effd1a9acfe41f9197decaef0051", size = 40806, upload-time = "2025-07-29T07:42:48.166Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/b0/92e7103f3b20646e255b699e2d0327ce53a3f250e44367a99dc8be0b7c7a/mmh3-5.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:7fddccd4113e7b736706e17a239a696332360cbaddf25ae75b57ba1acce65081", size = 41600, upload-time = "2025-07-29T07:42:49.371Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/22/0b2bd679a84574647de538c5b07ccaa435dbccc37815067fe15b90fe8dad/mmh3-5.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa0c966ee727aad5406d516375593c5f058c766b21236ab8985693934bb5085b", size = 39349, upload-time = "2025-07-29T07:42:50.268Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/ca/a20db059a8a47048aaf550da14a145b56e9c7386fb8280d3ce2962dcebf7/mmh3-5.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:e5015f0bb6eb50008bed2d4b1ce0f2a294698a926111e4bb202c0987b4f89078", size = 39209, upload-time = "2025-07-29T07:42:51.559Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/dd/e5094799d55c7482d814b979a0fd608027d0af1b274bfb4c3ea3e950bfd5/mmh3-5.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:e0f3ed828d709f5b82d8bfe14f8856120718ec4bd44a5b26102c3030a1e12501", size = 39843, upload-time = "2025-07-29T07:42:52.536Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/6b/7844d7f832c85400e7cc89a1348e4e1fdd38c5a38415bb5726bbb8fcdb6c/mmh3-5.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:f35727c5118aba95f0397e18a1a5b8405425581bfe53e821f0fb444cbdc2bc9b", size = 40648, upload-time = "2025-07-29T07:42:53.392Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/bf/71f791f48a21ff3190ba5225807cbe4f7223360e96862c376e6e3fb7efa7/mmh3-5.2.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bc244802ccab5220008cb712ca1508cb6a12f0eb64ad62997156410579a1770", size = 56164, upload-time = "2025-07-29T07:42:54.267Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/1f/f87e3d34d83032b4f3f0f528c6d95a98290fcacf019da61343a49dccfd51/mmh3-5.2.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ff3d50dc3fe8a98059f99b445dfb62792b5d006c5e0b8f03c6de2813b8376110", size = 40692, upload-time = "2025-07-29T07:42:55.234Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/e2/db849eaed07117086f3452feca8c839d30d38b830ac59fe1ce65af8be5ad/mmh3-5.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:37a358cc881fe796e099c1db6ce07ff757f088827b4e8467ac52b7a7ffdca647", size = 40068, upload-time = "2025-07-29T07:42:56.158Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/6b/209af927207af77425b044e32f77f49105a0b05d82ff88af6971d8da4e19/mmh3-5.2.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b9a87025121d1c448f24f27ff53a5fe7b6ef980574b4a4f11acaabe702420d63", size = 97367, upload-time = "2025-07-29T07:42:57.037Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/e0/78adf4104c425606a9ce33fb351f790c76a6c2314969c4a517d1ffc92196/mmh3-5.2.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ba55d6ca32eeef8b2625e1e4bfc3b3db52bc63014bd7e5df8cc11bf2b036b12", size = 103306, upload-time = "2025-07-29T07:42:58.522Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/79/c2b89f91b962658b890104745b1b6c9ce38d50a889f000b469b91eeb1b9e/mmh3-5.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9ff37ba9f15637e424c2ab57a1a590c52897c845b768e4e0a4958084ec87f22", size = 106312, upload-time = "2025-07-29T07:42:59.552Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/14/659d4095528b1a209be90934778c5ffe312177d51e365ddcbca2cac2ec7c/mmh3-5.2.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a094319ec0db52a04af9fdc391b4d39a1bc72bc8424b47c4411afb05413a44b5", size = 113135, upload-time = "2025-07-29T07:43:00.745Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/6f/cd7734a779389a8a467b5c89a48ff476d6f2576e78216a37551a97e9e42a/mmh3-5.2.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c5584061fd3da584659b13587f26c6cad25a096246a481636d64375d0c1f6c07", size = 120775, upload-time = "2025-07-29T07:43:02.124Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/ca/8256e3b96944408940de3f9291d7e38a283b5761fe9614d4808fcf27bd62/mmh3-5.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecbfc0437ddfdced5e7822d1ce4855c9c64f46819d0fdc4482c53f56c707b935", size = 99178, upload-time = "2025-07-29T07:43:03.182Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/32/39e2b3cf06b6e2eb042c984dab8680841ac2a0d3ca6e0bea30db1f27b565/mmh3-5.2.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:7b986d506a8e8ea345791897ba5d8ba0d9d8820cd4fc3e52dbe6de19388de2e7", size = 98738, upload-time = "2025-07-29T07:43:04.207Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/d3/7bbc8e0e8cf65ebbe1b893ffa0467b7ecd1bd07c3bbf6c9db4308ada22ec/mmh3-5.2.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:38d899a156549da8ef6a9f1d6f7ef231228d29f8f69bce2ee12f5fba6d6fd7c5", size = 106510, upload-time = "2025-07-29T07:43:05.656Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/99/b97e53724b52374e2f3859046f0eb2425192da356cb19784d64bc17bb1cf/mmh3-5.2.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d86651fa45799530885ba4dab3d21144486ed15285e8784181a0ab37a4552384", size = 110053, upload-time = "2025-07-29T07:43:07.204Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/62/3688c7d975ed195155671df68788c83fed6f7909b6ec4951724c6860cb97/mmh3-5.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c463d7c1c4cfc9d751efeaadd936bbba07b5b0ed81a012b3a9f5a12f0872bd6e", size = 97546, upload-time = "2025-07-29T07:43:08.226Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/3b/c6153250f03f71a8b7634cded82939546cdfba02e32f124ff51d52c6f991/mmh3-5.2.0-cp314-cp314-win32.whl", hash = "sha256:bb4fe46bdc6104fbc28db7a6bacb115ee6368ff993366bbd8a2a7f0076e6f0c0", size = 41422, upload-time = "2025-07-29T07:43:09.216Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/01/a27d98bab083a435c4c07e9d1d720d4c8a578bf4c270bae373760b1022be/mmh3-5.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:7c7f0b342fd06044bedd0b6e72177ddc0076f54fd89ee239447f8b271d919d9b", size = 42135, upload-time = "2025-07-29T07:43:10.183Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/c9/dbba5507e95429b8b380e2ba091eff5c20a70a59560934dff0ad8392b8c8/mmh3-5.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:3193752fc05ea72366c2b63ff24b9a190f422e32d75fdeae71087c08fff26115", size = 39879, upload-time = "2025-07-29T07:43:11.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/d1/c8c0ef839c17258b9de41b84f663574fabcf8ac2007b7416575e0f65ff6e/mmh3-5.2.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:69fc339d7202bea69ef9bd7c39bfdf9fdabc8e6822a01eba62fb43233c1b3932", size = 57696, upload-time = "2025-07-29T07:43:11.989Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/55/95e2b9ff201e89f9fe37036037ab61a6c941942b25cdb7b6a9df9b931993/mmh3-5.2.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:12da42c0a55c9d86ab566395324213c319c73ecb0c239fad4726324212b9441c", size = 41421, upload-time = "2025-07-29T07:43:13.269Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/79/9be23ad0b7001a4b22752e7693be232428ecc0a35068a4ff5c2f14ef8b20/mmh3-5.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f7f9034c7cf05ddfaac8d7a2e63a3c97a840d4615d0a0e65ba8bdf6f8576e3be", size = 40853, upload-time = "2025-07-29T07:43:14.888Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/1b/96b32058eda1c1dee8264900c37c359a7325c1f11f5ff14fd2be8e24eff9/mmh3-5.2.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:11730eeb16dfcf9674fdea9bb6b8e6dd9b40813b7eb839bc35113649eef38aeb", size = 109694, upload-time = "2025-07-29T07:43:15.816Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/6f/a2ae44cd7dad697b6dea48390cbc977b1e5ca58fda09628cbcb2275af064/mmh3-5.2.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:932a6eec1d2e2c3c9e630d10f7128d80e70e2d47fe6b8c7ea5e1afbd98733e65", size = 117438, upload-time = "2025-07-29T07:43:16.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/08/bfb75451c83f05224a28afeaf3950c7b793c0b71440d571f8e819cfb149a/mmh3-5.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ca975c51c5028947bbcfc24966517aac06a01d6c921e30f7c5383c195f87991", size = 120409, upload-time = "2025-07-29T07:43:18.207Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/ea/8b118b69b2ff8df568f742387d1a159bc654a0f78741b31437dd047ea28e/mmh3-5.2.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5b0b58215befe0f0e120b828f7645e97719bbba9f23b69e268ed0ac7adde8645", size = 125909, upload-time = "2025-07-29T07:43:19.39Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/11/168cc0b6a30650032e351a3b89b8a47382da541993a03af91e1ba2501234/mmh3-5.2.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29c2b9ce61886809d0492a274a5a53047742dea0f703f9c4d5d223c3ea6377d3", size = 135331, upload-time = "2025-07-29T07:43:20.435Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/05/e3a9849b1c18a7934c64e831492c99e67daebe84a8c2f2c39a7096a830e3/mmh3-5.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a367d4741ac0103f8198c82f429bccb9359f543ca542b06a51f4f0332e8de279", size = 110085, upload-time = "2025-07-29T07:43:21.92Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/d5/a96bcc306e3404601418b2a9a370baec92af84204528ba659fdfe34c242f/mmh3-5.2.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5a5dba98e514fb26241868f6eb90a7f7ca0e039aed779342965ce24ea32ba513", size = 111195, upload-time = "2025-07-29T07:43:23.066Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/29/0fd49801fec5bff37198684e0849b58e0dab3a2a68382a357cfffb0fafc3/mmh3-5.2.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:941603bfd75a46023807511c1ac2f1b0f39cccc393c15039969806063b27e6db", size = 116919, upload-time = "2025-07-29T07:43:24.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/04/4f3c32b0a2ed762edca45d8b46568fc3668e34f00fb1e0a3b5451ec1281c/mmh3-5.2.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:132dd943451a7c7546978863d2f5a64977928410782e1a87d583cb60eb89e667", size = 123160, upload-time = "2025-07-29T07:43:25.26Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/76/3d29eaa38821730633d6a240d36fa8ad2807e9dfd432c12e1a472ed211eb/mmh3-5.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f698733a8a494466432d611a8f0d1e026f5286dee051beea4b3c3146817e35d5", size = 110206, upload-time = "2025-07-29T07:43:26.699Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/1c/ccf35892684d3a408202e296e56843743e0b4fb1629e59432ea88cdb3909/mmh3-5.2.0-cp314-cp314t-win32.whl", hash = "sha256:6d541038b3fc360ec538fc116de87462627944765a6750308118f8b509a8eec7", size = 41970, upload-time = "2025-07-29T07:43:27.666Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/b2/b9e4f1e5adb5e21eb104588fcee2cd1eaa8308255173481427d5ecc4284e/mmh3-5.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e912b19cf2378f2967d0c08e86ff4c6c360129887f678e27e4dde970d21b3f4d", size = 43063, upload-time = "2025-07-29T07:43:28.582Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/fc/0e61d9a4e29c8679356795a40e48f647b4aad58d71bfc969f0f8f56fb912/mmh3-5.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:e7884931fe5e788163e7b3c511614130c2c59feffdc21112290a194487efb2e9", size = 40455, upload-time = "2025-07-29T07:43:29.563Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "morphys"
|
||||
version = "1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/4f/cb781d0ac5d079adabc77dc4f0bc99fc81c390029bd33c6e70552139e762/morphys-1.0-py2.py3-none-any.whl", hash = "sha256:76d6dbaa4d65f597e59d332c81da786d83e4669387b9b2a750cfec74e7beec20", size = 5618, upload-time = "2017-01-10T20:08:56.872Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "msgspec"
|
||||
version = "0.19.0"
|
||||
|
|
@ -161,6 +356,47 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/23/d8/f15b40611c2d5753d1abb0ca0da0c75348daf1252220e5dda2867bd81062/msgspec-0.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:317050bc0f7739cb30d257ff09152ca309bf5a369854bbf1e57dffc310c1f20f", size = 187432, upload-time = "2024-12-27T17:40:16.256Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "multiaddr"
|
||||
version = "0.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "base58" },
|
||||
{ name = "dnspython" },
|
||||
{ name = "idna" },
|
||||
{ name = "netaddr" },
|
||||
{ name = "psutil" },
|
||||
{ name = "py-cid" },
|
||||
{ name = "py-multibase" },
|
||||
{ name = "py-multicodec" },
|
||||
{ name = "py-multihash" },
|
||||
{ name = "trio" },
|
||||
{ name = "trio-typing" },
|
||||
{ name = "varint" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cf/5c/6d27f2b04c54e7c85b4a05760ac7dfaa7c68214b917e0d4a5043c01cf231/multiaddr-0.1.1.tar.gz", hash = "sha256:04da0afd2097625569073776526eb9733db9d9713286bada44632a9d7275b9bb", size = 54186, upload-time = "2025-12-07T17:19:07.996Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/07/ad/bd8a1748953a8f7a8167ec7d02eeefbed469595089d75e3fcb53047e7e20/multiaddr-0.1.1-py3-none-any.whl", hash = "sha256:d95333effddbd372009dbfce4d2ec922dd633697b6cc89a5af90ae082c4e68d4", size = 38374, upload-time = "2025-12-07T17:19:05.849Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mypy-extensions"
|
||||
version = "1.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "netaddr"
|
||||
version = "1.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/54/90/188b2a69654f27b221fba92fda7217778208532c962509e959a9cee5229d/netaddr-1.3.0.tar.gz", hash = "sha256:5c3c3d9895b551b763779ba7db7a03487dc1f8e3b385af819af341ae9ef6e48a", size = 2260504, upload-time = "2024-05-28T21:30:37.743Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/12/cc/f4fe2c7ce68b92cbf5b2d379ca366e1edae38cccaad00f69f529b460c3ef/netaddr-1.3.0-py3-none-any.whl", hash = "sha256:c2c6a8ebe5554ce33b7d5b3a306b71bbb373e000bbbf2350dd5213cc56e3dbbe", size = 2262023, upload-time = "2024-05-28T21:30:34.191Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "outcome"
|
||||
version = "1.3.0.post0"
|
||||
|
|
@ -262,6 +498,64 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "py-cid"
|
||||
version = "0.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "morphys" },
|
||||
{ name = "py-multibase" },
|
||||
{ name = "py-multicodec" },
|
||||
{ name = "py-multihash" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/96/8e/68c2bd0346247570e8e01e8c170a0237884e95cdfa43989527b71adaa978/py_cid-0.5.0.tar.gz", hash = "sha256:93c62586c672353a9862f3fce13c9848ea39a00378e0980e2f0eed91631f3d28", size = 38028, upload-time = "2026-02-13T19:03:28.603Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/18/eaea1571ae8b4fa490793a4b78a9641c4579a884f7a26f3d1b019d7e91c2/py_cid-0.5.0-py3-none-any.whl", hash = "sha256:2fbad437384534e2a0ab0c4068aac3e510c4cb710c89c8f6bf98f4b07ed54e3e", size = 16046, upload-time = "2026-02-13T19:03:27.516Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "py-multibase"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "morphys" },
|
||||
{ name = "python-baseconv" },
|
||||
{ name = "six" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bc/52/5ed393ab49df7e3b03995d3c4e53bae1e8c2ca40909cf25a41b346c09a38/py_multibase-2.0.0.tar.gz", hash = "sha256:58c1a264195fa1ae29ea707c6fc8196446f4bdb92e0f9a0f131e0f280b238839", size = 26857, upload-time = "2025-12-18T02:24:49.132Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/36/c7/38035079d9978b32b962f996f1cccaa166ecfe38723ab4349ab32166c037/py_multibase-2.0.0-py3-none-any.whl", hash = "sha256:b29ce489b556134e73998a11712c406b70950812955df64084754e0774e40900", size = 10608, upload-time = "2025-12-18T02:24:47.827Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "py-multicodec"
|
||||
version = "1.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "varint" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5e/26/ef24db0fbfec080b72c5ac4a1000da3a4d696a1e31862c695d683097a1b5/py_multicodec-1.0.0.tar.gz", hash = "sha256:78e4e3e47b6288cf635c3ca987152e6cb5510bdcdab307e7690c76ec3d5bbfeb", size = 44668, upload-time = "2025-12-18T20:41:37.976Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/76/da/768d07490faeae88ac361184164be9c262fececc3c6241b5fc471be4f659/py_multicodec-1.0.0-py3-none-any.whl", hash = "sha256:ae2e687bac8fdf54e3f5b3feded36b61a304d5e3c3af9438f7481f543ec15b8d", size = 26200, upload-time = "2025-12-18T20:41:37.055Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "py-multihash"
|
||||
version = "3.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "base58" },
|
||||
{ name = "blake3" },
|
||||
{ name = "mmh3" },
|
||||
{ name = "morphys" },
|
||||
{ name = "six" },
|
||||
{ name = "varint" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/11/3d/ed68b0eccd0654f7f3c163d9b3d428f903e5e3e884ab1f0d0a16ba6a4f11/py_multihash-3.0.0.tar.gz", hash = "sha256:2e848941de5ef0533ca26b81940e2ffcf7b4322a3f803e8c97f4f0eca8767aa7", size = 41630, upload-time = "2025-12-17T19:30:00.596Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/24/e2/d65606db8369916fb5a9b4fe14df7e6072970d919300f3fb1c989a1d8e7d/py_multihash-3.0.0-py3-none-any.whl", hash = "sha256:3863ec1313b4eac1e5169137c143d40bf77456e57388f839441deba089f87326", size = 21215, upload-time = "2025-12-17T19:29:59.322Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "2.22"
|
||||
|
|
@ -273,11 +567,11 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.2"
|
||||
version = "2.20.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -310,6 +604,12 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-baseconv"
|
||||
version = "1.2.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/33/d0/9297d7d8dd74767b4d5560d834b30b2fff17d39987c23ed8656f476e0d9b/python-baseconv-1.2.2.tar.gz", hash = "sha256:0539f8bd0464013b05ad62e0a1673f0ac9086c76b43ebf9f833053527cd9931b", size = 4929, upload-time = "2019-04-04T19:28:57.17Z" }
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.14.14"
|
||||
|
|
@ -336,6 +636,15 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.17.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
|
|
@ -384,6 +693,7 @@ dependencies = [
|
|||
{ name = "cffi" },
|
||||
{ name = "colorlog" },
|
||||
{ name = "msgspec" },
|
||||
{ name = "multiaddr" },
|
||||
{ name = "pdbp" },
|
||||
{ name = "platformdirs" },
|
||||
{ name = "tricycle" },
|
||||
|
|
@ -428,6 +738,7 @@ requires-dist = [
|
|||
{ name = "cffi", specifier = ">=1.17.1" },
|
||||
{ name = "colorlog", specifier = ">=6.8.2,<7" },
|
||||
{ name = "msgspec", specifier = ">=0.19.0" },
|
||||
{ name = "multiaddr", specifier = ">=0.1.1" },
|
||||
{ name = "pdbp", specifier = ">=1.8.2,<2" },
|
||||
{ name = "platformdirs", specifier = ">=4.4.0" },
|
||||
{ name = "tricycle", specifier = ">=0.4.1,<0.5" },
|
||||
|
|
@ -493,6 +804,23 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/c9/55/c4d9bea8b3d7937901958f65124123512419ab0eb73695e5f382521abbfb/trio-0.29.0-py3-none-any.whl", hash = "sha256:d8c463f1a9cc776ff63e331aba44c125f423a5a13c684307e828d930e625ba66", size = 492920, upload-time = "2025-02-14T07:13:48.696Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "trio-typing"
|
||||
version = "0.10.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "async-generator" },
|
||||
{ name = "importlib-metadata" },
|
||||
{ name = "mypy-extensions" },
|
||||
{ name = "packaging" },
|
||||
{ name = "trio" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b5/74/a87aafa40ec3a37089148b859892cbe2eef08d132c816d58a60459be5337/trio-typing-0.10.0.tar.gz", hash = "sha256:065ee684296d52a8ab0e2374666301aec36ee5747ac0e7a61f230250f8907ac3", size = 38747, upload-time = "2023-12-01T02:54:55.508Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/89/ff/9bd795273eb14fac7f6a59d16cc8c4d0948a619a1193d375437c7f50f3eb/trio_typing-0.10.0-py3-none-any.whl", hash = "sha256:6d0e7ec9d837a2fe03591031a172533fbf4a1a95baf369edebfc51d5a49f0264", size = 42224, upload-time = "2023-12-01T02:54:54.1Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.14.1"
|
||||
|
|
@ -502,6 +830,12 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "varint"
|
||||
version = "1.0.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a8/fe/1ea0ba0896dfa47186692655b86db3214c4b7c9e0e76c7b1dc257d101ab1/varint-1.0.2.tar.gz", hash = "sha256:a6ecc02377ac5ee9d65a6a8ad45c9ff1dac8ccee19400a5950fb51d594214ca5", size = 1886, upload-time = "2016-02-24T20:42:38.5Z" }
|
||||
|
||||
[[package]]
|
||||
name = "wcwidth"
|
||||
version = "0.2.13"
|
||||
|
|
@ -563,3 +897,12 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/2e/c2/3dd498dc28d8f89cdd52e39950c5e591499ae423f61694c0bb4d03ed1d82/xonsh-0.22.4-py312-none-any.whl", hash = "sha256:4e538fac9f4c3d866ddbdeca068f0c0515469c997ed58d3bfee963878c6df5a5", size = 654300, upload-time = "2026-02-17T07:53:35.813Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/7d/1f9c7147518e9f03f6ce081b5bfc4f1aceb6ec5caba849024d005e41d3be/xonsh-0.22.4-py313-none-any.whl", hash = "sha256:cc5fabf0ad0c56a2a11bed1e6a43c4ec6416a5b30f24f126b8e768547c3793e2", size = 654818, upload-time = "2026-02-17T07:53:33.477Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zipp"
|
||||
version = "3.23.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" },
|
||||
]
|
||||
|
|
|
|||
Loading…
Reference in New Issue