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.
Files changed (71) hide show
  1. stranded/__init__.py +51 -0
  2. stranded/abc/__init__.py +21 -0
  3. stranded/abc/composer_.py +191 -0
  4. stranded/argparse/__init__.py +24 -0
  5. stranded/argparse/abc/__init__.py +21 -0
  6. stranded/argparse/abc/argument_parser_.py +539 -0
  7. stranded/argparse/argument_parser_.py +13 -0
  8. stranded/argparse/asyncio/__init__.py +24 -0
  9. stranded/argparse/asyncio/argument_parser_.py +50 -0
  10. stranded/argparse/threading/__init__.py +24 -0
  11. stranded/argparse/threading/argument_parser_.py +52 -0
  12. stranded/asyncio/__init__.py +21 -0
  13. stranded/asyncio/composer_.py +76 -0
  14. stranded/builtins/__init__.py +21 -0
  15. stranded/builtins/exception_.py +6 -0
  16. stranded/composer_.py +27 -0
  17. stranded/execution/__init__.py +30 -0
  18. stranded/execution/abc/__init__.py +21 -0
  19. stranded/execution/abc/scheduler_.py +59 -0
  20. stranded/execution/asyncio/__init__.py +21 -0
  21. stranded/execution/asyncio/scheduler_.py +86 -0
  22. stranded/execution/scheduler_.py +59 -0
  23. stranded/execution/threading/__init__.py +21 -0
  24. stranded/execution/threading/scheduler_.py +116 -0
  25. stranded/functools/__init__.py +51 -0
  26. stranded/functools/abc/__init__.py +39 -0
  27. stranded/functools/abc/herd_.py +96 -0
  28. stranded/functools/abc/lru_cache_.py +192 -0
  29. stranded/functools/abc/retry_.py +60 -0
  30. stranded/functools/abc/throttle_.py +97 -0
  31. stranded/functools/asyncio/__init__.py +51 -0
  32. stranded/functools/asyncio/herd_.py +89 -0
  33. stranded/functools/asyncio/lru_cache_.py +92 -0
  34. stranded/functools/asyncio/retry_.py +43 -0
  35. stranded/functools/asyncio/throttle_.py +74 -0
  36. stranded/functools/herd_.py +18 -0
  37. stranded/functools/lru_cache_.py +18 -0
  38. stranded/functools/retry_.py +13 -0
  39. stranded/functools/threading/__init__.py +51 -0
  40. stranded/functools/threading/herd_.py +88 -0
  41. stranded/functools/threading/lru_cache_.py +90 -0
  42. stranded/functools/threading/retry_.py +42 -0
  43. stranded/functools/threading/throttle_.py +75 -0
  44. stranded/functools/throttle_.py +13 -0
  45. stranded/logging/__init__.py +24 -0
  46. stranded/logging/abc/__init__.py +21 -0
  47. stranded/logging/abc/logger_.py +96 -0
  48. stranded/logging/asyncio/__init__.py +21 -0
  49. stranded/logging/asyncio/logger_.py +41 -0
  50. stranded/logging/logger_.py +13 -0
  51. stranded/logging/threading/__init__.py +21 -0
  52. stranded/logging/threading/logger_.py +41 -0
  53. stranded/py.typed +0 -0
  54. stranded/sqlite3/__init__.py +24 -0
  55. stranded/sqlite3/abc/__init__.py +21 -0
  56. stranded/sqlite3/abc/db_.py +149 -0
  57. stranded/sqlite3/asyncio/__init__.py +24 -0
  58. stranded/sqlite3/asyncio/db_.py +43 -0
  59. stranded/sqlite3/db_.py +13 -0
  60. stranded/sqlite3/threading/__init__.py +24 -0
  61. stranded/sqlite3/threading/db_.py +43 -0
  62. stranded/threading/__init__.py +21 -0
  63. stranded/threading/composer_.py +74 -0
  64. stranded/types/__init__.py +21 -0
  65. stranded/types/constraint.py +7 -0
  66. stranded/types/convert.py +158 -0
  67. stranded-0.0.1.dist-info/METADATA +173 -0
  68. stranded-0.0.1.dist-info/RECORD +71 -0
  69. stranded-0.0.1.dist-info/WHEEL +5 -0
  70. stranded-0.0.1.dist-info/licenses/LICENSE +21 -0
  71. 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
+ )
@@ -0,0 +1,6 @@
1
+ from __future__ import annotations
2
+
3
+ import builtins
4
+
5
+
6
+ class Exception(builtins.Exception): ... # noqa
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
+ )