Compare commits
	
		
			32 Commits 
		
	
	
		
			24d21e7463
			...
			7b05547fcc
		
	
	| Author | SHA1 | Date | 
|---|---|---|
| 
							
							
								 | 
						7b05547fcc | |
| 
							
							
								 | 
						64f56e09b9 | |
| 
							
							
								 | 
						4185661c2e | |
| 
							
							
								 | 
						cc7bee683f | |
| 
							
							
								 | 
						2ed811fa0d | |
| 
							
							
								 | 
						b06c5bde49 | |
| 
							
							
								 | 
						f4c5a7e85c | |
| 
							
							
								 | 
						ee51226480 | |
| 
							
							
								 | 
						084d750604 | |
| 
							
							
								 | 
						de7ba6b1f4 | |
| 
							
							
								 | 
						145c44ce44 | |
| 
							
							
								 | 
						16dbf8fbca | |
| 
							
							
								 | 
						0d30b39e58 | |
| 
							
							
								 | 
						37c798dd43 | |
| 
							
							
								 | 
						8604c0c988 | |
| 
							
							
								 | 
						23845c4f83 | |
| 
							
							
								 | 
						9d67067d62 | |
| 
							
							
								 | 
						99d3455f57 | |
| 
							
							
								 | 
						dbdbd96a06 | |
| 
							
							
								 | 
						68f7614e3e | |
| 
							
							
								 | 
						659c85a099 | |
| 
							
							
								 | 
						48b6db5c68 | |
| 
							
							
								 | 
						029888cee8 | |
| 
							
							
								 | 
						223d885e22 | |
| 
							
							
								 | 
						a1f091882e | |
| 
							
							
								 | 
						e587f0da23 | |
| 
							
							
								 | 
						5138224625 | |
| 
							
							
								 | 
						ad72cd629f | |
| 
							
							
								 | 
						533e69baaf | |
| 
							
							
								 | 
						fbc9325184 | |
| 
							
							
								 | 
						3cd222959a | |
| 
							
							
								 | 
						2ea703cc75 | 
