stranded 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- stranded/__init__.py +51 -0
- stranded/abc/__init__.py +21 -0
- stranded/abc/composer_.py +191 -0
- stranded/argparse/__init__.py +24 -0
- stranded/argparse/abc/__init__.py +21 -0
- stranded/argparse/abc/argument_parser_.py +539 -0
- stranded/argparse/argument_parser_.py +13 -0
- stranded/argparse/asyncio/__init__.py +24 -0
- stranded/argparse/asyncio/argument_parser_.py +50 -0
- stranded/argparse/threading/__init__.py +24 -0
- stranded/argparse/threading/argument_parser_.py +52 -0
- stranded/asyncio/__init__.py +21 -0
- stranded/asyncio/composer_.py +76 -0
- stranded/builtins/__init__.py +21 -0
- stranded/builtins/exception_.py +6 -0
- stranded/composer_.py +27 -0
- stranded/execution/__init__.py +30 -0
- stranded/execution/abc/__init__.py +21 -0
- stranded/execution/abc/scheduler_.py +59 -0
- stranded/execution/asyncio/__init__.py +21 -0
- stranded/execution/asyncio/scheduler_.py +86 -0
- stranded/execution/scheduler_.py +59 -0
- stranded/execution/threading/__init__.py +21 -0
- stranded/execution/threading/scheduler_.py +116 -0
- stranded/functools/__init__.py +51 -0
- stranded/functools/abc/__init__.py +39 -0
- stranded/functools/abc/herd_.py +96 -0
- stranded/functools/abc/lru_cache_.py +192 -0
- stranded/functools/abc/retry_.py +60 -0
- stranded/functools/abc/throttle_.py +97 -0
- stranded/functools/asyncio/__init__.py +51 -0
- stranded/functools/asyncio/herd_.py +89 -0
- stranded/functools/asyncio/lru_cache_.py +92 -0
- stranded/functools/asyncio/retry_.py +43 -0
- stranded/functools/asyncio/throttle_.py +74 -0
- stranded/functools/herd_.py +18 -0
- stranded/functools/lru_cache_.py +18 -0
- stranded/functools/retry_.py +13 -0
- stranded/functools/threading/__init__.py +51 -0
- stranded/functools/threading/herd_.py +88 -0
- stranded/functools/threading/lru_cache_.py +90 -0
- stranded/functools/threading/retry_.py +42 -0
- stranded/functools/threading/throttle_.py +75 -0
- stranded/functools/throttle_.py +13 -0
- stranded/logging/__init__.py +24 -0
- stranded/logging/abc/__init__.py +21 -0
- stranded/logging/abc/logger_.py +96 -0
- stranded/logging/asyncio/__init__.py +21 -0
- stranded/logging/asyncio/logger_.py +41 -0
- stranded/logging/logger_.py +13 -0
- stranded/logging/threading/__init__.py +21 -0
- stranded/logging/threading/logger_.py +41 -0
- stranded/py.typed +0 -0
- stranded/sqlite3/__init__.py +24 -0
- stranded/sqlite3/abc/__init__.py +21 -0
- stranded/sqlite3/abc/db_.py +149 -0
- stranded/sqlite3/asyncio/__init__.py +24 -0
- stranded/sqlite3/asyncio/db_.py +43 -0
- stranded/sqlite3/db_.py +13 -0
- stranded/sqlite3/threading/__init__.py +24 -0
- stranded/sqlite3/threading/db_.py +43 -0
- stranded/threading/__init__.py +21 -0
- stranded/threading/composer_.py +74 -0
- stranded/types/__init__.py +21 -0
- stranded/types/constraint.py +7 -0
- stranded/types/convert.py +158 -0
- stranded-0.0.1.dist-info/METADATA +173 -0
- stranded-0.0.1.dist-info/RECORD +71 -0
- stranded-0.0.1.dist-info/WHEEL +5 -0
- stranded-0.0.1.dist-info/licenses/LICENSE +21 -0
- stranded-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import dataclasses
|
|
5
|
+
import inspect
|
|
6
|
+
import typing
|
|
7
|
+
|
|
8
|
+
from ...threading import composer_
|
|
9
|
+
from ..abc import argument_parser_
|
|
10
|
+
|
|
11
|
+
@typing.runtime_checkable
|
|
12
|
+
class Composee[**ParamT, RetT](composer_.Composee[ParamT, RetT], argument_parser_.Composee[ParamT, RetT], typing.Protocol): ...
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@typing.final
|
|
16
|
+
@dataclasses.dataclass(frozen=True, kw_only=True)
|
|
17
|
+
class Connect[**ParamT, RetT](composer_.Connect[ParamT, RetT], argument_parser_.Connect[ParamT, RetT]): ...
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@typing.final
|
|
21
|
+
@dataclasses.dataclass(frozen=True, kw_only=True)
|
|
22
|
+
class Exit[**ParamT, RetT](composer_.Exit[ParamT, RetT], argument_parser_.Exit[ParamT, RetT]): ...
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@typing.final
|
|
26
|
+
@dataclasses.dataclass(frozen=True, kw_only=True)
|
|
27
|
+
class Enter[**ParamT, RetT](composer_.Enter[ParamT, RetT], argument_parser_.Enter[ParamT, RetT]): ...
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclasses.dataclass(frozen=True, kw_only=True)
|
|
31
|
+
class Composed[**ParamT, RetT](composer_.Composed[ParamT, RetT], argument_parser_.Composed[ParamT, RetT]):
|
|
32
|
+
def __call__(self, *argv: str) -> RetT: # type: ignore[override]
|
|
33
|
+
ret = None
|
|
34
|
+
for call in super(composer_.Composed, self).__call__(*argv):
|
|
35
|
+
ret = call()
|
|
36
|
+
if inspect.iscoroutine(ret):
|
|
37
|
+
ret = asyncio.run(ret)
|
|
38
|
+
return ret # type: ignore[return-value]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@typing.final
|
|
42
|
+
@dataclasses.dataclass(frozen=True, kw_only=True)
|
|
43
|
+
class ArgumentParser[**ParamT = ..., RetT = typing.Any](composer_.Composer[ParamT, RetT], argument_parser_.Composer[ParamT, RetT]):
|
|
44
|
+
Composee: typing.ClassVar = Composee
|
|
45
|
+
Connect: typing.ClassVar = Connect
|
|
46
|
+
Exit: typing.ClassVar = Exit
|
|
47
|
+
Enter: typing.ClassVar = Enter
|
|
48
|
+
Composed: typing.ClassVar = Composed
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
Composer = ArgumentParser
|
|
52
|
+
argument_parser = ArgumentParser()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib as _importlib
|
|
4
|
+
import typing as _typing
|
|
5
|
+
|
|
6
|
+
if _typing.TYPE_CHECKING:
|
|
7
|
+
from . import composer_
|
|
8
|
+
from .composer_ import Composer
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def __getattr__(name: str) -> _typing.Any:
|
|
12
|
+
match name:
|
|
13
|
+
case 'composer_': return _importlib.import_module('.composer_', __name__)
|
|
14
|
+
case 'Composer': return _importlib.import_module('.composer_', __name__).Composer
|
|
15
|
+
case _: raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
__all__ = (
|
|
19
|
+
'composer_',
|
|
20
|
+
'Composer',
|
|
21
|
+
)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import dataclasses
|
|
3
|
+
import inspect
|
|
4
|
+
import typing
|
|
5
|
+
|
|
6
|
+
from ..abc import composer_
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
Raise = composer_.Raise
|
|
10
|
+
Stop = composer_.Stop
|
|
11
|
+
Param = composer_.Param
|
|
12
|
+
Return = composer_.Return
|
|
13
|
+
type ValueT[**ParamT_, RetT_] = composer_.ValueT[ParamT_, RetT_]
|
|
14
|
+
type StackT = tuple[
|
|
15
|
+
ValueT[typing.Any, typing.Any]
|
|
16
|
+
| Composee[typing.Any, typing.Any]
|
|
17
|
+
| Connect[typing.Any, typing.Any]
|
|
18
|
+
| Exit[typing.Any, typing.Any]
|
|
19
|
+
| Enter[typing.Any, typing.Any]
|
|
20
|
+
| Composed[typing.Any, typing.Any],
|
|
21
|
+
...,
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Composee[**ParamT, RetT](composer_.Composee[ParamT, RetT], typing.Protocol):
|
|
26
|
+
async def __call__(*args: ParamT.args, **kwargs: ParamT.kwargs) -> RetT: ...
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclasses.dataclass(frozen=True, kw_only=True)
|
|
30
|
+
class Connect[**ParamT, RetT](composer_.Connect[ParamT, RetT], abc.ABC):
|
|
31
|
+
async def __call__(self, value: ValueT[ParamT, RetT], /) -> StackT: return super().__call__(value)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclasses.dataclass(frozen=True, kw_only=True)
|
|
35
|
+
class Exit[**ParamT, RetT](composer_.Exit[ParamT, RetT], abc.ABC):
|
|
36
|
+
async def __call__(self, value: ValueT[ParamT, RetT], /) -> StackT: return super().__call__(value)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclasses.dataclass(frozen=True, kw_only=True)
|
|
40
|
+
class Enter[**ParamT, RetT](composer_.Enter[ParamT, RetT], abc.ABC):
|
|
41
|
+
async def __call__(self, value: ValueT[ParamT, RetT], /) -> StackT: return super().__call__(value)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@inspect.markcoroutinefunction
|
|
45
|
+
@dataclasses.dataclass(frozen=True, kw_only=True)
|
|
46
|
+
class Composed[**ParamT, RetT](composer_.Composed[ParamT, RetT], abc.ABC):
|
|
47
|
+
async def __call__(self, *args: ParamT.args, **kwargs: ParamT.kwargs) -> RetT:
|
|
48
|
+
value: Param[ParamT] | Raise | Return[RetT] | Stop = Param(args=args, kwargs=kwargs)
|
|
49
|
+
stack = list(self.stack)
|
|
50
|
+
while stack:
|
|
51
|
+
match stack.pop():
|
|
52
|
+
case Param() | Raise() | Return() | Stop() as value_: value = value_
|
|
53
|
+
case Enter() | Exit() | Connect() as get_stack_: stack += await get_stack_(value)
|
|
54
|
+
case Composed() as composed_ if isinstance(value, Param): stack += composed_.stack
|
|
55
|
+
case composee_ if isinstance(value, Param):
|
|
56
|
+
try:
|
|
57
|
+
value = Return(ret=await composee_(*value.args, **value.kwargs))
|
|
58
|
+
except Stop as stopped_:
|
|
59
|
+
value = stopped_
|
|
60
|
+
except Exception as exception_:
|
|
61
|
+
value = Raise(exc_type=type(exception_), exc_val=exception_, exc_tb=exception_.__traceback__)
|
|
62
|
+
|
|
63
|
+
match value:
|
|
64
|
+
case Stop() as stop_: raise stop_
|
|
65
|
+
case Raise() as raise_: raise raise_.exc_val
|
|
66
|
+
case Return() as return_: return return_.ret # type: ignore[no-any-return]
|
|
67
|
+
case _: raise Exception(f'{type(self).__name__} call completed with invalid {value=!r}')
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclasses.dataclass(frozen=True, kw_only=True)
|
|
71
|
+
class Composer[**ParamT, RetT](composer_.Composer[ParamT, RetT]):
|
|
72
|
+
Composee: typing.ClassVar = Composee
|
|
73
|
+
Connect: typing.ClassVar = Connect
|
|
74
|
+
Exit: typing.ClassVar = Exit
|
|
75
|
+
Enter: typing.ClassVar = Enter
|
|
76
|
+
Composed: typing.ClassVar = Composed
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib as _importlib
|
|
4
|
+
import typing as _typing
|
|
5
|
+
|
|
6
|
+
if _typing.TYPE_CHECKING:
|
|
7
|
+
from . import exception_
|
|
8
|
+
from .exception_ import Exception
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def __getattr__(name: str) -> _typing.Any:
|
|
12
|
+
match name:
|
|
13
|
+
case 'exception_': return _importlib.import_module('.exception_', __name__)
|
|
14
|
+
case 'Exception': return _importlib.import_module('.exception_', __name__).Exception
|
|
15
|
+
case _: raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
__all__ = (
|
|
19
|
+
'exception_',
|
|
20
|
+
'Exception',
|
|
21
|
+
)
|
stranded/composer_.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import importlib
|
|
3
|
+
import inspect
|
|
4
|
+
import typing
|
|
5
|
+
|
|
6
|
+
from .abc import composer_
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclasses.dataclass(frozen=True, kw_only=True)
|
|
10
|
+
class Composer[**ParamT, RetT](composer_.Composer[ParamT, RetT]):
|
|
11
|
+
# Stub bindings for the agnostic dispatcher — never instantiated, since
|
|
12
|
+
# __call__ delegates to the asyncio/threading concrete Composer.
|
|
13
|
+
Composee: typing.ClassVar = composer_.Composee
|
|
14
|
+
Connect: typing.ClassVar = composer_.Connect
|
|
15
|
+
Exit: typing.ClassVar = composer_.Exit
|
|
16
|
+
Enter: typing.ClassVar = composer_.Enter
|
|
17
|
+
Composed: typing.ClassVar = composer_.Composed
|
|
18
|
+
|
|
19
|
+
def __call__[ComposeeT](self, composee: ComposeeT) -> ComposeeT: # type: ignore[override]
|
|
20
|
+
(name_parts := self.__module__.split('.')).insert(
|
|
21
|
+
-1,
|
|
22
|
+
'asyncio' if inspect.iscoroutinefunction(composee) else 'threading'
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
return importlib.import_module( # type: ignore[no-any-return]
|
|
26
|
+
name='.'.join(name_parts),
|
|
27
|
+
).Composer(**dataclasses.asdict(self))(composee) # noqa
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib as _importlib
|
|
4
|
+
import typing as _typing
|
|
5
|
+
|
|
6
|
+
if _typing.TYPE_CHECKING:
|
|
7
|
+
from . import abc
|
|
8
|
+
from . import asyncio
|
|
9
|
+
from . import scheduler_
|
|
10
|
+
from .scheduler_ import Scheduler
|
|
11
|
+
from . import threading
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def __getattr__(name: str) -> _typing.Any:
|
|
15
|
+
match name:
|
|
16
|
+
case 'abc': return _importlib.import_module('.abc', __name__)
|
|
17
|
+
case 'asyncio': return _importlib.import_module('.asyncio', __name__)
|
|
18
|
+
case 'scheduler_': return _importlib.import_module('.scheduler_', __name__)
|
|
19
|
+
case 'Scheduler': return _importlib.import_module('.scheduler_', __name__).Scheduler
|
|
20
|
+
case 'threading': return _importlib.import_module('.threading', __name__)
|
|
21
|
+
case _: raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
__all__ = (
|
|
25
|
+
'abc',
|
|
26
|
+
'asyncio',
|
|
27
|
+
'scheduler_',
|
|
28
|
+
'Scheduler',
|
|
29
|
+
'threading',
|
|
30
|
+
)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib as _importlib
|
|
4
|
+
import typing as _typing
|
|
5
|
+
|
|
6
|
+
if _typing.TYPE_CHECKING:
|
|
7
|
+
from . import scheduler_
|
|
8
|
+
from .scheduler_ import Scheduler
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def __getattr__(name: str) -> _typing.Any:
|
|
12
|
+
match name:
|
|
13
|
+
case 'scheduler_': return _importlib.import_module('.scheduler_', __name__)
|
|
14
|
+
case 'Scheduler': return _importlib.import_module('.scheduler_', __name__).Scheduler
|
|
15
|
+
case _: raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
__all__ = (
|
|
19
|
+
'scheduler_',
|
|
20
|
+
'Scheduler',
|
|
21
|
+
)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
import dataclasses
|
|
5
|
+
import typing
|
|
6
|
+
|
|
7
|
+
from ...abc import composer_
|
|
8
|
+
from ...builtins import exception_
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Exception(exception_.Exception): ... # noqa
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@typing.runtime_checkable
|
|
15
|
+
class Composee[**ParamT, RetT](composer_.Composee[ParamT, RetT], typing.Protocol): ...
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclasses.dataclass(frozen=True, kw_only=True)
|
|
19
|
+
class Connect[**ParamT, RetT](composer_.Connect[ParamT, RetT], abc.ABC): ...
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclasses.dataclass(frozen=True, kw_only=True)
|
|
23
|
+
class Exit[**ParamT, RetT](composer_.Exit[ParamT, RetT], abc.ABC): ...
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclasses.dataclass(frozen=True, kw_only=True)
|
|
27
|
+
class Enter[**ParamT, RetT](composer_.Enter[ParamT, RetT], abc.ABC): ...
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclasses.dataclass(frozen=True, kw_only=True)
|
|
31
|
+
class Composed[**ParamT, RetT](composer_.Composed[ParamT, RetT], abc.ABC): ...
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclasses.dataclass(frozen=True, kw_only=True)
|
|
35
|
+
class Scheduler[**ParamT, RetT](composer_.Composer[ParamT, RetT], abc.ABC):
|
|
36
|
+
# A Scheduler is its own ergonomic composer: the wrapped Composee keeps the
|
|
37
|
+
# callee's call signature. The composition machinery still receives the real
|
|
38
|
+
# Composed produced by Composer.__call__.
|
|
39
|
+
def __call__[ComposeeT](self, composee: ComposeeT, /) -> ComposeeT: # type: ignore[override]
|
|
40
|
+
return typing.cast(
|
|
41
|
+
'ComposeeT',
|
|
42
|
+
super().__call__(typing.cast('composer_.Composee[typing.Any, typing.Any]', composee)),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
@abc.abstractmethod
|
|
46
|
+
def submit_sync(
|
|
47
|
+
self,
|
|
48
|
+
fn: typing.Callable[..., typing.Any],
|
|
49
|
+
args: tuple[typing.Any, ...],
|
|
50
|
+
kwargs: dict[str, typing.Any],
|
|
51
|
+
) -> typing.Any: ...
|
|
52
|
+
|
|
53
|
+
@abc.abstractmethod
|
|
54
|
+
async def submit_async(
|
|
55
|
+
self,
|
|
56
|
+
fn: typing.Callable[..., typing.Any],
|
|
57
|
+
args: tuple[typing.Any, ...],
|
|
58
|
+
kwargs: dict[str, typing.Any],
|
|
59
|
+
) -> typing.Any: ...
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib as _importlib
|
|
4
|
+
import typing as _typing
|
|
5
|
+
|
|
6
|
+
if _typing.TYPE_CHECKING:
|
|
7
|
+
from . import scheduler_
|
|
8
|
+
from .scheduler_ import Scheduler
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def __getattr__(name: str) -> _typing.Any:
|
|
12
|
+
match name:
|
|
13
|
+
case 'scheduler_': return _importlib.import_module('.scheduler_', __name__)
|
|
14
|
+
case 'Scheduler': return _importlib.import_module('.scheduler_', __name__).Scheduler
|
|
15
|
+
case _: raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
__all__ = (
|
|
19
|
+
'scheduler_',
|
|
20
|
+
'Scheduler',
|
|
21
|
+
)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import dataclasses
|
|
5
|
+
import inspect
|
|
6
|
+
import typing
|
|
7
|
+
|
|
8
|
+
from ...asyncio import composer_
|
|
9
|
+
from ..abc import scheduler_
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
Raise = composer_.Raise
|
|
13
|
+
Stop = composer_.Stop
|
|
14
|
+
Param = composer_.Param
|
|
15
|
+
Return = composer_.Return
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@typing.runtime_checkable
|
|
19
|
+
class Composee[**ParamT, RetT](composer_.Composee[ParamT, RetT], scheduler_.Composee[ParamT, RetT], typing.Protocol): ...
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclasses.dataclass(frozen=True, kw_only=True)
|
|
23
|
+
class Connect[**ParamT, RetT](composer_.Connect[ParamT, RetT], scheduler_.Connect[ParamT, RetT]): ...
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclasses.dataclass(frozen=True, kw_only=True)
|
|
27
|
+
class Exit[**ParamT, RetT](composer_.Exit[ParamT, RetT], scheduler_.Exit[ParamT, RetT]): ...
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclasses.dataclass(frozen=True, kw_only=True)
|
|
31
|
+
class Enter[**ParamT, RetT](composer_.Enter[ParamT, RetT], scheduler_.Enter[ParamT, RetT]):
|
|
32
|
+
@typing.overload # type: ignore[override]
|
|
33
|
+
async def __call__(self, value: Param[ParamT], /) -> tuple[composer_.Exit[ParamT, RetT], composer_.Composee[ParamT, RetT]]: ...
|
|
34
|
+
@typing.overload
|
|
35
|
+
async def __call__(self, value: Raise | Return[RetT] | Stop, /) -> tuple[()]: ...
|
|
36
|
+
async def __call__(
|
|
37
|
+
self, value: Param[ParamT] | Raise | Return[RetT] | Stop, /,
|
|
38
|
+
) -> tuple[composer_.Exit[ParamT, RetT], composer_.Composee[ParamT, RetT]] | tuple[()]:
|
|
39
|
+
if not isinstance(value, Param):
|
|
40
|
+
return ()
|
|
41
|
+
scheduler: Scheduler[ParamT, RetT] = typing.cast('Scheduler[ParamT, RetT]', self.composer)
|
|
42
|
+
inner = self.composee
|
|
43
|
+
|
|
44
|
+
async def wrapped(*args: typing.Any, **kwargs: typing.Any) -> typing.Any:
|
|
45
|
+
return await scheduler.submit_async(inner, args, kwargs)
|
|
46
|
+
|
|
47
|
+
return self.composer.Exit(enter=self), typing.cast(composer_.Composee[ParamT, RetT], wrapped)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclasses.dataclass(frozen=True, kw_only=True)
|
|
51
|
+
class Composed[**ParamT, RetT](composer_.Composed[ParamT, RetT], scheduler_.Composed[ParamT, RetT]): ...
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclasses.dataclass(frozen=True, kw_only=True)
|
|
55
|
+
class Scheduler[**ParamT = ..., RetT = typing.Any](composer_.Composer[ParamT, RetT], scheduler_.Scheduler[ParamT, RetT]):
|
|
56
|
+
Composee: typing.ClassVar = Composee
|
|
57
|
+
Connect: typing.ClassVar = Connect
|
|
58
|
+
Exit: typing.ClassVar = Exit
|
|
59
|
+
Enter: typing.ClassVar = Enter
|
|
60
|
+
Composed: typing.ClassVar = Composed
|
|
61
|
+
|
|
62
|
+
loop: asyncio.AbstractEventLoop | None = None
|
|
63
|
+
|
|
64
|
+
def submit_sync(
|
|
65
|
+
self,
|
|
66
|
+
fn: typing.Callable[..., typing.Any],
|
|
67
|
+
args: tuple[typing.Any, ...],
|
|
68
|
+
kwargs: dict[str, typing.Any],
|
|
69
|
+
) -> typing.Any:
|
|
70
|
+
if inspect.iscoroutinefunction(fn):
|
|
71
|
+
return asyncio.run(fn(*args, **kwargs))
|
|
72
|
+
return fn(*args, **kwargs)
|
|
73
|
+
|
|
74
|
+
async def submit_async(
|
|
75
|
+
self,
|
|
76
|
+
fn: typing.Callable[..., typing.Any],
|
|
77
|
+
args: tuple[typing.Any, ...],
|
|
78
|
+
kwargs: dict[str, typing.Any],
|
|
79
|
+
) -> typing.Any:
|
|
80
|
+
if inspect.iscoroutinefunction(fn):
|
|
81
|
+
coro = fn(*args, **kwargs)
|
|
82
|
+
if self.loop is None or self.loop is asyncio.get_running_loop():
|
|
83
|
+
return await asyncio.ensure_future(coro)
|
|
84
|
+
future = asyncio.run_coroutine_threadsafe(coro, self.loop)
|
|
85
|
+
return await asyncio.wrap_future(future)
|
|
86
|
+
return fn(*args, **kwargs)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import dataclasses
|
|
5
|
+
import importlib
|
|
6
|
+
import inspect
|
|
7
|
+
import typing
|
|
8
|
+
|
|
9
|
+
from ..abc import composer_
|
|
10
|
+
from .abc import scheduler_
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _dispatch(self: scheduler_.Scheduler[typing.Any, typing.Any], target: typing.Any) -> typing.Any:
|
|
14
|
+
flavor = 'asyncio' if inspect.iscoroutinefunction(target) else 'threading'
|
|
15
|
+
parts = type(self).__module__.split('.')
|
|
16
|
+
parts.insert(-1, flavor)
|
|
17
|
+
module = importlib.import_module('.'.join(parts))
|
|
18
|
+
cls = getattr(module, type(self).__name__)
|
|
19
|
+
target_field_names = {f.name for f in dataclasses.fields(cls)}
|
|
20
|
+
kwargs = {
|
|
21
|
+
f.name: getattr(self, f.name)
|
|
22
|
+
for f in dataclasses.fields(self)
|
|
23
|
+
if f.name in target_field_names
|
|
24
|
+
}
|
|
25
|
+
return cls(**kwargs)(target)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclasses.dataclass(frozen=True, kw_only=True)
|
|
29
|
+
class Scheduler[**ParamT, RetT](scheduler_.Scheduler[ParamT, RetT]):
|
|
30
|
+
Composee: typing.ClassVar = composer_.Composee
|
|
31
|
+
Connect: typing.ClassVar = composer_.Connect
|
|
32
|
+
Exit: typing.ClassVar = composer_.Exit
|
|
33
|
+
Enter: typing.ClassVar = composer_.Enter
|
|
34
|
+
Composed: typing.ClassVar = composer_.Composed
|
|
35
|
+
|
|
36
|
+
# Union of flavor-specific fields. The dispatcher forwards only the fields
|
|
37
|
+
# the target flavor's Scheduler actually accepts.
|
|
38
|
+
max_workers: int | None = None
|
|
39
|
+
thread_name_prefix: str = 'stranded'
|
|
40
|
+
loop: asyncio.AbstractEventLoop | None = None
|
|
41
|
+
|
|
42
|
+
def submit_sync(
|
|
43
|
+
self,
|
|
44
|
+
fn: typing.Callable[..., typing.Any],
|
|
45
|
+
args: tuple[typing.Any, ...],
|
|
46
|
+
kwargs: dict[str, typing.Any],
|
|
47
|
+
) -> typing.Any:
|
|
48
|
+
raise NotImplementedError
|
|
49
|
+
|
|
50
|
+
async def submit_async(
|
|
51
|
+
self,
|
|
52
|
+
fn: typing.Callable[..., typing.Any],
|
|
53
|
+
args: tuple[typing.Any, ...],
|
|
54
|
+
kwargs: dict[str, typing.Any],
|
|
55
|
+
) -> typing.Any:
|
|
56
|
+
raise NotImplementedError
|
|
57
|
+
|
|
58
|
+
def __call__[ComposeeT](self, target: ComposeeT) -> ComposeeT: # type: ignore[override]
|
|
59
|
+
return _dispatch(self, target)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib as _importlib
|
|
4
|
+
import typing as _typing
|
|
5
|
+
|
|
6
|
+
if _typing.TYPE_CHECKING:
|
|
7
|
+
from . import scheduler_
|
|
8
|
+
from .scheduler_ import Scheduler
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def __getattr__(name: str) -> _typing.Any:
|
|
12
|
+
match name:
|
|
13
|
+
case 'scheduler_': return _importlib.import_module('.scheduler_', __name__)
|
|
14
|
+
case 'Scheduler': return _importlib.import_module('.scheduler_', __name__).Scheduler
|
|
15
|
+
case _: raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
__all__ = (
|
|
19
|
+
'scheduler_',
|
|
20
|
+
'Scheduler',
|
|
21
|
+
)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import atexit
|
|
5
|
+
import concurrent.futures
|
|
6
|
+
import dataclasses
|
|
7
|
+
import functools
|
|
8
|
+
import inspect
|
|
9
|
+
import typing
|
|
10
|
+
|
|
11
|
+
from ...threading import composer_
|
|
12
|
+
from ..abc import scheduler_
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
Raise = composer_.Raise
|
|
16
|
+
Stop = composer_.Stop
|
|
17
|
+
Param = composer_.Param
|
|
18
|
+
Return = composer_.Return
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@typing.runtime_checkable
|
|
22
|
+
class Composee[**ParamT, RetT](composer_.Composee[ParamT, RetT], scheduler_.Composee[ParamT, RetT], typing.Protocol): ...
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclasses.dataclass(frozen=True, kw_only=True)
|
|
26
|
+
class Connect[**ParamT, RetT](composer_.Connect[ParamT, RetT], scheduler_.Connect[ParamT, RetT]): ...
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclasses.dataclass(frozen=True, kw_only=True)
|
|
30
|
+
class Exit[**ParamT, RetT](composer_.Exit[ParamT, RetT], scheduler_.Exit[ParamT, RetT]): ...
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclasses.dataclass(frozen=True, kw_only=True)
|
|
34
|
+
class Enter[**ParamT, RetT](composer_.Enter[ParamT, RetT], scheduler_.Enter[ParamT, RetT]):
|
|
35
|
+
@typing.overload # type: ignore[override]
|
|
36
|
+
def __call__(self, value: Param[ParamT], /) -> tuple[composer_.Exit[ParamT, RetT], composer_.Composee[ParamT, RetT]]: ...
|
|
37
|
+
@typing.overload
|
|
38
|
+
def __call__(self, value: Raise | Return[RetT] | Stop, /) -> tuple[()]: ...
|
|
39
|
+
def __call__(
|
|
40
|
+
self, value: Param[ParamT] | Raise | Return[RetT] | Stop, /,
|
|
41
|
+
) -> tuple[composer_.Exit[ParamT, RetT], composer_.Composee[ParamT, RetT]] | tuple[()]:
|
|
42
|
+
if not isinstance(value, Param):
|
|
43
|
+
return ()
|
|
44
|
+
scheduler: Scheduler[ParamT, RetT] = typing.cast('Scheduler[ParamT, RetT]', self.composer)
|
|
45
|
+
inner = self.composee
|
|
46
|
+
|
|
47
|
+
def wrapped(*args: typing.Any, **kwargs: typing.Any) -> typing.Any:
|
|
48
|
+
return scheduler.submit_sync(inner, args, kwargs)
|
|
49
|
+
|
|
50
|
+
return self.composer.Exit(enter=self), typing.cast(composer_.Composee[ParamT, RetT], wrapped)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclasses.dataclass(frozen=True, kw_only=True)
|
|
54
|
+
class Composed[**ParamT, RetT](composer_.Composed[ParamT, RetT], scheduler_.Composed[ParamT, RetT]): ...
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
_thread_pools: dict[tuple[int | None, str], concurrent.futures.ThreadPoolExecutor] = {}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _shared_thread_pool(
|
|
61
|
+
max_workers: int | None,
|
|
62
|
+
thread_name_prefix: str,
|
|
63
|
+
) -> concurrent.futures.ThreadPoolExecutor:
|
|
64
|
+
key = (max_workers, thread_name_prefix)
|
|
65
|
+
pool = _thread_pools.get(key)
|
|
66
|
+
if pool is None:
|
|
67
|
+
pool = concurrent.futures.ThreadPoolExecutor(
|
|
68
|
+
max_workers=max_workers,
|
|
69
|
+
thread_name_prefix=thread_name_prefix,
|
|
70
|
+
)
|
|
71
|
+
_thread_pools[key] = pool
|
|
72
|
+
return pool
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _shutdown_all() -> None:
|
|
76
|
+
for pool in _thread_pools.values():
|
|
77
|
+
pool.shutdown(wait=False)
|
|
78
|
+
_thread_pools.clear()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
atexit.register(_shutdown_all)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclasses.dataclass(frozen=True, kw_only=True)
|
|
85
|
+
class Scheduler[**ParamT = ..., RetT = typing.Any](composer_.Composer[ParamT, RetT], scheduler_.Scheduler[ParamT, RetT]):
|
|
86
|
+
Composee: typing.ClassVar = Composee
|
|
87
|
+
Connect: typing.ClassVar = Connect
|
|
88
|
+
Exit: typing.ClassVar = Exit
|
|
89
|
+
Enter: typing.ClassVar = Enter
|
|
90
|
+
Composed: typing.ClassVar = Composed
|
|
91
|
+
|
|
92
|
+
max_workers: int | None = None
|
|
93
|
+
thread_name_prefix: str = 'stranded'
|
|
94
|
+
|
|
95
|
+
def submit_sync(
|
|
96
|
+
self,
|
|
97
|
+
fn: typing.Callable[..., typing.Any],
|
|
98
|
+
args: tuple[typing.Any, ...],
|
|
99
|
+
kwargs: dict[str, typing.Any],
|
|
100
|
+
) -> typing.Any:
|
|
101
|
+
pool = _shared_thread_pool(self.max_workers, self.thread_name_prefix)
|
|
102
|
+
return pool.submit(fn, *args, **kwargs).result()
|
|
103
|
+
|
|
104
|
+
async def submit_async(
|
|
105
|
+
self,
|
|
106
|
+
fn: typing.Callable[..., typing.Any],
|
|
107
|
+
args: tuple[typing.Any, ...],
|
|
108
|
+
kwargs: dict[str, typing.Any],
|
|
109
|
+
) -> typing.Any:
|
|
110
|
+
loop = asyncio.get_running_loop()
|
|
111
|
+
pool = _shared_thread_pool(self.max_workers, self.thread_name_prefix)
|
|
112
|
+
if inspect.iscoroutinefunction(fn):
|
|
113
|
+
def runner() -> typing.Any:
|
|
114
|
+
return asyncio.run(fn(*args, **kwargs))
|
|
115
|
+
return await loop.run_in_executor(pool, runner)
|
|
116
|
+
return await loop.run_in_executor(pool, functools.partial(fn, *args, **kwargs))
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib as _importlib
|
|
4
|
+
import typing as _typing
|
|
5
|
+
|
|
6
|
+
if _typing.TYPE_CHECKING:
|
|
7
|
+
from . import herd_
|
|
8
|
+
from .herd_ import Herd
|
|
9
|
+
from .herd_ import herd
|
|
10
|
+
from . import lru_cache_
|
|
11
|
+
from .lru_cache_ import LruCache
|
|
12
|
+
from .lru_cache_ import lru_cache
|
|
13
|
+
from . import retry_
|
|
14
|
+
from .retry_ import Retry
|
|
15
|
+
from .retry_ import retry
|
|
16
|
+
from . import throttle_
|
|
17
|
+
from .throttle_ import Throttle
|
|
18
|
+
from .throttle_ import throttle
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def __getattr__(name: str) -> _typing.Any:
|
|
22
|
+
match name:
|
|
23
|
+
case 'herd_': return _importlib.import_module('.herd_', __name__)
|
|
24
|
+
case 'Herd': return _importlib.import_module('.herd_', __name__).Herd
|
|
25
|
+
case 'herd': return _importlib.import_module('.herd_', __name__).herd
|
|
26
|
+
case 'lru_cache_': return _importlib.import_module('.lru_cache_', __name__)
|
|
27
|
+
case 'LruCache': return _importlib.import_module('.lru_cache_', __name__).LruCache
|
|
28
|
+
case 'lru_cache': return _importlib.import_module('.lru_cache_', __name__).lru_cache
|
|
29
|
+
case 'retry_': return _importlib.import_module('.retry_', __name__)
|
|
30
|
+
case 'Retry': return _importlib.import_module('.retry_', __name__).Retry
|
|
31
|
+
case 'retry': return _importlib.import_module('.retry_', __name__).retry
|
|
32
|
+
case 'throttle_': return _importlib.import_module('.throttle_', __name__)
|
|
33
|
+
case 'Throttle': return _importlib.import_module('.throttle_', __name__).Throttle
|
|
34
|
+
case 'throttle': return _importlib.import_module('.throttle_', __name__).throttle
|
|
35
|
+
case _: raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
__all__ = (
|
|
39
|
+
'herd_',
|
|
40
|
+
'Herd',
|
|
41
|
+
'herd',
|
|
42
|
+
'lru_cache_',
|
|
43
|
+
'LruCache',
|
|
44
|
+
'lru_cache',
|
|
45
|
+
'retry_',
|
|
46
|
+
'Retry',
|
|
47
|
+
'retry',
|
|
48
|
+
'throttle_',
|
|
49
|
+
'Throttle',
|
|
50
|
+
'throttle',
|
|
51
|
+
)
|