| 
						 | 
				
			
			@ -29,7 +29,7 @@ async def bp_then_error(
 | 
			
		|||
    to_trio.send_nowait('start')
 | 
			
		||||
 | 
			
		||||
    # NOTE: what happens here inside the hook needs some refinement..
 | 
			
		||||
    # => seems like it's still `._debug._set_trace()` but
 | 
			
		||||
    # => seems like it's still `.debug._set_trace()` but
 | 
			
		||||
    #    we set `Lock.local_task_in_debug = 'sync'`, we probably want
 | 
			
		||||
    #    some further, at least, meta-data about the task/actor in debug
 | 
			
		||||
    #    in terms of making it clear it's `asyncio` mucking about.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,6 +4,11 @@ import sys
 | 
			
		|||
import trio
 | 
			
		||||
import tractor
 | 
			
		||||
 | 
			
		||||
# ensure mod-path is correct!
 | 
			
		||||
from tractor.devx.debug import (
 | 
			
		||||
    _sync_pause_from_builtin as _sync_pause_from_builtin,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def main() -> None:
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -13,19 +18,23 @@ async def main() -> None:
 | 
			
		|||
 | 
			
		||||
    async with tractor.open_nursery(
 | 
			
		||||
        debug_mode=True,
 | 
			
		||||
    ) as an:
 | 
			
		||||
        assert an
 | 
			
		||||
        loglevel='devx',
 | 
			
		||||
        maybe_enable_greenback=True,
 | 
			
		||||
        # ^XXX REQUIRED to enable `breakpoint()` support (from sync
 | 
			
		||||
        # fns) and thus required here to avoid an assertion err
 | 
			
		||||
        # on the next line
 | 
			
		||||
    ):
 | 
			
		||||
        assert (
 | 
			
		||||
            (pybp_var := os.environ['PYTHONBREAKPOINT'])
 | 
			
		||||
            ==
 | 
			
		||||
            'tractor.devx._debug._sync_pause_from_builtin'
 | 
			
		||||
            'tractor.devx.debug._sync_pause_from_builtin'
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # TODO: an assert that verifies the hook has indeed been, hooked
 | 
			
		||||
        # XD
 | 
			
		||||
        assert (
 | 
			
		||||
            (pybp_hook := sys.breakpointhook)
 | 
			
		||||
            is not tractor.devx._debug._set_trace
 | 
			
		||||
            is not tractor.devx.debug._set_trace
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        print(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,7 @@ import tractor
 | 
			
		|||
 | 
			
		||||
# TODO: only import these when not running from test harness?
 | 
			
		||||
# can we detect `pexpect` usage maybe?
 | 
			
		||||
# from tractor.devx._debug import (
 | 
			
		||||
# from tractor.devx.debug import (
 | 
			
		||||
#     get_lock,
 | 
			
		||||
#     get_debug_req,
 | 
			
		||||
# )
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -61,6 +61,7 @@ dev = [
 | 
			
		|||
  # `tractor.devx` tooling
 | 
			
		||||
  "greenback>=1.2.1,<2",
 | 
			
		||||
  "stackscope>=0.2.2,<0.3",
 | 
			
		||||
  "typing-extensions>=4.13.2",  # needed for stackscope
 | 
			
		||||
  "pyperclip>=1.9.0",
 | 
			
		||||
  "prompt-toolkit>=3.0.50",
 | 
			
		||||
  "xonsh>=0.19.2",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,21 +6,22 @@ from __future__ import annotations
 | 
			
		|||
import sys
 | 
			
		||||
import subprocess
 | 
			
		||||
import os
 | 
			
		||||
import random
 | 
			
		||||
import signal
 | 
			
		||||
import platform
 | 
			
		||||
import time
 | 
			
		||||
 | 
			
		||||
import pytest
 | 
			
		||||
import tractor
 | 
			
		||||
from tractor._testing import (
 | 
			
		||||
    examples_dir as examples_dir,
 | 
			
		||||
    tractor_test as tractor_test,
 | 
			
		||||
    expect_ctxc as expect_ctxc,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
# TODO: include wtv plugin(s) we build in `._testing.pytest`?
 | 
			
		||||
pytest_plugins = ['pytester']
 | 
			
		||||
pytest_plugins: list[str] = [
 | 
			
		||||
    'pytester',
 | 
			
		||||
    'tractor._testing.pytest',
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Sending signal.SIGINT on subprocess fails on windows. Use CTRL_* alternatives
 | 
			
		||||
if platform.system() == 'Windows':
 | 
			
		||||
| 
						 | 
				
			
			@ -48,6 +49,9 @@ no_windows = pytest.mark.skipif(
 | 
			
		|||
def pytest_addoption(
 | 
			
		||||
    parser: pytest.Parser,
 | 
			
		||||
):
 | 
			
		||||
    # ?TODO? should this be exposed from our `._testing.pytest`
 | 
			
		||||
    # plugin or should we make it more explicit with `--tl` for
 | 
			
		||||
    # tractor logging like we do in other client projects?
 | 
			
		||||
    parser.addoption(
 | 
			
		||||
        "--ll",
 | 
			
		||||
        action="store",
 | 
			
		||||
| 
						 | 
				
			
			@ -55,54 +59,10 @@ def pytest_addoption(
 | 
			
		|||
        default='ERROR', help="logging level to set when testing"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    parser.addoption(
 | 
			
		||||
        "--spawn-backend",
 | 
			
		||||
        action="store",
 | 
			
		||||
        dest='spawn_backend',
 | 
			
		||||
        default='trio',
 | 
			
		||||
        help="Processing spawning backend to use for test run",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    parser.addoption(
 | 
			
		||||
        "--tpdb",
 | 
			
		||||
        "--debug-mode",
 | 
			
		||||
        action="store_true",
 | 
			
		||||
        dest='tractor_debug_mode',
 | 
			
		||||
        # default=False,
 | 
			
		||||
        help=(
 | 
			
		||||
            'Enable a flag that can be used by tests to to set the '
 | 
			
		||||
            '`debug_mode: bool` for engaging the internal '
 | 
			
		||||
            'multi-proc debugger sys.'
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # provide which IPC transport protocols opting-in test suites
 | 
			
		||||
    # should accumulatively run against.
 | 
			
		||||
    parser.addoption(
 | 
			
		||||
        "--tpt-proto",
 | 
			
		||||
        nargs='+',  # accumulate-multiple-args
 | 
			
		||||
        action="store",
 | 
			
		||||
        dest='tpt_protos',
 | 
			
		||||
        default=['tcp'],
 | 
			
		||||
        help="Transport protocol to use under the `tractor.ipc.Channel`",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def pytest_configure(config):
 | 
			
		||||
    backend = config.option.spawn_backend
 | 
			
		||||
    tractor._spawn.try_set_start_method(backend)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture(scope='session')
 | 
			
		||||
def debug_mode(request) -> bool:
 | 
			
		||||
    debug_mode: bool = request.config.option.tractor_debug_mode
 | 
			
		||||
    # if debug_mode:
 | 
			
		||||
    #     breakpoint()
 | 
			
		||||
    return debug_mode
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture(scope='session', autouse=True)
 | 
			
		||||
def loglevel(request):
 | 
			
		||||
    import tractor
 | 
			
		||||
    orig = tractor.log._default_loglevel
 | 
			
		||||
    level = tractor.log._default_loglevel = request.config.option.loglevel
 | 
			
		||||
    tractor.log.get_console_log(level)
 | 
			
		||||
| 
						 | 
				
			
			@ -110,49 +70,6 @@ def loglevel(request):
 | 
			
		|||
    tractor.log._default_loglevel = orig
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture(scope='session')
 | 
			
		||||
def spawn_backend(request) -> str:
 | 
			
		||||
    return request.config.option.spawn_backend
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture(scope='session')
 | 
			
		||||
def tpt_protos(request) -> list[str]:
 | 
			
		||||
 | 
			
		||||
    # allow quoting on CLI
 | 
			
		||||
    proto_keys: list[str] = [
 | 
			
		||||
        proto_key.replace('"', '').replace("'", "")
 | 
			
		||||
        for proto_key in request.config.option.tpt_protos
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    # ?TODO, eventually support multiple protos per test-sesh?
 | 
			
		||||
    if len(proto_keys) > 1:
 | 
			
		||||
        pytest.fail(
 | 
			
		||||
            'We only support one `--tpt-proto <key>` atm!\n'
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    # XXX ensure we support the protocol by name via lookup!
 | 
			
		||||
    for proto_key in proto_keys:
 | 
			
		||||
        addr_type = tractor._addr._address_types[proto_key]
 | 
			
		||||
        assert addr_type.proto_key == proto_key
 | 
			
		||||
 | 
			
		||||
    yield proto_keys
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture(
 | 
			
		||||
    scope='session',
 | 
			
		||||
    autouse=True,
 | 
			
		||||
)
 | 
			
		||||
def tpt_proto(
 | 
			
		||||
    tpt_protos: list[str],
 | 
			
		||||
) -> str:
 | 
			
		||||
    proto_key: str = tpt_protos[0]
 | 
			
		||||
    from tractor import _state
 | 
			
		||||
    if _state._def_tpt_proto != proto_key:
 | 
			
		||||
        _state._def_tpt_proto = proto_key
 | 
			
		||||
    # breakpoint()
 | 
			
		||||
    yield proto_key
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
_ci_env: bool = os.environ.get('CI', False)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -165,80 +82,6 @@ def ci_env() -> bool:
 | 
			
		|||
    return _ci_env
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# TODO: also move this to `._testing` for now?
 | 
			
		||||
# -[ ] possibly generalize and re-use for multi-tree spawning
 | 
			
		||||
#    along with the new stuff for multi-addrs?
 | 
			
		||||
#
 | 
			
		||||
# choose random port at import time
 | 
			
		||||
_rando_port: str = random.randint(1000, 9999)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture(scope='session')
 | 
			
		||||
def reg_addr(
 | 
			
		||||
    tpt_proto: str,
 | 
			
		||||
) -> tuple[str, int|str]:
 | 
			
		||||
 | 
			
		||||
    # globally override the runtime to the per-test-session-dynamic
 | 
			
		||||
    # addr so that all tests never conflict with any other actor
 | 
			
		||||
    # tree using the default.
 | 
			
		||||
    from tractor import (
 | 
			
		||||
        _addr,
 | 
			
		||||
    )
 | 
			
		||||
    addr_type = _addr._address_types[tpt_proto]
 | 
			
		||||
    def_reg_addr: tuple[str, int] = _addr._default_lo_addrs[tpt_proto]
 | 
			
		||||
 | 
			
		||||
    testrun_reg_addr: tuple[str, int]
 | 
			
		||||
    match tpt_proto:
 | 
			
		||||
        case 'tcp':
 | 
			
		||||
            testrun_reg_addr = (
 | 
			
		||||
                addr_type.def_bindspace,
 | 
			
		||||
                _rando_port,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        # NOTE, file-name uniqueness (no-collisions) will be based on
 | 
			
		||||
        # the runtime-directory and root (pytest-proc's) pid.
 | 
			
		||||
        case 'uds':
 | 
			
		||||
            testrun_reg_addr = addr_type.get_random().unwrap()
 | 
			
		||||
 | 
			
		||||
    assert def_reg_addr != testrun_reg_addr
 | 
			
		||||
    return testrun_reg_addr
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def pytest_generate_tests(metafunc):
 | 
			
		||||
    spawn_backend: str = metafunc.config.option.spawn_backend
 | 
			
		||||
 | 
			
		||||
    if not spawn_backend:
 | 
			
		||||
        # XXX some weird windows bug with `pytest`?
 | 
			
		||||
        spawn_backend = 'trio'
 | 
			
		||||
 | 
			
		||||
    # TODO: maybe just use the literal `._spawn.SpawnMethodKey`?
 | 
			
		||||
    assert spawn_backend in (
 | 
			
		||||
        'mp_spawn',
 | 
			
		||||
        'mp_forkserver',
 | 
			
		||||
        'trio',
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # NOTE: used-to-be-used-to dyanmically parametrize tests for when
 | 
			
		||||
    # you just passed --spawn-backend=`mp` on the cli, but now we expect
 | 
			
		||||
    # that cli input to be manually specified, BUT, maybe we'll do
 | 
			
		||||
    # something like this again in the future?
 | 
			
		||||
    if 'start_method' in metafunc.fixturenames:
 | 
			
		||||
        metafunc.parametrize(
 | 
			
		||||
            "start_method",
 | 
			
		||||
            [spawn_backend],
 | 
			
		||||
            scope='module',
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    # TODO, parametrize any `tpt_proto: str` declaring tests!
 | 
			
		||||
    # proto_tpts: list[str] = metafunc.config.option.proto_tpts
 | 
			
		||||
    # if 'tpt_proto' in metafunc.fixturenames:
 | 
			
		||||
    #     metafunc.parametrize(
 | 
			
		||||
    #         'tpt_proto',
 | 
			
		||||
    #         proto_tpts,  # TODO, double check this list usage!
 | 
			
		||||
    #         scope='module',
 | 
			
		||||
    #     )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def sig_prog(
 | 
			
		||||
    proc: subprocess.Popen,
 | 
			
		||||
    sig: int,
 | 
			
		||||
| 
						 | 
				
			
			@ -260,7 +103,7 @@ def sig_prog(
 | 
			
		|||
def daemon(
 | 
			
		||||
    debug_mode: bool,
 | 
			
		||||
    loglevel: str,
 | 
			
		||||
    testdir,
 | 
			
		||||
    testdir: pytest.Pytester,
 | 
			
		||||
    reg_addr: tuple[str, int],
 | 
			
		||||
    tpt_proto: str,
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,9 +2,11 @@
 | 
			
		|||
`tractor.devx.*` tooling sub-pkg test space.
 | 
			
		||||
 | 
			
		||||
'''
 | 
			
		||||
from __future__ import annotations
 | 
			
		||||
import time
 | 
			
		||||
from typing import (
 | 
			
		||||
    Callable,
 | 
			
		||||
    TYPE_CHECKING,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
import pytest
 | 
			
		||||
| 
						 | 
				
			
			@ -16,7 +18,7 @@ from pexpect.spawnbase import SpawnBase
 | 
			
		|||
from tractor._testing import (
 | 
			
		||||
    mk_cmd,
 | 
			
		||||
)
 | 
			
		||||
from tractor.devx._debug import (
 | 
			
		||||
from tractor.devx.debug import (
 | 
			
		||||
    _pause_msg as _pause_msg,
 | 
			
		||||
    _crash_msg as _crash_msg,
 | 
			
		||||
    _repl_fail_msg as _repl_fail_msg,
 | 
			
		||||
| 
						 | 
				
			
			@ -26,14 +28,22 @@ from ..conftest import (
 | 
			
		|||
    _ci_env,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
    from pexpect import pty_spawn
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# a fn that sub-instantiates a `pexpect.spawn()`
 | 
			
		||||
# and returns it.
 | 
			
		||||
type PexpectSpawner = Callable[[str], pty_spawn.spawn]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture
 | 
			
		||||
def spawn(
 | 
			
		||||
    start_method,
 | 
			
		||||
    start_method: str,
 | 
			
		||||
    testdir: pytest.Pytester,
 | 
			
		||||
    reg_addr: tuple[str, int],
 | 
			
		||||
 | 
			
		||||
) -> Callable[[str], None]:
 | 
			
		||||
) -> PexpectSpawner:
 | 
			
		||||
    '''
 | 
			
		||||
    Use the `pexpect` module shipped via `testdir.spawn()` to
 | 
			
		||||
    run an `./examples/..` script by name.
 | 
			
		||||
| 
						 | 
				
			
			@ -59,7 +69,7 @@ def spawn(
 | 
			
		|||
    def _spawn(
 | 
			
		||||
        cmd: str,
 | 
			
		||||
        **mkcmd_kwargs,
 | 
			
		||||
    ):
 | 
			
		||||
    ) -> pty_spawn.spawn:
 | 
			
		||||
        unset_colors()
 | 
			
		||||
        return testdir.spawn(
 | 
			
		||||
            cmd=mk_cmd(
 | 
			
		||||
| 
						 | 
				
			
			@ -73,7 +83,7 @@ def spawn(
 | 
			
		|||
        )
 | 
			
		||||
 | 
			
		||||
    # such that test-dep can pass input script name.
 | 
			
		||||
    return _spawn
 | 
			
		||||
    return _spawn  # the `PexpectSpawner`, type alias.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture(
 | 
			
		||||
| 
						 | 
				
			
			@ -111,7 +121,7 @@ def ctlc(
 | 
			
		|||
        # XXX: disable pygments highlighting for auto-tests
 | 
			
		||||
        # since some envs (like actions CI) will struggle
 | 
			
		||||
        # the the added color-char encoding..
 | 
			
		||||
        from tractor.devx._debug import TractorConfig
 | 
			
		||||
        from tractor.devx.debug import TractorConfig
 | 
			
		||||
        TractorConfig.use_pygements = False
 | 
			
		||||
 | 
			
		||||
    yield use_ctlc
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -528,7 +528,7 @@ def test_multi_daemon_subactors(
 | 
			
		|||
    # now the root actor won't clobber the bp_forever child
 | 
			
		||||
    # during it's first access to the debug lock, but will instead
 | 
			
		||||
    # wait for the lock to release, by the edge triggered
 | 
			
		||||
    # ``devx._debug.Lock.no_remote_has_tty`` event before sending cancel messages
 | 
			
		||||
    # ``devx.debug.Lock.no_remote_has_tty`` event before sending cancel messages
 | 
			
		||||
    # (via portals) to its underlings B)
 | 
			
		||||
 | 
			
		||||
    # at some point here there should have been some warning msg from
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,9 +13,13 @@ TODO:
 | 
			
		|||
  when debugging a problem inside the stack vs. in their app.
 | 
			
		||||
 | 
			
		||||
'''
 | 
			
		||||
from __future__ import annotations
 | 
			
		||||
import os
 | 
			
		||||
import signal
 | 
			
		||||
import time
 | 
			
		||||
from typing import (
 | 
			
		||||
    TYPE_CHECKING,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
from .conftest import (
 | 
			
		||||
    expect,
 | 
			
		||||
| 
						 | 
				
			
			@ -29,9 +33,12 @@ from pexpect.exceptions import (
 | 
			
		|||
    EOF,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
    from ..conftest import PexpectSpawner
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_shield_pause(
 | 
			
		||||
    spawn,
 | 
			
		||||
    spawn: PexpectSpawner,
 | 
			
		||||
):
 | 
			
		||||
    '''
 | 
			
		||||
    Verify the `tractor.pause()/.post_mortem()` API works inside an
 | 
			
		||||
| 
						 | 
				
			
			@ -126,7 +133,7 @@ def test_shield_pause(
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
def test_breakpoint_hook_restored(
 | 
			
		||||
    spawn,
 | 
			
		||||
    spawn: PexpectSpawner,
 | 
			
		||||
):
 | 
			
		||||
    '''
 | 
			
		||||
    Ensures our actor runtime sets a custom `breakpoint()` hook
 | 
			
		||||
| 
						 | 
				
			
			@ -140,16 +147,22 @@ def test_breakpoint_hook_restored(
 | 
			
		|||
    child = spawn('restore_builtin_breakpoint')
 | 
			
		||||
 | 
			
		||||
    child.expect(PROMPT)
 | 
			
		||||
    assert_before(
 | 
			
		||||
        child,
 | 
			
		||||
        [
 | 
			
		||||
            _pause_msg,
 | 
			
		||||
            "<Task '__main__.main'",
 | 
			
		||||
            "('root'",
 | 
			
		||||
            "first bp, tractor hook set",
 | 
			
		||||
        ]
 | 
			
		||||
    )
 | 
			
		||||
    child.sendline('c')
 | 
			
		||||
    try:
 | 
			
		||||
        assert_before(
 | 
			
		||||
            child,
 | 
			
		||||
            [
 | 
			
		||||
                _pause_msg,
 | 
			
		||||
                "<Task '__main__.main'",
 | 
			
		||||
                "('root'",
 | 
			
		||||
                "first bp, tractor hook set",
 | 
			
		||||
            ]
 | 
			
		||||
        )
 | 
			
		||||
    # XXX if the above raises `AssertionError`, without sending
 | 
			
		||||
    # the final 'continue' cmd to the REPL-active sub-process,
 | 
			
		||||
    # we'll hang waiting for that pexpect instance to terminate..
 | 
			
		||||
    finally:
 | 
			
		||||
        child.sendline('c')
 | 
			
		||||
 | 
			
		||||
    child.expect(PROMPT)
 | 
			
		||||
    assert_before(
 | 
			
		||||
        child,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,4 @@
 | 
			
		|||
'''
 | 
			
		||||
`tractor.ipc` subsystem(s)/unit testing suites.
 | 
			
		||||
 | 
			
		||||
'''
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,72 @@
 | 
			
		|||
'''
 | 
			
		||||
High-level `.ipc._server` unit tests.
 | 
			
		||||
 | 
			
		||||
'''
 | 
			
		||||
from __future__ import annotations
 | 
			
		||||
 | 
			
		||||
import pytest
 | 
			
		||||
import trio
 | 
			
		||||
from tractor import (
 | 
			
		||||
    devx,
 | 
			
		||||
    ipc,
 | 
			
		||||
    log,
 | 
			
		||||
)
 | 
			
		||||
from tractor._testing.addr import (
 | 
			
		||||
    get_rando_addr,
 | 
			
		||||
)
 | 
			
		||||
# TODO, use/check-roundtripping with some of these wrapper types?
 | 
			
		||||
#
 | 
			
		||||
# from .._addr import Address
 | 
			
		||||
# from ._chan import Channel
 | 
			
		||||
# from ._transport import MsgTransport
 | 
			
		||||
# from ._uds import UDSAddress
 | 
			
		||||
# from ._tcp import TCPAddress
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.parametrize(
 | 
			
		||||
    '_tpt_proto',
 | 
			
		||||
    ['uds', 'tcp']
 | 
			
		||||
)
 | 
			
		||||
def test_basic_ipc_server(
 | 
			
		||||
    _tpt_proto: str,
 | 
			
		||||
    debug_mode: bool,
 | 
			
		||||
    loglevel: str,
 | 
			
		||||
):
 | 
			
		||||
 | 
			
		||||
    # so we see the socket-listener reporting on console
 | 
			
		||||
    log.get_console_log("INFO")
 | 
			
		||||
 | 
			
		||||
    rando_addr: tuple = get_rando_addr(
 | 
			
		||||
        tpt_proto=_tpt_proto,
 | 
			
		||||
    )
 | 
			
		||||
    async def main():
 | 
			
		||||
        async with ipc._server.open_ipc_server() as server:
 | 
			
		||||
 | 
			
		||||
            assert (
 | 
			
		||||
                server._parent_tn
 | 
			
		||||
                and
 | 
			
		||||
                server._parent_tn is server._stream_handler_tn
 | 
			
		||||
            )
 | 
			
		||||
            assert server._no_more_peers.is_set()
 | 
			
		||||
 | 
			
		||||
            eps: list[ipc.IPCEndpoint] = await server.listen_on(
 | 
			
		||||
                accept_addrs=[rando_addr],
 | 
			
		||||
                stream_handler_nursery=None,
 | 
			
		||||
            )
 | 
			
		||||
            assert (
 | 
			
		||||
                len(eps) == 1
 | 
			
		||||
                and
 | 
			
		||||
                (ep := eps[0])._listener
 | 
			
		||||
                and
 | 
			
		||||
                not ep.peer_tpts
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            server._parent_tn.cancel_scope.cancel()
 | 
			
		||||
 | 
			
		||||
        # !TODO! actually make a bg-task connection from a client
 | 
			
		||||
        # using `ipc._chan._connect_chan()`
 | 
			
		||||
 | 
			
		||||
    with devx.maybe_open_crash_handler(
 | 
			
		||||
        pdb=debug_mode,
 | 
			
		||||
    ):
 | 
			
		||||
        trio.run(main)
 | 
			
		||||
| 
						 | 
				
			
			@ -889,7 +889,7 @@ async def manage_file(
 | 
			
		|||
 | 
			
		||||
        # NOTE: turns out you don't even need to sched an aio task
 | 
			
		||||
        # since the original issue, even though seemingly was due to
 | 
			
		||||
        # the guest-run being abandoned + a `._debug.pause()` inside
 | 
			
		||||
        # the guest-run being abandoned + a `.debug.pause()` inside
 | 
			
		||||
        # `._runtime._async_main()` (which was originally trying to
 | 
			
		||||
        # debug the `.lifetime_stack` not closing), IS NOT actually
 | 
			
		||||
        # the core issue?
 | 
			
		||||
| 
						 | 
				
			
			@ -1101,7 +1101,7 @@ def test_sigint_closes_lifetime_stack(
 | 
			
		|||
#    => completed using `.bestow_portal(task)` inside
 | 
			
		||||
#     `.to_asyncio._run_asyncio_task()` right?
 | 
			
		||||
#   -[ ] translation func to get from `asyncio` task calling to 
 | 
			
		||||
#     `._debug.wait_for_parent_stdin_hijack()` which does root
 | 
			
		||||
#     `.debug.wait_for_parent_stdin_hijack()` which does root
 | 
			
		||||
#     call to do TTY locking.
 | 
			
		||||
#
 | 
			
		||||
def test_sync_breakpoint():
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -83,3 +83,26 @@ def test_implicit_root_via_first_nursery(
 | 
			
		|||
            assert tractor.current_actor().aid.name == 'root'
 | 
			
		||||
 | 
			
		||||
    trio.run(main)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_runtime_vars_unset(
 | 
			
		||||
    reg_addr: tuple,
 | 
			
		||||
    debug_mode: bool
 | 
			
		||||
):
 | 
			
		||||
    '''
 | 
			
		||||
    Ensure any `._state._runtime_vars` are restored to default values
 | 
			
		||||
    after the root actor-runtime exits!
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
    assert not tractor._state._runtime_vars['_debug_mode']
 | 
			
		||||
    async def main():
 | 
			
		||||
        assert not tractor._state._runtime_vars['_debug_mode']
 | 
			
		||||
        async with tractor.open_nursery(
 | 
			
		||||
            debug_mode=True,
 | 
			
		||||
        ):
 | 
			
		||||
            assert tractor._state._runtime_vars['_debug_mode']
 | 
			
		||||
 | 
			
		||||
        # after runtime closure, should be reverted!
 | 
			
		||||
        assert not tractor._state._runtime_vars['_debug_mode']
 | 
			
		||||
 | 
			
		||||
    trio.run(main)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -292,7 +292,7 @@ class Context:
 | 
			
		|||
    # - `._runtime._invoke()` will check this flag before engaging
 | 
			
		||||
    #   the crash handler REPL in such cases where the "callee"
 | 
			
		||||
    #   raises the cancellation,
 | 
			
		||||
    # - `.devx._debug.lock_stdio_for_peer()` will set it to `False` if
 | 
			
		||||
    # - `.devx.debug.lock_stdio_for_peer()` will set it to `False` if
 | 
			
		||||
    #   the global tty-lock has been configured to filter out some
 | 
			
		||||
    #   actors from being able to acquire the debugger lock.
 | 
			
		||||
    _enter_debugger_on_cancel: bool = True
 | 
			
		||||
| 
						 | 
				
			
			@ -1234,8 +1234,8 @@ class Context:
 | 
			
		|||
 | 
			
		||||
            # ?XXX, should already be set in `._deliver_msg()` right?
 | 
			
		||||
            if self._outcome_msg is not Unresolved:
 | 
			
		||||
                # from .devx import _debug
 | 
			
		||||
                # await _debug.pause()
 | 
			
		||||
                # from .devx import debug
 | 
			
		||||
                # await debug.pause()
 | 
			
		||||
                assert self._outcome_msg is outcome_msg
 | 
			
		||||
            else:
 | 
			
		||||
                self._outcome_msg = outcome_msg
 | 
			
		||||
| 
						 | 
				
			
			@ -2170,7 +2170,7 @@ async def open_context_from_portal(
 | 
			
		|||
        #   debugging the tractor-runtime itself using it's
 | 
			
		||||
        #   own `.devx.` tooling!
 | 
			
		||||
        # 
 | 
			
		||||
        # await _debug.pause()
 | 
			
		||||
        # await debug.pause()
 | 
			
		||||
 | 
			
		||||
        # CASE 2: context was cancelled by local task calling
 | 
			
		||||
        # `.cancel()`, we don't raise and the exit block should
 | 
			
		||||
| 
						 | 
				
			
			@ -2237,7 +2237,7 @@ async def open_context_from_portal(
 | 
			
		|||
        # NOTE: `Context.cancel()` is conversely NEVER CALLED in
 | 
			
		||||
        # the `ContextCancelled` "self cancellation absorbed" case
 | 
			
		||||
        # handled in the block above ^^^ !!
 | 
			
		||||
        # await _debug.pause()
 | 
			
		||||
        # await debug.pause()
 | 
			
		||||
        # log.cancel(
 | 
			
		||||
        match scope_err:
 | 
			
		||||
            case trio.Cancelled:
 | 
			
		||||
| 
						 | 
				
			
			@ -2252,11 +2252,11 @@ async def open_context_from_portal(
 | 
			
		|||
        )
 | 
			
		||||
 | 
			
		||||
        if debug_mode():
 | 
			
		||||
            # async with _debug.acquire_debug_lock(portal.actor.uid):
 | 
			
		||||
            # async with debug.acquire_debug_lock(portal.actor.uid):
 | 
			
		||||
            #     pass
 | 
			
		||||
            # TODO: factor ^ into below for non-root cases?
 | 
			
		||||
            #
 | 
			
		||||
            from .devx._debug import maybe_wait_for_debugger
 | 
			
		||||
            from .devx.debug import maybe_wait_for_debugger
 | 
			
		||||
            was_acquired: bool = await maybe_wait_for_debugger(
 | 
			
		||||
                # header_msg=(
 | 
			
		||||
                #     'Delaying `ctx.cancel()` until debug lock '
 | 
			
		||||
| 
						 | 
				
			
			@ -2319,8 +2319,8 @@ async def open_context_from_portal(
 | 
			
		|||
                raise
 | 
			
		||||
 | 
			
		||||
            # yes this worx!
 | 
			
		||||
            # from .devx import _debug
 | 
			
		||||
            # await _debug.pause()
 | 
			
		||||
            # from .devx import debug
 | 
			
		||||
            # await debug.pause()
 | 
			
		||||
 | 
			
		||||
            # an exception type boxed in a `RemoteActorError`
 | 
			
		||||
            # is returned (meaning it was obvi not raised)
 | 
			
		||||
| 
						 | 
				
			
			@ -2355,7 +2355,7 @@ async def open_context_from_portal(
 | 
			
		|||
        # where the root is waiting on the lock to clear but the
 | 
			
		||||
        # child has already cleared it and clobbered IPC.
 | 
			
		||||
        if debug_mode():
 | 
			
		||||
            from .devx._debug import maybe_wait_for_debugger
 | 
			
		||||
            from .devx.debug import maybe_wait_for_debugger
 | 
			
		||||
            await maybe_wait_for_debugger()
 | 
			
		||||
 | 
			
		||||
        # though it should be impossible for any tasks
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,7 +22,6 @@ from __future__ import annotations
 | 
			
		|||
from functools import partial
 | 
			
		||||
import multiprocessing as mp
 | 
			
		||||
import os
 | 
			
		||||
import textwrap
 | 
			
		||||
from typing import (
 | 
			
		||||
    Any,
 | 
			
		||||
    TYPE_CHECKING,
 | 
			
		||||
| 
						 | 
				
			
			@ -35,7 +34,10 @@ from .log import (
 | 
			
		|||
    get_logger,
 | 
			
		||||
)
 | 
			
		||||
from . import _state
 | 
			
		||||
from .devx import _debug
 | 
			
		||||
from .devx import (
 | 
			
		||||
    _frame_stack,
 | 
			
		||||
    pformat,
 | 
			
		||||
)
 | 
			
		||||
from .to_asyncio import run_as_asyncio_guest
 | 
			
		||||
from ._addr import UnwrappedAddress
 | 
			
		||||
from ._runtime import (
 | 
			
		||||
| 
						 | 
				
			
			@ -103,107 +105,6 @@ def _mp_main(
 | 
			
		|||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# TODO: move this func to some kinda `.devx._conc_lang.py` eventually
 | 
			
		||||
# as we work out our multi-domain state-flow-syntax!
 | 
			
		||||
def nest_from_op(
 | 
			
		||||
    input_op: str,
 | 
			
		||||
    #
 | 
			
		||||
    # ?TODO? an idea for a syntax to the state of concurrent systems
 | 
			
		||||
    # as a "3-domain" (execution, scope, storage) model and using
 | 
			
		||||
    # a minimal ascii/utf-8 operator-set.
 | 
			
		||||
    #
 | 
			
		||||
    # try not to take any of this seriously yet XD
 | 
			
		||||
    #
 | 
			
		||||
    # > is a "play operator" indicating (CPU bound)
 | 
			
		||||
    #   exec/work/ops required at the "lowest level computing"
 | 
			
		||||
    #
 | 
			
		||||
    # execution primititves (tasks, threads, actors..) denote their
 | 
			
		||||
    # lifetime with '(' and ')' since parentheses normally are used
 | 
			
		||||
    # in many langs to denote function calls.
 | 
			
		||||
    #
 | 
			
		||||
    # starting = (
 | 
			
		||||
    # >(  opening/starting; beginning of the thread-of-exec (toe?)
 | 
			
		||||
    # (>  opened/started,  (finished spawning toe)
 | 
			
		||||
    # |_<Task: blah blah..>  repr of toe, in py these look like <objs>
 | 
			
		||||
    #
 | 
			
		||||
    # >) closing/exiting/stopping,
 | 
			
		||||
    # )> closed/exited/stopped,
 | 
			
		||||
    # |_<Task: blah blah..>
 | 
			
		||||
    #   [OR <), )< ?? ]
 | 
			
		||||
    #
 | 
			
		||||
    # ending = )
 | 
			
		||||
    # >c) cancelling to close/exit
 | 
			
		||||
    # c)> cancelled (caused close), OR?
 | 
			
		||||
    #  |_<Actor: ..>
 | 
			
		||||
    #   OR maybe "<c)" which better indicates the cancel being
 | 
			
		||||
    #   "delivered/returned" / returned" to LHS?
 | 
			
		||||
    #
 | 
			
		||||
    # >x)  erroring to eventuall exit
 | 
			
		||||
    # x)>  errored and terminated
 | 
			
		||||
    #  |_<Actor: ...>
 | 
			
		||||
    #
 | 
			
		||||
    # scopes: supers/nurseries, IPC-ctxs, sessions, perms, etc.
 | 
			
		||||
    # >{  opening
 | 
			
		||||
    # {>  opened
 | 
			
		||||
    # }>  closed
 | 
			
		||||
    # >}  closing
 | 
			
		||||
    #
 | 
			
		||||
    # storage: like queues, shm-buffers, files, etc..
 | 
			
		||||
    # >[  opening
 | 
			
		||||
    # [>  opened
 | 
			
		||||
    #  |_<FileObj: ..>
 | 
			
		||||
    #
 | 
			
		||||
    # >]  closing
 | 
			
		||||
    # ]>  closed
 | 
			
		||||
 | 
			
		||||
    # IPC ops: channels, transports, msging
 | 
			
		||||
    # =>  req msg
 | 
			
		||||
    # <=  resp msg
 | 
			
		||||
    # <=> 2-way streaming (of msgs)
 | 
			
		||||
    # <-  recv 1 msg
 | 
			
		||||
    # ->  send 1 msg
 | 
			
		||||
    #
 | 
			
		||||
    # TODO: still not sure on R/L-HS approach..?
 | 
			
		||||
    # =>(  send-req to exec start (task, actor, thread..)
 | 
			
		||||
    # (<=  recv-req to ^
 | 
			
		||||
    #
 | 
			
		||||
    # (<=  recv-req ^
 | 
			
		||||
    # <=(  recv-resp opened remote exec primitive
 | 
			
		||||
    # <=)  recv-resp closed
 | 
			
		||||
    #
 | 
			
		||||
    # )<=c req to stop due to cancel
 | 
			
		||||
    # c=>) req to stop due to cancel
 | 
			
		||||
    #
 | 
			
		||||
    # =>{  recv-req to open
 | 
			
		||||
    # <={  send-status that it closed
 | 
			
		||||
 | 
			
		||||
    tree_str: str,
 | 
			
		||||
 | 
			
		||||
    # NOTE: so move back-from-the-left of the `input_op` by
 | 
			
		||||
    # this amount.
 | 
			
		||||
    back_from_op: int = 0,
 | 
			
		||||
) -> str:
 | 
			
		||||
    '''
 | 
			
		||||
    Depth-increment the input (presumably hierarchy/supervision)
 | 
			
		||||
    input "tree string" below the provided `input_op` execution
 | 
			
		||||
    operator, so injecting a `"\n|_{input_op}\n"`and indenting the
 | 
			
		||||
    `tree_str` to nest content aligned with the ops last char.
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
    return (
 | 
			
		||||
        f'{input_op}\n'
 | 
			
		||||
        +
 | 
			
		||||
        textwrap.indent(
 | 
			
		||||
            tree_str,
 | 
			
		||||
            prefix=(
 | 
			
		||||
                len(input_op)
 | 
			
		||||
                -
 | 
			
		||||
                (back_from_op + 1)
 | 
			
		||||
            ) * ' ',
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _trio_main(
 | 
			
		||||
    actor: Actor,
 | 
			
		||||
    *,
 | 
			
		||||
| 
						 | 
				
			
			@ -215,7 +116,7 @@ def _trio_main(
 | 
			
		|||
    Entry point for a `trio_run_in_process` subactor.
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
    _debug.hide_runtime_frames()
 | 
			
		||||
    _frame_stack.hide_runtime_frames()
 | 
			
		||||
 | 
			
		||||
    _state._current_actor = actor
 | 
			
		||||
    trio_main = partial(
 | 
			
		||||
| 
						 | 
				
			
			@ -236,7 +137,7 @@ def _trio_main(
 | 
			
		|||
        log.info(
 | 
			
		||||
            'Starting new `trio` subactor:\n'
 | 
			
		||||
            +
 | 
			
		||||
            nest_from_op(
 | 
			
		||||
            pformat.nest_from_op(
 | 
			
		||||
                input_op='>(',  # see syntax ideas above
 | 
			
		||||
                tree_str=actor_info,
 | 
			
		||||
                back_from_op=2,  # since "complete"
 | 
			
		||||
| 
						 | 
				
			
			@ -246,7 +147,7 @@ def _trio_main(
 | 
			
		|||
    exit_status: str = (
 | 
			
		||||
        'Subactor exited\n'
 | 
			
		||||
        +
 | 
			
		||||
        nest_from_op(
 | 
			
		||||
        pformat.nest_from_op(
 | 
			
		||||
            input_op=')>',  # like a "closed-to-play"-icon from super perspective
 | 
			
		||||
            tree_str=actor_info,
 | 
			
		||||
            back_from_op=1,
 | 
			
		||||
| 
						 | 
				
			
			@ -264,7 +165,7 @@ def _trio_main(
 | 
			
		|||
        exit_status: str = (
 | 
			
		||||
            'Actor received KBI (aka an OS-cancel)\n'
 | 
			
		||||
            +
 | 
			
		||||
            nest_from_op(
 | 
			
		||||
            pformat.nest_from_op(
 | 
			
		||||
                input_op='c)>',  # closed due to cancel (see above)
 | 
			
		||||
                tree_str=actor_info,
 | 
			
		||||
            )
 | 
			
		||||
| 
						 | 
				
			
			@ -274,7 +175,7 @@ def _trio_main(
 | 
			
		|||
        exit_status: str = (
 | 
			
		||||
            'Main actor task exited due to crash?\n'
 | 
			
		||||
            +
 | 
			
		||||
            nest_from_op(
 | 
			
		||||
            pformat.nest_from_op(
 | 
			
		||||
                input_op='x)>',  # closed by error
 | 
			
		||||
                tree_str=actor_info,
 | 
			
		||||
            )
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -582,8 +582,7 @@ async def open_portal(
 | 
			
		|||
            msg_loop_cs = await tn.start(
 | 
			
		||||
                partial(
 | 
			
		||||
                    _rpc.process_messages,
 | 
			
		||||
                    actor,
 | 
			
		||||
                    channel,
 | 
			
		||||
                    chan=channel,
 | 
			
		||||
                    # if the local task is cancelled we want to keep
 | 
			
		||||
                    # the msg loop running until our block ends
 | 
			
		||||
                    shield=True,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -44,7 +44,10 @@ from ._runtime import (
 | 
			
		|||
    # Arbiter as Registry,
 | 
			
		||||
    async_main,
 | 
			
		||||
)
 | 
			
		||||
from .devx import _debug
 | 
			
		||||
from .devx import (
 | 
			
		||||
    debug,
 | 
			
		||||
    _frame_stack,
 | 
			
		||||
)
 | 
			
		||||
from . import _spawn
 | 
			
		||||
from . import _state
 | 
			
		||||
from . import log
 | 
			
		||||
| 
						 | 
				
			
			@ -67,7 +70,7 @@ from ._exceptions import (
 | 
			
		|||
logger = log.get_logger('tractor')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# TODO: stick this in a `@acm` defined in `devx._debug`?
 | 
			
		||||
# TODO: stick this in a `@acm` defined in `devx.debug`?
 | 
			
		||||
# -[ ] also maybe consider making this a `wrapt`-deco to
 | 
			
		||||
#     save an indent level?
 | 
			
		||||
#
 | 
			
		||||
| 
						 | 
				
			
			@ -89,7 +92,7 @@ async def maybe_block_bp(
 | 
			
		|||
        debug_mode
 | 
			
		||||
        and maybe_enable_greenback
 | 
			
		||||
        and (
 | 
			
		||||
            maybe_mod := await _debug.maybe_init_greenback(
 | 
			
		||||
            maybe_mod := await debug.maybe_init_greenback(
 | 
			
		||||
                raise_not_found=False,
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
| 
						 | 
				
			
			@ -99,7 +102,7 @@ async def maybe_block_bp(
 | 
			
		|||
            'Enabling `tractor.pause_from_sync()` support!\n'
 | 
			
		||||
        )
 | 
			
		||||
        os.environ['PYTHONBREAKPOINT'] = (
 | 
			
		||||
            'tractor.devx._debug._sync_pause_from_builtin'
 | 
			
		||||
            'tractor.devx.debug._sync_pause_from_builtin'
 | 
			
		||||
        )
 | 
			
		||||
        _state._runtime_vars['use_greenback'] = True
 | 
			
		||||
        bp_blocked = False
 | 
			
		||||
| 
						 | 
				
			
			@ -163,7 +166,9 @@ async def open_root_actor(
 | 
			
		|||
 | 
			
		||||
    # enables the multi-process debugger support
 | 
			
		||||
    debug_mode: bool = False,
 | 
			
		||||
    maybe_enable_greenback: bool = True,  # `.pause_from_sync()/breakpoint()` support
 | 
			
		||||
    maybe_enable_greenback: bool = False,  # `.pause_from_sync()/breakpoint()` support
 | 
			
		||||
    # ^XXX NOTE^ the perf implications of use,
 | 
			
		||||
    # https://greenback.readthedocs.io/en/latest/principle.html#performance
 | 
			
		||||
    enable_stack_on_sig: bool = False,
 | 
			
		||||
 | 
			
		||||
    # internal logging
 | 
			
		||||
| 
						 | 
				
			
			@ -178,7 +183,7 @@ async def open_root_actor(
 | 
			
		|||
 | 
			
		||||
    hide_tb: bool = True,
 | 
			
		||||
 | 
			
		||||
    # XXX, proxied directly to `.devx._debug._maybe_enter_pm()`
 | 
			
		||||
    # XXX, proxied directly to `.devx.debug._maybe_enter_pm()`
 | 
			
		||||
    # for REPL-entry logic.
 | 
			
		||||
    debug_filter: Callable[
 | 
			
		||||
        [BaseException|BaseExceptionGroup],
 | 
			
		||||
| 
						 | 
				
			
			@ -223,12 +228,12 @@ async def open_root_actor(
 | 
			
		|||
                len(enable_transports) == 1
 | 
			
		||||
            ), 'No multi-tpt support yet!'
 | 
			
		||||
 | 
			
		||||
        _debug.hide_runtime_frames()
 | 
			
		||||
        _frame_stack.hide_runtime_frames()
 | 
			
		||||
        __tracebackhide__: bool = hide_tb
 | 
			
		||||
 | 
			
		||||
        # attempt to retreive ``trio``'s sigint handler and stash it
 | 
			
		||||
        # on our debugger lock state.
 | 
			
		||||
        _debug.DebugStatus._trio_handler = signal.getsignal(signal.SIGINT)
 | 
			
		||||
        debug.DebugStatus._trio_handler = signal.getsignal(signal.SIGINT)
 | 
			
		||||
 | 
			
		||||
        # mark top most level process as root actor
 | 
			
		||||
        _state._runtime_vars['_is_root'] = True
 | 
			
		||||
| 
						 | 
				
			
			@ -276,13 +281,14 @@ async def open_root_actor(
 | 
			
		|||
 | 
			
		||||
        if (
 | 
			
		||||
            debug_mode
 | 
			
		||||
            and _spawn._spawn_method == 'trio'
 | 
			
		||||
            and
 | 
			
		||||
            _spawn._spawn_method == 'trio'
 | 
			
		||||
        ):
 | 
			
		||||
            _state._runtime_vars['_debug_mode'] = True
 | 
			
		||||
 | 
			
		||||
            # expose internal debug module to every actor allowing for
 | 
			
		||||
            # use of ``await tractor.pause()``
 | 
			
		||||
            enable_modules.append('tractor.devx._debug')
 | 
			
		||||
            enable_modules.append('tractor.devx.debug._tty_lock')
 | 
			
		||||
 | 
			
		||||
            # if debug mode get's enabled *at least* use that level of
 | 
			
		||||
            # logging for some informative console prompts.
 | 
			
		||||
| 
						 | 
				
			
			@ -464,7 +470,7 @@ async def open_root_actor(
 | 
			
		|||
                    # TODO, in beginning to handle the subsubactor with
 | 
			
		||||
                    # crashed grandparent cases..
 | 
			
		||||
                    #
 | 
			
		||||
                    # was_locked: bool = await _debug.maybe_wait_for_debugger(
 | 
			
		||||
                    # was_locked: bool = await debug.maybe_wait_for_debugger(
 | 
			
		||||
                    #     child_in_debug=True,
 | 
			
		||||
                    # )
 | 
			
		||||
                    # XXX NOTE XXX see equiv note inside
 | 
			
		||||
| 
						 | 
				
			
			@ -472,7 +478,7 @@ async def open_root_actor(
 | 
			
		|||
                    # non-root or root-that-opened-this-mahually case we
 | 
			
		||||
                    # wait for the local actor-nursery to exit before
 | 
			
		||||
                    # exiting the transport channel handler.
 | 
			
		||||
                    entered: bool = await _debug._maybe_enter_pm(
 | 
			
		||||
                    entered: bool = await debug._maybe_enter_pm(
 | 
			
		||||
                        err,
 | 
			
		||||
                        api_frame=inspect.currentframe(),
 | 
			
		||||
                        debug_filter=debug_filter,
 | 
			
		||||
| 
						 | 
				
			
			@ -512,8 +518,17 @@ async def open_root_actor(
 | 
			
		|||
                    )
 | 
			
		||||
                    await actor.cancel(None)  # self cancel
 | 
			
		||||
        finally:
 | 
			
		||||
            # revert all process-global runtime state
 | 
			
		||||
            if (
 | 
			
		||||
                debug_mode
 | 
			
		||||
                and
 | 
			
		||||
                _spawn._spawn_method == 'trio'
 | 
			
		||||
            ):
 | 
			
		||||
                _state._runtime_vars['_debug_mode'] = False
 | 
			
		||||
 | 
			
		||||
            _state._current_actor = None
 | 
			
		||||
            _state._last_actor_terminated = actor
 | 
			
		||||
 | 
			
		||||
            logger.runtime(
 | 
			
		||||
                f'Root actor terminated\n'
 | 
			
		||||
                f')>\n'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -57,7 +57,7 @@ from ._exceptions import (
 | 
			
		|||
    unpack_error,
 | 
			
		||||
)
 | 
			
		||||
from .devx import (
 | 
			
		||||
    _debug,
 | 
			
		||||
    debug,
 | 
			
		||||
    add_div,
 | 
			
		||||
)
 | 
			
		||||
from . import _state
 | 
			
		||||
| 
						 | 
				
			
			@ -266,7 +266,7 @@ async def _errors_relayed_via_ipc(
 | 
			
		|||
 | 
			
		||||
    # TODO: a debug nursery when in debug mode!
 | 
			
		||||
    # async with maybe_open_debugger_nursery() as debug_tn:
 | 
			
		||||
    # => see matching comment in side `._debug._pause()`
 | 
			
		||||
    # => see matching comment in side `.debug._pause()`
 | 
			
		||||
    rpc_err: BaseException|None = None
 | 
			
		||||
    try:
 | 
			
		||||
        yield  # run RPC invoke body
 | 
			
		||||
| 
						 | 
				
			
			@ -318,7 +318,7 @@ async def _errors_relayed_via_ipc(
 | 
			
		|||
                    'RPC task crashed, attempting to enter debugger\n'
 | 
			
		||||
                    f'|_{ctx}'
 | 
			
		||||
                )
 | 
			
		||||
                entered_debug = await _debug._maybe_enter_pm(
 | 
			
		||||
                entered_debug = await debug._maybe_enter_pm(
 | 
			
		||||
                    err,
 | 
			
		||||
                    api_frame=inspect.currentframe(),
 | 
			
		||||
                )
 | 
			
		||||
| 
						 | 
				
			
			@ -462,7 +462,7 @@ async def _invoke(
 | 
			
		|||
    ):
 | 
			
		||||
        # XXX for .pause_from_sync()` usage we need to make sure
 | 
			
		||||
        # `greenback` is boostrapped in the subactor!
 | 
			
		||||
        await _debug.maybe_init_greenback()
 | 
			
		||||
        await debug.maybe_init_greenback()
 | 
			
		||||
 | 
			
		||||
    # TODO: possibly a specially formatted traceback
 | 
			
		||||
    # (not sure what typing is for this..)?
 | 
			
		||||
| 
						 | 
				
			
			@ -751,7 +751,7 @@ async def _invoke(
 | 
			
		|||
                and 'Cancel scope stack corrupted' in scope_error.args[0]
 | 
			
		||||
            ):
 | 
			
		||||
                log.exception('Cancel scope stack corrupted!?\n')
 | 
			
		||||
                # _debug.mk_pdb().set_trace()
 | 
			
		||||
                # debug.mk_pdb().set_trace()
 | 
			
		||||
 | 
			
		||||
            # always set this (child) side's exception as the
 | 
			
		||||
            # local error on the context
 | 
			
		||||
| 
						 | 
				
			
			@ -779,7 +779,7 @@ async def _invoke(
 | 
			
		|||
 | 
			
		||||
            # don't pop the local context until we know the
 | 
			
		||||
            # associated child isn't in debug any more
 | 
			
		||||
            await _debug.maybe_wait_for_debugger()
 | 
			
		||||
            await debug.maybe_wait_for_debugger()
 | 
			
		||||
            ctx: Context = actor._contexts.pop((
 | 
			
		||||
                chan.uid,
 | 
			
		||||
                cid,
 | 
			
		||||
| 
						 | 
				
			
			@ -869,7 +869,6 @@ async def try_ship_error_to_remote(
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
async def process_messages(
 | 
			
		||||
    actor: Actor,
 | 
			
		||||
    chan: Channel,
 | 
			
		||||
    shield: bool = False,
 | 
			
		||||
    task_status: TaskStatus[CancelScope] = trio.TASK_STATUS_IGNORED,
 | 
			
		||||
| 
						 | 
				
			
			@ -907,6 +906,7 @@ async def process_messages(
 | 
			
		|||
      (as utilized inside `Portal.cancel_actor()` ).
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
    actor: Actor = _state.current_actor()
 | 
			
		||||
    assert actor._service_n  # runtime state sanity
 | 
			
		||||
 | 
			
		||||
    # TODO: once `trio` get's an "obvious way" for req/resp we
 | 
			
		||||
| 
						 | 
				
			
			@ -983,7 +983,7 @@ async def process_messages(
 | 
			
		|||
                        # XXX NOTE XXX don't start entire actor
 | 
			
		||||
                        # runtime cancellation if this actor is
 | 
			
		||||
                        # currently in debug mode!
 | 
			
		||||
                        pdb_complete: trio.Event|None = _debug.DebugStatus.repl_release
 | 
			
		||||
                        pdb_complete: trio.Event|None = debug.DebugStatus.repl_release
 | 
			
		||||
                        if pdb_complete:
 | 
			
		||||
                            await pdb_complete.wait()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -44,6 +44,7 @@ from functools import partial
 | 
			
		|||
import importlib
 | 
			
		||||
import importlib.util
 | 
			
		||||
import os
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
from pprint import pformat
 | 
			
		||||
import signal
 | 
			
		||||
import sys
 | 
			
		||||
| 
						 | 
				
			
			@ -96,7 +97,7 @@ from ._exceptions import (
 | 
			
		|||
    MsgTypeError,
 | 
			
		||||
    unpack_error,
 | 
			
		||||
)
 | 
			
		||||
from .devx import _debug
 | 
			
		||||
from .devx import debug
 | 
			
		||||
from ._discovery import get_registry
 | 
			
		||||
from ._portal import Portal
 | 
			
		||||
from . import _state
 | 
			
		||||
| 
						 | 
				
			
			@ -111,8 +112,22 @@ if TYPE_CHECKING:
 | 
			
		|||
log = get_logger('tractor')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _get_mod_abspath(module):
 | 
			
		||||
    return os.path.abspath(module.__file__)
 | 
			
		||||
def _get_mod_abspath(module: ModuleType) -> Path:
 | 
			
		||||
    return Path(module.__file__).absolute()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_mod_nsps2fps(mod_ns_paths: list[str]) -> dict[str, str]:
 | 
			
		||||
    '''
 | 
			
		||||
    Deliver a table of py module namespace-path-`str`s mapped to
 | 
			
		||||
    their "physical" `.py` file paths in the file-sys.
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
    nsp2fp: dict[str, str] = {}
 | 
			
		||||
    for nsp in mod_ns_paths:
 | 
			
		||||
        mod: ModuleType = importlib.import_module(nsp)
 | 
			
		||||
        nsp2fp[nsp] = str(_get_mod_abspath(mod))
 | 
			
		||||
 | 
			
		||||
    return nsp2fp
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Actor:
 | 
			
		||||
| 
						 | 
				
			
			@ -219,13 +234,14 @@ class Actor:
 | 
			
		|||
        # will be passed to children
 | 
			
		||||
        self._parent_main_data = _mp_fixup_main._mp_figure_out_main()
 | 
			
		||||
 | 
			
		||||
        # TODO? only add this when `is_debug_mode() == True` no?
 | 
			
		||||
        # always include debugging tools module
 | 
			
		||||
        enable_modules.append('tractor.devx._debug')
 | 
			
		||||
        if _state.is_root_process():
 | 
			
		||||
            enable_modules.append('tractor.devx.debug._tty_lock')
 | 
			
		||||
 | 
			
		||||
        self.enable_modules: dict[str, str] = {}
 | 
			
		||||
        for name in enable_modules:
 | 
			
		||||
            mod: ModuleType = importlib.import_module(name)
 | 
			
		||||
            self.enable_modules[name] = _get_mod_abspath(mod)
 | 
			
		||||
        self.enable_modules: dict[str, str] = get_mod_nsps2fps(
 | 
			
		||||
            mod_ns_paths=enable_modules,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self._mods: dict[str, ModuleType] = {}
 | 
			
		||||
        self.loglevel: str = loglevel
 | 
			
		||||
| 
						 | 
				
			
			@ -391,7 +407,6 @@ class Actor:
 | 
			
		|||
 | 
			
		||||
    def load_modules(
 | 
			
		||||
        self,
 | 
			
		||||
        # debug_mode: bool = False,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        '''
 | 
			
		||||
        Load explicitly enabled python modules from local fs after
 | 
			
		||||
| 
						 | 
				
			
			@ -413,6 +428,9 @@ class Actor:
 | 
			
		|||
                        parent_data['init_main_from_path'])
 | 
			
		||||
 | 
			
		||||
            status: str = 'Attempting to import enabled modules:\n'
 | 
			
		||||
 | 
			
		||||
            modpath: str
 | 
			
		||||
            filepath: str
 | 
			
		||||
            for modpath, filepath in self.enable_modules.items():
 | 
			
		||||
                # XXX append the allowed module to the python path which
 | 
			
		||||
                # should allow for relative (at least downward) imports.
 | 
			
		||||
| 
						 | 
				
			
			@ -729,25 +747,33 @@ class Actor:
 | 
			
		|||
                            f'Received invalid non-`SpawnSpec` payload !?\n'
 | 
			
		||||
                            f'{spawnspec}\n'
 | 
			
		||||
                        )
 | 
			
		||||
 | 
			
		||||
                # ^^TODO XXX!! when the `SpawnSpec` fails to decode
 | 
			
		||||
                # the above will raise a `MsgTypeError` which if we
 | 
			
		||||
                # do NOT ALSO RAISE it will tried to be pprinted in
 | 
			
		||||
                # the log.runtime() below..
 | 
			
		||||
                # ^^XXX TODO XXX^^^
 | 
			
		||||
                # when the `SpawnSpec` fails to decode the above will
 | 
			
		||||
                # raise a `MsgTypeError` which if we do NOT ALSO
 | 
			
		||||
                # RAISE it will tried to be pprinted in the
 | 
			
		||||
                # log.runtime() below..
 | 
			
		||||
                #
 | 
			
		||||
                # SO we gotta look at how other `chan.recv()` calls
 | 
			
		||||
                # are wrapped and do the same for this spec receive!
 | 
			
		||||
                # -[ ] see `._rpc` likely has the answer?
 | 
			
		||||
 | 
			
		||||
                # ^^^XXX NOTE XXX^^^, can't be called here!
 | 
			
		||||
                #
 | 
			
		||||
                # XXX NOTE, can't be called here in subactor
 | 
			
		||||
                # bc we haven't yet received the
 | 
			
		||||
                # `SpawnSpec._runtime_vars: dict` which would
 | 
			
		||||
                # declare whether `debug_mode` is set!
 | 
			
		||||
                # breakpoint()
 | 
			
		||||
                # import pdbp; pdbp.set_trace()
 | 
			
		||||
                #
 | 
			
		||||
                # => bc we haven't yet received the
 | 
			
		||||
                # `spawnspec._runtime_vars` which contains
 | 
			
		||||
                # `debug_mode: bool`..
 | 
			
		||||
 | 
			
		||||
                # `SpawnSpec.bind_addrs`
 | 
			
		||||
                #  ---------------------
 | 
			
		||||
                accept_addrs: list[UnwrappedAddress] = spawnspec.bind_addrs
 | 
			
		||||
 | 
			
		||||
                # TODO: another `Struct` for rtvs..
 | 
			
		||||
                # `SpawnSpec._runtime_vars`
 | 
			
		||||
                # -------------------------
 | 
			
		||||
                # => update process-wide globals
 | 
			
		||||
                # TODO! -[ ] another `Struct` for rtvs..
 | 
			
		||||
                rvs: dict[str, Any] = spawnspec._runtime_vars
 | 
			
		||||
                if rvs['_debug_mode']:
 | 
			
		||||
                    from .devx import (
 | 
			
		||||
| 
						 | 
				
			
			@ -805,18 +831,20 @@ class Actor:
 | 
			
		|||
                        f'self._infected_aio = {aio_attr}\n'
 | 
			
		||||
                    )
 | 
			
		||||
                if aio_rtv:
 | 
			
		||||
                    assert trio_runtime.GLOBAL_RUN_CONTEXT.runner.is_guest
 | 
			
		||||
                    # ^TODO^ possibly add a `sniffio` or
 | 
			
		||||
                    # `trio` pub-API for `is_guest_mode()`?
 | 
			
		||||
                    assert (
 | 
			
		||||
                        trio_runtime.GLOBAL_RUN_CONTEXT.runner.is_guest
 | 
			
		||||
                        # and
 | 
			
		||||
                        # ^TODO^ possibly add a `sniffio` or
 | 
			
		||||
                        # `trio` pub-API for `is_guest_mode()`?
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
                rvs['_is_root'] = False  # obvi XD
 | 
			
		||||
 | 
			
		||||
                # update process-wide globals
 | 
			
		||||
                _state._runtime_vars.update(rvs)
 | 
			
		||||
 | 
			
		||||
                # XXX: ``msgspec`` doesn't support serializing tuples
 | 
			
		||||
                # so just cash manually here since it's what our
 | 
			
		||||
                # internals expect.
 | 
			
		||||
                # `SpawnSpec.reg_addrs`
 | 
			
		||||
                # ---------------------
 | 
			
		||||
                # => update parent provided registrar contact info
 | 
			
		||||
                #
 | 
			
		||||
                self.reg_addrs = [
 | 
			
		||||
                    # TODO: we don't really NEED these as tuples?
 | 
			
		||||
| 
						 | 
				
			
			@ -827,12 +855,24 @@ class Actor:
 | 
			
		|||
                    for val in spawnspec.reg_addrs
 | 
			
		||||
                ]
 | 
			
		||||
 | 
			
		||||
                # TODO: better then monkey patching..
 | 
			
		||||
                # -[ ] maybe read the actual f#$-in `._spawn_spec` XD
 | 
			
		||||
                for _, attr, value in pretty_struct.iter_fields(
 | 
			
		||||
                    spawnspec,
 | 
			
		||||
                ):
 | 
			
		||||
                    setattr(self, attr, value)
 | 
			
		||||
                # `SpawnSpec.enable_modules`
 | 
			
		||||
                # ---------------------
 | 
			
		||||
                # => extend RPC-python-module (capabilities) with
 | 
			
		||||
                #   those permitted by parent.
 | 
			
		||||
                #
 | 
			
		||||
                # NOTE, only the root actor should have
 | 
			
		||||
                # a pre-permitted entry for `.devx.debug._tty_lock`.
 | 
			
		||||
                assert not self.enable_modules
 | 
			
		||||
                self.enable_modules.update(
 | 
			
		||||
                    spawnspec.enable_modules
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
                self._parent_main_data = spawnspec._parent_main_data
 | 
			
		||||
                # XXX QUESTION(s)^^^
 | 
			
		||||
                # -[ ] already set in `.__init__()` right, but how is
 | 
			
		||||
                #      it diff from this blatant parent copy?
 | 
			
		||||
                #    -[ ] do we need/want the .__init__() value in
 | 
			
		||||
                #       just the root case orr?
 | 
			
		||||
 | 
			
		||||
            return (
 | 
			
		||||
                chan,
 | 
			
		||||
| 
						 | 
				
			
			@ -930,7 +970,7 @@ class Actor:
 | 
			
		|||
 | 
			
		||||
            # kill any debugger request task to avoid deadlock
 | 
			
		||||
            # with the root actor in this tree
 | 
			
		||||
            debug_req = _debug.DebugStatus
 | 
			
		||||
            debug_req = debug.DebugStatus
 | 
			
		||||
            lock_req_ctx: Context = debug_req.req_ctx
 | 
			
		||||
            if (
 | 
			
		||||
                lock_req_ctx
 | 
			
		||||
| 
						 | 
				
			
			@ -940,7 +980,7 @@ class Actor:
 | 
			
		|||
                msg += (
 | 
			
		||||
                    f'\n'
 | 
			
		||||
                    f'-> Cancelling active debugger request..\n'
 | 
			
		||||
                    f'|_{_debug.Lock.repr()}\n\n'
 | 
			
		||||
                    f'|_{debug.Lock.repr()}\n\n'
 | 
			
		||||
                    f'|_{lock_req_ctx}\n\n'
 | 
			
		||||
                )
 | 
			
		||||
                # lock_req_ctx._scope.cancel()
 | 
			
		||||
| 
						 | 
				
			
			@ -1262,11 +1302,15 @@ async def async_main(
 | 
			
		|||
    the actor's "runtime" and all thus all ongoing RPC tasks.
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
    # XXX NOTE, `_state._current_actor` **must** be set prior to
 | 
			
		||||
    # calling this core runtime entrypoint!
 | 
			
		||||
    assert actor is _state.current_actor()
 | 
			
		||||
 | 
			
		||||
    actor._task: trio.Task = trio.lowlevel.current_task()
 | 
			
		||||
 | 
			
		||||
    # attempt to retreive ``trio``'s sigint handler and stash it
 | 
			
		||||
    # on our debugger state.
 | 
			
		||||
    _debug.DebugStatus._trio_handler = signal.getsignal(signal.SIGINT)
 | 
			
		||||
    debug.DebugStatus._trio_handler = signal.getsignal(signal.SIGINT)
 | 
			
		||||
 | 
			
		||||
    is_registered: bool = False
 | 
			
		||||
    try:
 | 
			
		||||
| 
						 | 
				
			
			@ -1321,7 +1365,6 @@ async def async_main(
 | 
			
		|||
                ) as service_nursery,
 | 
			
		||||
 | 
			
		||||
                _server.open_ipc_server(
 | 
			
		||||
                    actor=actor,
 | 
			
		||||
                    parent_tn=service_nursery,
 | 
			
		||||
                    stream_handler_tn=service_nursery,
 | 
			
		||||
                ) as ipc_server,
 | 
			
		||||
| 
						 | 
				
			
			@ -1360,7 +1403,7 @@ async def async_main(
 | 
			
		|||
                # try:
 | 
			
		||||
                #     actor.load_modules()
 | 
			
		||||
                # except ModuleNotFoundError as err:
 | 
			
		||||
                #     _debug.pause_from_sync()
 | 
			
		||||
                #     debug.pause_from_sync()
 | 
			
		||||
                #     import pdbp; pdbp.set_trace()
 | 
			
		||||
                #     raise
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1375,7 +1418,6 @@ async def async_main(
 | 
			
		|||
                        'Booting IPC server'
 | 
			
		||||
                    )
 | 
			
		||||
                    eps: list = await ipc_server.listen_on(
 | 
			
		||||
                        actor=actor,
 | 
			
		||||
                        accept_addrs=accept_addrs,
 | 
			
		||||
                        stream_handler_nursery=service_nursery,
 | 
			
		||||
                    )
 | 
			
		||||
| 
						 | 
				
			
			@ -1393,7 +1435,7 @@ async def async_main(
 | 
			
		|||
                    # tranport address bind errors - normally it's
 | 
			
		||||
                    # something silly like the wrong socket-address
 | 
			
		||||
                    # passed via a config or CLI Bo
 | 
			
		||||
                    entered_debug: bool = await _debug._maybe_enter_pm(
 | 
			
		||||
                    entered_debug: bool = await debug._maybe_enter_pm(
 | 
			
		||||
                        oserr,
 | 
			
		||||
                    )
 | 
			
		||||
                    if not entered_debug:
 | 
			
		||||
| 
						 | 
				
			
			@ -1431,7 +1473,7 @@ async def async_main(
 | 
			
		|||
                        waddr = wrap_address(addr)
 | 
			
		||||
                        assert waddr.is_valid
 | 
			
		||||
                    except AssertionError:
 | 
			
		||||
                        await _debug.pause()
 | 
			
		||||
                        await debug.pause()
 | 
			
		||||
 | 
			
		||||
                    async with get_registry(addr) as reg_portal:
 | 
			
		||||
                        for accept_addr in accept_addrs:
 | 
			
		||||
| 
						 | 
				
			
			@ -1460,8 +1502,7 @@ async def async_main(
 | 
			
		|||
                    await root_nursery.start(
 | 
			
		||||
                        partial(
 | 
			
		||||
                            _rpc.process_messages,
 | 
			
		||||
                            actor,
 | 
			
		||||
                            actor._parent_chan,
 | 
			
		||||
                            chan=actor._parent_chan,
 | 
			
		||||
                            shield=True,
 | 
			
		||||
                        )
 | 
			
		||||
                    )
 | 
			
		||||
| 
						 | 
				
			
			@ -1549,7 +1590,7 @@ async def async_main(
 | 
			
		|||
            # prevents any `infected_aio` actor from continuing
 | 
			
		||||
            # and any callbacks in the `ls` here WILL NOT be
 | 
			
		||||
            # called!!
 | 
			
		||||
            # await _debug.pause(shield=True)
 | 
			
		||||
            # await debug.pause(shield=True)
 | 
			
		||||
 | 
			
		||||
        ls.close()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1562,7 +1603,7 @@ async def async_main(
 | 
			
		|||
        #
 | 
			
		||||
        # if actor.name == 'brokerd.ib':
 | 
			
		||||
        #     with CancelScope(shield=True):
 | 
			
		||||
        #         await _debug.breakpoint()
 | 
			
		||||
        #         await debug.breakpoint()
 | 
			
		||||
 | 
			
		||||
        # Unregister actor from the registry-sys / registrar.
 | 
			
		||||
        if (
 | 
			
		||||
| 
						 | 
				
			
			@ -1751,7 +1792,7 @@ class Arbiter(Actor):
 | 
			
		|||
        waddr: Address = wrap_address(addr)
 | 
			
		||||
        if not waddr.is_valid:
 | 
			
		||||
            # should never be 0-dynamic-os-alloc
 | 
			
		||||
            await _debug.pause()
 | 
			
		||||
            await debug.pause()
 | 
			
		||||
 | 
			
		||||
        self._registry[uid] = addr
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -34,7 +34,7 @@ from typing import (
 | 
			
		|||
import trio
 | 
			
		||||
from trio import TaskStatus
 | 
			
		||||
 | 
			
		||||
from .devx._debug import (
 | 
			
		||||
from .devx.debug import (
 | 
			
		||||
    maybe_wait_for_debugger,
 | 
			
		||||
    acquire_debug_lock,
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -41,16 +41,24 @@ _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!
 | 
			
		||||
# -[ ] (eventually) mk wire-ready for monitoring?
 | 
			
		||||
_runtime_vars: dict[str, Any] = {
 | 
			
		||||
    '_debug_mode': False,
 | 
			
		||||
    '_is_root': False,
 | 
			
		||||
    '_root_mailbox': (None, None),
 | 
			
		||||
    # root of actor-process tree info
 | 
			
		||||
    '_is_root': False,  # bool
 | 
			
		||||
    '_root_mailbox': (None, None),  # tuple[str|None, str|None]
 | 
			
		||||
 | 
			
		||||
    # registrar info
 | 
			
		||||
    '_registry_addrs': [],
 | 
			
		||||
 | 
			
		||||
    '_is_infected_aio': False,
 | 
			
		||||
 | 
			
		||||
    # `debug_mode: bool` settings
 | 
			
		||||
    '_debug_mode': False,  # bool
 | 
			
		||||
    'repl_fixture': False,  # |AbstractContextManager[bool]
 | 
			
		||||
    # for `tractor.pause_from_sync()` & `breakpoint()` support
 | 
			
		||||
    'use_greenback': False,
 | 
			
		||||
 | 
			
		||||
    # infected-`asyncio`-mode: `trio` running as guest.
 | 
			
		||||
    '_is_infected_aio': False,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -426,8 +426,8 @@ class MsgStream(trio.abc.Channel):
 | 
			
		|||
            self._closed = re
 | 
			
		||||
 | 
			
		||||
        # if caught_eoc:
 | 
			
		||||
        #     # from .devx import _debug
 | 
			
		||||
        #     # await _debug.pause()
 | 
			
		||||
        #     # from .devx import debug
 | 
			
		||||
        #     # await debug.pause()
 | 
			
		||||
        #     with trio.CancelScope(shield=True):
 | 
			
		||||
        #         await rx_chan.aclose()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -31,7 +31,7 @@ import warnings
 | 
			
		|||
import trio
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
from .devx._debug import maybe_wait_for_debugger
 | 
			
		||||
from .devx.debug import maybe_wait_for_debugger
 | 
			
		||||
from ._addr import (
 | 
			
		||||
    UnwrappedAddress,
 | 
			
		||||
    mk_uuid,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,7 +26,7 @@ import os
 | 
			
		|||
import pathlib
 | 
			
		||||
 | 
			
		||||
import tractor
 | 
			
		||||
from tractor.devx._debug import (
 | 
			
		||||
from tractor.devx.debug import (
 | 
			
		||||
    BoxedMaybeException,
 | 
			
		||||
)
 | 
			
		||||
from .pytest import (
 | 
			
		||||
| 
						 | 
				
			
			@ -37,6 +37,9 @@ from .fault_simulation import (
 | 
			
		|||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# TODO, use dulwhich for this instead?
 | 
			
		||||
# -> we're going to likely need it (or something similar)
 | 
			
		||||
#   for supporting hot-coad reload feats eventually anyway!
 | 
			
		||||
def repodir() -> pathlib.Path:
 | 
			
		||||
    '''
 | 
			
		||||
    Return the abspath to the repo directory.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,70 @@
 | 
			
		|||
# 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/>.
 | 
			
		||||
 | 
			
		||||
'''
 | 
			
		||||
Random IPC addr generation for isolating
 | 
			
		||||
the discovery space between test sessions.
 | 
			
		||||
 | 
			
		||||
Might be eventually useful to expose as a util set from
 | 
			
		||||
our `tractor.discovery` subsys?
 | 
			
		||||
 | 
			
		||||
'''
 | 
			
		||||
import random
 | 
			
		||||
from typing import (
 | 
			
		||||
    Type,
 | 
			
		||||
)
 | 
			
		||||
from tractor import (
 | 
			
		||||
    _addr,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_rando_addr(
 | 
			
		||||
    tpt_proto: str,
 | 
			
		||||
    *,
 | 
			
		||||
 | 
			
		||||
    # choose random port at import time
 | 
			
		||||
    _rando_port: str = random.randint(1000, 9999)
 | 
			
		||||
 | 
			
		||||
) -> tuple[str, str|int]:
 | 
			
		||||
    '''
 | 
			
		||||
    Used to globally override the runtime to the
 | 
			
		||||
    per-test-session-dynamic addr so that all tests never conflict
 | 
			
		||||
    with any other actor tree using the default.
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
    addr_type: Type[_addr.Addres] = _addr._address_types[tpt_proto]
 | 
			
		||||
    def_reg_addr: tuple[str, int] = _addr._default_lo_addrs[tpt_proto]
 | 
			
		||||
 | 
			
		||||
    # this is the "unwrapped" form expected to be passed to
 | 
			
		||||
    # `.open_root_actor()` by test body.
 | 
			
		||||
    testrun_reg_addr: tuple[str, int|str]
 | 
			
		||||
    match tpt_proto:
 | 
			
		||||
        case 'tcp':
 | 
			
		||||
            testrun_reg_addr = (
 | 
			
		||||
                addr_type.def_bindspace,
 | 
			
		||||
                _rando_port,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        # NOTE, file-name uniqueness (no-collisions) will be based on
 | 
			
		||||
        # the runtime-directory and root (pytest-proc's) pid.
 | 
			
		||||
        case 'uds':
 | 
			
		||||
            testrun_reg_addr = addr_type.get_random().unwrap()
 | 
			
		||||
 | 
			
		||||
    # XXX, as sanity it should never the same as the default for the
 | 
			
		||||
    # host-singleton registry actor.
 | 
			
		||||
    assert def_reg_addr != testrun_reg_addr
 | 
			
		||||
 | 
			
		||||
    return testrun_reg_addr
 | 
			
		||||
| 
						 | 
				
			
			@ -26,29 +26,46 @@ from functools import (
 | 
			
		|||
import inspect
 | 
			
		||||
import platform
 | 
			
		||||
 | 
			
		||||
import pytest
 | 
			
		||||
import tractor
 | 
			
		||||
import trio
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def tractor_test(fn):
 | 
			
		||||
    '''
 | 
			
		||||
    Decorator for async test funcs to present them as "native"
 | 
			
		||||
    looking sync funcs runnable by `pytest` using `trio.run()`.
 | 
			
		||||
    Decorator for async test fns to decorator-wrap them as "native"
 | 
			
		||||
    looking sync funcs runnable by `pytest` and auto invoked with
 | 
			
		||||
    `trio.run()` (much like the `pytest-trio` plugin's approach).
 | 
			
		||||
 | 
			
		||||
    Use:
 | 
			
		||||
    Further the test fn body will be invoked AFTER booting the actor
 | 
			
		||||
    runtime, i.e. from inside a `tractor.open_root_actor()` block AND
 | 
			
		||||
    with various runtime and tooling parameters implicitly passed as
 | 
			
		||||
    requested by by the test session's config; see immediately below.
 | 
			
		||||
 | 
			
		||||
    @tractor_test
 | 
			
		||||
    async def test_whatever():
 | 
			
		||||
        await ...
 | 
			
		||||
    Basic deco use:
 | 
			
		||||
    ---------------
 | 
			
		||||
 | 
			
		||||
    If fixtures:
 | 
			
		||||
      @tractor_test
 | 
			
		||||
      async def test_whatever():
 | 
			
		||||
          await ...
 | 
			
		||||
 | 
			
		||||
        - ``reg_addr`` (a socket addr tuple where arbiter is listening)
 | 
			
		||||
        - ``loglevel`` (logging level passed to tractor internals)
 | 
			
		||||
        - ``start_method`` (subprocess spawning backend)
 | 
			
		||||
 | 
			
		||||
    are defined in the `pytest` fixture space they will be automatically
 | 
			
		||||
    injected to tests declaring these funcargs.
 | 
			
		||||
    Runtime config via special fixtures:
 | 
			
		||||
    ------------------------------------
 | 
			
		||||
    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)
 | 
			
		||||
    - `loglevel` (logging level passed to tractor internals)
 | 
			
		||||
    - `start_method` (subprocess spawning backend)
 | 
			
		||||
 | 
			
		||||
    (TODO support)
 | 
			
		||||
    - `tpt_proto` (IPC transport protocol key)
 | 
			
		||||
 | 
			
		||||
    they will be automatically injected to each test as normally
 | 
			
		||||
    expected as well as passed to the initial
 | 
			
		||||
    `tractor.open_root_actor()` funcargs.
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
    @wraps(fn)
 | 
			
		||||
    def wrapper(
 | 
			
		||||
| 
						 | 
				
			
			@ -111,3 +128,164 @@ def tractor_test(fn):
 | 
			
		|||
        return trio.run(main)
 | 
			
		||||
 | 
			
		||||
    return wrapper
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def pytest_addoption(
 | 
			
		||||
    parser: pytest.Parser,
 | 
			
		||||
):
 | 
			
		||||
    # parser.addoption(
 | 
			
		||||
    #     "--ll",
 | 
			
		||||
    #     action="store",
 | 
			
		||||
    #     dest='loglevel',
 | 
			
		||||
    #     default='ERROR', help="logging level to set when testing"
 | 
			
		||||
    # )
 | 
			
		||||
 | 
			
		||||
    parser.addoption(
 | 
			
		||||
        "--spawn-backend",
 | 
			
		||||
        action="store",
 | 
			
		||||
        dest='spawn_backend',
 | 
			
		||||
        default='trio',
 | 
			
		||||
        help="Processing spawning backend to use for test run",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    parser.addoption(
 | 
			
		||||
        "--tpdb",
 | 
			
		||||
        "--debug-mode",
 | 
			
		||||
        action="store_true",
 | 
			
		||||
        dest='tractor_debug_mode',
 | 
			
		||||
        # default=False,
 | 
			
		||||
        help=(
 | 
			
		||||
            'Enable a flag that can be used by tests to to set the '
 | 
			
		||||
            '`debug_mode: bool` for engaging the internal '
 | 
			
		||||
            'multi-proc debugger sys.'
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # provide which IPC transport protocols opting-in test suites
 | 
			
		||||
    # should accumulatively run against.
 | 
			
		||||
    parser.addoption(
 | 
			
		||||
        "--tpt-proto",
 | 
			
		||||
        nargs='+',  # accumulate-multiple-args
 | 
			
		||||
        action="store",
 | 
			
		||||
        dest='tpt_protos',
 | 
			
		||||
        default=['tcp'],
 | 
			
		||||
        help="Transport protocol to use under the `tractor.ipc.Channel`",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def pytest_configure(config):
 | 
			
		||||
    backend = config.option.spawn_backend
 | 
			
		||||
    tractor._spawn.try_set_start_method(backend)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture(scope='session')
 | 
			
		||||
def debug_mode(request) -> bool:
 | 
			
		||||
    '''
 | 
			
		||||
    Flag state for whether `--tpdb` (for `tractor`-py-debugger)
 | 
			
		||||
    was passed to the test run.
 | 
			
		||||
 | 
			
		||||
    Normally tests should pass this directly to `.open_root_actor()`
 | 
			
		||||
    to allow the user to opt into suite-wide crash handling.
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
    debug_mode: bool = request.config.option.tractor_debug_mode
 | 
			
		||||
    return debug_mode
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture(scope='session')
 | 
			
		||||
def spawn_backend(request) -> str:
 | 
			
		||||
    return request.config.option.spawn_backend
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture(scope='session')
 | 
			
		||||
def tpt_protos(request) -> list[str]:
 | 
			
		||||
 | 
			
		||||
    # allow quoting on CLI
 | 
			
		||||
    proto_keys: list[str] = [
 | 
			
		||||
        proto_key.replace('"', '').replace("'", "")
 | 
			
		||||
        for proto_key in request.config.option.tpt_protos
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    # ?TODO, eventually support multiple protos per test-sesh?
 | 
			
		||||
    if len(proto_keys) > 1:
 | 
			
		||||
        pytest.fail(
 | 
			
		||||
            'We only support one `--tpt-proto <key>` atm!\n'
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    # XXX ensure we support the protocol by name via lookup!
 | 
			
		||||
    for proto_key in proto_keys:
 | 
			
		||||
        addr_type = tractor._addr._address_types[proto_key]
 | 
			
		||||
        assert addr_type.proto_key == proto_key
 | 
			
		||||
 | 
			
		||||
    yield proto_keys
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture(
 | 
			
		||||
    scope='session',
 | 
			
		||||
    autouse=True,
 | 
			
		||||
)
 | 
			
		||||
def tpt_proto(
 | 
			
		||||
    tpt_protos: list[str],
 | 
			
		||||
) -> str:
 | 
			
		||||
    proto_key: str = tpt_protos[0]
 | 
			
		||||
 | 
			
		||||
    from tractor import _state
 | 
			
		||||
    if _state._def_tpt_proto != proto_key:
 | 
			
		||||
        _state._def_tpt_proto = proto_key
 | 
			
		||||
 | 
			
		||||
    yield proto_key
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture(scope='session')
 | 
			
		||||
def reg_addr(
 | 
			
		||||
    tpt_proto: str,
 | 
			
		||||
) -> tuple[str, int|str]:
 | 
			
		||||
    '''
 | 
			
		||||
    Deliver a test-sesh unique registry address such
 | 
			
		||||
    that each run's (tests which use this fixture) will
 | 
			
		||||
    have no conflicts/cross-talk when running simultaneously
 | 
			
		||||
    nor will interfere with other live `tractor` apps active
 | 
			
		||||
    on the same network-host (namespace).
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
    from tractor._testing.addr import get_rando_addr
 | 
			
		||||
    return get_rando_addr(
 | 
			
		||||
        tpt_proto=tpt_proto,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def pytest_generate_tests(
 | 
			
		||||
    metafunc: pytest.Metafunc,
 | 
			
		||||
):
 | 
			
		||||
    spawn_backend: str = metafunc.config.option.spawn_backend
 | 
			
		||||
 | 
			
		||||
    if not spawn_backend:
 | 
			
		||||
        # XXX some weird windows bug with `pytest`?
 | 
			
		||||
        spawn_backend = 'trio'
 | 
			
		||||
 | 
			
		||||
    # TODO: maybe just use the literal `._spawn.SpawnMethodKey`?
 | 
			
		||||
    assert spawn_backend in (
 | 
			
		||||
        'mp_spawn',
 | 
			
		||||
        'mp_forkserver',
 | 
			
		||||
        'trio',
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # NOTE: used-to-be-used-to dyanmically parametrize tests for when
 | 
			
		||||
    # you just passed --spawn-backend=`mp` on the cli, but now we expect
 | 
			
		||||
    # that cli input to be manually specified, BUT, maybe we'll do
 | 
			
		||||
    # something like this again in the future?
 | 
			
		||||
    if 'start_method' in metafunc.fixturenames:
 | 
			
		||||
        metafunc.parametrize(
 | 
			
		||||
            "start_method",
 | 
			
		||||
            [spawn_backend],
 | 
			
		||||
            scope='module',
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    # TODO, parametrize any `tpt_proto: str` declaring tests!
 | 
			
		||||
    # proto_tpts: list[str] = metafunc.config.option.proto_tpts
 | 
			
		||||
    # if 'tpt_proto' in metafunc.fixturenames:
 | 
			
		||||
    #     metafunc.parametrize(
 | 
			
		||||
    #         'tpt_proto',
 | 
			
		||||
    #         proto_tpts,  # TODO, double check this list usage!
 | 
			
		||||
    #         scope='module',
 | 
			
		||||
    #     )
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,7 +20,7 @@ Runtime "developer experience" utils and addons to aid our
 | 
			
		|||
and working with/on the actor runtime.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
from ._debug import (
 | 
			
		||||
from .debug import (
 | 
			
		||||
    maybe_wait_for_debugger as maybe_wait_for_debugger,
 | 
			
		||||
    acquire_debug_lock as acquire_debug_lock,
 | 
			
		||||
    breakpoint as breakpoint,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| 
						 | 
				
			
			@ -20,13 +20,18 @@ as it pertains to improving the grok-ability of our runtime!
 | 
			
		|||
 | 
			
		||||
'''
 | 
			
		||||
from __future__ import annotations
 | 
			
		||||
from contextlib import (
 | 
			
		||||
    _GeneratorContextManager,
 | 
			
		||||
    _AsyncGeneratorContextManager,
 | 
			
		||||
)
 | 
			
		||||
from functools import partial
 | 
			
		||||
import inspect
 | 
			
		||||
import textwrap
 | 
			
		||||
from types import (
 | 
			
		||||
    FrameType,
 | 
			
		||||
    FunctionType,
 | 
			
		||||
    MethodType,
 | 
			
		||||
    # CodeType,
 | 
			
		||||
    CodeType,
 | 
			
		||||
)
 | 
			
		||||
from typing import (
 | 
			
		||||
    Any,
 | 
			
		||||
| 
						 | 
				
			
			@ -34,6 +39,9 @@ from typing import (
 | 
			
		|||
    Type,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
import pdbp
 | 
			
		||||
from tractor.log import get_logger
 | 
			
		||||
import trio
 | 
			
		||||
from tractor.msg import (
 | 
			
		||||
    pretty_struct,
 | 
			
		||||
    NamespacePath,
 | 
			
		||||
| 
						 | 
				
			
			@ -41,6 +49,8 @@ from tractor.msg import (
 | 
			
		|||
import wrapt
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
log = get_logger(__name__)
 | 
			
		||||
 | 
			
		||||
# TODO: yeah, i don't love this and we should prolly just
 | 
			
		||||
# write a decorator that actually keeps a stupid ref to the func
 | 
			
		||||
# obj..
 | 
			
		||||
| 
						 | 
				
			
			@ -301,3 +311,70 @@ def api_frame(
 | 
			
		|||
#     error_set: set[BaseException],
 | 
			
		||||
# ) -> TracebackType:
 | 
			
		||||
#     ...
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def hide_runtime_frames() -> dict[FunctionType, CodeType]:
 | 
			
		||||
    '''
 | 
			
		||||
    Hide call-stack frames for various std-lib and `trio`-API primitives
 | 
			
		||||
    such that the tracebacks presented from our runtime are as minimized
 | 
			
		||||
    as possible, particularly from inside a `PdbREPL`.
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
    # XXX HACKZONE XXX
 | 
			
		||||
    #  hide exit stack frames on nurseries and cancel-scopes!
 | 
			
		||||
    # |_ so avoid seeing it when the `pdbp` REPL is first engaged from
 | 
			
		||||
    #    inside a `trio.open_nursery()` scope (with no line after it
 | 
			
		||||
    #    in before the block end??).
 | 
			
		||||
    #
 | 
			
		||||
    # TODO: FINALLY got this workin originally with
 | 
			
		||||
    #  `@pdbp.hideframe` around the `wrapper()` def embedded inside
 | 
			
		||||
    #  `_ki_protection_decoratior()`.. which is in the module:
 | 
			
		||||
    #  /home/goodboy/.virtualenvs/tractor311/lib/python3.11/site-packages/trio/_core/_ki.py
 | 
			
		||||
    #
 | 
			
		||||
    # -[ ] make an issue and patch for `trio` core? maybe linked
 | 
			
		||||
    #    to the long outstanding `pdb` one below?
 | 
			
		||||
    #   |_ it's funny that there's frame hiding throughout `._run.py`
 | 
			
		||||
    #      but not where it matters on the below exit funcs..
 | 
			
		||||
    #
 | 
			
		||||
    # -[ ] provide a patchset for the lonstanding
 | 
			
		||||
    #   |_ https://github.com/python-trio/trio/issues/1155
 | 
			
		||||
    #
 | 
			
		||||
    # -[ ] make a linked issue to ^ and propose allowing all the
 | 
			
		||||
    #     `._core._run` code to have their `__tracebackhide__` value
 | 
			
		||||
    #     configurable by a `RunVar` to allow getting scheduler frames
 | 
			
		||||
    #     if desired through configuration?
 | 
			
		||||
    #
 | 
			
		||||
    # -[ ] maybe dig into the core `pdb` issue why the extra frame is shown
 | 
			
		||||
    #      at all?
 | 
			
		||||
    #
 | 
			
		||||
    funcs: list[FunctionType] = [
 | 
			
		||||
        trio._core._run.NurseryManager.__aexit__,
 | 
			
		||||
        trio._core._run.CancelScope.__exit__,
 | 
			
		||||
         _GeneratorContextManager.__exit__,
 | 
			
		||||
         _AsyncGeneratorContextManager.__aexit__,
 | 
			
		||||
         _AsyncGeneratorContextManager.__aenter__,
 | 
			
		||||
         trio.Event.wait,
 | 
			
		||||
    ]
 | 
			
		||||
    func_list_str: str = textwrap.indent(
 | 
			
		||||
        "\n".join(f.__qualname__ for f in funcs),
 | 
			
		||||
        prefix=' |_ ',
 | 
			
		||||
    )
 | 
			
		||||
    log.devx(
 | 
			
		||||
        'Hiding the following runtime frames by default:\n'
 | 
			
		||||
        f'{func_list_str}\n'
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    codes: dict[FunctionType, CodeType] = {}
 | 
			
		||||
    for ref in funcs:
 | 
			
		||||
        # stash a pre-modified version of each ref's code-obj
 | 
			
		||||
        # so it can be reverted later if needed.
 | 
			
		||||
        codes[ref] = ref.__code__
 | 
			
		||||
        pdbp.hideframe(ref)
 | 
			
		||||
    #
 | 
			
		||||
    # pdbp.hideframe(trio._core._run.NurseryManager.__aexit__)
 | 
			
		||||
    # pdbp.hideframe(trio._core._run.CancelScope.__exit__)
 | 
			
		||||
    # pdbp.hideframe(_GeneratorContextManager.__exit__)
 | 
			
		||||
    # pdbp.hideframe(_AsyncGeneratorContextManager.__aexit__)
 | 
			
		||||
    # pdbp.hideframe(_AsyncGeneratorContextManager.__aenter__)
 | 
			
		||||
    # pdbp.hideframe(trio.Event.wait)
 | 
			
		||||
    return codes
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -49,7 +49,7 @@ from tractor import (
 | 
			
		|||
    _state,
 | 
			
		||||
    log as logmod,
 | 
			
		||||
)
 | 
			
		||||
from tractor.devx import _debug
 | 
			
		||||
from tractor.devx import debug
 | 
			
		||||
 | 
			
		||||
log = logmod.get_logger(__name__)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -82,7 +82,7 @@ def dump_task_tree() -> None:
 | 
			
		|||
    if (
 | 
			
		||||
        current_sigint_handler
 | 
			
		||||
        is not
 | 
			
		||||
        _debug.DebugStatus._trio_handler
 | 
			
		||||
        debug.DebugStatus._trio_handler
 | 
			
		||||
    ):
 | 
			
		||||
        sigint_handler_report: str = (
 | 
			
		||||
            'The default `trio` SIGINT handler was replaced?!'
 | 
			
		||||
| 
						 | 
				
			
			@ -237,7 +237,7 @@ def enable_stack_on_sig(
 | 
			
		|||
    try:
 | 
			
		||||
        import stackscope
 | 
			
		||||
    except ImportError:
 | 
			
		||||
        log.warning(
 | 
			
		||||
        log.error(
 | 
			
		||||
            '`stackscope` not installed for use in debug mode!'
 | 
			
		||||
        )
 | 
			
		||||
        return None
 | 
			
		||||
| 
						 | 
				
			
			@ -255,8 +255,8 @@ def enable_stack_on_sig(
 | 
			
		|||
        dump_tree_on_sig,
 | 
			
		||||
    )
 | 
			
		||||
    log.devx(
 | 
			
		||||
        'Enabling trace-trees on `SIGUSR1` '
 | 
			
		||||
        'since `stackscope` is installed @ \n'
 | 
			
		||||
        f'Enabling trace-trees on `SIGUSR1` '
 | 
			
		||||
        f'since `stackscope` is installed @ \n'
 | 
			
		||||
        f'{stackscope!r}\n\n'
 | 
			
		||||
        f'With `SIGUSR1` handler\n'
 | 
			
		||||
        f'|_{dump_tree_on_sig}\n'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,100 @@
 | 
			
		|||
# 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/>.
 | 
			
		||||
 | 
			
		||||
'''
 | 
			
		||||
Multi-actor debugging for da peeps!
 | 
			
		||||
 | 
			
		||||
'''
 | 
			
		||||
from __future__ import annotations
 | 
			
		||||
from tractor.log import get_logger
 | 
			
		||||
from ._repl import (
 | 
			
		||||
    PdbREPL as PdbREPL,
 | 
			
		||||
    mk_pdb as mk_pdb,
 | 
			
		||||
    TractorConfig as TractorConfig,
 | 
			
		||||
)
 | 
			
		||||
from ._tty_lock import (
 | 
			
		||||
    DebugStatus as DebugStatus,
 | 
			
		||||
    DebugStateError as DebugStateError,
 | 
			
		||||
)
 | 
			
		||||
from ._trace import (
 | 
			
		||||
    Lock as Lock,
 | 
			
		||||
    _pause_msg as _pause_msg,
 | 
			
		||||
    _repl_fail_msg as _repl_fail_msg,
 | 
			
		||||
    _set_trace as _set_trace,
 | 
			
		||||
    _sync_pause_from_builtin as _sync_pause_from_builtin,
 | 
			
		||||
    breakpoint as breakpoint,
 | 
			
		||||
    maybe_init_greenback as maybe_init_greenback,
 | 
			
		||||
    maybe_import_greenback as maybe_import_greenback,
 | 
			
		||||
    pause as pause,
 | 
			
		||||
    pause_from_sync as pause_from_sync,
 | 
			
		||||
)
 | 
			
		||||
from ._post_mortem import (
 | 
			
		||||
    BoxedMaybeException as BoxedMaybeException,
 | 
			
		||||
    maybe_open_crash_handler as maybe_open_crash_handler,
 | 
			
		||||
    open_crash_handler as open_crash_handler,
 | 
			
		||||
    post_mortem as post_mortem,
 | 
			
		||||
    _crash_msg as _crash_msg,
 | 
			
		||||
    _maybe_enter_pm as _maybe_enter_pm,
 | 
			
		||||
)
 | 
			
		||||
from ._sync import (
 | 
			
		||||
    maybe_wait_for_debugger as maybe_wait_for_debugger,
 | 
			
		||||
    acquire_debug_lock as acquire_debug_lock,
 | 
			
		||||
)
 | 
			
		||||
from ._sigint import (
 | 
			
		||||
    sigint_shield as sigint_shield,
 | 
			
		||||
    _ctlc_ignore_header as _ctlc_ignore_header
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
log = get_logger(__name__)
 | 
			
		||||
 | 
			
		||||
# ----------------
 | 
			
		||||
# XXX PKG TODO XXX
 | 
			
		||||
# ----------------
 | 
			
		||||
# refine the internal impl and APIs!
 | 
			
		||||
#
 | 
			
		||||
# -[ ] rework `._pause()` and it's branch-cases for root vs.
 | 
			
		||||
#     subactor:
 | 
			
		||||
#  -[ ] `._pause_from_root()` + `_pause_from_subactor()`?
 | 
			
		||||
#  -[ ]  do the de-factor based on bg-thread usage in
 | 
			
		||||
#    `.pause_from_sync()` & `_pause_from_bg_root_thread()`.
 | 
			
		||||
#  -[ ] drop `debug_func == None` case which is confusing af..
 | 
			
		||||
#  -[ ]  factor out `_enter_repl_sync()` into a util func for calling
 | 
			
		||||
#    the `_set_trace()` / `_post_mortem()` APIs?
 | 
			
		||||
#
 | 
			
		||||
# -[ ] figure out if we need `acquire_debug_lock()` and/or re-implement
 | 
			
		||||
#    it as part of the `.pause_from_sync()` rework per above?
 | 
			
		||||
#
 | 
			
		||||
# -[ ] pair the `._pause_from_subactor()` impl with a "debug nursery"
 | 
			
		||||
#   that's dynamically allocated inside the `._rpc` task thus
 | 
			
		||||
#   avoiding the `._service_n.start()` usage for the IPC request?
 | 
			
		||||
#  -[ ] see the TODO inside `._rpc._errors_relayed_via_ipc()`
 | 
			
		||||
#
 | 
			
		||||
# -[ ] impl a `open_debug_request()` which encaps all
 | 
			
		||||
#   `request_root_stdio_lock()` task scheduling deats
 | 
			
		||||
#   + `DebugStatus` state mgmt; which should prolly be re-branded as
 | 
			
		||||
#   a `DebugRequest` type anyway AND with suppoort for bg-thread
 | 
			
		||||
#   (from root actor) usage?
 | 
			
		||||
#
 | 
			
		||||
# -[ ] handle the `xonsh` case for bg-root-threads in the SIGINT
 | 
			
		||||
#     handler!
 | 
			
		||||
#   -[ ] do we need to do the same for subactors?
 | 
			
		||||
#   -[ ] make the failing tests finally pass XD
 | 
			
		||||
#
 | 
			
		||||
# -[ ] simplify `maybe_wait_for_debugger()` to be a root-task only
 | 
			
		||||
#     API?
 | 
			
		||||
#   -[ ] currently it's implemented as that so might as well make it
 | 
			
		||||
#     formal?
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,410 @@
 | 
			
		|||
# 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/>.
 | 
			
		||||
 | 
			
		||||
'''
 | 
			
		||||
Post-mortem debugging APIs and surrounding machinery for both
 | 
			
		||||
sync and async contexts.
 | 
			
		||||
 | 
			
		||||
Generally we maintain the same semantics a `pdb.post.mortem()` but
 | 
			
		||||
with actor-tree-wide sync/cooperation around any (sub)actor's use of
 | 
			
		||||
the root's TTY.
 | 
			
		||||
 | 
			
		||||
'''
 | 
			
		||||
from __future__ import annotations
 | 
			
		||||
import bdb
 | 
			
		||||
from contextlib import (
 | 
			
		||||
    AbstractContextManager,
 | 
			
		||||
    contextmanager as cm,
 | 
			
		||||
    nullcontext,
 | 
			
		||||
)
 | 
			
		||||
from functools import (
 | 
			
		||||
    partial,
 | 
			
		||||
)
 | 
			
		||||
import inspect
 | 
			
		||||
import sys
 | 
			
		||||
import traceback
 | 
			
		||||
from typing import (
 | 
			
		||||
    Callable,
 | 
			
		||||
    Sequence,
 | 
			
		||||
    Type,
 | 
			
		||||
    TYPE_CHECKING,
 | 
			
		||||
)
 | 
			
		||||
from types import (
 | 
			
		||||
    TracebackType,
 | 
			
		||||
    FrameType,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
from msgspec import Struct
 | 
			
		||||
import trio
 | 
			
		||||
from tractor._exceptions import (
 | 
			
		||||
    NoRuntime,
 | 
			
		||||
)
 | 
			
		||||
from tractor import _state
 | 
			
		||||
from tractor._state import (
 | 
			
		||||
    current_actor,
 | 
			
		||||
    debug_mode,
 | 
			
		||||
)
 | 
			
		||||
from tractor.log import get_logger
 | 
			
		||||
from tractor._exceptions import (
 | 
			
		||||
    is_multi_cancelled,
 | 
			
		||||
)
 | 
			
		||||
from ._trace import (
 | 
			
		||||
    _pause,
 | 
			
		||||
)
 | 
			
		||||
from ._tty_lock import (
 | 
			
		||||
    DebugStatus,
 | 
			
		||||
)
 | 
			
		||||
from ._repl import (
 | 
			
		||||
    PdbREPL,
 | 
			
		||||
    mk_pdb,
 | 
			
		||||
    TractorConfig as TractorConfig,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
    from trio.lowlevel import Task
 | 
			
		||||
    from tractor._runtime import (
 | 
			
		||||
        Actor,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
_crash_msg: str = (
 | 
			
		||||
    'Opening a pdb REPL in crashed actor'
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
log = get_logger(__package__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BoxedMaybeException(Struct):
 | 
			
		||||
    '''
 | 
			
		||||
    Box a maybe-exception for post-crash introspection usage
 | 
			
		||||
    from the body of a `open_crash_handler()` scope.
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
    value: BaseException|None = None
 | 
			
		||||
 | 
			
		||||
    # handler can suppress crashes dynamically
 | 
			
		||||
    raise_on_exit: bool|Sequence[Type[BaseException]] = True
 | 
			
		||||
 | 
			
		||||
    def pformat(self) -> str:
 | 
			
		||||
        '''
 | 
			
		||||
        Repr the boxed `.value` error in more-than-string
 | 
			
		||||
        repr form.
 | 
			
		||||
 | 
			
		||||
        '''
 | 
			
		||||
        if not self.value:
 | 
			
		||||
            return f'<{type(self).__name__}( .value=None )>'
 | 
			
		||||
 | 
			
		||||
        return (
 | 
			
		||||
            f'<{type(self.value).__name__}(\n'
 | 
			
		||||
            f' |_.value = {self.value}\n'
 | 
			
		||||
            f')>\n'
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    __repr__ = pformat
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _post_mortem(
 | 
			
		||||
    repl: PdbREPL,  # normally passed by `_pause()`
 | 
			
		||||
 | 
			
		||||
    # XXX all `partial`-ed in by `post_mortem()` below!
 | 
			
		||||
    tb: TracebackType,
 | 
			
		||||
    api_frame: FrameType,
 | 
			
		||||
 | 
			
		||||
    shield: bool = False,
 | 
			
		||||
    hide_tb: bool = True,
 | 
			
		||||
 | 
			
		||||
    # maybe pre/post REPL entry
 | 
			
		||||
    repl_fixture: (
 | 
			
		||||
        AbstractContextManager[bool]
 | 
			
		||||
        |None
 | 
			
		||||
    ) = None,
 | 
			
		||||
 | 
			
		||||
    boxed_maybe_exc: BoxedMaybeException|None = None,
 | 
			
		||||
 | 
			
		||||
) -> None:
 | 
			
		||||
    '''
 | 
			
		||||
    Enter the ``pdbpp`` port mortem entrypoint using our custom
 | 
			
		||||
    debugger instance.
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
    __tracebackhide__: bool = hide_tb
 | 
			
		||||
 | 
			
		||||
    # maybe enter any user fixture
 | 
			
		||||
    enter_repl: bool = DebugStatus.maybe_enter_repl_fixture(
 | 
			
		||||
        repl=repl,
 | 
			
		||||
        repl_fixture=repl_fixture,
 | 
			
		||||
        boxed_maybe_exc=boxed_maybe_exc,
 | 
			
		||||
    )
 | 
			
		||||
    if not enter_repl:
 | 
			
		||||
        return
 | 
			
		||||
    try:
 | 
			
		||||
        actor: Actor = current_actor()
 | 
			
		||||
        actor_repr: str = str(actor.uid)
 | 
			
		||||
        # ^TODO, instead a nice runtime-info + maddr + uid?
 | 
			
		||||
        # -[ ] impl a `Actor.__repr()__`??
 | 
			
		||||
        #  |_ <task>:<thread> @ <actor>
 | 
			
		||||
 | 
			
		||||
    except NoRuntime:
 | 
			
		||||
        actor_repr: str = '<no-actor-runtime?>'
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        task_repr: Task = trio.lowlevel.current_task()
 | 
			
		||||
    except RuntimeError:
 | 
			
		||||
        task_repr: str = '<unknown-Task>'
 | 
			
		||||
 | 
			
		||||
    # TODO: print the actor supervion tree up to the root
 | 
			
		||||
    # here! Bo
 | 
			
		||||
    log.pdb(
 | 
			
		||||
        f'{_crash_msg}\n'
 | 
			
		||||
        f'x>(\n'
 | 
			
		||||
        f' |_ {task_repr} @ {actor_repr}\n'
 | 
			
		||||
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # XXX NOTE(s) on `pdbp.xpm()` version..
 | 
			
		||||
    #
 | 
			
		||||
    # - seems to lose the up-stack tb-info?
 | 
			
		||||
    # - currently we're (only) replacing this from `pdbp.xpm()`
 | 
			
		||||
    #   to add the `end=''` to the print XD
 | 
			
		||||
    #
 | 
			
		||||
    print(traceback.format_exc(), end='')
 | 
			
		||||
    caller_frame: FrameType = api_frame.f_back
 | 
			
		||||
 | 
			
		||||
    # NOTE, see the impl details of these in the lib to
 | 
			
		||||
    # understand usage:
 | 
			
		||||
    # - `pdbp.post_mortem()`
 | 
			
		||||
    # - `pdbp.xps()`
 | 
			
		||||
    # - `bdb.interaction()`
 | 
			
		||||
    repl.reset()
 | 
			
		||||
    repl.interaction(
 | 
			
		||||
        frame=caller_frame,
 | 
			
		||||
        # frame=None,
 | 
			
		||||
        traceback=tb,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # XXX NOTE XXX: this is abs required to avoid hangs!
 | 
			
		||||
    #
 | 
			
		||||
    # Since we presume the post-mortem was enaged to
 | 
			
		||||
    # a task-ending error, we MUST release the local REPL request
 | 
			
		||||
    # so that not other local task nor the root remains blocked!
 | 
			
		||||
    DebugStatus.release()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def post_mortem(
 | 
			
		||||
    *,
 | 
			
		||||
    tb: TracebackType|None = None,
 | 
			
		||||
    api_frame: FrameType|None = None,
 | 
			
		||||
    hide_tb: bool = False,
 | 
			
		||||
 | 
			
		||||
    # TODO: support shield here just like in `pause()`?
 | 
			
		||||
    # shield: bool = False,
 | 
			
		||||
 | 
			
		||||
    **_pause_kwargs,
 | 
			
		||||
 | 
			
		||||
) -> None:
 | 
			
		||||
    '''
 | 
			
		||||
    Our builtin async equivalient of `pdb.post_mortem()` which can be
 | 
			
		||||
    used inside exception handlers.
 | 
			
		||||
 | 
			
		||||
    It's also used for the crash handler when `debug_mode == True` ;)
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
    __tracebackhide__: bool = hide_tb
 | 
			
		||||
 | 
			
		||||
    tb: TracebackType = tb or sys.exc_info()[2]
 | 
			
		||||
 | 
			
		||||
    # TODO: do upward stack scan for highest @api_frame and
 | 
			
		||||
    # use its parent frame as the expected user-app code
 | 
			
		||||
    # interact point.
 | 
			
		||||
    api_frame: FrameType = api_frame or inspect.currentframe()
 | 
			
		||||
 | 
			
		||||
    # TODO, move to submod `._pausing` or ._api? _trace
 | 
			
		||||
    await _pause(
 | 
			
		||||
        debug_func=partial(
 | 
			
		||||
            _post_mortem,
 | 
			
		||||
            api_frame=api_frame,
 | 
			
		||||
            tb=tb,
 | 
			
		||||
        ),
 | 
			
		||||
        hide_tb=hide_tb,
 | 
			
		||||
        **_pause_kwargs
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def _maybe_enter_pm(
 | 
			
		||||
    err: BaseException,
 | 
			
		||||
    *,
 | 
			
		||||
    tb: TracebackType|None = None,
 | 
			
		||||
    api_frame: FrameType|None = None,
 | 
			
		||||
    hide_tb: bool = False,
 | 
			
		||||
 | 
			
		||||
    # only enter debugger REPL when returns `True`
 | 
			
		||||
    debug_filter: Callable[
 | 
			
		||||
        [BaseException|BaseExceptionGroup],
 | 
			
		||||
        bool,
 | 
			
		||||
    ] = lambda err: not is_multi_cancelled(err),
 | 
			
		||||
    **_pause_kws,
 | 
			
		||||
):
 | 
			
		||||
    if (
 | 
			
		||||
        debug_mode()
 | 
			
		||||
 | 
			
		||||
        # NOTE: don't enter debug mode recursively after quitting pdb
 | 
			
		||||
        # Iow, don't re-enter the repl if the `quit` command was issued
 | 
			
		||||
        # by the user.
 | 
			
		||||
        and not isinstance(err, bdb.BdbQuit)
 | 
			
		||||
 | 
			
		||||
        # XXX: if the error is the likely result of runtime-wide
 | 
			
		||||
        # cancellation, we don't want to enter the debugger since
 | 
			
		||||
        # there's races between when the parent actor has killed all
 | 
			
		||||
        # comms and when the child tries to contact said parent to
 | 
			
		||||
        # acquire the tty lock.
 | 
			
		||||
 | 
			
		||||
        # Really we just want to mostly avoid catching KBIs here so there
 | 
			
		||||
        # might be a simpler check we can do?
 | 
			
		||||
        and
 | 
			
		||||
        debug_filter(err)
 | 
			
		||||
    ):
 | 
			
		||||
        api_frame: FrameType = api_frame or inspect.currentframe()
 | 
			
		||||
        tb: TracebackType = tb or sys.exc_info()[2]
 | 
			
		||||
        await post_mortem(
 | 
			
		||||
            api_frame=api_frame,
 | 
			
		||||
            tb=tb,
 | 
			
		||||
            **_pause_kws,
 | 
			
		||||
        )
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    else:
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# TODO: better naming and what additionals?
 | 
			
		||||
# - [ ] optional runtime plugging?
 | 
			
		||||
# - [ ] detection for sync vs. async code?
 | 
			
		||||
# - [ ] specialized REPL entry when in distributed mode?
 | 
			
		||||
# -[x] hide tb by def
 | 
			
		||||
# - [x] allow ignoring kbi Bo
 | 
			
		||||
@cm
 | 
			
		||||
def open_crash_handler(
 | 
			
		||||
    catch: set[BaseException] = {
 | 
			
		||||
        BaseException,
 | 
			
		||||
    },
 | 
			
		||||
    ignore: set[BaseException] = {
 | 
			
		||||
        KeyboardInterrupt,
 | 
			
		||||
        trio.Cancelled,
 | 
			
		||||
    },
 | 
			
		||||
    hide_tb: bool = True,
 | 
			
		||||
 | 
			
		||||
    repl_fixture: (
 | 
			
		||||
        AbstractContextManager[bool]  # pre/post REPL entry
 | 
			
		||||
        |None
 | 
			
		||||
    ) = None,
 | 
			
		||||
    raise_on_exit: bool|Sequence[Type[BaseException]] = True,
 | 
			
		||||
):
 | 
			
		||||
    '''
 | 
			
		||||
    Generic "post mortem" crash handler using `pdbp` REPL debugger.
 | 
			
		||||
 | 
			
		||||
    We expose this as a CLI framework addon to both `click` and
 | 
			
		||||
    `typer` users so they can quickly wrap cmd endpoints which get
 | 
			
		||||
    automatically wrapped to use the runtime's `debug_mode: bool`
 | 
			
		||||
    AND `pdbp.pm()` around any code that is PRE-runtime entry
 | 
			
		||||
    - any sync code which runs BEFORE the main call to
 | 
			
		||||
      `trio.run()`.
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
    __tracebackhide__: bool = hide_tb
 | 
			
		||||
 | 
			
		||||
    # TODO, yield a `outcome.Error`-like boxed type?
 | 
			
		||||
    # -[~] use `outcome.Value/Error` X-> frozen!
 | 
			
		||||
    # -[x] write our own..?
 | 
			
		||||
    # -[ ] consider just wtv is used by `pytest.raises()`?
 | 
			
		||||
    #
 | 
			
		||||
    boxed_maybe_exc = BoxedMaybeException(
 | 
			
		||||
        raise_on_exit=raise_on_exit,
 | 
			
		||||
    )
 | 
			
		||||
    err: BaseException
 | 
			
		||||
    try:
 | 
			
		||||
        yield boxed_maybe_exc
 | 
			
		||||
    except tuple(catch) as err:
 | 
			
		||||
        boxed_maybe_exc.value = err
 | 
			
		||||
        if (
 | 
			
		||||
            type(err) not in ignore
 | 
			
		||||
            and
 | 
			
		||||
            not is_multi_cancelled(
 | 
			
		||||
                err,
 | 
			
		||||
                ignore_nested=ignore
 | 
			
		||||
            )
 | 
			
		||||
        ):
 | 
			
		||||
            try:
 | 
			
		||||
                # use our re-impl-ed version of `pdbp.xpm()`
 | 
			
		||||
                _post_mortem(
 | 
			
		||||
                    repl=mk_pdb(),
 | 
			
		||||
                    tb=sys.exc_info()[2],
 | 
			
		||||
                    api_frame=inspect.currentframe().f_back,
 | 
			
		||||
                    hide_tb=hide_tb,
 | 
			
		||||
 | 
			
		||||
                    repl_fixture=repl_fixture,
 | 
			
		||||
                    boxed_maybe_exc=boxed_maybe_exc,
 | 
			
		||||
                )
 | 
			
		||||
            except bdb.BdbQuit:
 | 
			
		||||
                __tracebackhide__: bool = False
 | 
			
		||||
                raise err
 | 
			
		||||
 | 
			
		||||
        if (
 | 
			
		||||
            raise_on_exit is True
 | 
			
		||||
            or (
 | 
			
		||||
                raise_on_exit is not False
 | 
			
		||||
                and (
 | 
			
		||||
                    set(raise_on_exit)
 | 
			
		||||
                    and
 | 
			
		||||
                    type(err) in raise_on_exit
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            and
 | 
			
		||||
            boxed_maybe_exc.raise_on_exit == raise_on_exit
 | 
			
		||||
        ):
 | 
			
		||||
            raise err
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@cm
 | 
			
		||||
def maybe_open_crash_handler(
 | 
			
		||||
    pdb: bool|None = None,
 | 
			
		||||
    hide_tb: bool = True,
 | 
			
		||||
 | 
			
		||||
    **kwargs,
 | 
			
		||||
):
 | 
			
		||||
    '''
 | 
			
		||||
    Same as `open_crash_handler()` but with bool input flag
 | 
			
		||||
    to allow conditional handling.
 | 
			
		||||
 | 
			
		||||
    Normally this is used with CLI endpoints such that if the --pdb
 | 
			
		||||
    flag is passed the pdb REPL is engaed on any crashes B)
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
    __tracebackhide__: bool = hide_tb
 | 
			
		||||
 | 
			
		||||
    if pdb is None:
 | 
			
		||||
        pdb: bool = _state.is_debug_mode()
 | 
			
		||||
 | 
			
		||||
    rtctx = nullcontext(
 | 
			
		||||
        enter_result=BoxedMaybeException()
 | 
			
		||||
    )
 | 
			
		||||
    if pdb:
 | 
			
		||||
        rtctx = open_crash_handler(
 | 
			
		||||
            hide_tb=hide_tb,
 | 
			
		||||
            **kwargs,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    with rtctx as boxed_maybe_exc:
 | 
			
		||||
        yield boxed_maybe_exc
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,207 @@
 | 
			
		|||
# 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/>.
 | 
			
		||||
 | 
			
		||||
'''
 | 
			
		||||
`pdpp.Pdb` extentions/customization and other delegate usage.
 | 
			
		||||
 | 
			
		||||
'''
 | 
			
		||||
from functools import (
 | 
			
		||||
    cached_property,
 | 
			
		||||
)
 | 
			
		||||
import os
 | 
			
		||||
 | 
			
		||||
import pdbp
 | 
			
		||||
from tractor._state import (
 | 
			
		||||
    is_root_process,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
from ._tty_lock import (
 | 
			
		||||
    Lock,
 | 
			
		||||
    DebugStatus,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TractorConfig(pdbp.DefaultConfig):
 | 
			
		||||
    '''
 | 
			
		||||
    Custom `pdbp` config which tries to use the best tradeoff
 | 
			
		||||
    between pretty and minimal.
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
    use_pygments: bool = True
 | 
			
		||||
    sticky_by_default: bool = False
 | 
			
		||||
    enable_hidden_frames: bool = True
 | 
			
		||||
 | 
			
		||||
    # much thanks @mdmintz for the hot tip!
 | 
			
		||||
    # fixes line spacing issue when resizing terminal B)
 | 
			
		||||
    truncate_long_lines: bool = False
 | 
			
		||||
 | 
			
		||||
    # ------ - ------
 | 
			
		||||
    # our own custom config vars mostly
 | 
			
		||||
    # for syncing with the actor tree's singleton
 | 
			
		||||
    # TTY `Lock`.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PdbREPL(pdbp.Pdb):
 | 
			
		||||
    '''
 | 
			
		||||
    Add teardown hooks and local state describing any
 | 
			
		||||
    ongoing TTY `Lock` request dialog.
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
    # override the pdbp config with our coolio one
 | 
			
		||||
    # NOTE: this is only loaded when no `~/.pdbrc` exists
 | 
			
		||||
    # so we should prolly pass it into the .__init__() instead?
 | 
			
		||||
    # i dunno, see the `DefaultFactory` and `pdb.Pdb` impls.
 | 
			
		||||
    DefaultConfig = TractorConfig
 | 
			
		||||
 | 
			
		||||
    status = DebugStatus
 | 
			
		||||
 | 
			
		||||
    # NOTE: see details in stdlib's `bdb.py`
 | 
			
		||||
    # def user_exception(self, frame, exc_info):
 | 
			
		||||
    #     '''
 | 
			
		||||
    #     Called when we stop on an exception.
 | 
			
		||||
    #     '''
 | 
			
		||||
    #     log.warning(
 | 
			
		||||
    #         'Exception during REPL sesh\n\n'
 | 
			
		||||
    #         f'{frame}\n\n'
 | 
			
		||||
    #         f'{exc_info}\n\n'
 | 
			
		||||
    #     )
 | 
			
		||||
 | 
			
		||||
    # NOTE: this actually hooks but i don't see anyway to detect
 | 
			
		||||
    # if an error was caught.. this is why currently we just always
 | 
			
		||||
    # call `DebugStatus.release` inside `_post_mortem()`.
 | 
			
		||||
    # def preloop(self):
 | 
			
		||||
    #     print('IN PRELOOP')
 | 
			
		||||
    #     super().preloop()
 | 
			
		||||
 | 
			
		||||
    # TODO: cleaner re-wrapping of all this?
 | 
			
		||||
    # -[ ] figure out how to disallow recursive .set_trace() entry
 | 
			
		||||
    #     since that'll cause deadlock for us.
 | 
			
		||||
    # -[ ] maybe a `@cm` to call `super().<same_meth_name>()`?
 | 
			
		||||
    # -[ ] look at hooking into the `pp` hook specially with our
 | 
			
		||||
    #     own set of pretty-printers?
 | 
			
		||||
    #    * `.pretty_struct.Struct.pformat()`
 | 
			
		||||
    #    * `.pformat(MsgType.pld)`
 | 
			
		||||
    #    * `.pformat(Error.tb_str)`?
 | 
			
		||||
    #    * .. maybe more?
 | 
			
		||||
    #
 | 
			
		||||
    def set_continue(self):
 | 
			
		||||
        try:
 | 
			
		||||
            super().set_continue()
 | 
			
		||||
        finally:
 | 
			
		||||
            # NOTE: for subactors the stdio lock is released via the
 | 
			
		||||
            # allocated RPC locker task, so for root we have to do it
 | 
			
		||||
            # manually.
 | 
			
		||||
            if (
 | 
			
		||||
                is_root_process()
 | 
			
		||||
                and
 | 
			
		||||
                Lock._debug_lock.locked()
 | 
			
		||||
                and
 | 
			
		||||
                DebugStatus.is_main_trio_thread()
 | 
			
		||||
            ):
 | 
			
		||||
                # Lock.release(raise_on_thread=False)
 | 
			
		||||
                Lock.release()
 | 
			
		||||
 | 
			
		||||
            # XXX AFTER `Lock.release()` for root local repl usage
 | 
			
		||||
            DebugStatus.release()
 | 
			
		||||
 | 
			
		||||
    def set_quit(self):
 | 
			
		||||
        try:
 | 
			
		||||
            super().set_quit()
 | 
			
		||||
        finally:
 | 
			
		||||
            if (
 | 
			
		||||
                is_root_process()
 | 
			
		||||
                and
 | 
			
		||||
                Lock._debug_lock.locked()
 | 
			
		||||
                and
 | 
			
		||||
                DebugStatus.is_main_trio_thread()
 | 
			
		||||
            ):
 | 
			
		||||
                # Lock.release(raise_on_thread=False)
 | 
			
		||||
                Lock.release()
 | 
			
		||||
 | 
			
		||||
            # XXX after `Lock.release()` for root local repl usage
 | 
			
		||||
            DebugStatus.release()
 | 
			
		||||
 | 
			
		||||
    # XXX NOTE: we only override this because apparently the stdlib pdb
 | 
			
		||||
    # bois likes to touch the SIGINT handler as much as i like to touch
 | 
			
		||||
    # my d$%&.
 | 
			
		||||
    def _cmdloop(self):
 | 
			
		||||
        self.cmdloop()
 | 
			
		||||
 | 
			
		||||
    @cached_property
 | 
			
		||||
    def shname(self) -> str | None:
 | 
			
		||||
        '''
 | 
			
		||||
        Attempt to return the login shell name with a special check for
 | 
			
		||||
        the infamous `xonsh` since it seems to have some issues much
 | 
			
		||||
        different from std shells when it comes to flushing the prompt?
 | 
			
		||||
 | 
			
		||||
        '''
 | 
			
		||||
        # SUPER HACKY and only really works if `xonsh` is not used
 | 
			
		||||
        # before spawning further sub-shells..
 | 
			
		||||
        shpath = os.getenv('SHELL', None)
 | 
			
		||||
 | 
			
		||||
        if shpath:
 | 
			
		||||
            if (
 | 
			
		||||
                os.getenv('XONSH_LOGIN', default=False)
 | 
			
		||||
                or 'xonsh' in shpath
 | 
			
		||||
            ):
 | 
			
		||||
                return 'xonsh'
 | 
			
		||||
 | 
			
		||||
            return os.path.basename(shpath)
 | 
			
		||||
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def mk_pdb() -> PdbREPL:
 | 
			
		||||
    '''
 | 
			
		||||
    Deliver a new `PdbREPL`: a multi-process safe `pdbp.Pdb`-variant
 | 
			
		||||
    using the magic of `tractor`'s SC-safe IPC.
 | 
			
		||||
 | 
			
		||||
    B)
 | 
			
		||||
 | 
			
		||||
    Our `pdb.Pdb` subtype accomplishes multi-process safe debugging
 | 
			
		||||
    by:
 | 
			
		||||
 | 
			
		||||
    - mutexing access to the root process' std-streams (& thus parent
 | 
			
		||||
      process TTY) via an IPC managed `Lock` singleton per
 | 
			
		||||
      actor-process tree.
 | 
			
		||||
 | 
			
		||||
    - temporarily overriding any subactor's SIGINT handler to shield
 | 
			
		||||
      during live REPL sessions in sub-actors such that cancellation
 | 
			
		||||
      is never (mistakenly) triggered by a ctrl-c and instead only by
 | 
			
		||||
      explicit runtime API requests or after the
 | 
			
		||||
      `pdb.Pdb.interaction()` call has returned.
 | 
			
		||||
 | 
			
		||||
    FURTHER, the `pdbp.Pdb` instance is configured to be `trio`
 | 
			
		||||
    "compatible" from a SIGINT handling perspective; we mask out
 | 
			
		||||
    the default `pdb` handler and instead apply `trio`s default
 | 
			
		||||
    which mostly addresses all issues described in:
 | 
			
		||||
 | 
			
		||||
     - https://github.com/python-trio/trio/issues/1155
 | 
			
		||||
 | 
			
		||||
    The instance returned from this factory should always be
 | 
			
		||||
    preferred over the default `pdb[p].set_trace()` whenever using
 | 
			
		||||
    a `pdb` REPL inside a `trio` based runtime.
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
    pdb = PdbREPL()
 | 
			
		||||
 | 
			
		||||
    # XXX: These are the important flags mentioned in
 | 
			
		||||
    # https://github.com/python-trio/trio/issues/1155
 | 
			
		||||
    # which resolve the traceback spews to console.
 | 
			
		||||
    pdb.allow_kbdint = True
 | 
			
		||||
    pdb.nosigint = True
 | 
			
		||||
    return pdb
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,333 @@
 | 
			
		|||
# 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/>.
 | 
			
		||||
 | 
			
		||||
'''
 | 
			
		||||
A custom SIGINT handler which mainly shields actor (task)
 | 
			
		||||
cancellation during REPL interaction.
 | 
			
		||||
 | 
			
		||||
'''
 | 
			
		||||
from __future__ import annotations
 | 
			
		||||
from typing import (
 | 
			
		||||
    TYPE_CHECKING,
 | 
			
		||||
)
 | 
			
		||||
import trio
 | 
			
		||||
from tractor.log import get_logger
 | 
			
		||||
from tractor._state import (
 | 
			
		||||
    current_actor,
 | 
			
		||||
    is_root_process,
 | 
			
		||||
)
 | 
			
		||||
from ._repl import (
 | 
			
		||||
    PdbREPL,
 | 
			
		||||
)
 | 
			
		||||
from ._tty_lock import (
 | 
			
		||||
    any_connected_locker_child,
 | 
			
		||||
    DebugStatus,
 | 
			
		||||
    Lock,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
    from tractor.ipc import (
 | 
			
		||||
        Channel,
 | 
			
		||||
    )
 | 
			
		||||
    from tractor._runtime import (
 | 
			
		||||
        Actor,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
log = get_logger(__name__)
 | 
			
		||||
 | 
			
		||||
_ctlc_ignore_header: str = (
 | 
			
		||||
    'Ignoring SIGINT while debug REPL in use'
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def sigint_shield(
 | 
			
		||||
    signum: int,
 | 
			
		||||
    frame: 'frame',  # type: ignore # noqa
 | 
			
		||||
    *args,
 | 
			
		||||
 | 
			
		||||
) -> None:
 | 
			
		||||
    '''
 | 
			
		||||
    Specialized, debugger-aware SIGINT handler.
 | 
			
		||||
 | 
			
		||||
    In childred we always ignore/shield for SIGINT to avoid
 | 
			
		||||
    deadlocks since cancellation should always be managed by the
 | 
			
		||||
    supervising parent actor. The root actor-proces is always
 | 
			
		||||
    cancelled on ctrl-c.
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
    __tracebackhide__: bool = True
 | 
			
		||||
    actor: Actor = current_actor()
 | 
			
		||||
 | 
			
		||||
    def do_cancel():
 | 
			
		||||
        # If we haven't tried to cancel the runtime then do that instead
 | 
			
		||||
        # of raising a KBI (which may non-gracefully destroy
 | 
			
		||||
        # a ``trio.run()``).
 | 
			
		||||
        if not actor._cancel_called:
 | 
			
		||||
            actor.cancel_soon()
 | 
			
		||||
 | 
			
		||||
        # If the runtime is already cancelled it likely means the user
 | 
			
		||||
        # hit ctrl-c again because teardown didn't fully take place in
 | 
			
		||||
        # which case we do the "hard" raising of a local KBI.
 | 
			
		||||
        else:
 | 
			
		||||
            raise KeyboardInterrupt
 | 
			
		||||
 | 
			
		||||
    # only set in the actor actually running the REPL
 | 
			
		||||
    repl: PdbREPL|None = DebugStatus.repl
 | 
			
		||||
 | 
			
		||||
    # TODO: maybe we should flatten out all these cases using
 | 
			
		||||
    # a match/case?
 | 
			
		||||
    #
 | 
			
		||||
    # root actor branch that reports whether or not a child
 | 
			
		||||
    # has locked debugger.
 | 
			
		||||
    if is_root_process():
 | 
			
		||||
        # log.warning(
 | 
			
		||||
        log.devx(
 | 
			
		||||
            'Handling SIGINT in root actor\n'
 | 
			
		||||
            f'{Lock.repr()}'
 | 
			
		||||
            f'{DebugStatus.repr()}\n'
 | 
			
		||||
        )
 | 
			
		||||
        # try to see if the supposed (sub)actor in debug still
 | 
			
		||||
        # has an active connection to *this* actor, and if not
 | 
			
		||||
        # it's likely they aren't using the TTY lock / debugger
 | 
			
		||||
        # and we should propagate SIGINT normally.
 | 
			
		||||
        any_connected: bool = any_connected_locker_child()
 | 
			
		||||
 | 
			
		||||
        problem = (
 | 
			
		||||
            f'root {actor.uid} handling SIGINT\n'
 | 
			
		||||
            f'any_connected: {any_connected}\n\n'
 | 
			
		||||
 | 
			
		||||
            f'{Lock.repr()}\n'
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        if (
 | 
			
		||||
            (ctx := Lock.ctx_in_debug)
 | 
			
		||||
            and
 | 
			
		||||
            (uid_in_debug := ctx.chan.uid) # "someone" is (ostensibly) using debug `Lock`
 | 
			
		||||
        ):
 | 
			
		||||
            name_in_debug: str = uid_in_debug[0]
 | 
			
		||||
            assert not repl
 | 
			
		||||
            # if not repl:  # but it's NOT us, the root actor.
 | 
			
		||||
            # sanity: since no repl ref is set, we def shouldn't
 | 
			
		||||
            # be the lock owner!
 | 
			
		||||
            assert name_in_debug != 'root'
 | 
			
		||||
 | 
			
		||||
            # IDEAL CASE: child has REPL as expected
 | 
			
		||||
            if any_connected:  # there are subactors we can contact
 | 
			
		||||
                # XXX: only if there is an existing connection to the
 | 
			
		||||
                # (sub-)actor in debug do we ignore SIGINT in this
 | 
			
		||||
                # parent! Otherwise we may hang waiting for an actor
 | 
			
		||||
                # which has already terminated to unlock.
 | 
			
		||||
                #
 | 
			
		||||
                # NOTE: don't emit this with `.pdb()` level in
 | 
			
		||||
                # root without a higher level.
 | 
			
		||||
                log.runtime(
 | 
			
		||||
                    _ctlc_ignore_header
 | 
			
		||||
                    +
 | 
			
		||||
                    f' by child '
 | 
			
		||||
                    f'{uid_in_debug}\n'
 | 
			
		||||
                )
 | 
			
		||||
                problem = None
 | 
			
		||||
 | 
			
		||||
            else:
 | 
			
		||||
                problem += (
 | 
			
		||||
                    '\n'
 | 
			
		||||
                    f'A `pdb` REPL is SUPPOSEDLY in use by child {uid_in_debug}\n'
 | 
			
		||||
                    f'BUT, no child actors are IPC contactable!?!?\n'
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
        # IDEAL CASE: root has REPL as expected
 | 
			
		||||
        else:
 | 
			
		||||
            # root actor still has this SIGINT handler active without
 | 
			
		||||
            # an actor using the `Lock` (a bug state) ??
 | 
			
		||||
            # => so immediately cancel any stale lock cs and revert
 | 
			
		||||
            # the handler!
 | 
			
		||||
            if not DebugStatus.repl:
 | 
			
		||||
                # TODO: WHEN should we revert back to ``trio``
 | 
			
		||||
                # handler if this one is stale?
 | 
			
		||||
                # -[ ] maybe after a counts work of ctl-c mashes?
 | 
			
		||||
                # -[ ] use a state var like `stale_handler: bool`?
 | 
			
		||||
                problem += (
 | 
			
		||||
                    'No subactor is using a `pdb` REPL according `Lock.ctx_in_debug`?\n'
 | 
			
		||||
                    'BUT, the root should be using it, WHY this handler ??\n\n'
 | 
			
		||||
                    'So either..\n'
 | 
			
		||||
                    '- some root-thread is using it but has no `.repl` set?, OR\n'
 | 
			
		||||
                    '- something else weird is going on outside the runtime!?\n'
 | 
			
		||||
                )
 | 
			
		||||
            else:
 | 
			
		||||
                # NOTE: since we emit this msg on ctl-c, we should
 | 
			
		||||
                # also always re-print the prompt the tail block!
 | 
			
		||||
                log.pdb(
 | 
			
		||||
                    _ctlc_ignore_header
 | 
			
		||||
                    +
 | 
			
		||||
                    f' by root actor..\n'
 | 
			
		||||
                    f'{DebugStatus.repl_task}\n'
 | 
			
		||||
                    f' |_{repl}\n'
 | 
			
		||||
                )
 | 
			
		||||
                problem = None
 | 
			
		||||
 | 
			
		||||
        # XXX if one is set it means we ARE NOT operating an ideal
 | 
			
		||||
        # case where a child subactor or us (the root) has the
 | 
			
		||||
        # lock without any other detected problems.
 | 
			
		||||
        if problem:
 | 
			
		||||
 | 
			
		||||
            # detect, report and maybe clear a stale lock request
 | 
			
		||||
            # cancel scope.
 | 
			
		||||
            lock_cs: trio.CancelScope = Lock.get_locking_task_cs()
 | 
			
		||||
            maybe_stale_lock_cs: bool = (
 | 
			
		||||
                lock_cs is not None
 | 
			
		||||
                and not lock_cs.cancel_called
 | 
			
		||||
            )
 | 
			
		||||
            if maybe_stale_lock_cs:
 | 
			
		||||
                problem += (
 | 
			
		||||
                    '\n'
 | 
			
		||||
                    'Stale `Lock.ctx_in_debug._scope: CancelScope` detected?\n'
 | 
			
		||||
                    f'{Lock.ctx_in_debug}\n\n'
 | 
			
		||||
 | 
			
		||||
                    '-> Calling ctx._scope.cancel()!\n'
 | 
			
		||||
                )
 | 
			
		||||
                lock_cs.cancel()
 | 
			
		||||
 | 
			
		||||
            # TODO: wen do we actually want/need this, see above.
 | 
			
		||||
            # DebugStatus.unshield_sigint()
 | 
			
		||||
            log.warning(problem)
 | 
			
		||||
 | 
			
		||||
    # child actor that has locked the debugger
 | 
			
		||||
    elif not is_root_process():
 | 
			
		||||
        log.debug(
 | 
			
		||||
            f'Subactor {actor.uid} handling SIGINT\n\n'
 | 
			
		||||
            f'{Lock.repr()}\n'
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        rent_chan: Channel = actor._parent_chan
 | 
			
		||||
        if (
 | 
			
		||||
            rent_chan is None
 | 
			
		||||
            or
 | 
			
		||||
            not rent_chan.connected()
 | 
			
		||||
        ):
 | 
			
		||||
            log.warning(
 | 
			
		||||
                'This sub-actor thinks it is debugging '
 | 
			
		||||
                'but it has no connection to its parent ??\n'
 | 
			
		||||
                f'{actor.uid}\n'
 | 
			
		||||
                'Allowing SIGINT propagation..'
 | 
			
		||||
            )
 | 
			
		||||
            DebugStatus.unshield_sigint()
 | 
			
		||||
 | 
			
		||||
        repl_task: str|None = DebugStatus.repl_task
 | 
			
		||||
        req_task: str|None = DebugStatus.req_task
 | 
			
		||||
        if (
 | 
			
		||||
            repl_task
 | 
			
		||||
            and
 | 
			
		||||
            repl
 | 
			
		||||
        ):
 | 
			
		||||
            log.pdb(
 | 
			
		||||
                _ctlc_ignore_header
 | 
			
		||||
                +
 | 
			
		||||
                f' by local task\n\n'
 | 
			
		||||
                f'{repl_task}\n'
 | 
			
		||||
                f' |_{repl}\n'
 | 
			
		||||
            )
 | 
			
		||||
        elif req_task:
 | 
			
		||||
            log.debug(
 | 
			
		||||
                _ctlc_ignore_header
 | 
			
		||||
                +
 | 
			
		||||
                f' by local request-task and either,\n'
 | 
			
		||||
                f'- someone else is already REPL-in and has the `Lock`, or\n'
 | 
			
		||||
                f'- some other local task already is replin?\n\n'
 | 
			
		||||
                f'{req_task}\n'
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        # TODO can we remove this now?
 | 
			
		||||
        # -[ ] does this path ever get hit any more?
 | 
			
		||||
        else:
 | 
			
		||||
            msg: str = (
 | 
			
		||||
                'SIGINT shield handler still active BUT, \n\n'
 | 
			
		||||
            )
 | 
			
		||||
            if repl_task is None:
 | 
			
		||||
                msg += (
 | 
			
		||||
                    '- No local task claims to be in debug?\n'
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            if repl is None:
 | 
			
		||||
                msg += (
 | 
			
		||||
                    '- No local REPL is currently active?\n'
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            if req_task is None:
 | 
			
		||||
                msg += (
 | 
			
		||||
                    '- No debug request task is active?\n'
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            log.warning(
 | 
			
		||||
                msg
 | 
			
		||||
                +
 | 
			
		||||
                'Reverting handler to `trio` default!\n'
 | 
			
		||||
            )
 | 
			
		||||
            DebugStatus.unshield_sigint()
 | 
			
		||||
 | 
			
		||||
            # XXX ensure that the reverted-to-handler actually is
 | 
			
		||||
            # able to rx what should have been **this** KBI ;)
 | 
			
		||||
            do_cancel()
 | 
			
		||||
 | 
			
		||||
        # TODO: how to handle the case of an intermediary-child actor
 | 
			
		||||
        # that **is not** marked in debug mode? See oustanding issue:
 | 
			
		||||
        # https://github.com/goodboy/tractor/issues/320
 | 
			
		||||
        # elif debug_mode():
 | 
			
		||||
 | 
			
		||||
    # maybe redraw/print last REPL output to console since
 | 
			
		||||
    # we want to alert the user that more input is expect since
 | 
			
		||||
    # nothing has been done dur to ignoring sigint.
 | 
			
		||||
    if (
 | 
			
		||||
        DebugStatus.repl  # only when current actor has a REPL engaged
 | 
			
		||||
    ):
 | 
			
		||||
        flush_status: str = (
 | 
			
		||||
            'Flushing stdout to ensure new prompt line!\n'
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # XXX: yah, mega hack, but how else do we catch this madness XD
 | 
			
		||||
        if (
 | 
			
		||||
            repl.shname == 'xonsh'
 | 
			
		||||
        ):
 | 
			
		||||
            flush_status += (
 | 
			
		||||
                '-> ALSO re-flushing due to `xonsh`..\n'
 | 
			
		||||
            )
 | 
			
		||||
            repl.stdout.write(repl.prompt)
 | 
			
		||||
 | 
			
		||||
        # log.warning(
 | 
			
		||||
        log.devx(
 | 
			
		||||
            flush_status
 | 
			
		||||
        )
 | 
			
		||||
        repl.stdout.flush()
 | 
			
		||||
 | 
			
		||||
        # TODO: better console UX to match the current "mode":
 | 
			
		||||
        # -[ ] for example if in sticky mode where if there is output
 | 
			
		||||
        #   detected as written to the tty we redraw this part underneath
 | 
			
		||||
        #   and erase the past draw of this same bit above?
 | 
			
		||||
        # repl.sticky = True
 | 
			
		||||
        # repl._print_if_sticky()
 | 
			
		||||
 | 
			
		||||
        # also see these links for an approach from `ptk`:
 | 
			
		||||
        # https://github.com/goodboy/tractor/issues/130#issuecomment-663752040
 | 
			
		||||
        # https://github.com/prompt-toolkit/python-prompt-toolkit/blob/c2c6af8a0308f9e5d7c0e28cb8a02963fe0ce07a/prompt_toolkit/patch_stdout.py
 | 
			
		||||
    else:
 | 
			
		||||
        log.devx(
 | 
			
		||||
        # log.warning(
 | 
			
		||||
            'Not flushing stdout since not needed?\n'
 | 
			
		||||
            f'|_{repl}\n'
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    # XXX only for tracing this handler
 | 
			
		||||
    log.devx('exiting SIGINT')
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,220 @@
 | 
			
		|||
# 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/>.
 | 
			
		||||
 | 
			
		||||
'''
 | 
			
		||||
Debugger synchronization APIs to ensure orderly access and
 | 
			
		||||
non-TTY-clobbering graceful teardown.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
'''
 | 
			
		||||
from __future__ import annotations
 | 
			
		||||
from contextlib import (
 | 
			
		||||
    asynccontextmanager as acm,
 | 
			
		||||
)
 | 
			
		||||
from functools import (
 | 
			
		||||
    partial,
 | 
			
		||||
)
 | 
			
		||||
from typing import (
 | 
			
		||||
    AsyncGenerator,
 | 
			
		||||
    Callable,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
from tractor.log import get_logger
 | 
			
		||||
import trio
 | 
			
		||||
from trio.lowlevel import (
 | 
			
		||||
    current_task,
 | 
			
		||||
    Task,
 | 
			
		||||
)
 | 
			
		||||
from tractor._context import Context
 | 
			
		||||
from tractor._state import (
 | 
			
		||||
    current_actor,
 | 
			
		||||
    debug_mode,
 | 
			
		||||
    is_root_process,
 | 
			
		||||
)
 | 
			
		||||
from ._repl import (
 | 
			
		||||
    TractorConfig as TractorConfig,
 | 
			
		||||
)
 | 
			
		||||
from ._tty_lock import (
 | 
			
		||||
    Lock,
 | 
			
		||||
    request_root_stdio_lock,
 | 
			
		||||
    any_connected_locker_child,
 | 
			
		||||
)
 | 
			
		||||
from ._sigint import (
 | 
			
		||||
    sigint_shield as sigint_shield,
 | 
			
		||||
    _ctlc_ignore_header as _ctlc_ignore_header
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
log = get_logger(__package__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def maybe_wait_for_debugger(
 | 
			
		||||
    poll_steps: int = 2,
 | 
			
		||||
    poll_delay: float = 0.1,
 | 
			
		||||
    child_in_debug: bool = False,
 | 
			
		||||
 | 
			
		||||
    header_msg: str = '',
 | 
			
		||||
    _ll: str = 'devx',
 | 
			
		||||
 | 
			
		||||
) -> bool:  # was locked and we polled?
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
        not debug_mode()
 | 
			
		||||
        and
 | 
			
		||||
        not child_in_debug
 | 
			
		||||
    ):
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    logmeth: Callable = getattr(log, _ll)
 | 
			
		||||
 | 
			
		||||
    msg: str = header_msg
 | 
			
		||||
    if (
 | 
			
		||||
        is_root_process()
 | 
			
		||||
    ):
 | 
			
		||||
        # If we error in the root but the debugger is
 | 
			
		||||
        # engaged we don't want to prematurely kill (and
 | 
			
		||||
        # thus clobber access to) the local tty since it
 | 
			
		||||
        # will make the pdb repl unusable.
 | 
			
		||||
        # Instead try to wait for pdb to be released before
 | 
			
		||||
        # tearing down.
 | 
			
		||||
        ctx_in_debug: Context|None = Lock.ctx_in_debug
 | 
			
		||||
        in_debug: tuple[str, str]|None = (
 | 
			
		||||
            ctx_in_debug.chan.uid
 | 
			
		||||
            if ctx_in_debug
 | 
			
		||||
            else None
 | 
			
		||||
        )
 | 
			
		||||
        if in_debug == current_actor().uid:
 | 
			
		||||
            log.debug(
 | 
			
		||||
                msg
 | 
			
		||||
                +
 | 
			
		||||
                'Root already owns the TTY LOCK'
 | 
			
		||||
            )
 | 
			
		||||
            return True
 | 
			
		||||
 | 
			
		||||
        elif in_debug:
 | 
			
		||||
            msg += (
 | 
			
		||||
                f'Debug `Lock` in use by subactor\n|\n|_{in_debug}\n'
 | 
			
		||||
            )
 | 
			
		||||
            # TODO: could this make things more deterministic?
 | 
			
		||||
            # wait to see if a sub-actor task will be
 | 
			
		||||
            # scheduled and grab the tty lock on the next
 | 
			
		||||
            # tick?
 | 
			
		||||
            # XXX => but it doesn't seem to work..
 | 
			
		||||
            # await trio.testing.wait_all_tasks_blocked(cushion=0)
 | 
			
		||||
        else:
 | 
			
		||||
            logmeth(
 | 
			
		||||
                msg
 | 
			
		||||
                +
 | 
			
		||||
                'Root immediately acquired debug TTY LOCK'
 | 
			
		||||
            )
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
        for istep in range(poll_steps):
 | 
			
		||||
            if (
 | 
			
		||||
                Lock.req_handler_finished is not None
 | 
			
		||||
                and not Lock.req_handler_finished.is_set()
 | 
			
		||||
                and in_debug is not None
 | 
			
		||||
            ):
 | 
			
		||||
                # caller_frame_info: str = pformat_caller_frame()
 | 
			
		||||
                logmeth(
 | 
			
		||||
                    msg
 | 
			
		||||
                    +
 | 
			
		||||
                    '\n^^ Root is waiting on tty lock release.. ^^\n'
 | 
			
		||||
                    # f'{caller_frame_info}\n'
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
                if not any_connected_locker_child():
 | 
			
		||||
                    Lock.get_locking_task_cs().cancel()
 | 
			
		||||
 | 
			
		||||
                with trio.CancelScope(shield=True):
 | 
			
		||||
                    await Lock.req_handler_finished.wait()
 | 
			
		||||
 | 
			
		||||
                log.devx(
 | 
			
		||||
                    f'Subactor released debug lock\n'
 | 
			
		||||
                    f'|_{in_debug}\n'
 | 
			
		||||
                )
 | 
			
		||||
                break
 | 
			
		||||
 | 
			
		||||
            # is no subactor locking debugger currently?
 | 
			
		||||
            if (
 | 
			
		||||
                in_debug is None
 | 
			
		||||
                and (
 | 
			
		||||
                    Lock.req_handler_finished is None
 | 
			
		||||
                    or Lock.req_handler_finished.is_set()
 | 
			
		||||
                )
 | 
			
		||||
            ):
 | 
			
		||||
                logmeth(
 | 
			
		||||
                    msg
 | 
			
		||||
                    +
 | 
			
		||||
                    'Root acquired tty lock!'
 | 
			
		||||
                )
 | 
			
		||||
                break
 | 
			
		||||
 | 
			
		||||
            else:
 | 
			
		||||
                logmeth(
 | 
			
		||||
                    'Root polling for debug:\n'
 | 
			
		||||
                    f'poll step: {istep}\n'
 | 
			
		||||
                    f'poll delya: {poll_delay}\n\n'
 | 
			
		||||
                    f'{Lock.repr()}\n'
 | 
			
		||||
                )
 | 
			
		||||
                with trio.CancelScope(shield=True):
 | 
			
		||||
                    await trio.sleep(poll_delay)
 | 
			
		||||
                    continue
 | 
			
		||||
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    # else:
 | 
			
		||||
    #     # TODO: non-root call for #320?
 | 
			
		||||
    #     this_uid: tuple[str, str] = current_actor().uid
 | 
			
		||||
    #     async with acquire_debug_lock(
 | 
			
		||||
    #         subactor_uid=this_uid,
 | 
			
		||||
    #     ):
 | 
			
		||||
    #         pass
 | 
			
		||||
    return False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@acm
 | 
			
		||||
async def acquire_debug_lock(
 | 
			
		||||
    subactor_uid: tuple[str, str],
 | 
			
		||||
) -> AsyncGenerator[
 | 
			
		||||
    trio.CancelScope|None,
 | 
			
		||||
    tuple,
 | 
			
		||||
]:
 | 
			
		||||
    '''
 | 
			
		||||
    Request to acquire the TTY `Lock` in the root actor, release on
 | 
			
		||||
    exit.
 | 
			
		||||
 | 
			
		||||
    This helper is for actor's who don't actually need to acquired
 | 
			
		||||
    the debugger but want to wait until the lock is free in the
 | 
			
		||||
    process-tree root such that they don't clobber an ongoing pdb
 | 
			
		||||
    REPL session in some peer or child!
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
    if not debug_mode():
 | 
			
		||||
        yield None
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    task: Task = current_task()
 | 
			
		||||
    async with trio.open_nursery() as n:
 | 
			
		||||
        ctx: Context = await n.start(
 | 
			
		||||
            partial(
 | 
			
		||||
                request_root_stdio_lock,
 | 
			
		||||
                actor_uid=subactor_uid,
 | 
			
		||||
                task_uid=(task.name, id(task)),
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
        yield ctx
 | 
			
		||||
        ctx.cancel()
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| 
						 | 
				
			
			@ -247,3 +247,104 @@ def pformat_cs(
 | 
			
		|||
        +
 | 
			
		||||
        fields
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# TODO: move this func to some kinda `.devx.pformat.py` eventually
 | 
			
		||||
# as we work out our multi-domain state-flow-syntax!
 | 
			
		||||
def nest_from_op(
 | 
			
		||||
    input_op: str,
 | 
			
		||||
    #
 | 
			
		||||
    # ?TODO? an idea for a syntax to the state of concurrent systems
 | 
			
		||||
    # as a "3-domain" (execution, scope, storage) model and using
 | 
			
		||||
    # a minimal ascii/utf-8 operator-set.
 | 
			
		||||
    #
 | 
			
		||||
    # try not to take any of this seriously yet XD
 | 
			
		||||
    #
 | 
			
		||||
    # > is a "play operator" indicating (CPU bound)
 | 
			
		||||
    #   exec/work/ops required at the "lowest level computing"
 | 
			
		||||
    #
 | 
			
		||||
    # execution primititves (tasks, threads, actors..) denote their
 | 
			
		||||
    # lifetime with '(' and ')' since parentheses normally are used
 | 
			
		||||
    # in many langs to denote function calls.
 | 
			
		||||
    #
 | 
			
		||||
    # starting = (
 | 
			
		||||
    # >(  opening/starting; beginning of the thread-of-exec (toe?)
 | 
			
		||||
    # (>  opened/started,  (finished spawning toe)
 | 
			
		||||
    # |_<Task: blah blah..>  repr of toe, in py these look like <objs>
 | 
			
		||||
    #
 | 
			
		||||
    # >) closing/exiting/stopping,
 | 
			
		||||
    # )> closed/exited/stopped,
 | 
			
		||||
    # |_<Task: blah blah..>
 | 
			
		||||
    #   [OR <), )< ?? ]
 | 
			
		||||
    #
 | 
			
		||||
    # ending = )
 | 
			
		||||
    # >c) cancelling to close/exit
 | 
			
		||||
    # c)> cancelled (caused close), OR?
 | 
			
		||||
    #  |_<Actor: ..>
 | 
			
		||||
    #   OR maybe "<c)" which better indicates the cancel being
 | 
			
		||||
    #   "delivered/returned" / returned" to LHS?
 | 
			
		||||
    #
 | 
			
		||||
    # >x)  erroring to eventuall exit
 | 
			
		||||
    # x)>  errored and terminated
 | 
			
		||||
    #  |_<Actor: ...>
 | 
			
		||||
    #
 | 
			
		||||
    # scopes: supers/nurseries, IPC-ctxs, sessions, perms, etc.
 | 
			
		||||
    # >{  opening
 | 
			
		||||
    # {>  opened
 | 
			
		||||
    # }>  closed
 | 
			
		||||
    # >}  closing
 | 
			
		||||
    #
 | 
			
		||||
    # storage: like queues, shm-buffers, files, etc..
 | 
			
		||||
    # >[  opening
 | 
			
		||||
    # [>  opened
 | 
			
		||||
    #  |_<FileObj: ..>
 | 
			
		||||
    #
 | 
			
		||||
    # >]  closing
 | 
			
		||||
    # ]>  closed
 | 
			
		||||
 | 
			
		||||
    # IPC ops: channels, transports, msging
 | 
			
		||||
    # =>  req msg
 | 
			
		||||
    # <=  resp msg
 | 
			
		||||
    # <=> 2-way streaming (of msgs)
 | 
			
		||||
    # <-  recv 1 msg
 | 
			
		||||
    # ->  send 1 msg
 | 
			
		||||
    #
 | 
			
		||||
    # TODO: still not sure on R/L-HS approach..?
 | 
			
		||||
    # =>(  send-req to exec start (task, actor, thread..)
 | 
			
		||||
    # (<=  recv-req to ^
 | 
			
		||||
    #
 | 
			
		||||
    # (<=  recv-req ^
 | 
			
		||||
    # <=(  recv-resp opened remote exec primitive
 | 
			
		||||
    # <=)  recv-resp closed
 | 
			
		||||
    #
 | 
			
		||||
    # )<=c req to stop due to cancel
 | 
			
		||||
    # c=>) req to stop due to cancel
 | 
			
		||||
    #
 | 
			
		||||
    # =>{  recv-req to open
 | 
			
		||||
    # <={  send-status that it closed
 | 
			
		||||
 | 
			
		||||
    tree_str: str,
 | 
			
		||||
 | 
			
		||||
    # NOTE: so move back-from-the-left of the `input_op` by
 | 
			
		||||
    # this amount.
 | 
			
		||||
    back_from_op: int = 0,
 | 
			
		||||
) -> str:
 | 
			
		||||
    '''
 | 
			
		||||
    Depth-increment the input (presumably hierarchy/supervision)
 | 
			
		||||
    input "tree string" below the provided `input_op` execution
 | 
			
		||||
    operator, so injecting a `"\n|_{input_op}\n"`and indenting the
 | 
			
		||||
    `tree_str` to nest content aligned with the ops last char.
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
    return (
 | 
			
		||||
        f'{input_op}\n'
 | 
			
		||||
        +
 | 
			
		||||
        textwrap.indent(
 | 
			
		||||
            tree_str,
 | 
			
		||||
            prefix=(
 | 
			
		||||
                len(input_op)
 | 
			
		||||
                -
 | 
			
		||||
                (back_from_op + 1)
 | 
			
		||||
            ) * ' ',
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -43,7 +43,7 @@ from trio import (
 | 
			
		|||
    SocketListener,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
# from ..devx import _debug
 | 
			
		||||
# from ..devx import debug
 | 
			
		||||
from .._exceptions import (
 | 
			
		||||
    TransportClosed,
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			@ -72,11 +72,223 @@ if TYPE_CHECKING:
 | 
			
		|||
log = log.get_logger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def maybe_wait_on_canced_subs(
 | 
			
		||||
    uid: tuple[str, str],
 | 
			
		||||
    chan: Channel,
 | 
			
		||||
    disconnected: bool,
 | 
			
		||||
 | 
			
		||||
    actor: Actor|None = None,
 | 
			
		||||
    chan_drain_timeout: float = 0.5,
 | 
			
		||||
    an_exit_timeout: float = 0.5,
 | 
			
		||||
 | 
			
		||||
) -> ActorNursery|None:
 | 
			
		||||
    '''
 | 
			
		||||
    When a process-local actor-nursery is found for the given actor
 | 
			
		||||
    `uid` (i.e. that peer is **also** a subactor of this parent), we
 | 
			
		||||
    attempt to (with timeouts) wait on,
 | 
			
		||||
 | 
			
		||||
    - all IPC msgs to drain on the (common) `Channel` such that all
 | 
			
		||||
      local `Context`-parent-tasks can also gracefully collect
 | 
			
		||||
      `ContextCancelled` msgs from their respective remote children
 | 
			
		||||
      vs. a `chan_drain_timeout`.
 | 
			
		||||
 | 
			
		||||
    - the actor-nursery to cancel-n-join all its supervised children
 | 
			
		||||
      (processes) *gracefully* vs. a `an_exit_timeout` and thus also
 | 
			
		||||
      detect cases where the IPC transport connection broke but
 | 
			
		||||
      a sub-process is detected as still alive (a case that happens
 | 
			
		||||
      when the subactor is still in an active debugger REPL session).
 | 
			
		||||
 | 
			
		||||
    If the timeout expires in either case we ofc report with warning.
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
    actor = actor or _state.current_actor()
 | 
			
		||||
 | 
			
		||||
    # XXX running outside actor-runtime usage,
 | 
			
		||||
    # - unit testing
 | 
			
		||||
    # - possibly manual usage (eventually) ?
 | 
			
		||||
    if not actor:
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    local_nursery: (
 | 
			
		||||
        ActorNursery|None
 | 
			
		||||
    ) = actor._actoruid2nursery.get(uid)
 | 
			
		||||
 | 
			
		||||
    # This is set in `Portal.cancel_actor()`. So if
 | 
			
		||||
    # the peer was cancelled we try to wait for them
 | 
			
		||||
    # to tear down their side of the connection before
 | 
			
		||||
    # moving on with closing our own side.
 | 
			
		||||
    if (
 | 
			
		||||
        local_nursery
 | 
			
		||||
        and (
 | 
			
		||||
            actor._cancel_called
 | 
			
		||||
            or
 | 
			
		||||
            chan._cancel_called
 | 
			
		||||
        )
 | 
			
		||||
        #
 | 
			
		||||
        # ^-TODO-^ along with this is there another condition
 | 
			
		||||
        # that we should filter with to avoid entering this
 | 
			
		||||
        # waiting block needlessly?
 | 
			
		||||
        # -[ ] maybe `and local_nursery.cancelled` and/or
 | 
			
		||||
        #     only if the `._children` table is empty or has
 | 
			
		||||
        #     only `Portal`s with .chan._cancel_called ==
 | 
			
		||||
        #     True` as per what we had below; the MAIN DIFF
 | 
			
		||||
        #     BEING that just bc one `Portal.cancel_actor()`
 | 
			
		||||
        #     was called, doesn't mean the whole actor-nurse
 | 
			
		||||
        #     is gonna exit any time soon right!?
 | 
			
		||||
        #
 | 
			
		||||
        # or
 | 
			
		||||
        # all(chan._cancel_called for chan in chans)
 | 
			
		||||
 | 
			
		||||
    ):
 | 
			
		||||
        log.cancel(
 | 
			
		||||
            'Waiting on cancel request to peer..\n'
 | 
			
		||||
            f'c)=>\n'
 | 
			
		||||
            f'  |_{chan.uid}\n'
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # XXX: this is a soft wait on the channel (and its
 | 
			
		||||
        # underlying transport protocol) to close from the
 | 
			
		||||
        # remote peer side since we presume that any channel
 | 
			
		||||
        # which is mapped to a sub-actor (i.e. it's managed
 | 
			
		||||
        # by local actor-nursery) has a message that is sent
 | 
			
		||||
        # to the peer likely by this actor (which may be in
 | 
			
		||||
        # a shutdown sequence due to cancellation) when the
 | 
			
		||||
        # local runtime here is now cancelled while
 | 
			
		||||
        # (presumably) in the middle of msg loop processing.
 | 
			
		||||
        chan_info: str = (
 | 
			
		||||
            f'{chan.uid}\n'
 | 
			
		||||
            f'|_{chan}\n'
 | 
			
		||||
            f'  |_{chan.transport}\n\n'
 | 
			
		||||
        )
 | 
			
		||||
        with trio.move_on_after(chan_drain_timeout) as drain_cs:
 | 
			
		||||
            drain_cs.shield = True
 | 
			
		||||
 | 
			
		||||
            # attempt to wait for the far end to close the
 | 
			
		||||
            # channel and bail after timeout (a 2-generals
 | 
			
		||||
            # problem on closure).
 | 
			
		||||
            assert chan.transport
 | 
			
		||||
            async for msg in chan.transport.drain():
 | 
			
		||||
 | 
			
		||||
                # try to deliver any lingering msgs
 | 
			
		||||
                # before we destroy the channel.
 | 
			
		||||
                # This accomplishes deterministic
 | 
			
		||||
                # ``Portal.cancel_actor()`` cancellation by
 | 
			
		||||
                # making sure any RPC response to that call is
 | 
			
		||||
                # delivered the local calling task.
 | 
			
		||||
                # TODO: factor this into a helper?
 | 
			
		||||
                log.warning(
 | 
			
		||||
                    'Draining msg from disconnected peer\n'
 | 
			
		||||
                    f'{chan_info}'
 | 
			
		||||
                    f'{pformat(msg)}\n'
 | 
			
		||||
                )
 | 
			
		||||
                # cid: str|None = msg.get('cid')
 | 
			
		||||
                cid: str|None = msg.cid
 | 
			
		||||
                if cid:
 | 
			
		||||
                    # deliver response to local caller/waiter
 | 
			
		||||
                    await actor._deliver_ctx_payload(
 | 
			
		||||
                        chan,
 | 
			
		||||
                        cid,
 | 
			
		||||
                        msg,
 | 
			
		||||
                    )
 | 
			
		||||
        if drain_cs.cancelled_caught:
 | 
			
		||||
            log.warning(
 | 
			
		||||
                'Timed out waiting on IPC transport channel to drain?\n'
 | 
			
		||||
                f'{chan_info}'
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        # XXX NOTE XXX when no explicit call to
 | 
			
		||||
        # `open_root_actor()` was made by the application
 | 
			
		||||
        # (normally we implicitly make that call inside
 | 
			
		||||
        # the first `.open_nursery()` in root-actor
 | 
			
		||||
        # user/app code), we can assume that either we
 | 
			
		||||
        # are NOT the root actor or are root but the
 | 
			
		||||
        # runtime was started manually. and thus DO have
 | 
			
		||||
        # to wait for the nursery-enterer to exit before
 | 
			
		||||
        # shutting down the local runtime to avoid
 | 
			
		||||
        # clobbering any ongoing subactor
 | 
			
		||||
        # teardown/debugging/graceful-cancel.
 | 
			
		||||
        #
 | 
			
		||||
        # see matching  note inside `._supervise.open_nursery()`
 | 
			
		||||
        #
 | 
			
		||||
        # TODO: should we have a separate cs + timeout
 | 
			
		||||
        # block here?
 | 
			
		||||
        if (
 | 
			
		||||
            # XXX SO either,
 | 
			
		||||
            #  - not root OR,
 | 
			
		||||
            #  - is root but `open_root_actor()` was
 | 
			
		||||
            #    entered manually (in which case we do
 | 
			
		||||
            #    the equiv wait there using the
 | 
			
		||||
            #    `devx.debug` sub-sys APIs).
 | 
			
		||||
            not local_nursery._implicit_runtime_started
 | 
			
		||||
        ):
 | 
			
		||||
            log.runtime(
 | 
			
		||||
                'Waiting on local actor nursery to exit..\n'
 | 
			
		||||
                f'|_{local_nursery}\n'
 | 
			
		||||
            )
 | 
			
		||||
            with trio.move_on_after(an_exit_timeout) as an_exit_cs:
 | 
			
		||||
                an_exit_cs.shield = True
 | 
			
		||||
                await local_nursery.exited.wait()
 | 
			
		||||
 | 
			
		||||
            # TODO: currently this is always triggering for every
 | 
			
		||||
            # sub-daemon spawned from the `piker.services._mngr`?
 | 
			
		||||
            # -[ ] how do we ensure that the IPC is supposed to
 | 
			
		||||
            #      be long lived and isn't just a register?
 | 
			
		||||
            # |_ in the register case how can we signal that the
 | 
			
		||||
            #    ephemeral msg loop was intentional?
 | 
			
		||||
            if (
 | 
			
		||||
                # not local_nursery._implicit_runtime_started
 | 
			
		||||
                # and
 | 
			
		||||
                an_exit_cs.cancelled_caught
 | 
			
		||||
            ):
 | 
			
		||||
                report: str = (
 | 
			
		||||
                    'Timed out waiting on local actor-nursery to exit?\n'
 | 
			
		||||
                    f'c)>\n'
 | 
			
		||||
                    f' |_{local_nursery}\n'
 | 
			
		||||
                )
 | 
			
		||||
                if children := local_nursery._children:
 | 
			
		||||
                    # indent from above local-nurse repr
 | 
			
		||||
                    report += (
 | 
			
		||||
                        f'   |_{pformat(children)}\n'
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
                log.warning(report)
 | 
			
		||||
 | 
			
		||||
        if disconnected:
 | 
			
		||||
            # if the transport died and this actor is still
 | 
			
		||||
            # registered within a local nursery, we report
 | 
			
		||||
            # that the IPC layer may have failed
 | 
			
		||||
            # unexpectedly since it may be the cause of
 | 
			
		||||
            # other downstream errors.
 | 
			
		||||
            entry: tuple|None = local_nursery._children.get(uid)
 | 
			
		||||
            if entry:
 | 
			
		||||
                proc: trio.Process
 | 
			
		||||
                _, proc, _ = entry
 | 
			
		||||
 | 
			
		||||
                if (
 | 
			
		||||
                    (poll := getattr(proc, 'poll', None))
 | 
			
		||||
                    and
 | 
			
		||||
                    poll() is None  # proc still alive
 | 
			
		||||
                ):
 | 
			
		||||
                    # TODO: change log level based on
 | 
			
		||||
                    # detecting whether chan was created for
 | 
			
		||||
                    # ephemeral `.register_actor()` request!
 | 
			
		||||
                    # -[ ] also, that should be avoidable by
 | 
			
		||||
                    #   re-using any existing chan from the
 | 
			
		||||
                    #   `._discovery.get_registry()` call as
 | 
			
		||||
                    #   well..
 | 
			
		||||
                    log.runtime(
 | 
			
		||||
                        f'Peer IPC broke but subproc is alive?\n\n'
 | 
			
		||||
 | 
			
		||||
                        f'<=x {chan.uid}@{chan.raddr}\n'
 | 
			
		||||
                        f'   |_{proc}\n'
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
    return local_nursery
 | 
			
		||||
 | 
			
		||||
# TODO multi-tpt support with per-proto peer tracking?
 | 
			
		||||
#
 | 
			
		||||
# -[x] maybe change to mod-func and rename for implied
 | 
			
		||||
#    multi-transport semantics?
 | 
			
		||||
#
 | 
			
		||||
# -[ ] register each stream/tpt/chan with the owning `IPCEndpoint`
 | 
			
		||||
#     so that we can query per tpt all peer contact infos?
 | 
			
		||||
#  |_[ ] possibly provide a global viewing via a
 | 
			
		||||
| 
						 | 
				
			
			@ -87,7 +299,6 @@ async def handle_stream_from_peer(
 | 
			
		|||
 | 
			
		||||
    *,
 | 
			
		||||
    server: IPCServer,
 | 
			
		||||
    actor: Actor,
 | 
			
		||||
 | 
			
		||||
) -> None:
 | 
			
		||||
    '''
 | 
			
		||||
| 
						 | 
				
			
			@ -107,7 +318,7 @@ async def handle_stream_from_peer(
 | 
			
		|||
    server._no_more_peers = trio.Event()  # unset by making new
 | 
			
		||||
 | 
			
		||||
    # TODO, debug_mode tooling for when hackin this lower layer?
 | 
			
		||||
    # with _debug.maybe_open_crash_handler(
 | 
			
		||||
    # with debug.maybe_open_crash_handler(
 | 
			
		||||
    #     pdb=True,
 | 
			
		||||
    # ) as boxerr:
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -119,9 +330,10 @@ async def handle_stream_from_peer(
 | 
			
		|||
 | 
			
		||||
    # initial handshake with peer phase
 | 
			
		||||
    try:
 | 
			
		||||
        peer_aid: msgtypes.Aid = await chan._do_handshake(
 | 
			
		||||
            aid=actor.aid,
 | 
			
		||||
        )
 | 
			
		||||
        if actor := _state.current_actor():
 | 
			
		||||
            peer_aid: msgtypes.Aid = await chan._do_handshake(
 | 
			
		||||
                aid=actor.aid,
 | 
			
		||||
            )
 | 
			
		||||
    except (
 | 
			
		||||
        TransportClosed,
 | 
			
		||||
        # ^XXX NOTE, the above wraps `trio` exc types raised
 | 
			
		||||
| 
						 | 
				
			
			@ -222,8 +434,7 @@ async def handle_stream_from_peer(
 | 
			
		|||
            disconnected,
 | 
			
		||||
            last_msg,
 | 
			
		||||
        ) = await _rpc.process_messages(
 | 
			
		||||
            actor,
 | 
			
		||||
            chan,
 | 
			
		||||
            chan=chan,
 | 
			
		||||
        )
 | 
			
		||||
    except trio.Cancelled:
 | 
			
		||||
        log.cancel(
 | 
			
		||||
| 
						 | 
				
			
			@ -234,179 +445,16 @@ async def handle_stream_from_peer(
 | 
			
		|||
        raise
 | 
			
		||||
 | 
			
		||||
    finally:
 | 
			
		||||
        local_nursery: (
 | 
			
		||||
            ActorNursery|None
 | 
			
		||||
        ) = actor._actoruid2nursery.get(uid)
 | 
			
		||||
 | 
			
		||||
        # This is set in ``Portal.cancel_actor()``. So if
 | 
			
		||||
        # the peer was cancelled we try to wait for them
 | 
			
		||||
        # to tear down their side of the connection before
 | 
			
		||||
        # moving on with closing our own side.
 | 
			
		||||
        if (
 | 
			
		||||
            local_nursery
 | 
			
		||||
            and (
 | 
			
		||||
                actor._cancel_called
 | 
			
		||||
                or
 | 
			
		||||
                chan._cancel_called
 | 
			
		||||
            )
 | 
			
		||||
            #
 | 
			
		||||
            # ^-TODO-^ along with this is there another condition
 | 
			
		||||
            # that we should filter with to avoid entering this
 | 
			
		||||
            # waiting block needlessly?
 | 
			
		||||
            # -[ ] maybe `and local_nursery.cancelled` and/or
 | 
			
		||||
            #     only if the `._children` table is empty or has
 | 
			
		||||
            #     only `Portal`s with .chan._cancel_called ==
 | 
			
		||||
            #     True` as per what we had below; the MAIN DIFF
 | 
			
		||||
            #     BEING that just bc one `Portal.cancel_actor()`
 | 
			
		||||
            #     was called, doesn't mean the whole actor-nurse
 | 
			
		||||
            #     is gonna exit any time soon right!?
 | 
			
		||||
            #
 | 
			
		||||
            # or
 | 
			
		||||
            # all(chan._cancel_called for chan in chans)
 | 
			
		||||
 | 
			
		||||
        ):
 | 
			
		||||
            log.cancel(
 | 
			
		||||
                'Waiting on cancel request to peer..\n'
 | 
			
		||||
                f'c)=>\n'
 | 
			
		||||
                f'  |_{chan.uid}\n'
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            # XXX: this is a soft wait on the channel (and its
 | 
			
		||||
            # underlying transport protocol) to close from the
 | 
			
		||||
            # remote peer side since we presume that any channel
 | 
			
		||||
            # which is mapped to a sub-actor (i.e. it's managed
 | 
			
		||||
            # by local actor-nursery) has a message that is sent
 | 
			
		||||
            # to the peer likely by this actor (which may be in
 | 
			
		||||
            # a shutdown sequence due to cancellation) when the
 | 
			
		||||
            # local runtime here is now cancelled while
 | 
			
		||||
            # (presumably) in the middle of msg loop processing.
 | 
			
		||||
            chan_info: str = (
 | 
			
		||||
                f'{chan.uid}\n'
 | 
			
		||||
                f'|_{chan}\n'
 | 
			
		||||
                f'  |_{chan.transport}\n\n'
 | 
			
		||||
            )
 | 
			
		||||
            with trio.move_on_after(0.5) as drain_cs:
 | 
			
		||||
                drain_cs.shield = True
 | 
			
		||||
 | 
			
		||||
                # attempt to wait for the far end to close the
 | 
			
		||||
                # channel and bail after timeout (a 2-generals
 | 
			
		||||
                # problem on closure).
 | 
			
		||||
                assert chan.transport
 | 
			
		||||
                async for msg in chan.transport.drain():
 | 
			
		||||
 | 
			
		||||
                    # try to deliver any lingering msgs
 | 
			
		||||
                    # before we destroy the channel.
 | 
			
		||||
                    # This accomplishes deterministic
 | 
			
		||||
                    # ``Portal.cancel_actor()`` cancellation by
 | 
			
		||||
                    # making sure any RPC response to that call is
 | 
			
		||||
                    # delivered the local calling task.
 | 
			
		||||
                    # TODO: factor this into a helper?
 | 
			
		||||
                    log.warning(
 | 
			
		||||
                        'Draining msg from disconnected peer\n'
 | 
			
		||||
                        f'{chan_info}'
 | 
			
		||||
                        f'{pformat(msg)}\n'
 | 
			
		||||
                    )
 | 
			
		||||
                    # cid: str|None = msg.get('cid')
 | 
			
		||||
                    cid: str|None = msg.cid
 | 
			
		||||
                    if cid:
 | 
			
		||||
                        # deliver response to local caller/waiter
 | 
			
		||||
                        await actor._deliver_ctx_payload(
 | 
			
		||||
                            chan,
 | 
			
		||||
                            cid,
 | 
			
		||||
                            msg,
 | 
			
		||||
                        )
 | 
			
		||||
            if drain_cs.cancelled_caught:
 | 
			
		||||
                log.warning(
 | 
			
		||||
                    'Timed out waiting on IPC transport channel to drain?\n'
 | 
			
		||||
                    f'{chan_info}'
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            # XXX NOTE XXX when no explicit call to
 | 
			
		||||
            # `open_root_actor()` was made by the application
 | 
			
		||||
            # (normally we implicitly make that call inside
 | 
			
		||||
            # the first `.open_nursery()` in root-actor
 | 
			
		||||
            # user/app code), we can assume that either we
 | 
			
		||||
            # are NOT the root actor or are root but the
 | 
			
		||||
            # runtime was started manually. and thus DO have
 | 
			
		||||
            # to wait for the nursery-enterer to exit before
 | 
			
		||||
            # shutting down the local runtime to avoid
 | 
			
		||||
            # clobbering any ongoing subactor
 | 
			
		||||
            # teardown/debugging/graceful-cancel.
 | 
			
		||||
            #
 | 
			
		||||
            # see matching  note inside `._supervise.open_nursery()`
 | 
			
		||||
            #
 | 
			
		||||
            # TODO: should we have a separate cs + timeout
 | 
			
		||||
            # block here?
 | 
			
		||||
            if (
 | 
			
		||||
                # XXX SO either,
 | 
			
		||||
                #  - not root OR,
 | 
			
		||||
                #  - is root but `open_root_actor()` was
 | 
			
		||||
                #    entered manually (in which case we do
 | 
			
		||||
                #    the equiv wait there using the
 | 
			
		||||
                #    `devx._debug` sub-sys APIs).
 | 
			
		||||
                not local_nursery._implicit_runtime_started
 | 
			
		||||
            ):
 | 
			
		||||
                log.runtime(
 | 
			
		||||
                    'Waiting on local actor nursery to exit..\n'
 | 
			
		||||
                    f'|_{local_nursery}\n'
 | 
			
		||||
                )
 | 
			
		||||
                with trio.move_on_after(0.5) as an_exit_cs:
 | 
			
		||||
                    an_exit_cs.shield = True
 | 
			
		||||
                    await local_nursery.exited.wait()
 | 
			
		||||
 | 
			
		||||
                # TODO: currently this is always triggering for every
 | 
			
		||||
                # sub-daemon spawned from the `piker.services._mngr`?
 | 
			
		||||
                # -[ ] how do we ensure that the IPC is supposed to
 | 
			
		||||
                #      be long lived and isn't just a register?
 | 
			
		||||
                # |_ in the register case how can we signal that the
 | 
			
		||||
                #    ephemeral msg loop was intentional?
 | 
			
		||||
                if (
 | 
			
		||||
                    # not local_nursery._implicit_runtime_started
 | 
			
		||||
                    # and
 | 
			
		||||
                    an_exit_cs.cancelled_caught
 | 
			
		||||
                ):
 | 
			
		||||
                    report: str = (
 | 
			
		||||
                        'Timed out waiting on local actor-nursery to exit?\n'
 | 
			
		||||
                        f'c)>\n'
 | 
			
		||||
                        f' |_{local_nursery}\n'
 | 
			
		||||
                    )
 | 
			
		||||
                    if children := local_nursery._children:
 | 
			
		||||
                        # indent from above local-nurse repr
 | 
			
		||||
                        report += (
 | 
			
		||||
                            f'   |_{pformat(children)}\n'
 | 
			
		||||
                        )
 | 
			
		||||
 | 
			
		||||
                    log.warning(report)
 | 
			
		||||
 | 
			
		||||
            if disconnected:
 | 
			
		||||
                # if the transport died and this actor is still
 | 
			
		||||
                # registered within a local nursery, we report
 | 
			
		||||
                # that the IPC layer may have failed
 | 
			
		||||
                # unexpectedly since it may be the cause of
 | 
			
		||||
                # other downstream errors.
 | 
			
		||||
                entry: tuple|None = local_nursery._children.get(uid)
 | 
			
		||||
                if entry:
 | 
			
		||||
                    proc: trio.Process
 | 
			
		||||
                    _, proc, _ = entry
 | 
			
		||||
 | 
			
		||||
                    if (
 | 
			
		||||
                        (poll := getattr(proc, 'poll', None))
 | 
			
		||||
                        and
 | 
			
		||||
                        poll() is None  # proc still alive
 | 
			
		||||
                    ):
 | 
			
		||||
                        # TODO: change log level based on
 | 
			
		||||
                        # detecting whether chan was created for
 | 
			
		||||
                        # ephemeral `.register_actor()` request!
 | 
			
		||||
                        # -[ ] also, that should be avoidable by
 | 
			
		||||
                        #   re-using any existing chan from the
 | 
			
		||||
                        #   `._discovery.get_registry()` call as
 | 
			
		||||
                        #   well..
 | 
			
		||||
                        log.runtime(
 | 
			
		||||
                            f'Peer IPC broke but subproc is alive?\n\n'
 | 
			
		||||
 | 
			
		||||
                            f'<=x {chan.uid}@{chan.raddr}\n'
 | 
			
		||||
                            f'   |_{proc}\n'
 | 
			
		||||
                        )
 | 
			
		||||
        # check if there are subs which we should gracefully join at
 | 
			
		||||
        # both the inter-actor-task and subprocess levels to
 | 
			
		||||
        # gracefully remote cancel and later disconnect (particularly
 | 
			
		||||
        # for permitting subs engaged in active debug-REPL sessions).
 | 
			
		||||
        local_nursery: ActorNursery|None = await maybe_wait_on_canced_subs(
 | 
			
		||||
            uid=uid,
 | 
			
		||||
            chan=chan,
 | 
			
		||||
            disconnected=disconnected,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # ``Channel`` teardown and closure sequence
 | 
			
		||||
        # drop ref to channel so it can be gc-ed and disconnected
 | 
			
		||||
| 
						 | 
				
			
			@ -456,8 +504,8 @@ async def handle_stream_from_peer(
 | 
			
		|||
                and
 | 
			
		||||
                _state.is_debug_mode()
 | 
			
		||||
            ):
 | 
			
		||||
                from ..devx import _debug
 | 
			
		||||
                pdb_lock = _debug.Lock
 | 
			
		||||
                from ..devx import debug
 | 
			
		||||
                pdb_lock = debug.Lock
 | 
			
		||||
                pdb_lock._blocked.add(uid)
 | 
			
		||||
 | 
			
		||||
                # TODO: NEEEDS TO BE TESTED!
 | 
			
		||||
| 
						 | 
				
			
			@ -467,11 +515,11 @@ async def handle_stream_from_peer(
 | 
			
		|||
                # from broken debug TTY locking due to
 | 
			
		||||
                # msg-spec races on application using RunVar...
 | 
			
		||||
                if (
 | 
			
		||||
                    local_nursery
 | 
			
		||||
                    and
 | 
			
		||||
                    (ctx_in_debug := pdb_lock.ctx_in_debug)
 | 
			
		||||
                    and
 | 
			
		||||
                    (pdb_user_uid := ctx_in_debug.chan.uid)
 | 
			
		||||
                    and
 | 
			
		||||
                    local_nursery
 | 
			
		||||
                ):
 | 
			
		||||
                    entry: tuple|None = local_nursery._children.get(
 | 
			
		||||
                        tuple(pdb_user_uid)
 | 
			
		||||
| 
						 | 
				
			
			@ -492,7 +540,7 @@ async def handle_stream_from_peer(
 | 
			
		|||
                                f'last disconnected child uid: {uid}\n'
 | 
			
		||||
                                f'locking child uid: {pdb_user_uid}\n'
 | 
			
		||||
                            )
 | 
			
		||||
                            await _debug.maybe_wait_for_debugger(
 | 
			
		||||
                            await debug.maybe_wait_for_debugger(
 | 
			
		||||
                                child_in_debug=True
 | 
			
		||||
                            )
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -804,7 +852,6 @@ class IPCServer(Struct):
 | 
			
		|||
    async def listen_on(
 | 
			
		||||
        self,
 | 
			
		||||
        *,
 | 
			
		||||
        actor: Actor,
 | 
			
		||||
        accept_addrs: list[tuple[str, int|str]]|None = None,
 | 
			
		||||
        stream_handler_nursery: Nursery|None = None,
 | 
			
		||||
    ) -> list[IPCEndpoint]:
 | 
			
		||||
| 
						 | 
				
			
			@ -837,20 +884,19 @@ class IPCServer(Struct):
 | 
			
		|||
                f'{self}\n'
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        log.info(
 | 
			
		||||
        log.runtime(
 | 
			
		||||
            f'Binding to endpoints for,\n'
 | 
			
		||||
            f'{accept_addrs}\n'
 | 
			
		||||
        )
 | 
			
		||||
        eps: list[IPCEndpoint] = await self._parent_tn.start(
 | 
			
		||||
            partial(
 | 
			
		||||
                _serve_ipc_eps,
 | 
			
		||||
                actor=actor,
 | 
			
		||||
                server=self,
 | 
			
		||||
                stream_handler_tn=stream_handler_nursery,
 | 
			
		||||
                listen_addrs=accept_addrs,
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
        log.info(
 | 
			
		||||
        log.runtime(
 | 
			
		||||
            f'Started IPC endpoints\n'
 | 
			
		||||
            f'{eps}\n'
 | 
			
		||||
        )
 | 
			
		||||
| 
						 | 
				
			
			@ -873,7 +919,6 @@ class IPCServer(Struct):
 | 
			
		|||
 | 
			
		||||
async def _serve_ipc_eps(
 | 
			
		||||
    *,
 | 
			
		||||
    actor: Actor,
 | 
			
		||||
    server: IPCServer,
 | 
			
		||||
    stream_handler_tn: Nursery,
 | 
			
		||||
    listen_addrs: list[tuple[str, int|str]],
 | 
			
		||||
| 
						 | 
				
			
			@ -907,12 +952,13 @@ async def _serve_ipc_eps(
 | 
			
		|||
                    stream_handler_tn=stream_handler_tn,
 | 
			
		||||
                )
 | 
			
		||||
                try:
 | 
			
		||||
                    log.info(
 | 
			
		||||
                    log.runtime(
 | 
			
		||||
                        f'Starting new endpoint listener\n'
 | 
			
		||||
                        f'{ep}\n'
 | 
			
		||||
                    )
 | 
			
		||||
                    listener: trio.abc.Listener = await ep.start_listener()
 | 
			
		||||
                    assert listener is ep._listener
 | 
			
		||||
                    # actor = _state.current_actor()
 | 
			
		||||
                    # if actor.is_registry:
 | 
			
		||||
                    #     import pdbp; pdbp.set_trace()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -937,7 +983,6 @@ async def _serve_ipc_eps(
 | 
			
		|||
                    handler=partial(
 | 
			
		||||
                        handle_stream_from_peer,
 | 
			
		||||
                        server=server,
 | 
			
		||||
                        actor=actor,
 | 
			
		||||
                    ),
 | 
			
		||||
                    listeners=listeners,
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -948,13 +993,13 @@ async def _serve_ipc_eps(
 | 
			
		|||
                )
 | 
			
		||||
            )
 | 
			
		||||
            # TODO, wow make this message better! XD
 | 
			
		||||
            log.info(
 | 
			
		||||
            log.runtime(
 | 
			
		||||
                'Started server(s)\n'
 | 
			
		||||
                +
 | 
			
		||||
                '\n'.join([f'|_{addr}' for addr in listen_addrs])
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            log.info(
 | 
			
		||||
            log.runtime(
 | 
			
		||||
                f'Started IPC endpoints\n'
 | 
			
		||||
                f'{eps}\n'
 | 
			
		||||
            )
 | 
			
		||||
| 
						 | 
				
			
			@ -970,6 +1015,7 @@ async def _serve_ipc_eps(
 | 
			
		|||
                ep.close_listener()
 | 
			
		||||
                server._endpoints.remove(ep)
 | 
			
		||||
 | 
			
		||||
        # actor = _state.current_actor()
 | 
			
		||||
        # if actor.is_arbiter:
 | 
			
		||||
        #     import pdbp; pdbp.set_trace()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -980,7 +1026,6 @@ async def _serve_ipc_eps(
 | 
			
		|||
 | 
			
		||||
@acm
 | 
			
		||||
async def open_ipc_server(
 | 
			
		||||
    actor: Actor,
 | 
			
		||||
    parent_tn: Nursery|None = None,
 | 
			
		||||
    stream_handler_tn: Nursery|None = None,
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -127,6 +127,11 @@ async def start_listener(
 | 
			
		|||
    Start a TCP socket listener on the given `TCPAddress`.
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
    log.info(
 | 
			
		||||
        f'Attempting to bind TCP socket\n'
 | 
			
		||||
        f'>[\n'
 | 
			
		||||
        f'|_{addr}\n'
 | 
			
		||||
    )
 | 
			
		||||
    # ?TODO, maybe we should just change the lower-level call this is
 | 
			
		||||
    # using internall per-listener?
 | 
			
		||||
    listeners: list[SocketListener] = await open_tcp_listeners(
 | 
			
		||||
| 
						 | 
				
			
			@ -140,6 +145,12 @@ async def start_listener(
 | 
			
		|||
    assert len(listeners) == 1
 | 
			
		||||
    listener = listeners[0]
 | 
			
		||||
    host, port = listener.socket.getsockname()[:2]
 | 
			
		||||
 | 
			
		||||
    log.info(
 | 
			
		||||
        f'Listening on TCP socket\n'
 | 
			
		||||
        f'[>\n'
 | 
			
		||||
        f' |_{addr}\n'
 | 
			
		||||
    )
 | 
			
		||||
    return listener
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -270,7 +270,9 @@ def get_logger(
 | 
			
		|||
    subsys_spec: str|None = None,
 | 
			
		||||
 | 
			
		||||
) -> StackLevelAdapter:
 | 
			
		||||
    '''Return the package log or a sub-logger for ``name`` if provided.
 | 
			
		||||
    '''
 | 
			
		||||
    Return the `tractor`-library root logger or a sub-logger for
 | 
			
		||||
    `name` if provided.
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
    log: Logger
 | 
			
		||||
| 
						 | 
				
			
			@ -282,7 +284,7 @@ def get_logger(
 | 
			
		|||
        name != _proj_name
 | 
			
		||||
    ):
 | 
			
		||||
 | 
			
		||||
        # NOTE: for handling for modules that use ``get_logger(__name__)``
 | 
			
		||||
        # NOTE: for handling for modules that use `get_logger(__name__)`
 | 
			
		||||
        # we make the following stylistic choice:
 | 
			
		||||
        # - always avoid duplicate project-package token
 | 
			
		||||
        #   in msg output: i.e. tractor.tractor.ipc._chan.py in header
 | 
			
		||||
| 
						 | 
				
			
			@ -331,7 +333,7 @@ def get_logger(
 | 
			
		|||
 | 
			
		||||
def get_console_log(
 | 
			
		||||
    level: str|None = None,
 | 
			
		||||
    logger: Logger|None = None,
 | 
			
		||||
    logger: Logger|StackLevelAdapter|None = None,
 | 
			
		||||
    **kwargs,
 | 
			
		||||
 | 
			
		||||
) -> LoggerAdapter:
 | 
			
		||||
| 
						 | 
				
			
			@ -344,12 +346,23 @@ def get_console_log(
 | 
			
		|||
    Yeah yeah, i know we can use `logging.config.dictConfig()`. You do it.
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
    log = get_logger(
 | 
			
		||||
        logger=logger,
 | 
			
		||||
        **kwargs
 | 
			
		||||
    )  # set a root logger
 | 
			
		||||
    logger: Logger = log.logger
 | 
			
		||||
    # get/create a stack-aware-adapter
 | 
			
		||||
    if (
 | 
			
		||||
        logger
 | 
			
		||||
        and
 | 
			
		||||
        isinstance(logger, StackLevelAdapter)
 | 
			
		||||
    ):
 | 
			
		||||
        # XXX, for ex. when passed in by a caller wrapping some
 | 
			
		||||
        # other lib's logger instance with our level-adapter.
 | 
			
		||||
        log = logger
 | 
			
		||||
 | 
			
		||||
    else:
 | 
			
		||||
        log: StackLevelAdapter = get_logger(
 | 
			
		||||
            logger=logger,
 | 
			
		||||
            **kwargs
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    logger: Logger|StackLevelAdapter = log.logger
 | 
			
		||||
    if not level:
 | 
			
		||||
        return log
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -367,10 +380,7 @@ def get_console_log(
 | 
			
		|||
            None,
 | 
			
		||||
        )
 | 
			
		||||
    ):
 | 
			
		||||
        fmt = LOG_FORMAT
 | 
			
		||||
        # if logger:
 | 
			
		||||
        #     fmt = None
 | 
			
		||||
 | 
			
		||||
        fmt: str = LOG_FORMAT  # always apply our format?
 | 
			
		||||
        handler = StreamHandler()
 | 
			
		||||
        formatter = colorlog.ColoredFormatter(
 | 
			
		||||
            fmt=fmt,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -608,7 +608,7 @@ async def drain_to_final_msg(
 | 
			
		|||
            #
 | 
			
		||||
            # -[ ] make sure pause points work here for REPLing
 | 
			
		||||
            #   the runtime itself; i.e. ensure there's no hangs!
 | 
			
		||||
            # |_from tractor.devx._debug import pause
 | 
			
		||||
            # |_from tractor.devx.debug import pause
 | 
			
		||||
            #   await pause()
 | 
			
		||||
 | 
			
		||||
        # NOTE: we get here if the far end was
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -49,7 +49,7 @@ from tractor._state import (
 | 
			
		|||
    _runtime_vars,
 | 
			
		||||
)
 | 
			
		||||
from tractor._context import Unresolved
 | 
			
		||||
from tractor.devx import _debug
 | 
			
		||||
from tractor.devx import debug
 | 
			
		||||
from tractor.log import (
 | 
			
		||||
    get_logger,
 | 
			
		||||
    StackLevelAdapter,
 | 
			
		||||
| 
						 | 
				
			
			@ -479,12 +479,12 @@ def _run_asyncio_task(
 | 
			
		|||
    if (
 | 
			
		||||
        debug_mode()
 | 
			
		||||
        and
 | 
			
		||||
        (greenback := _debug.maybe_import_greenback(
 | 
			
		||||
        (greenback := debug.maybe_import_greenback(
 | 
			
		||||
            force_reload=True,
 | 
			
		||||
            raise_not_found=False,
 | 
			
		||||
        ))
 | 
			
		||||
    ):
 | 
			
		||||
        log.info(
 | 
			
		||||
        log.devx(
 | 
			
		||||
            f'Bestowing `greenback` portal for `asyncio`-task\n'
 | 
			
		||||
            f'{task}\n'
 | 
			
		||||
        )
 | 
			
		||||
| 
						 | 
				
			
			@ -841,7 +841,7 @@ async def translate_aio_errors(
 | 
			
		|||
    except BaseException as _trio_err:
 | 
			
		||||
        trio_err = chan._trio_err = _trio_err
 | 
			
		||||
        # await tractor.pause(shield=True)  # workx!
 | 
			
		||||
        entered: bool = await _debug._maybe_enter_pm(
 | 
			
		||||
        entered: bool = await debug._maybe_enter_pm(
 | 
			
		||||
            trio_err,
 | 
			
		||||
            api_frame=inspect.currentframe(),
 | 
			
		||||
        )
 | 
			
		||||
| 
						 | 
				
			
			@ -1406,7 +1406,7 @@ def run_as_asyncio_guest(
 | 
			
		|||
            )
 | 
			
		||||
            # XXX make it obvi we know this isn't supported yet!
 | 
			
		||||
            assert 0
 | 
			
		||||
            # await _debug.maybe_init_greenback(
 | 
			
		||||
            # await debug.maybe_init_greenback(
 | 
			
		||||
            #     force_reload=True,
 | 
			
		||||
            # )
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -31,4 +31,5 @@ from ._broadcast import (
 | 
			
		|||
)
 | 
			
		||||
from ._beg import (
 | 
			
		||||
    collapse_eg as collapse_eg,
 | 
			
		||||
    maybe_collapse_eg as maybe_collapse_eg,
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										419
									
								
								uv.lock
								
								
								
								
							
							
						
						
									
										419
									
								
								uv.lock
								
								
								
								
							| 
						 | 
				
			
			@ -1,23 +1,23 @@
 | 
			
		|||
version = 1
 | 
			
		||||
revision = 1
 | 
			
		||||
revision = 2
 | 
			
		||||
requires-python = ">=3.11"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "attrs"
 | 
			
		||||
version = "24.3.0"
 | 
			
		||||
source = { registry = "https://pypi.org/simple" }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/48/c8/6260f8ccc11f0917360fc0da435c5c9c7504e3db174d5a12a1494887b045/attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff", size = 805984 }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/48/c8/6260f8ccc11f0917360fc0da435c5c9c7504e3db174d5a12a1494887b045/attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff", size = 805984, upload-time = "2024-12-16T06:59:29.899Z" }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/89/aa/ab0f7891a01eeb2d2e338ae8fecbe57fcebea1a24dbb64d45801bfab481d/attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308", size = 63397 },
 | 
			
		||||
    { 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 = "bidict"
 | 
			
		||||
version = "0.23.1"
 | 
			
		||||
source = { registry = "https://pypi.org/simple" }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/9a/6e/026678aa5a830e07cd9498a05d3e7e650a4f56a42f267a53d22bcda1bdc9/bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71", size = 29093 }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/9a/6e/026678aa5a830e07cd9498a05d3e7e650a4f56a42f267a53d22bcda1bdc9/bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71", size = 29093, upload-time = "2024-02-18T19:09:05.748Z" }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5", size = 32764 },
 | 
			
		||||
    { 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]]
 | 
			
		||||
| 
						 | 
				
			
			@ -27,51 +27,51 @@ source = { registry = "https://pypi.org/simple" }
 | 
			
		|||
dependencies = [
 | 
			
		||||
    { name = "pycparser" },
 | 
			
		||||
]
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload-time = "2024-09-04T20:44:09.481Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload-time = "2024-09-04T20:44:10.873Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "colorama"
 | 
			
		||||
version = "0.4.6"
 | 
			
		||||
source = { registry = "https://pypi.org/simple" }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
| 
						 | 
				
			
			@ -81,9 +81,9 @@ source = { registry = "https://pypi.org/simple" }
 | 
			
		|||
dependencies = [
 | 
			
		||||
    { name = "colorama", marker = "sys_platform == 'win32'" },
 | 
			
		||||
]
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/d3/7a/359f4d5df2353f26172b3cc39ea32daa39af8de522205f512f458923e677/colorlog-6.9.0.tar.gz", hash = "sha256:bfba54a1b93b94f54e1f4fe48395725a3d92fd2a4af702f6bd70946bdc0c6ac2", size = 16624 }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/d3/7a/359f4d5df2353f26172b3cc39ea32daa39af8de522205f512f458923e677/colorlog-6.9.0.tar.gz", hash = "sha256:bfba54a1b93b94f54e1f4fe48395725a3d92fd2a4af702f6bd70946bdc0c6ac2", size = 16624, upload-time = "2024-10-29T18:34:51.011Z" }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/e3/51/9b208e85196941db2f0654ad0357ca6388ab3ed67efdbfc799f35d1f83aa/colorlog-6.9.0-py3-none-any.whl", hash = "sha256:5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff", size = 11424 },
 | 
			
		||||
    { 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]]
 | 
			
		||||
| 
						 | 
				
			
			@ -95,98 +95,98 @@ dependencies = [
 | 
			
		|||
    { name = "outcome" },
 | 
			
		||||
    { name = "sniffio" },
 | 
			
		||||
]
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/dc/c1/ab3a42c0f3ed56df9cd33de1539b3198d98c6ccbaf88a73d6be0b72d85e0/greenback-1.2.1.tar.gz", hash = "sha256:de3ca656885c03b96dab36079f3de74bb5ba061da9bfe3bb69dccc866ef95ea3", size = 42597 }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/dc/c1/ab3a42c0f3ed56df9cd33de1539b3198d98c6ccbaf88a73d6be0b72d85e0/greenback-1.2.1.tar.gz", hash = "sha256:de3ca656885c03b96dab36079f3de74bb5ba061da9bfe3bb69dccc866ef95ea3", size = 42597, upload-time = "2024-02-20T21:23:13.239Z" }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/71/d0/b8dc79d5ecfffacad9c844b6ae76b9c6259935796d3c561deccbf8fa421d/greenback-1.2.1-py3-none-any.whl", hash = "sha256:98768edbbe4340091a9730cf64a683fcbaa3f2cb81e4ac41d7ed28d3b6f74b79", size = 28062 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/71/d0/b8dc79d5ecfffacad9c844b6ae76b9c6259935796d3c561deccbf8fa421d/greenback-1.2.1-py3-none-any.whl", hash = "sha256:98768edbbe4340091a9730cf64a683fcbaa3f2cb81e4ac41d7ed28d3b6f74b79", size = 28062, upload-time = "2024-02-20T21:23:12.031Z" },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "greenlet"
 | 
			
		||||
version = "3.1.1"
 | 
			
		||||
source = { registry = "https://pypi.org/simple" }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/2f/ff/df5fede753cc10f6a5be0931204ea30c35fa2f2ea7a35b25bdaf4fe40e46/greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", size = 186022 }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/2f/ff/df5fede753cc10f6a5be0931204ea30c35fa2f2ea7a35b25bdaf4fe40e46/greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", size = 186022, upload-time = "2024-09-20T18:21:04.506Z" }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/28/62/1c2665558618553c42922ed47a4e6d6527e2fa3516a8256c2f431c5d0441/greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70", size = 272479 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/76/9d/421e2d5f07285b6e4e3a676b016ca781f63cfe4a0cd8eaecf3fd6f7a71ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159", size = 640404 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/e5/de/6e05f5c59262a584e502dd3d261bbdd2c97ab5416cc9c0b91ea38932a901/greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e", size = 652813 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/49/93/d5f93c84241acdea15a8fd329362c2c71c79e1a507c3f142a5d67ea435ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1", size = 648517 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/15/85/72f77fc02d00470c86a5c982b8daafdf65d38aefbbe441cebff3bf7037fc/greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383", size = 647831 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/f7/4b/1c9695aa24f808e156c8f4813f685d975ca73c000c2a5056c514c64980f6/greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a", size = 602413 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/76/70/ad6e5b31ef330f03b12559d19fda2606a522d3849cde46b24f223d6d1619/greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511", size = 1129619 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/f4/fb/201e1b932e584066e0f0658b538e73c459b34d44b4bd4034f682423bc801/greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395", size = 1155198 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/12/da/b9ed5e310bb8b89661b80cbcd4db5a067903bbcd7fc854923f5ebb4144f0/greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39", size = 298930 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/7d/ec/bad1ac26764d26aa1353216fcbfa4670050f66d445448aafa227f8b16e80/greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d", size = 274260 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/66/d4/c8c04958870f482459ab5956c2942c4ec35cac7fe245527f1039837c17a9/greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", size = 649064 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/51/41/467b12a8c7c1303d20abcca145db2be4e6cd50a951fa30af48b6ec607581/greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa", size = 663420 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/27/8f/2a93cd9b1e7107d5c7b3b7816eeadcac2ebcaf6d6513df9abaf0334777f6/greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441", size = 658035 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/57/5c/7c6f50cb12be092e1dccb2599be5a942c3416dbcfb76efcf54b3f8be4d8d/greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36", size = 660105 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/f1/66/033e58a50fd9ec9df00a8671c74f1f3a320564c6415a4ed82a1c651654ba/greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9", size = 613077 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/19/c5/36384a06f748044d06bdd8776e231fadf92fc896bd12cb1c9f5a1bda9578/greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0", size = 1135975 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/38/f9/c0a0eb61bdf808d23266ecf1d63309f0e1471f284300ce6dac0ae1231881/greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942", size = 1163955 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/43/21/a5d9df1d21514883333fc86584c07c2b49ba7c602e670b174bd73cfc9c7f/greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01", size = 299655 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/f3/57/0db4940cd7bb461365ca8d6fd53e68254c9dbbcc2b452e69d0d41f10a85e/greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1", size = 272990 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/1c/ec/423d113c9f74e5e402e175b157203e9102feeb7088cee844d735b28ef963/greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff", size = 649175 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/a9/46/ddbd2db9ff209186b7b7c621d1432e2f21714adc988703dbdd0e65155c77/greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a", size = 663425 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/bc/f9/9c82d6b2b04aa37e38e74f0c429aece5eeb02bab6e3b98e7db89b23d94c6/greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e", size = 657736 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/d9/42/b87bc2a81e3a62c3de2b0d550bf91a86939442b7ff85abb94eec3fc0e6aa/greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4", size = 660347 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/37/fa/71599c3fd06336cdc3eac52e6871cfebab4d9d70674a9a9e7a482c318e99/greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e", size = 615583 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/4e/96/e9ef85de031703ee7a4483489b40cf307f93c1824a02e903106f2ea315fe/greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1", size = 1133039 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/87/76/b2b6362accd69f2d1889db61a18c94bc743e961e3cab344c2effaa4b4a25/greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c", size = 1160716 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/1f/1b/54336d876186920e185066d8c3024ad55f21d7cc3683c856127ddb7b13ce/greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761", size = 299490 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/5f/17/bea55bf36990e1638a2af5ba10c1640273ef20f627962cf97107f1e5d637/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011", size = 643731 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/78/d2/aa3d2157f9ab742a08e0fd8f77d4699f37c22adfbfeb0c610a186b5f75e0/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13", size = 649304 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/f1/8e/d0aeffe69e53ccff5a28fa86f07ad1d2d2d6537a9506229431a2a02e2f15/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475", size = 646537 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/05/79/e15408220bbb989469c8871062c97c6c9136770657ba779711b90870d867/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b", size = 642506 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/18/87/470e01a940307796f1d25f8167b551a968540fbe0551c0ebb853cb527dd6/greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822", size = 602753 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/e2/72/576815ba674eddc3c25028238f74d7b8068902b3968cbe456771b166455e/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01", size = 1122731 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/ac/38/08cc303ddddc4b3d7c628c3039a61a3aae36c241ed01393d00c2fd663473/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6", size = 1142112 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/28/62/1c2665558618553c42922ed47a4e6d6527e2fa3516a8256c2f431c5d0441/greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70", size = 272479, upload-time = "2024-09-20T17:07:22.332Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/76/9d/421e2d5f07285b6e4e3a676b016ca781f63cfe4a0cd8eaecf3fd6f7a71ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159", size = 640404, upload-time = "2024-09-20T17:36:45.588Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/e5/de/6e05f5c59262a584e502dd3d261bbdd2c97ab5416cc9c0b91ea38932a901/greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e", size = 652813, upload-time = "2024-09-20T17:39:19.052Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/49/93/d5f93c84241acdea15a8fd329362c2c71c79e1a507c3f142a5d67ea435ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1", size = 648517, upload-time = "2024-09-20T17:44:24.101Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/15/85/72f77fc02d00470c86a5c982b8daafdf65d38aefbbe441cebff3bf7037fc/greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383", size = 647831, upload-time = "2024-09-20T17:08:40.577Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/f7/4b/1c9695aa24f808e156c8f4813f685d975ca73c000c2a5056c514c64980f6/greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a", size = 602413, upload-time = "2024-09-20T17:08:31.728Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/76/70/ad6e5b31ef330f03b12559d19fda2606a522d3849cde46b24f223d6d1619/greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511", size = 1129619, upload-time = "2024-09-20T17:44:14.222Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/f4/fb/201e1b932e584066e0f0658b538e73c459b34d44b4bd4034f682423bc801/greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395", size = 1155198, upload-time = "2024-09-20T17:09:23.903Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/12/da/b9ed5e310bb8b89661b80cbcd4db5a067903bbcd7fc854923f5ebb4144f0/greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39", size = 298930, upload-time = "2024-09-20T17:25:18.656Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/7d/ec/bad1ac26764d26aa1353216fcbfa4670050f66d445448aafa227f8b16e80/greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d", size = 274260, upload-time = "2024-09-20T17:08:07.301Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/66/d4/c8c04958870f482459ab5956c2942c4ec35cac7fe245527f1039837c17a9/greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", size = 649064, upload-time = "2024-09-20T17:36:47.628Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/51/41/467b12a8c7c1303d20abcca145db2be4e6cd50a951fa30af48b6ec607581/greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa", size = 663420, upload-time = "2024-09-20T17:39:21.258Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/27/8f/2a93cd9b1e7107d5c7b3b7816eeadcac2ebcaf6d6513df9abaf0334777f6/greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441", size = 658035, upload-time = "2024-09-20T17:44:26.501Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/57/5c/7c6f50cb12be092e1dccb2599be5a942c3416dbcfb76efcf54b3f8be4d8d/greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36", size = 660105, upload-time = "2024-09-20T17:08:42.048Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/f1/66/033e58a50fd9ec9df00a8671c74f1f3a320564c6415a4ed82a1c651654ba/greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9", size = 613077, upload-time = "2024-09-20T17:08:33.707Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/19/c5/36384a06f748044d06bdd8776e231fadf92fc896bd12cb1c9f5a1bda9578/greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0", size = 1135975, upload-time = "2024-09-20T17:44:15.989Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/38/f9/c0a0eb61bdf808d23266ecf1d63309f0e1471f284300ce6dac0ae1231881/greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942", size = 1163955, upload-time = "2024-09-20T17:09:25.539Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/43/21/a5d9df1d21514883333fc86584c07c2b49ba7c602e670b174bd73cfc9c7f/greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01", size = 299655, upload-time = "2024-09-20T17:21:22.427Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/f3/57/0db4940cd7bb461365ca8d6fd53e68254c9dbbcc2b452e69d0d41f10a85e/greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1", size = 272990, upload-time = "2024-09-20T17:08:26.312Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/1c/ec/423d113c9f74e5e402e175b157203e9102feeb7088cee844d735b28ef963/greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff", size = 649175, upload-time = "2024-09-20T17:36:48.983Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/a9/46/ddbd2db9ff209186b7b7c621d1432e2f21714adc988703dbdd0e65155c77/greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a", size = 663425, upload-time = "2024-09-20T17:39:22.705Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/bc/f9/9c82d6b2b04aa37e38e74f0c429aece5eeb02bab6e3b98e7db89b23d94c6/greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e", size = 657736, upload-time = "2024-09-20T17:44:28.544Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/d9/42/b87bc2a81e3a62c3de2b0d550bf91a86939442b7ff85abb94eec3fc0e6aa/greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4", size = 660347, upload-time = "2024-09-20T17:08:45.56Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/37/fa/71599c3fd06336cdc3eac52e6871cfebab4d9d70674a9a9e7a482c318e99/greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e", size = 615583, upload-time = "2024-09-20T17:08:36.85Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/4e/96/e9ef85de031703ee7a4483489b40cf307f93c1824a02e903106f2ea315fe/greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1", size = 1133039, upload-time = "2024-09-20T17:44:18.287Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/87/76/b2b6362accd69f2d1889db61a18c94bc743e961e3cab344c2effaa4b4a25/greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c", size = 1160716, upload-time = "2024-09-20T17:09:27.112Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/1f/1b/54336d876186920e185066d8c3024ad55f21d7cc3683c856127ddb7b13ce/greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761", size = 299490, upload-time = "2024-09-20T17:17:09.501Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/5f/17/bea55bf36990e1638a2af5ba10c1640273ef20f627962cf97107f1e5d637/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011", size = 643731, upload-time = "2024-09-20T17:36:50.376Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/78/d2/aa3d2157f9ab742a08e0fd8f77d4699f37c22adfbfeb0c610a186b5f75e0/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13", size = 649304, upload-time = "2024-09-20T17:39:24.55Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/f1/8e/d0aeffe69e53ccff5a28fa86f07ad1d2d2d6537a9506229431a2a02e2f15/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475", size = 646537, upload-time = "2024-09-20T17:44:31.102Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/05/79/e15408220bbb989469c8871062c97c6c9136770657ba779711b90870d867/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b", size = 642506, upload-time = "2024-09-20T17:08:47.852Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/18/87/470e01a940307796f1d25f8167b551a968540fbe0551c0ebb853cb527dd6/greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822", size = 602753, upload-time = "2024-09-20T17:08:38.079Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/e2/72/576815ba674eddc3c25028238f74d7b8068902b3968cbe456771b166455e/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01", size = 1122731, upload-time = "2024-09-20T17:44:20.556Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/ac/38/08cc303ddddc4b3d7c628c3039a61a3aae36c241ed01393d00c2fd663473/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6", size = 1142112, upload-time = "2024-09-20T17:09:28.753Z" },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "idna"
 | 
			
		||||
version = "3.10"
 | 
			
		||||
source = { registry = "https://pypi.org/simple" }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
 | 
			
		||||
    { 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 = "iniconfig"
 | 
			
		||||
version = "2.0.0"
 | 
			
		||||
source = { registry = "https://pypi.org/simple" }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646, upload-time = "2023-01-07T11:08:11.254Z" }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
 | 
			
		||||
    { 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 = "msgspec"
 | 
			
		||||
version = "0.19.0"
 | 
			
		||||
source = { registry = "https://pypi.org/simple" }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/cf/9b/95d8ce458462b8b71b8a70fa94563b2498b89933689f3a7b8911edfae3d7/msgspec-0.19.0.tar.gz", hash = "sha256:604037e7cd475345848116e89c553aa9a233259733ab51986ac924ab1b976f8e", size = 216934 }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/cf/9b/95d8ce458462b8b71b8a70fa94563b2498b89933689f3a7b8911edfae3d7/msgspec-0.19.0.tar.gz", hash = "sha256:604037e7cd475345848116e89c553aa9a233259733ab51986ac924ab1b976f8e", size = 216934, upload-time = "2024-12-27T17:40:28.597Z" }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/24/d4/2ec2567ac30dab072cce3e91fb17803c52f0a37aab6b0c24375d2b20a581/msgspec-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa77046904db764b0462036bc63ef71f02b75b8f72e9c9dd4c447d6da1ed8f8e", size = 187939 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/2b/c0/18226e4328897f4f19875cb62bb9259fe47e901eade9d9376ab5f251a929/msgspec-0.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:047cfa8675eb3bad68722cfe95c60e7afabf84d1bd8938979dd2b92e9e4a9551", size = 182202 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/81/25/3a4b24d468203d8af90d1d351b77ea3cffb96b29492855cf83078f16bfe4/msgspec-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e78f46ff39a427e10b4a61614a2777ad69559cc8d603a7c05681f5a595ea98f7", size = 209029 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/85/2e/db7e189b57901955239f7689b5dcd6ae9458637a9c66747326726c650523/msgspec-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c7adf191e4bd3be0e9231c3b6dc20cf1199ada2af523885efc2ed218eafd011", size = 210682 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/03/97/7c8895c9074a97052d7e4a1cc1230b7b6e2ca2486714eb12c3f08bb9d284/msgspec-0.19.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f04cad4385e20be7c7176bb8ae3dca54a08e9756cfc97bcdb4f18560c3042063", size = 214003 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/61/61/e892997bcaa289559b4d5869f066a8021b79f4bf8e955f831b095f47a4cd/msgspec-0.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45c8fb410670b3b7eb884d44a75589377c341ec1392b778311acdbfa55187716", size = 216833 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/ce/3d/71b2dffd3a1c743ffe13296ff701ee503feaebc3f04d0e75613b6563c374/msgspec-0.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:70eaef4934b87193a27d802534dc466778ad8d536e296ae2f9334e182ac27b6c", size = 186184 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/b2/5f/a70c24f075e3e7af2fae5414c7048b0e11389685b7f717bb55ba282a34a7/msgspec-0.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f98bd8962ad549c27d63845b50af3f53ec468b6318400c9f1adfe8b092d7b62f", size = 190485 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/89/b0/1b9763938cfae12acf14b682fcf05c92855974d921a5a985ecc197d1c672/msgspec-0.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:43bbb237feab761b815ed9df43b266114203f53596f9b6e6f00ebd79d178cdf2", size = 183910 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/87/81/0c8c93f0b92c97e326b279795f9c5b956c5a97af28ca0fbb9fd86c83737a/msgspec-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cfc033c02c3e0aec52b71710d7f84cb3ca5eb407ab2ad23d75631153fdb1f12", size = 210633 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/d0/ef/c5422ce8af73928d194a6606f8ae36e93a52fd5e8df5abd366903a5ca8da/msgspec-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d911c442571605e17658ca2b416fd8579c5050ac9adc5e00c2cb3126c97f73bc", size = 213594 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/19/2b/4137bc2ed45660444842d042be2cf5b18aa06efd2cda107cff18253b9653/msgspec-0.19.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:757b501fa57e24896cf40a831442b19a864f56d253679f34f260dcb002524a6c", size = 214053 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/9d/e6/8ad51bdc806aac1dc501e8fe43f759f9ed7284043d722b53323ea421c360/msgspec-0.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5f0f65f29b45e2816d8bded36e6b837a4bf5fb60ec4bc3c625fa2c6da4124537", size = 219081 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/b1/ef/27dd35a7049c9a4f4211c6cd6a8c9db0a50647546f003a5867827ec45391/msgspec-0.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:067f0de1c33cfa0b6a8206562efdf6be5985b988b53dd244a8e06f993f27c8c0", size = 187467 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/3c/cb/2842c312bbe618d8fefc8b9cedce37f773cdc8fa453306546dba2c21fd98/msgspec-0.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f12d30dd6266557aaaf0aa0f9580a9a8fbeadfa83699c487713e355ec5f0bd86", size = 190498 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/58/95/c40b01b93465e1a5f3b6c7d91b10fb574818163740cc3acbe722d1e0e7e4/msgspec-0.19.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82b2c42c1b9ebc89e822e7e13bbe9d17ede0c23c187469fdd9505afd5a481314", size = 183950 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/e8/f0/5b764e066ce9aba4b70d1db8b087ea66098c7c27d59b9dd8a3532774d48f/msgspec-0.19.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19746b50be214a54239aab822964f2ac81e38b0055cca94808359d779338c10e", size = 210647 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/9d/87/bc14f49bc95c4cb0dd0a8c56028a67c014ee7e6818ccdce74a4862af259b/msgspec-0.19.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60ef4bdb0ec8e4ad62e5a1f95230c08efb1f64f32e6e8dd2ced685bcc73858b5", size = 213563 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/53/2f/2b1c2b056894fbaa975f68f81e3014bb447516a8b010f1bed3fb0e016ed7/msgspec-0.19.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac7f7c377c122b649f7545810c6cd1b47586e3aa3059126ce3516ac7ccc6a6a9", size = 213996 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/aa/5a/4cd408d90d1417e8d2ce6a22b98a6853c1b4d7cb7669153e4424d60087f6/msgspec-0.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5bc1472223a643f5ffb5bf46ccdede7f9795078194f14edd69e3aab7020d327", size = 219087 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/23/d8/f15b40611c2d5753d1abb0ca0da0c75348daf1252220e5dda2867bd81062/msgspec-0.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:317050bc0f7739cb30d257ff09152ca309bf5a369854bbf1e57dffc310c1f20f", size = 187432 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/24/d4/2ec2567ac30dab072cce3e91fb17803c52f0a37aab6b0c24375d2b20a581/msgspec-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa77046904db764b0462036bc63ef71f02b75b8f72e9c9dd4c447d6da1ed8f8e", size = 187939, upload-time = "2024-12-27T17:39:32.347Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/2b/c0/18226e4328897f4f19875cb62bb9259fe47e901eade9d9376ab5f251a929/msgspec-0.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:047cfa8675eb3bad68722cfe95c60e7afabf84d1bd8938979dd2b92e9e4a9551", size = 182202, upload-time = "2024-12-27T17:39:33.633Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/81/25/3a4b24d468203d8af90d1d351b77ea3cffb96b29492855cf83078f16bfe4/msgspec-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e78f46ff39a427e10b4a61614a2777ad69559cc8d603a7c05681f5a595ea98f7", size = 209029, upload-time = "2024-12-27T17:39:35.023Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/85/2e/db7e189b57901955239f7689b5dcd6ae9458637a9c66747326726c650523/msgspec-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c7adf191e4bd3be0e9231c3b6dc20cf1199ada2af523885efc2ed218eafd011", size = 210682, upload-time = "2024-12-27T17:39:36.384Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/03/97/7c8895c9074a97052d7e4a1cc1230b7b6e2ca2486714eb12c3f08bb9d284/msgspec-0.19.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f04cad4385e20be7c7176bb8ae3dca54a08e9756cfc97bcdb4f18560c3042063", size = 214003, upload-time = "2024-12-27T17:39:39.097Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/61/61/e892997bcaa289559b4d5869f066a8021b79f4bf8e955f831b095f47a4cd/msgspec-0.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45c8fb410670b3b7eb884d44a75589377c341ec1392b778311acdbfa55187716", size = 216833, upload-time = "2024-12-27T17:39:41.203Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/ce/3d/71b2dffd3a1c743ffe13296ff701ee503feaebc3f04d0e75613b6563c374/msgspec-0.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:70eaef4934b87193a27d802534dc466778ad8d536e296ae2f9334e182ac27b6c", size = 186184, upload-time = "2024-12-27T17:39:43.702Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/b2/5f/a70c24f075e3e7af2fae5414c7048b0e11389685b7f717bb55ba282a34a7/msgspec-0.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f98bd8962ad549c27d63845b50af3f53ec468b6318400c9f1adfe8b092d7b62f", size = 190485, upload-time = "2024-12-27T17:39:44.974Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/89/b0/1b9763938cfae12acf14b682fcf05c92855974d921a5a985ecc197d1c672/msgspec-0.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:43bbb237feab761b815ed9df43b266114203f53596f9b6e6f00ebd79d178cdf2", size = 183910, upload-time = "2024-12-27T17:39:46.401Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/87/81/0c8c93f0b92c97e326b279795f9c5b956c5a97af28ca0fbb9fd86c83737a/msgspec-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cfc033c02c3e0aec52b71710d7f84cb3ca5eb407ab2ad23d75631153fdb1f12", size = 210633, upload-time = "2024-12-27T17:39:49.099Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/d0/ef/c5422ce8af73928d194a6606f8ae36e93a52fd5e8df5abd366903a5ca8da/msgspec-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d911c442571605e17658ca2b416fd8579c5050ac9adc5e00c2cb3126c97f73bc", size = 213594, upload-time = "2024-12-27T17:39:51.204Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/19/2b/4137bc2ed45660444842d042be2cf5b18aa06efd2cda107cff18253b9653/msgspec-0.19.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:757b501fa57e24896cf40a831442b19a864f56d253679f34f260dcb002524a6c", size = 214053, upload-time = "2024-12-27T17:39:52.866Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/9d/e6/8ad51bdc806aac1dc501e8fe43f759f9ed7284043d722b53323ea421c360/msgspec-0.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5f0f65f29b45e2816d8bded36e6b837a4bf5fb60ec4bc3c625fa2c6da4124537", size = 219081, upload-time = "2024-12-27T17:39:55.142Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/b1/ef/27dd35a7049c9a4f4211c6cd6a8c9db0a50647546f003a5867827ec45391/msgspec-0.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:067f0de1c33cfa0b6a8206562efdf6be5985b988b53dd244a8e06f993f27c8c0", size = 187467, upload-time = "2024-12-27T17:39:56.531Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/3c/cb/2842c312bbe618d8fefc8b9cedce37f773cdc8fa453306546dba2c21fd98/msgspec-0.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f12d30dd6266557aaaf0aa0f9580a9a8fbeadfa83699c487713e355ec5f0bd86", size = 190498, upload-time = "2024-12-27T17:40:00.427Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/58/95/c40b01b93465e1a5f3b6c7d91b10fb574818163740cc3acbe722d1e0e7e4/msgspec-0.19.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82b2c42c1b9ebc89e822e7e13bbe9d17ede0c23c187469fdd9505afd5a481314", size = 183950, upload-time = "2024-12-27T17:40:04.219Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/e8/f0/5b764e066ce9aba4b70d1db8b087ea66098c7c27d59b9dd8a3532774d48f/msgspec-0.19.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19746b50be214a54239aab822964f2ac81e38b0055cca94808359d779338c10e", size = 210647, upload-time = "2024-12-27T17:40:05.606Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/9d/87/bc14f49bc95c4cb0dd0a8c56028a67c014ee7e6818ccdce74a4862af259b/msgspec-0.19.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60ef4bdb0ec8e4ad62e5a1f95230c08efb1f64f32e6e8dd2ced685bcc73858b5", size = 213563, upload-time = "2024-12-27T17:40:10.516Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/53/2f/2b1c2b056894fbaa975f68f81e3014bb447516a8b010f1bed3fb0e016ed7/msgspec-0.19.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac7f7c377c122b649f7545810c6cd1b47586e3aa3059126ce3516ac7ccc6a6a9", size = 213996, upload-time = "2024-12-27T17:40:12.244Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/aa/5a/4cd408d90d1417e8d2ce6a22b98a6853c1b4d7cb7669153e4424d60087f6/msgspec-0.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5bc1472223a643f5ffb5bf46ccdede7f9795078194f14edd69e3aab7020d327", size = 219087, upload-time = "2024-12-27T17:40:14.881Z" },
 | 
			
		||||
    { 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]]
 | 
			
		||||
| 
						 | 
				
			
			@ -196,18 +196,18 @@ source = { registry = "https://pypi.org/simple" }
 | 
			
		|||
dependencies = [
 | 
			
		||||
    { name = "attrs" },
 | 
			
		||||
]
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/98/df/77698abfac98571e65ffeb0c1fba8ffd692ab8458d617a0eed7d9a8d38f2/outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8", size = 21060 }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/98/df/77698abfac98571e65ffeb0c1fba8ffd692ab8458d617a0eed7d9a8d38f2/outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8", size = 21060, upload-time = "2023-10-26T04:26:04.361Z" }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/55/8b/5ab7257531a5d830fc8000c476e63c935488d74609b50f9384a643ec0a62/outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b", size = 10692 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/55/8b/5ab7257531a5d830fc8000c476e63c935488d74609b50f9384a643ec0a62/outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b", size = 10692, upload-time = "2023-10-26T04:26:02.532Z" },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "packaging"
 | 
			
		||||
version = "24.2"
 | 
			
		||||
source = { registry = "https://pypi.org/simple" }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950, upload-time = "2024-11-08T09:47:47.202Z" }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451, upload-time = "2024-11-08T09:47:44.722Z" },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
| 
						 | 
				
			
			@ -219,9 +219,9 @@ dependencies = [
 | 
			
		|||
    { name = "pygments" },
 | 
			
		||||
    { name = "tabcompleter" },
 | 
			
		||||
]
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/69/13/80da03638f62facbee76312ca9ee5941c017b080f2e4c6919fd4e87e16e3/pdbp-1.6.1.tar.gz", hash = "sha256:f4041642952a05df89664e166d5bd379607a0866ddd753c06874f65552bdf40b", size = 25322 }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/69/13/80da03638f62facbee76312ca9ee5941c017b080f2e4c6919fd4e87e16e3/pdbp-1.6.1.tar.gz", hash = "sha256:f4041642952a05df89664e166d5bd379607a0866ddd753c06874f65552bdf40b", size = 25322, upload-time = "2024-11-07T15:36:43.062Z" }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/29/93/d56fb9ba5569dc29d8263c72e46d21a2fd38741339ebf03f54cf7561828c/pdbp-1.6.1-py3-none-any.whl", hash = "sha256:f10bad2ee044c0e5c168cb0825abfdbdc01c50013e9755df5261b060bdd35c22", size = 21495 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/29/93/d56fb9ba5569dc29d8263c72e46d21a2fd38741339ebf03f54cf7561828c/pdbp-1.6.1-py3-none-any.whl", hash = "sha256:f10bad2ee044c0e5c168cb0825abfdbdc01c50013e9755df5261b060bdd35c22", size = 21495, upload-time = "2024-11-07T15:36:41.061Z" },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
| 
						 | 
				
			
			@ -231,18 +231,18 @@ source = { registry = "https://pypi.org/simple" }
 | 
			
		|||
dependencies = [
 | 
			
		||||
    { name = "ptyprocess" },
 | 
			
		||||
]
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450 }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "pluggy"
 | 
			
		||||
version = "1.5.0"
 | 
			
		||||
source = { registry = "https://pypi.org/simple" }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
| 
						 | 
				
			
			@ -252,66 +252,66 @@ source = { registry = "https://pypi.org/simple" }
 | 
			
		|||
dependencies = [
 | 
			
		||||
    { name = "wcwidth" },
 | 
			
		||||
]
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/e1/bd15cb8ffdcfeeb2bdc215de3c3cffca11408d829e4b8416dcfe71ba8854/prompt_toolkit-3.0.50.tar.gz", hash = "sha256:544748f3860a2623ca5cd6d2795e7a14f3d0e1c3c9728359013f79877fc89bab", size = 429087 }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/e1/bd15cb8ffdcfeeb2bdc215de3c3cffca11408d829e4b8416dcfe71ba8854/prompt_toolkit-3.0.50.tar.gz", hash = "sha256:544748f3860a2623ca5cd6d2795e7a14f3d0e1c3c9728359013f79877fc89bab", size = 429087, upload-time = "2025-01-20T15:55:35.072Z" }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/e4/ea/d836f008d33151c7a1f62caf3d8dd782e4d15f6a43897f64480c2b8de2ad/prompt_toolkit-3.0.50-py3-none-any.whl", hash = "sha256:9b6427eb19e479d98acff65196a307c555eb567989e6d88ebbb1b509d9779198", size = 387816 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/e4/ea/d836f008d33151c7a1f62caf3d8dd782e4d15f6a43897f64480c2b8de2ad/prompt_toolkit-3.0.50-py3-none-any.whl", hash = "sha256:9b6427eb19e479d98acff65196a307c555eb567989e6d88ebbb1b509d9779198", size = 387816, upload-time = "2025-01-20T15:55:29.98Z" },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "psutil"
 | 
			
		||||
version = "7.0.0"
 | 
			
		||||
source = { registry = "https://pypi.org/simple" }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003 }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003, upload-time = "2025-02-13T21:54:07.946Z" }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051, upload-time = "2025-02-13T21:54:12.36Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535, upload-time = "2025-02-13T21:54:16.07Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004, upload-time = "2025-02-13T21:54:18.662Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986, upload-time = "2025-02-13T21:54:21.811Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544, upload-time = "2025-02-13T21:54:24.68Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053, upload-time = "2025-02-13T21:54:34.31Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload-time = "2025-02-13T21:54:37.486Z" },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "ptyprocess"
 | 
			
		||||
version = "0.7.0"
 | 
			
		||||
source = { registry = "https://pypi.org/simple" }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762 }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993 },
 | 
			
		||||
    { 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 = "pycparser"
 | 
			
		||||
version = "2.22"
 | 
			
		||||
source = { registry = "https://pypi.org/simple" }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "pygments"
 | 
			
		||||
version = "2.19.1"
 | 
			
		||||
source = { registry = "https://pypi.org/simple" }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "pyperclip"
 | 
			
		||||
version = "1.9.0"
 | 
			
		||||
source = { registry = "https://pypi.org/simple" }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/30/23/2f0a3efc4d6a32f3b63cdff36cd398d9701d26cda58e3ab97ac79fb5e60d/pyperclip-1.9.0.tar.gz", hash = "sha256:b7de0142ddc81bfc5c7507eea19da920b92252b548b96186caf94a5e2527d310", size = 20961 }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/30/23/2f0a3efc4d6a32f3b63cdff36cd398d9701d26cda58e3ab97ac79fb5e60d/pyperclip-1.9.0.tar.gz", hash = "sha256:b7de0142ddc81bfc5c7507eea19da920b92252b548b96186caf94a5e2527d310", size = 20961, upload-time = "2024-06-18T20:38:48.401Z" }
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "pyreadline3"
 | 
			
		||||
version = "3.5.4"
 | 
			
		||||
source = { registry = "https://pypi.org/simple" }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/0f/49/4cea918a08f02817aabae639e3d0ac046fef9f9180518a3ad394e22da148/pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7", size = 99839 }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/0f/49/4cea918a08f02817aabae639e3d0ac046fef9f9180518a3ad394e22da148/pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7", size = 99839, upload-time = "2024-09-19T02:40:10.062Z" }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178, upload-time = "2024-09-19T02:40:08.598Z" },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
| 
						 | 
				
			
			@ -324,36 +324,36 @@ dependencies = [
 | 
			
		|||
    { name = "packaging" },
 | 
			
		||||
    { name = "pluggy" },
 | 
			
		||||
]
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 },
 | 
			
		||||
    { 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 = "sniffio"
 | 
			
		||||
version = "1.3.1"
 | 
			
		||||
source = { registry = "https://pypi.org/simple" }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "sortedcontainers"
 | 
			
		||||
version = "2.4.0"
 | 
			
		||||
source = { registry = "https://pypi.org/simple" }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594 }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "stackscope"
 | 
			
		||||
version = "0.2.2"
 | 
			
		||||
source = { registry = "https://pypi.org/simple" }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/4a/fc/20dbb993353f31230138f3c63f3f0c881d1853e70d7a30cd68d2ba4cf1e2/stackscope-0.2.2.tar.gz", hash = "sha256:f508c93eb4861ada466dd3ff613ca203962ceb7587ad013759f15394e6a4e619", size = 90479 }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/4a/fc/20dbb993353f31230138f3c63f3f0c881d1853e70d7a30cd68d2ba4cf1e2/stackscope-0.2.2.tar.gz", hash = "sha256:f508c93eb4861ada466dd3ff613ca203962ceb7587ad013759f15394e6a4e619", size = 90479, upload-time = "2024-02-27T22:02:15.831Z" }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/f1/5f/0a674fcafa03528089badb46419413f342537b5b57d2fefc9900fb8ee4e4/stackscope-0.2.2-py3-none-any.whl", hash = "sha256:c199b0cda738d39c993ee04eb01961b06b7e9aeb43ebf9fd6226cdd72ea9faf6", size = 80807 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/f1/5f/0a674fcafa03528089badb46419413f342537b5b57d2fefc9900fb8ee4e4/stackscope-0.2.2-py3-none-any.whl", hash = "sha256:c199b0cda738d39c993ee04eb01961b06b7e9aeb43ebf9fd6226cdd72ea9faf6", size = 80807, upload-time = "2024-02-27T22:02:13.692Z" },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
| 
						 | 
				
			
			@ -363,9 +363,9 @@ source = { registry = "https://pypi.org/simple" }
 | 
			
		|||
dependencies = [
 | 
			
		||||
    { name = "pyreadline3", marker = "sys_platform == 'win32'" },
 | 
			
		||||
]
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/73/1a/ed3544579628c5709bae6fae2255e94c6982a9ff77d42d8ba59fd2f3b21a/tabcompleter-1.4.0.tar.gz", hash = "sha256:7562a9938e62f8e7c3be612c3ac4e14c5ec4307b58ba9031c148260e866e8814", size = 10431 }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/73/1a/ed3544579628c5709bae6fae2255e94c6982a9ff77d42d8ba59fd2f3b21a/tabcompleter-1.4.0.tar.gz", hash = "sha256:7562a9938e62f8e7c3be612c3ac4e14c5ec4307b58ba9031c148260e866e8814", size = 10431, upload-time = "2024-10-28T00:44:52.665Z" }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/65/44/bb509c3d2c0b5a87e7a5af1d5917a402a32ff026f777a6d7cb6990746cbb/tabcompleter-1.4.0-py3-none-any.whl", hash = "sha256:d744aa735b49c0a6cc2fb8fcd40077fec47425e4388301010b14e6ce3311368b", size = 6725 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/65/44/bb509c3d2c0b5a87e7a5af1d5917a402a32ff026f777a6d7cb6990746cbb/tabcompleter-1.4.0-py3-none-any.whl", hash = "sha256:d744aa735b49c0a6cc2fb8fcd40077fec47425e4388301010b14e6ce3311368b", size = 6725, upload-time = "2024-10-28T00:44:51.267Z" },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
| 
						 | 
				
			
			@ -392,6 +392,7 @@ dev = [
 | 
			
		|||
    { name = "pyperclip" },
 | 
			
		||||
    { name = "pytest" },
 | 
			
		||||
    { name = "stackscope" },
 | 
			
		||||
    { name = "typing-extensions" },
 | 
			
		||||
    { name = "xonsh" },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -416,6 +417,7 @@ dev = [
 | 
			
		|||
    { name = "pyperclip", specifier = ">=1.9.0" },
 | 
			
		||||
    { name = "pytest", specifier = ">=8.3.5" },
 | 
			
		||||
    { name = "stackscope", specifier = ">=0.2.2,<0.3" },
 | 
			
		||||
    { name = "typing-extensions", specifier = ">=4.13.2" },
 | 
			
		||||
    { name = "xonsh", specifier = ">=0.19.2" },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -426,9 +428,9 @@ source = { registry = "https://pypi.org/simple" }
 | 
			
		|||
dependencies = [
 | 
			
		||||
    { name = "trio" },
 | 
			
		||||
]
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/f8/8e/fdd7bc467b40eedd0a5f2ed36b0d692c6e6f2473be00c8160e2e9f53adc1/tricycle-0.4.1.tar.gz", hash = "sha256:f56edb4b3e1bed3e2552b1b499b24a2dab47741e92e9b4d806acc5c35c9e6066", size = 41551 }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/f8/8e/fdd7bc467b40eedd0a5f2ed36b0d692c6e6f2473be00c8160e2e9f53adc1/tricycle-0.4.1.tar.gz", hash = "sha256:f56edb4b3e1bed3e2552b1b499b24a2dab47741e92e9b4d806acc5c35c9e6066", size = 41551, upload-time = "2024-02-02T20:41:15.298Z" }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/d7/c6/7cc05d60e21c683df99167db071ce5d848f5063c2a63971a8443466f603e/tricycle-0.4.1-py3-none-any.whl", hash = "sha256:67900995a73e7445e2c70250cdca04a778d9c3923dd960a97ad4569085e0fb3f", size = 35316 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/d7/c6/7cc05d60e21c683df99167db071ce5d848f5063c2a63971a8443466f603e/tricycle-0.4.1-py3-none-any.whl", hash = "sha256:67900995a73e7445e2c70250cdca04a778d9c3923dd960a97ad4569085e0fb3f", size = 35316, upload-time = "2024-02-02T20:41:14.108Z" },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
| 
						 | 
				
			
			@ -443,82 +445,91 @@ dependencies = [
 | 
			
		|||
    { name = "sniffio" },
 | 
			
		||||
    { name = "sortedcontainers" },
 | 
			
		||||
]
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/47/f62e62a1a6f37909aed0bf8f5d5411e06fa03846cfcb64540cd1180ccc9f/trio-0.29.0.tar.gz", hash = "sha256:ea0d3967159fc130acb6939a0be0e558e364fee26b5deeecc893a6b08c361bdf", size = 588952 }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/47/f62e62a1a6f37909aed0bf8f5d5411e06fa03846cfcb64540cd1180ccc9f/trio-0.29.0.tar.gz", hash = "sha256:ea0d3967159fc130acb6939a0be0e558e364fee26b5deeecc893a6b08c361bdf", size = 588952, upload-time = "2025-02-14T07:13:50.724Z" }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/c9/55/c4d9bea8b3d7937901958f65124123512419ab0eb73695e5f382521abbfb/trio-0.29.0-py3-none-any.whl", hash = "sha256:d8c463f1a9cc776ff63e331aba44c125f423a5a13c684307e828d930e625ba66", size = 492920 },
 | 
			
		||||
    { 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 = "typing-extensions"
 | 
			
		||||
version = "4.13.2"
 | 
			
		||||
source = { registry = "https://pypi.org/simple" }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "wcwidth"
 | 
			
		||||
version = "0.2.13"
 | 
			
		||||
source = { registry = "https://pypi.org/simple" }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301, upload-time = "2024-01-06T02:10:57.829Z" }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "wrapt"
 | 
			
		||||
version = "1.17.2"
 | 
			
		||||
source = { registry = "https://pypi.org/simple" }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531 }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531, upload-time = "2025-01-14T10:35:45.465Z" }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/cd/f7/a2aab2cbc7a665efab072344a8949a71081eed1d2f451f7f7d2b966594a2/wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58", size = 53308 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/50/ff/149aba8365fdacef52b31a258c4dc1c57c79759c335eff0b3316a2664a64/wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda", size = 38488 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/65/46/5a917ce85b5c3b490d35c02bf71aedaa9f2f63f2d15d9949cc4ba56e8ba9/wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438", size = 38776 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/ca/74/336c918d2915a4943501c77566db41d1bd6e9f4dbc317f356b9a244dfe83/wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a", size = 83776 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/09/99/c0c844a5ccde0fe5761d4305485297f91d67cf2a1a824c5f282e661ec7ff/wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000", size = 75420 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/b4/b0/9fc566b0fe08b282c850063591a756057c3247b2362b9286429ec5bf1721/wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6", size = 83199 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/9d/4b/71996e62d543b0a0bd95dda485219856def3347e3e9380cc0d6cf10cfb2f/wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b", size = 82307 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/39/35/0282c0d8789c0dc9bcc738911776c762a701f95cfe113fb8f0b40e45c2b9/wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662", size = 75025 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/4f/6d/90c9fd2c3c6fee181feecb620d95105370198b6b98a0770cba090441a828/wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72", size = 81879 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/8f/fa/9fb6e594f2ce03ef03eddbdb5f4f90acb1452221a5351116c7c4708ac865/wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317", size = 36419 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/47/f8/fb1773491a253cbc123c5d5dc15c86041f746ed30416535f2a8df1f4a392/wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3", size = 38773 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/a1/bd/ab55f849fd1f9a58ed7ea47f5559ff09741b25f00c191231f9f059c83949/wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925", size = 53799 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/53/18/75ddc64c3f63988f5a1d7e10fb204ffe5762bc663f8023f18ecaf31a332e/wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392", size = 38821 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/48/2a/97928387d6ed1c1ebbfd4efc4133a0633546bec8481a2dd5ec961313a1c7/wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40", size = 38919 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/73/54/3bfe5a1febbbccb7a2f77de47b989c0b85ed3a6a41614b104204a788c20e/wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d", size = 88721 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/25/cb/7262bc1b0300b4b64af50c2720ef958c2c1917525238d661c3e9a2b71b7b/wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b", size = 80899 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/2a/5a/04cde32b07a7431d4ed0553a76fdb7a61270e78c5fd5a603e190ac389f14/wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98", size = 89222 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/09/28/2e45a4f4771fcfb109e244d5dbe54259e970362a311b67a965555ba65026/wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82", size = 86707 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/c6/d2/dcb56bf5f32fcd4bd9aacc77b50a539abdd5b6536872413fd3f428b21bed/wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae", size = 79685 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/80/4e/eb8b353e36711347893f502ce91c770b0b0929f8f0bed2670a6856e667a9/wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9", size = 87567 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/17/27/4fe749a54e7fae6e7146f1c7d914d28ef599dacd4416566c055564080fe2/wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9", size = 36672 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/15/06/1dbf478ea45c03e78a6a8c4be4fdc3c3bddea5c8de8a93bc971415e47f0f/wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991", size = 38865 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/ce/b9/0ffd557a92f3b11d4c5d5e0c5e4ad057bd9eb8586615cdaf901409920b14/wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125", size = 53800 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/c0/ef/8be90a0b7e73c32e550c73cfb2fa09db62234227ece47b0e80a05073b375/wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998", size = 38824 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/36/89/0aae34c10fe524cce30fe5fc433210376bce94cf74d05b0d68344c8ba46e/wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5", size = 38920 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/3b/24/11c4510de906d77e0cfb5197f1b1445d4fec42c9a39ea853d482698ac681/wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8", size = 88690 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/71/d7/cfcf842291267bf455b3e266c0c29dcb675b5540ee8b50ba1699abf3af45/wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6", size = 80861 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/d5/66/5d973e9f3e7370fd686fb47a9af3319418ed925c27d72ce16b791231576d/wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc", size = 89174 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/a7/d3/8e17bb70f6ae25dabc1aaf990f86824e4fd98ee9cadf197054e068500d27/wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2", size = 86721 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/6f/54/f170dfb278fe1c30d0ff864513cff526d624ab8de3254b20abb9cffedc24/wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b", size = 79763 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/4a/98/de07243751f1c4a9b15c76019250210dd3486ce098c3d80d5f729cba029c/wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504", size = 87585 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/f9/f0/13925f4bd6548013038cdeb11ee2cbd4e37c30f8bfd5db9e5a2a370d6e20/wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a", size = 36676 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/bf/ae/743f16ef8c2e3628df3ddfd652b7d4c555d12c84b53f3d8218498f4ade9b/wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845", size = 38871 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/3d/bc/30f903f891a82d402ffb5fda27ec1d621cc97cb74c16fea0b6141f1d4e87/wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192", size = 56312 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/8a/04/c97273eb491b5f1c918857cd26f314b74fc9b29224521f5b83f872253725/wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b", size = 40062 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/4e/ca/3b7afa1eae3a9e7fefe499db9b96813f41828b9fdb016ee836c4c379dadb/wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0", size = 40155 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/89/be/7c1baed43290775cb9030c774bc53c860db140397047cc49aedaf0a15477/wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306", size = 113471 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/32/98/4ed894cf012b6d6aae5f5cc974006bdeb92f0241775addad3f8cd6ab71c8/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb", size = 101208 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/ea/fd/0c30f2301ca94e655e5e057012e83284ce8c545df7661a78d8bfca2fac7a/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681", size = 109339 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/75/56/05d000de894c4cfcb84bcd6b1df6214297b8089a7bd324c21a4765e49b14/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6", size = 110232 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/53/f8/c3f6b2cf9b9277fb0813418e1503e68414cd036b3b099c823379c9575e6d/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6", size = 100476 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/a7/b1/0bb11e29aa5139d90b770ebbfa167267b1fc548d2302c30c8f7572851738/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f", size = 106377 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/6a/e1/0122853035b40b3f333bbb25f1939fc1045e21dd518f7f0922b60c156f7c/wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555", size = 37986 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/09/5e/1655cf481e079c1f22d0cabdd4e51733679932718dc23bf2db175f329b76/wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c", size = 40750 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/cd/f7/a2aab2cbc7a665efab072344a8949a71081eed1d2f451f7f7d2b966594a2/wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58", size = 53308, upload-time = "2025-01-14T10:33:33.992Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/50/ff/149aba8365fdacef52b31a258c4dc1c57c79759c335eff0b3316a2664a64/wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda", size = 38488, upload-time = "2025-01-14T10:33:35.264Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/65/46/5a917ce85b5c3b490d35c02bf71aedaa9f2f63f2d15d9949cc4ba56e8ba9/wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438", size = 38776, upload-time = "2025-01-14T10:33:38.28Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/ca/74/336c918d2915a4943501c77566db41d1bd6e9f4dbc317f356b9a244dfe83/wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a", size = 83776, upload-time = "2025-01-14T10:33:40.678Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/09/99/c0c844a5ccde0fe5761d4305485297f91d67cf2a1a824c5f282e661ec7ff/wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000", size = 75420, upload-time = "2025-01-14T10:33:41.868Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/b4/b0/9fc566b0fe08b282c850063591a756057c3247b2362b9286429ec5bf1721/wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6", size = 83199, upload-time = "2025-01-14T10:33:43.598Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/9d/4b/71996e62d543b0a0bd95dda485219856def3347e3e9380cc0d6cf10cfb2f/wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b", size = 82307, upload-time = "2025-01-14T10:33:48.499Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/39/35/0282c0d8789c0dc9bcc738911776c762a701f95cfe113fb8f0b40e45c2b9/wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662", size = 75025, upload-time = "2025-01-14T10:33:51.191Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/4f/6d/90c9fd2c3c6fee181feecb620d95105370198b6b98a0770cba090441a828/wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72", size = 81879, upload-time = "2025-01-14T10:33:52.328Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/8f/fa/9fb6e594f2ce03ef03eddbdb5f4f90acb1452221a5351116c7c4708ac865/wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317", size = 36419, upload-time = "2025-01-14T10:33:53.551Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/47/f8/fb1773491a253cbc123c5d5dc15c86041f746ed30416535f2a8df1f4a392/wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3", size = 38773, upload-time = "2025-01-14T10:33:56.323Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/a1/bd/ab55f849fd1f9a58ed7ea47f5559ff09741b25f00c191231f9f059c83949/wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925", size = 53799, upload-time = "2025-01-14T10:33:57.4Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/53/18/75ddc64c3f63988f5a1d7e10fb204ffe5762bc663f8023f18ecaf31a332e/wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392", size = 38821, upload-time = "2025-01-14T10:33:59.334Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/48/2a/97928387d6ed1c1ebbfd4efc4133a0633546bec8481a2dd5ec961313a1c7/wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40", size = 38919, upload-time = "2025-01-14T10:34:04.093Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/73/54/3bfe5a1febbbccb7a2f77de47b989c0b85ed3a6a41614b104204a788c20e/wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d", size = 88721, upload-time = "2025-01-14T10:34:07.163Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/25/cb/7262bc1b0300b4b64af50c2720ef958c2c1917525238d661c3e9a2b71b7b/wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b", size = 80899, upload-time = "2025-01-14T10:34:09.82Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/2a/5a/04cde32b07a7431d4ed0553a76fdb7a61270e78c5fd5a603e190ac389f14/wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98", size = 89222, upload-time = "2025-01-14T10:34:11.258Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/09/28/2e45a4f4771fcfb109e244d5dbe54259e970362a311b67a965555ba65026/wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82", size = 86707, upload-time = "2025-01-14T10:34:12.49Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/c6/d2/dcb56bf5f32fcd4bd9aacc77b50a539abdd5b6536872413fd3f428b21bed/wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae", size = 79685, upload-time = "2025-01-14T10:34:15.043Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/80/4e/eb8b353e36711347893f502ce91c770b0b0929f8f0bed2670a6856e667a9/wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9", size = 87567, upload-time = "2025-01-14T10:34:16.563Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/17/27/4fe749a54e7fae6e7146f1c7d914d28ef599dacd4416566c055564080fe2/wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9", size = 36672, upload-time = "2025-01-14T10:34:17.727Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/15/06/1dbf478ea45c03e78a6a8c4be4fdc3c3bddea5c8de8a93bc971415e47f0f/wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991", size = 38865, upload-time = "2025-01-14T10:34:19.577Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/ce/b9/0ffd557a92f3b11d4c5d5e0c5e4ad057bd9eb8586615cdaf901409920b14/wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125", size = 53800, upload-time = "2025-01-14T10:34:21.571Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/c0/ef/8be90a0b7e73c32e550c73cfb2fa09db62234227ece47b0e80a05073b375/wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998", size = 38824, upload-time = "2025-01-14T10:34:22.999Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/36/89/0aae34c10fe524cce30fe5fc433210376bce94cf74d05b0d68344c8ba46e/wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5", size = 38920, upload-time = "2025-01-14T10:34:25.386Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/3b/24/11c4510de906d77e0cfb5197f1b1445d4fec42c9a39ea853d482698ac681/wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8", size = 88690, upload-time = "2025-01-14T10:34:28.058Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/71/d7/cfcf842291267bf455b3e266c0c29dcb675b5540ee8b50ba1699abf3af45/wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6", size = 80861, upload-time = "2025-01-14T10:34:29.167Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/d5/66/5d973e9f3e7370fd686fb47a9af3319418ed925c27d72ce16b791231576d/wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc", size = 89174, upload-time = "2025-01-14T10:34:31.702Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/a7/d3/8e17bb70f6ae25dabc1aaf990f86824e4fd98ee9cadf197054e068500d27/wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2", size = 86721, upload-time = "2025-01-14T10:34:32.91Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/6f/54/f170dfb278fe1c30d0ff864513cff526d624ab8de3254b20abb9cffedc24/wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b", size = 79763, upload-time = "2025-01-14T10:34:34.903Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/4a/98/de07243751f1c4a9b15c76019250210dd3486ce098c3d80d5f729cba029c/wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504", size = 87585, upload-time = "2025-01-14T10:34:36.13Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/f9/f0/13925f4bd6548013038cdeb11ee2cbd4e37c30f8bfd5db9e5a2a370d6e20/wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a", size = 36676, upload-time = "2025-01-14T10:34:37.962Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/bf/ae/743f16ef8c2e3628df3ddfd652b7d4c555d12c84b53f3d8218498f4ade9b/wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845", size = 38871, upload-time = "2025-01-14T10:34:39.13Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/3d/bc/30f903f891a82d402ffb5fda27ec1d621cc97cb74c16fea0b6141f1d4e87/wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192", size = 56312, upload-time = "2025-01-14T10:34:40.604Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/8a/04/c97273eb491b5f1c918857cd26f314b74fc9b29224521f5b83f872253725/wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b", size = 40062, upload-time = "2025-01-14T10:34:45.011Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/4e/ca/3b7afa1eae3a9e7fefe499db9b96813f41828b9fdb016ee836c4c379dadb/wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0", size = 40155, upload-time = "2025-01-14T10:34:47.25Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/89/be/7c1baed43290775cb9030c774bc53c860db140397047cc49aedaf0a15477/wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306", size = 113471, upload-time = "2025-01-14T10:34:50.934Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/32/98/4ed894cf012b6d6aae5f5cc974006bdeb92f0241775addad3f8cd6ab71c8/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb", size = 101208, upload-time = "2025-01-14T10:34:52.297Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/ea/fd/0c30f2301ca94e655e5e057012e83284ce8c545df7661a78d8bfca2fac7a/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681", size = 109339, upload-time = "2025-01-14T10:34:53.489Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/75/56/05d000de894c4cfcb84bcd6b1df6214297b8089a7bd324c21a4765e49b14/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6", size = 110232, upload-time = "2025-01-14T10:34:55.327Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/53/f8/c3f6b2cf9b9277fb0813418e1503e68414cd036b3b099c823379c9575e6d/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6", size = 100476, upload-time = "2025-01-14T10:34:58.055Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/a7/b1/0bb11e29aa5139d90b770ebbfa167267b1fc548d2302c30c8f7572851738/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f", size = 106377, upload-time = "2025-01-14T10:34:59.3Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/6a/e1/0122853035b40b3f333bbb25f1939fc1045e21dd518f7f0922b60c156f7c/wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555", size = 37986, upload-time = "2025-01-14T10:35:00.498Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/09/5e/1655cf481e079c1f22d0cabdd4e51733679932718dc23bf2db175f329b76/wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c", size = 40750, upload-time = "2025-01-14T10:35:03.378Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594, upload-time = "2025-01-14T10:35:44.018Z" },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "xonsh"
 | 
			
		||||
version = "0.19.2"
 | 
			
		||||
source = { registry = "https://pypi.org/simple" }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/68/4e/56e95a5e607eb3b0da37396f87cde70588efc8ef819ab16f02d5b8378dc4/xonsh-0.19.2.tar.gz", hash = "sha256:cfdd0680d954a2c3aefd6caddcc7143a3d06aa417ed18365a08219bb71b960b0", size = 799960 }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/68/4e/56e95a5e607eb3b0da37396f87cde70588efc8ef819ab16f02d5b8378dc4/xonsh-0.19.2.tar.gz", hash = "sha256:cfdd0680d954a2c3aefd6caddcc7143a3d06aa417ed18365a08219bb71b960b0", size = 799960, upload-time = "2025-02-11T17:10:43.563Z" }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/6c/13/281094759df87b23b3c02dc4a16603ab08ea54d7f6acfeb69f3341137c7a/xonsh-0.19.2-py310-none-any.whl", hash = "sha256:ec7f163fd3a4943782aa34069d4e72793328c916a5975949dbec8536cbfc089b", size = 642301 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/29/41/a51e4c3918fe9a293b150cb949b1b8c6d45eb17dfed480dcb76ea43df4e7/xonsh-0.19.2-py311-none-any.whl", hash = "sha256:53c45f7a767901f2f518f9b8dd60fc653e0498e56e89825e1710bb0859985049", size = 642286 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/0a/93/9a77b731f492fac27c577dea2afb5a2bcc2a6a1c79be0c86c95498060270/xonsh-0.19.2-py312-none-any.whl", hash = "sha256:b24c619aa52b59eae4d35c4195dba9b19a2c548fb5c42c6f85f2b8ccb96807b5", size = 642386 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/be/75/070324769c1ff88d971ce040f4f486339be98e0a365c8dd9991eb654265b/xonsh-0.19.2-py313-none-any.whl", hash = "sha256:c53ef6c19f781fbc399ed1b382b5c2aac2125010679a3b61d643978273c27df0", size = 642873 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/fa/cb/2c7ccec54f5b0e73fdf7650e8336582ff0347d9001c5ef8271dc00c034fe/xonsh-0.19.2-py39-none-any.whl", hash = "sha256:bcc0225dc3847f1ed2f175dac6122fbcc54cea67d9c2dc2753d9615e2a5ff284", size = 634602 },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/6c/13/281094759df87b23b3c02dc4a16603ab08ea54d7f6acfeb69f3341137c7a/xonsh-0.19.2-py310-none-any.whl", hash = "sha256:ec7f163fd3a4943782aa34069d4e72793328c916a5975949dbec8536cbfc089b", size = 642301, upload-time = "2025-02-11T17:10:39.244Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/29/41/a51e4c3918fe9a293b150cb949b1b8c6d45eb17dfed480dcb76ea43df4e7/xonsh-0.19.2-py311-none-any.whl", hash = "sha256:53c45f7a767901f2f518f9b8dd60fc653e0498e56e89825e1710bb0859985049", size = 642286, upload-time = "2025-02-11T17:10:41.678Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/0a/93/9a77b731f492fac27c577dea2afb5a2bcc2a6a1c79be0c86c95498060270/xonsh-0.19.2-py312-none-any.whl", hash = "sha256:b24c619aa52b59eae4d35c4195dba9b19a2c548fb5c42c6f85f2b8ccb96807b5", size = 642386, upload-time = "2025-02-11T17:10:43.688Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/be/75/070324769c1ff88d971ce040f4f486339be98e0a365c8dd9991eb654265b/xonsh-0.19.2-py313-none-any.whl", hash = "sha256:c53ef6c19f781fbc399ed1b382b5c2aac2125010679a3b61d643978273c27df0", size = 642873, upload-time = "2025-02-11T17:10:39.297Z" },
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/fa/cb/2c7ccec54f5b0e73fdf7650e8336582ff0347d9001c5ef8271dc00c034fe/xonsh-0.19.2-py39-none-any.whl", hash = "sha256:bcc0225dc3847f1ed2f175dac6122fbcc54cea67d9c2dc2753d9615e2a5ff284", size = 634602, upload-time = "2025-02-11T17:10:37.004Z" },
 | 
			
		||||
]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue