omlish 0.0.0.dev1__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.

Potentially problematic release.


This version of omlish might be problematic. Click here for more details.

Files changed (187) hide show
  1. omlish/__about__.py +7 -0
  2. omlish/__init__.py +0 -0
  3. omlish/argparse.py +223 -0
  4. omlish/asyncs/__init__.py +17 -0
  5. omlish/asyncs/anyio.py +23 -0
  6. omlish/asyncs/asyncio.py +19 -0
  7. omlish/asyncs/asyncs.py +76 -0
  8. omlish/asyncs/futures.py +179 -0
  9. omlish/asyncs/trio.py +11 -0
  10. omlish/c3.py +173 -0
  11. omlish/cached.py +9 -0
  12. omlish/check.py +231 -0
  13. omlish/collections/__init__.py +63 -0
  14. omlish/collections/_abc.py +156 -0
  15. omlish/collections/_io_abc.py +78 -0
  16. omlish/collections/cache/__init__.py +11 -0
  17. omlish/collections/cache/descriptor.py +188 -0
  18. omlish/collections/cache/impl.py +485 -0
  19. omlish/collections/cache/types.py +37 -0
  20. omlish/collections/coerce.py +337 -0
  21. omlish/collections/frozen.py +148 -0
  22. omlish/collections/identity.py +106 -0
  23. omlish/collections/indexed.py +75 -0
  24. omlish/collections/mappings.py +127 -0
  25. omlish/collections/ordered.py +81 -0
  26. omlish/collections/persistent.py +36 -0
  27. omlish/collections/skiplist.py +193 -0
  28. omlish/collections/sorted.py +126 -0
  29. omlish/collections/treap.py +228 -0
  30. omlish/collections/treapmap.py +144 -0
  31. omlish/collections/unmodifiable.py +174 -0
  32. omlish/collections/utils.py +110 -0
  33. omlish/configs/__init__.py +0 -0
  34. omlish/configs/flattening.py +147 -0
  35. omlish/configs/props.py +64 -0
  36. omlish/dataclasses/__init__.py +83 -0
  37. omlish/dataclasses/impl/__init__.py +6 -0
  38. omlish/dataclasses/impl/api.py +260 -0
  39. omlish/dataclasses/impl/as_.py +76 -0
  40. omlish/dataclasses/impl/exceptions.py +2 -0
  41. omlish/dataclasses/impl/fields.py +148 -0
  42. omlish/dataclasses/impl/frozen.py +55 -0
  43. omlish/dataclasses/impl/hashing.py +85 -0
  44. omlish/dataclasses/impl/init.py +173 -0
  45. omlish/dataclasses/impl/internals.py +118 -0
  46. omlish/dataclasses/impl/main.py +150 -0
  47. omlish/dataclasses/impl/metaclass.py +126 -0
  48. omlish/dataclasses/impl/metadata.py +74 -0
  49. omlish/dataclasses/impl/order.py +47 -0
  50. omlish/dataclasses/impl/params.py +150 -0
  51. omlish/dataclasses/impl/processing.py +16 -0
  52. omlish/dataclasses/impl/reflect.py +173 -0
  53. omlish/dataclasses/impl/replace.py +40 -0
  54. omlish/dataclasses/impl/repr.py +34 -0
  55. omlish/dataclasses/impl/simple.py +92 -0
  56. omlish/dataclasses/impl/slots.py +80 -0
  57. omlish/dataclasses/impl/utils.py +167 -0
  58. omlish/defs.py +193 -0
  59. omlish/dispatch/__init__.py +3 -0
  60. omlish/dispatch/dispatch.py +137 -0
  61. omlish/dispatch/functions.py +52 -0
  62. omlish/dispatch/methods.py +162 -0
  63. omlish/docker.py +149 -0
  64. omlish/dynamic.py +220 -0
  65. omlish/graphs/__init__.py +0 -0
  66. omlish/graphs/dot/__init__.py +19 -0
  67. omlish/graphs/dot/items.py +162 -0
  68. omlish/graphs/dot/rendering.py +147 -0
  69. omlish/graphs/dot/utils.py +30 -0
  70. omlish/graphs/trees.py +249 -0
  71. omlish/http/__init__.py +0 -0
  72. omlish/http/consts.py +20 -0
  73. omlish/http/wsgi.py +34 -0
  74. omlish/inject/__init__.py +85 -0
  75. omlish/inject/binder.py +12 -0
  76. omlish/inject/bindings.py +49 -0
  77. omlish/inject/eagers.py +21 -0
  78. omlish/inject/elements.py +43 -0
  79. omlish/inject/exceptions.py +49 -0
  80. omlish/inject/impl/__init__.py +0 -0
  81. omlish/inject/impl/bindings.py +19 -0
  82. omlish/inject/impl/elements.py +154 -0
  83. omlish/inject/impl/injector.py +182 -0
  84. omlish/inject/impl/inspect.py +98 -0
  85. omlish/inject/impl/private.py +109 -0
  86. omlish/inject/impl/providers.py +132 -0
  87. omlish/inject/impl/scopes.py +198 -0
  88. omlish/inject/injector.py +40 -0
  89. omlish/inject/inspect.py +14 -0
  90. omlish/inject/keys.py +43 -0
  91. omlish/inject/managed.py +24 -0
  92. omlish/inject/overrides.py +18 -0
  93. omlish/inject/private.py +29 -0
  94. omlish/inject/providers.py +111 -0
  95. omlish/inject/proxy.py +48 -0
  96. omlish/inject/scopes.py +84 -0
  97. omlish/inject/types.py +21 -0
  98. omlish/iterators.py +184 -0
  99. omlish/json.py +194 -0
  100. omlish/lang/__init__.py +112 -0
  101. omlish/lang/cached.py +267 -0
  102. omlish/lang/classes/__init__.py +24 -0
  103. omlish/lang/classes/abstract.py +74 -0
  104. omlish/lang/classes/restrict.py +137 -0
  105. omlish/lang/classes/simple.py +120 -0
  106. omlish/lang/classes/test/__init__.py +0 -0
  107. omlish/lang/classes/test/test_abstract.py +89 -0
  108. omlish/lang/classes/test/test_restrict.py +71 -0
  109. omlish/lang/classes/test/test_simple.py +58 -0
  110. omlish/lang/classes/test/test_virtual.py +72 -0
  111. omlish/lang/classes/virtual.py +130 -0
  112. omlish/lang/clsdct.py +67 -0
  113. omlish/lang/cmp.py +63 -0
  114. omlish/lang/contextmanagers.py +249 -0
  115. omlish/lang/datetimes.py +67 -0
  116. omlish/lang/descriptors.py +52 -0
  117. omlish/lang/functions.py +126 -0
  118. omlish/lang/imports.py +153 -0
  119. omlish/lang/iterables.py +54 -0
  120. omlish/lang/maybes.py +136 -0
  121. omlish/lang/objects.py +103 -0
  122. omlish/lang/resolving.py +50 -0
  123. omlish/lang/strings.py +128 -0
  124. omlish/lang/typing.py +92 -0
  125. omlish/libc.py +532 -0
  126. omlish/logs/__init__.py +9 -0
  127. omlish/logs/_abc.py +247 -0
  128. omlish/logs/configs.py +62 -0
  129. omlish/logs/filters.py +9 -0
  130. omlish/logs/formatters.py +67 -0
  131. omlish/logs/utils.py +20 -0
  132. omlish/marshal/__init__.py +52 -0
  133. omlish/marshal/any.py +25 -0
  134. omlish/marshal/base.py +201 -0
  135. omlish/marshal/base64.py +25 -0
  136. omlish/marshal/dataclasses.py +115 -0
  137. omlish/marshal/datetimes.py +90 -0
  138. omlish/marshal/enums.py +43 -0
  139. omlish/marshal/exceptions.py +7 -0
  140. omlish/marshal/factories.py +129 -0
  141. omlish/marshal/global_.py +33 -0
  142. omlish/marshal/iterables.py +57 -0
  143. omlish/marshal/mappings.py +66 -0
  144. omlish/marshal/naming.py +17 -0
  145. omlish/marshal/objects.py +106 -0
  146. omlish/marshal/optionals.py +49 -0
  147. omlish/marshal/polymorphism.py +147 -0
  148. omlish/marshal/primitives.py +43 -0
  149. omlish/marshal/registries.py +57 -0
  150. omlish/marshal/standard.py +80 -0
  151. omlish/marshal/utils.py +23 -0
  152. omlish/marshal/uuids.py +29 -0
  153. omlish/marshal/values.py +30 -0
  154. omlish/math.py +184 -0
  155. omlish/os.py +32 -0
  156. omlish/reflect.py +359 -0
  157. omlish/replserver/__init__.py +5 -0
  158. omlish/replserver/__main__.py +4 -0
  159. omlish/replserver/console.py +247 -0
  160. omlish/replserver/server.py +146 -0
  161. omlish/runmodule.py +28 -0
  162. omlish/stats.py +342 -0
  163. omlish/term.py +222 -0
  164. omlish/testing/__init__.py +7 -0
  165. omlish/testing/pydevd.py +225 -0
  166. omlish/testing/pytest/__init__.py +8 -0
  167. omlish/testing/pytest/helpers.py +35 -0
  168. omlish/testing/pytest/inject/__init__.py +1 -0
  169. omlish/testing/pytest/inject/harness.py +159 -0
  170. omlish/testing/pytest/plugins/__init__.py +20 -0
  171. omlish/testing/pytest/plugins/_registry.py +6 -0
  172. omlish/testing/pytest/plugins/logging.py +13 -0
  173. omlish/testing/pytest/plugins/pycharm.py +54 -0
  174. omlish/testing/pytest/plugins/repeat.py +19 -0
  175. omlish/testing/pytest/plugins/skips.py +32 -0
  176. omlish/testing/pytest/plugins/spacing.py +19 -0
  177. omlish/testing/pytest/plugins/switches.py +70 -0
  178. omlish/testing/testing.py +102 -0
  179. omlish/text/__init__.py +0 -0
  180. omlish/text/delimit.py +171 -0
  181. omlish/text/indent.py +50 -0
  182. omlish/text/parts.py +265 -0
  183. omlish-0.0.0.dev1.dist-info/LICENSE +21 -0
  184. omlish-0.0.0.dev1.dist-info/METADATA +17 -0
  185. omlish-0.0.0.dev1.dist-info/RECORD +187 -0
  186. omlish-0.0.0.dev1.dist-info/WHEEL +5 -0
  187. omlish-0.0.0.dev1.dist-info/top_level.txt +1 -0
omlish/defs.py ADDED
@@ -0,0 +1,193 @@
1
+ """
2
+ A self-awarely unpythonic set of helpers for defining common boilerplate methods (repr, hash_eq, delegates, etc) in
3
+ class definitions. Should be used sparingly for methods not directly used by humans (like repr) - @property's should
4
+ remain @property's for type annotation, tool assistance, debugging, and otherwise, but these are still nice to have in
5
+ certain circumstances (the real-world alternative usually being simply not adding them).
6
+ """
7
+ import abc
8
+ import functools
9
+ import operator
10
+ import threading
11
+
12
+ from . import lang
13
+
14
+
15
+ _repr = repr
16
+
17
+ BASICS = {}
18
+
19
+
20
+ def _basic(fn):
21
+ if fn.__name__ in BASICS:
22
+ raise NameError(fn.__name__)
23
+ BASICS[fn.__name__] = fn
24
+ return fn
25
+
26
+
27
+ @lang.cls_dct_fn()
28
+ def basic(cls_dct, *attrs, basics=None):
29
+ if basics is None:
30
+ basics = BASICS.keys()
31
+ for k in basics:
32
+ fn = BASICS[k]
33
+ fn(*attrs, cls_dct=cls_dct)
34
+
35
+
36
+ _REPR_SEEN = threading.local()
37
+
38
+
39
+ def _repr_guard(fn):
40
+ @functools.wraps(fn)
41
+ def inner(obj, *args, **kwargs):
42
+ try:
43
+ ids = _REPR_SEEN.ids
44
+ except AttributeError:
45
+ ids = _REPR_SEEN.ids = set()
46
+ try:
47
+ ids.add(id(obj))
48
+ return fn(obj, *args, **kwargs)
49
+ finally:
50
+ del _REPR_SEEN.ids
51
+ else:
52
+ if id(obj) in ids:
53
+ return f'<seen:{type(obj).__name__}@{hex(id(obj))[2:]}>'
54
+ ids.add(id(obj))
55
+ return fn(obj, *args, **kwargs)
56
+
57
+ return inner
58
+
59
+
60
+ @_repr_guard
61
+ def build_attr_repr(obj, *, mro=False):
62
+ if mro:
63
+ attrs = [
64
+ attr
65
+ for ty in sorted(reversed(type(obj).__mro__), key=lambda _ty: _ty.__dict__.get('__repr_priority__', 0))
66
+ for attr in ty.__dict__.get('__repr_attrs__', [])]
67
+ else:
68
+ attrs = obj.__repr_attrs__
69
+ return '%s@%s(%s)' % (
70
+ type(obj).__name__,
71
+ hex(id(obj))[2:],
72
+ ', '.join('%s=%s' % (attr, '<self>' if value is obj else _repr(value))
73
+ for attr in attrs for value in [getattr(obj, attr)]))
74
+
75
+
76
+ @_repr_guard
77
+ def build_repr(obj, *attrs):
78
+ return '%s@%s(%s)' % (
79
+ type(obj).__name__,
80
+ hex(id(obj))[2:],
81
+ ', '.join('%s=%r' % (attr, getattr(obj, attr)) for attr in attrs))
82
+
83
+
84
+ @_basic
85
+ @lang.cls_dct_fn()
86
+ def repr(cls_dct, *attrs, mro=False, priority=None):
87
+ def __repr__(self):
88
+ return build_attr_repr(self, mro=mro)
89
+
90
+ cls_dct['__repr_attrs__'] = attrs
91
+ if priority is not None:
92
+ cls_dct['__repr_priority__'] = priority
93
+ cls_dct['__repr__'] = __repr__
94
+
95
+
96
+ @lang.cls_dct_fn()
97
+ def bare_repr(cls_dct, *attrs):
98
+ def __repr__(self):
99
+ return lang.attr_repr(self, *attrs)
100
+
101
+ cls_dct['__repr__'] = __repr__
102
+
103
+
104
+ @lang.cls_dct_fn()
105
+ def name_repr(cls_dct):
106
+ def __repr__(self):
107
+ return self.__name__
108
+
109
+ cls_dct['__repr__'] = __repr__
110
+
111
+
112
+ @lang.cls_dct_fn()
113
+ def ne(cls_dct):
114
+ def __ne__(self, other):
115
+ return not (self == other)
116
+
117
+ cls_dct['__ne__'] = __ne__
118
+
119
+
120
+ @lang.cls_dct_fn()
121
+ def no_order(cls_dct, *, raise_=None):
122
+ def fn(self, other):
123
+ if raise_ is None:
124
+ return NotImplemented
125
+ else:
126
+ raise raise_
127
+
128
+ for att in [
129
+ '__lt__',
130
+ '__le__',
131
+ '__gt__',
132
+ '__ge__',
133
+ ]:
134
+ cls_dct[att] = fn
135
+
136
+
137
+ @_basic
138
+ @lang.cls_dct_fn()
139
+ def hash_eq(cls_dct, *attrs):
140
+ def __hash__(self):
141
+ return hash(tuple(getattr(self, attr) for attr in attrs))
142
+
143
+ cls_dct['__hash__'] = __hash__
144
+
145
+ def __eq__(self, other):
146
+ if type(other) is not type(self):
147
+ return False
148
+ for attr in attrs:
149
+ if getattr(self, attr) != getattr(other, attr):
150
+ return False
151
+ return True
152
+
153
+ cls_dct['__eq__'] = __eq__
154
+
155
+ ne(cls_dct=cls_dct)
156
+
157
+
158
+ @lang.cls_dct_fn()
159
+ def getter(cls_dct, *attrs):
160
+ for attr in attrs:
161
+ cls_dct[attr] = property(operator.attrgetter('_' + attr))
162
+
163
+
164
+ @lang.cls_dct_fn()
165
+ def not_implemented(cls_dct, *names, **kwargs):
166
+ wrapper = kwargs.pop('wrapper', lambda _: _)
167
+ if kwargs:
168
+ raise TypeError(kwargs)
169
+ ret = []
170
+ for name in names:
171
+ @wrapper
172
+ def not_implemented(self, *args, **kwargs):
173
+ raise NotImplementedError
174
+
175
+ not_implemented.__name__ = name
176
+ cls_dct[name] = not_implemented
177
+ ret.append(not_implemented)
178
+ return tuple(ret)
179
+
180
+
181
+ @lang.cls_dct_fn()
182
+ def abstract_method(cls_dct, *names):
183
+ return not_implemented(cls_dct, *names, wrapper=abc.abstractmethod)
184
+
185
+
186
+ @lang.cls_dct_fn()
187
+ def abstract_property(cls_dct, *names):
188
+ return not_implemented(cls_dct, *names, wrapper=abc.abstractmethod)
189
+
190
+
191
+ @lang.cls_dct_fn()
192
+ def abstract_hash_eq(cls_dct):
193
+ return not_implemented(cls_dct, '__hash__', '__eq__', '__ne__', wrapper=abc.abstractmethod)
@@ -0,0 +1,3 @@
1
+ from .dispatch import Dispatcher # noqa
2
+ from .functions import function # noqa
3
+ from .methods import method # noqa
@@ -0,0 +1,137 @@
1
+ import abc
2
+ import typing as ta
3
+ import weakref
4
+
5
+ from .. import c3
6
+ from .. import check
7
+ from .. import reflect as rfl
8
+
9
+
10
+ T = ta.TypeVar('T')
11
+
12
+
13
+ ##
14
+
15
+
16
+ _IMPL_FUNC_CLS_SET_CACHE: ta.MutableMapping[ta.Callable, ta.FrozenSet[type]] = weakref.WeakKeyDictionary()
17
+
18
+
19
+ def get_impl_func_cls_set(func: ta.Callable) -> ta.FrozenSet[type]:
20
+ try:
21
+ return _IMPL_FUNC_CLS_SET_CACHE[func]
22
+ except KeyError:
23
+ pass
24
+
25
+ ann = getattr(func, '__annotations__', {})
26
+ if not ann:
27
+ raise TypeError(f'Invalid impl func: {func!r}')
28
+
29
+ _, cls = next(iter(ta.get_type_hints(func).items()))
30
+ if rfl.is_union_type(cls):
31
+ ret = frozenset(check.isinstance(arg, type) for arg in ta.get_args(cls))
32
+ else:
33
+ ret = frozenset([check.isinstance(cls, type)])
34
+
35
+ _IMPL_FUNC_CLS_SET_CACHE[func] = ret
36
+ return ret
37
+
38
+
39
+ def find_impl(cls: type, registry: ta.Mapping[type, T]) -> ta.Optional[T]:
40
+ mro = c3.compose_mro(cls, registry.keys())
41
+
42
+ match: ta.Optional[type] = None
43
+ for t in mro:
44
+ if match is not None:
45
+ # If *match* is an implicit ABC but there is another unrelated, equally matching implicit ABC, refuse the
46
+ # temptation to guess.
47
+ if (
48
+ t in registry
49
+ and t not in cls.__mro__
50
+ and match not in cls.__mro__
51
+ and not issubclass(match, t)
52
+ ):
53
+ raise RuntimeError(f'Ambiguous dispatch: {match} or {t}')
54
+ break
55
+
56
+ if t in registry:
57
+ match = t
58
+
59
+ if match is None:
60
+ return None
61
+ return registry.get(match)
62
+
63
+
64
+ ##
65
+
66
+
67
+ class Dispatcher(ta.Generic[T]):
68
+ def __init__(self) -> None:
69
+ super().__init__()
70
+
71
+ impls_by_arg_cls: dict[type, T] = {}
72
+ self._impls_by_arg_cls = impls_by_arg_cls
73
+
74
+ dispatch_cache: dict[ta.Any, ta.Optional[T]] = {}
75
+ self._get_dispatch_cache = lambda: dispatch_cache
76
+
77
+ def cache_remove(k, self_ref=weakref.ref(self)):
78
+ if (ref_self := self_ref()) is not None:
79
+ cache = ref_self._get_dispatch_cache() # noqa
80
+ try:
81
+ del cache[k]
82
+ except KeyError:
83
+ pass
84
+
85
+ cache_token: ta.Any = None
86
+ self._get_cache_token = lambda: cache_token
87
+
88
+ weakref_ref_ = weakref.ref
89
+
90
+ def dispatch(cls: type) -> ta.Optional[T]:
91
+ nonlocal cache_token
92
+
93
+ if cache_token is not None and (current_token := abc.get_cache_token()) != cache_token:
94
+ dispatch_cache.clear()
95
+ cache_token = current_token
96
+
97
+ cls_ref = weakref_ref_(cls)
98
+ try:
99
+ return dispatch_cache[cls_ref]
100
+ except KeyError:
101
+ pass
102
+ del cls_ref
103
+
104
+ impl: T | None
105
+ try:
106
+ impl = impls_by_arg_cls[cls]
107
+ except KeyError:
108
+ impl = find_impl(cls, impls_by_arg_cls)
109
+
110
+ dispatch_cache[weakref_ref_(cls, cache_remove)] = impl
111
+ return impl
112
+
113
+ self.dispatch = dispatch
114
+
115
+ def register(impl: T, cls_col: ta.Iterable[type]) -> T:
116
+ nonlocal cache_token
117
+
118
+ for cls in cls_col:
119
+ impls_by_arg_cls[cls] = impl
120
+
121
+ if cache_token is None and hasattr(cls, '__abstractmethods__'):
122
+ cache_token = abc.get_cache_token()
123
+
124
+ dispatch_cache.clear()
125
+ return impl
126
+
127
+ self.register = register
128
+
129
+ _get_cache_token: ta.Callable[[], int]
130
+ _get_dispatch_cache: ta.Callable[[], ta.Any]
131
+
132
+ def cache_size(self) -> int:
133
+ return len(self._get_dispatch_cache())
134
+
135
+ dispatch: ta.Callable[[type], ta.Optional[T]]
136
+
137
+ register: ta.Callable[[T, ta.Iterable[type]], T]
@@ -0,0 +1,52 @@
1
+ import functools
2
+ import typing as ta
3
+
4
+ from .. import check
5
+ from .dispatch import Dispatcher
6
+ from .dispatch import get_impl_func_cls_set
7
+
8
+
9
+ # USE_EXTENSION = True
10
+ USE_EXTENSION = False
11
+
12
+
13
+ def function(func):
14
+ disp = Dispatcher() # type: ignore
15
+ disp.register(func, [object])
16
+
17
+ func_name = getattr(func, '__name__', 'singledispatch function')
18
+
19
+ def register(impl, cls=None):
20
+ check.callable(impl)
21
+ cls_col: ta.Iterable[type]
22
+ if cls is None:
23
+ cls_col = get_impl_func_cls_set(impl)
24
+ else:
25
+ cls_col = frozenset([cls])
26
+ disp.register(impl, cls_col)
27
+ return impl
28
+
29
+ if not USE_EXTENSION:
30
+ disp_dispatch = disp.dispatch
31
+
32
+ @functools.wraps(func)
33
+ def wrapper(*args, **kwargs):
34
+ if not args:
35
+ raise TypeError(f'{func_name} requires at least 1 positional argument')
36
+ if (impl := disp_dispatch(type(args[0]))) is not None:
37
+ return impl(*args, **kwargs)
38
+ raise RuntimeError(f'No dispatch: {type(args[0])}')
39
+ wrapper.register = register # type: ignore
40
+ wrapper.dispatch = disp.dispatch # type: ignore
41
+
42
+ else:
43
+ from x.c._dispatch import function_wrapper # noqa
44
+ wrapper = function_wrapper(
45
+ disp.dispatch,
46
+ **{k: getattr(func, k) for k in functools.WRAPPER_ASSIGNMENTS if hasattr(func, k)},
47
+ __wrapped__=func,
48
+ register=register,
49
+ dispatcher=disp,
50
+ )
51
+
52
+ return wrapper
@@ -0,0 +1,162 @@
1
+ """
2
+ TODO:
3
+ - .super(instance[_cls], owner)
4
+ - ALT: A.f(super(), ... ? :/
5
+ - classmethod/staticmethod
6
+ """
7
+ import functools
8
+ import typing as ta
9
+ import weakref
10
+
11
+ from .. import check
12
+ from .dispatch import Dispatcher
13
+ from .dispatch import get_impl_func_cls_set
14
+
15
+
16
+ T = ta.TypeVar('T')
17
+
18
+
19
+ def build_mro_dct(instance_cls: type, owner_cls: ta.Optional[type] = None) -> ta.Mapping[str, ta.Any]:
20
+ if owner_cls is None:
21
+ owner_cls = instance_cls
22
+ mro = instance_cls.__mro__[-2::-1]
23
+ try:
24
+ pos = mro.index(owner_cls)
25
+ except ValueError:
26
+ raise TypeError(f'Owner class {owner_cls} not in mro of instance class {instance_cls}')
27
+ dct: dict[str, ta.Any] = {}
28
+ for cur_cls in mro[:pos + 1]:
29
+ dct.update(cur_cls.__dict__)
30
+ return dct
31
+
32
+
33
+ class Method:
34
+ def __init__(self, func: ta.Callable) -> None:
35
+ if not callable(func) and not hasattr(func, '__get__'): # type: ignore
36
+ raise TypeError(f'{func!r} is not callable or a descriptor')
37
+
38
+ self._func = func
39
+
40
+ self._impls: ta.MutableSet[ta.Callable] = weakref.WeakSet()
41
+
42
+ # bpo-45678: special-casing for classmethod/staticmethod in Python <=3.9, as functools.update_wrapper doesn't
43
+ # work properly in singledispatchmethod.__get__ if it is applied to an unbound classmethod/staticmethod
44
+ unwrapped_func: ta.Any
45
+ if isinstance(func, (staticmethod, classmethod)):
46
+ unwrapped_func = func.__func__
47
+ else:
48
+ unwrapped_func = func
49
+ self._unwrapped_func = unwrapped_func
50
+ self._is_abstractmethod = getattr(func, '__isabstractmethod__', False) # noqa
51
+ self.update_wrapper(self)
52
+
53
+ self._dispatch_func_cache: dict[ta.Any, ta.Callable] = {}
54
+
55
+ def dispatch_func_cache_remove(k, self_ref=weakref.ref(self)):
56
+ if (ref_self := self_ref()) is not None:
57
+ cache = ref_self._dispatch_func_cache # noqa
58
+ try:
59
+ del cache[k]
60
+ except KeyError:
61
+ pass
62
+
63
+ self._dispatch_func_cache_remove = dispatch_func_cache_remove
64
+
65
+ def __repr__(self) -> str:
66
+ return f'<{type(self).__module__}.{type(self).__qualname__}:{self._func} at 0x{id(self):x}>'
67
+
68
+ def update_wrapper(self, wrapper: T) -> T:
69
+ for attr in functools.WRAPPER_ASSIGNMENTS:
70
+ try:
71
+ value = getattr(self._unwrapped_func, attr)
72
+ except AttributeError:
73
+ continue
74
+ setattr(wrapper, attr, value)
75
+ setattr(wrapper, '__isabstractmethod__', self._is_abstractmethod) # noqa
76
+ return wrapper
77
+
78
+ def register(self, impl: T) -> T:
79
+ # bpo-39679: in Python <= 3.9, classmethods and staticmethods don't inherit __annotations__ of the wrapped
80
+ # function (fixed in 3.10+ as a side-effect of bpo-43682) but we need that for annotation-derived
81
+ # singledispatches. So we add that just-in-time here.
82
+ if isinstance(impl, (staticmethod, classmethod)):
83
+ impl.__annotations__ = getattr(impl.__func__, '__annotations__', {})
84
+
85
+ check.callable(impl)
86
+ if impl not in self._impls:
87
+ self._impls.add(impl) # type: ignore
88
+ self._dispatch_func_cache.clear()
89
+
90
+ return impl
91
+
92
+ def build_attr_dispatcher(self, instance_cls: type, owner_cls: ta.Optional[type] = None) -> Dispatcher[str]:
93
+ disp: Dispatcher[str] = Dispatcher()
94
+
95
+ mro_dct = build_mro_dct(instance_cls, owner_cls)
96
+ seen: ta.Mapping[ta.Any, str] = {}
97
+ for nam, att in mro_dct.items():
98
+ if att in self._impls:
99
+ try:
100
+ ex_nam = seen[att]
101
+ except KeyError:
102
+ pass
103
+ else:
104
+ raise TypeError(f'Duplicate impl: {owner_cls} {instance_cls} {nam} {ex_nam}')
105
+ disp.register(nam, get_impl_func_cls_set(att))
106
+
107
+ return disp
108
+
109
+ def build_dispatch_func(self, disp: Dispatcher[str]) -> ta.Callable:
110
+ dispatch = disp.dispatch
111
+ type_ = type
112
+ getattr_ = getattr
113
+ base_func = self._func
114
+ func_name = getattr(base_func, '__name__', 'singledispatch method')
115
+
116
+ def __call__(self, *args, **kwargs): # noqa
117
+ if not args:
118
+ raise TypeError(f'{func_name} requires at least 1 positional argument')
119
+ if (impl_att := dispatch(type_(args[0]))) is not None:
120
+ fn = getattr_(self, impl_att)
121
+ return fn(*args, **kwargs)
122
+ return base_func.__get__(self)(*args, **kwargs) # noqa
123
+
124
+ self.update_wrapper(__call__)
125
+ return __call__
126
+
127
+ def get_dispatch_func(self, instance_cls: type) -> ta.Callable:
128
+ cls_ref = weakref.ref(instance_cls)
129
+ try:
130
+ return self._dispatch_func_cache[cls_ref]
131
+ except KeyError:
132
+ pass
133
+ del cls_ref
134
+
135
+ att_disp = self.build_attr_dispatcher(instance_cls)
136
+ func = self.build_dispatch_func(att_disp)
137
+ self._dispatch_func_cache[weakref.ref(instance_cls, self._dispatch_func_cache_remove)] = func
138
+ return func
139
+
140
+ def __get__(self, instance, owner=None):
141
+ if instance is None:
142
+ # FIXME: classmethod/staticmethod
143
+ return self
144
+
145
+ instance_cls = type(instance)
146
+ try:
147
+ func = self._dispatch_func_cache[weakref.ref(instance_cls)]
148
+ except KeyError:
149
+ func = self.get_dispatch_func(instance_cls)
150
+ return func.__get__(instance, owner) # noqa
151
+
152
+ def __call__(self, instance, *args, **kwargs):
153
+ instance_cls = type(instance)
154
+ try:
155
+ func = self._dispatch_func_cache[weakref.ref(instance_cls)]
156
+ except KeyError:
157
+ func = self.get_dispatch_func(instance_cls)
158
+ return func.__get__(instance)(*args, **kwargs) # noqa
159
+
160
+
161
+ def method(func):
162
+ return Method(func)
omlish/docker.py ADDED
@@ -0,0 +1,149 @@
1
+ """
2
+ TODO:
3
+ - merged compose configs: https://github.com/wrmsr/bane/blob/27647abdcfb323b73e6982a5c318c7029496b203/core/dev/docker/compose.go#L38
4
+ - https://github.com/mag37/dockcheck/blob/3d122f2b868eb53a25a3014f0f6bd499390a3a29/dockcheck.sh
5
+ - https://github.com/regclient/regclient
6
+ - https://stackoverflow.com/questions/71409458/how-to-download-docker-image-using-http-api-using-docker-hub-credentials
7
+ - https://stackoverflow.com/questions/55386202/how-can-i-use-the-docker-registry-api-to-pull-information-about-a-container-get
8
+ - https://ops.tips/blog/inspecting-docker-image-without-pull/
9
+
10
+ repo=library/nginx
11
+ token=$(curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:${repo}:pull" | jq -r '.token')
12
+ curl -H "Authorization: Bearer $token" -s "https://registry-1.docker.io/v2/${repo}/tags/list" | jq
13
+ api="application/vnd.docker.distribution.manifest.v2+json"
14
+ apil="application/vnd.docker.distribution.manifest.list.v2+json"
15
+ curl -H "Accept: ${api}" -H "Accept: ${apil}" -H "Authorization: Bearer $token" -s "https://registry-1.docker.io/v2/${repo}/manifests/latest" | jq .
16
+ """ # noqa
17
+ import datetime
18
+ import re
19
+ import shlex
20
+ import subprocess
21
+ import typing as ta
22
+
23
+ from . import check
24
+ from . import dataclasses as dc
25
+ from . import json
26
+ from . import lang
27
+ from . import marshal as msh
28
+
29
+ if ta.TYPE_CHECKING:
30
+ import yaml
31
+ else:
32
+ yaml = lang.proxy_import('yaml')
33
+
34
+
35
+ @dc.dataclass(frozen=True)
36
+ class PsItem(lang.Final):
37
+ dc.metadata(msh.ObjectMetadata(
38
+ field_naming=msh.Naming.CAMEL,
39
+ unknown_field='x',
40
+ ))
41
+
42
+ command: str
43
+ created_at: datetime.datetime
44
+ id: str = dc.field(metadata={msh.FieldMetadata: msh.FieldMetadata(name='ID')})
45
+ image: str
46
+ labels: str
47
+ local_volumes: str
48
+ mounts: str
49
+ names: str
50
+ networks: str
51
+ ports: str
52
+ running_for: str
53
+ size: str
54
+ state: str
55
+ status: str
56
+
57
+ x: ta.Mapping[str, ta.Any] | None = None
58
+
59
+
60
+ class Port(ta.NamedTuple):
61
+ ip: str
62
+ from_port: int
63
+ to_port: int
64
+ proto: str
65
+
66
+
67
+ _PORT_PAT = re.compile(r'(?P<ip>[^:]+):(?P<from_port>\d+)->(?P<to_port>\d+)/(?P<proto>\w+)')
68
+
69
+
70
+ def parse_port(s: str) -> Port:
71
+ # '0.0.0.0:35221->22/tcp, 0.0.0.0:35220->8000/tcp'
72
+ m = check.not_none(_PORT_PAT.fullmatch(s))
73
+ return Port(
74
+ m.group('ip'),
75
+ int(m.group('from_port')),
76
+ int(m.group('to_port')),
77
+ m.group('proto'),
78
+ )
79
+
80
+
81
+ def cli_ps() -> list[PsItem]:
82
+ o = subprocess.check_output([
83
+ 'docker',
84
+ 'ps',
85
+ '--no-trunc',
86
+ '--format', '{{json .}}',
87
+ ])
88
+
89
+ ret: list[PsItem] = []
90
+ for l in o.decode().splitlines():
91
+ d = json.loads(l)
92
+ pi = msh.unmarshal(d, PsItem)
93
+ ret.append(pi)
94
+
95
+ return ret
96
+
97
+
98
+ @dc.dataclass(frozen=True)
99
+ class Inspect(lang.Final):
100
+ dc.metadata(msh.ObjectMetadata(
101
+ field_naming=msh.Naming.CAMEL,
102
+ unknown_field='x',
103
+ ))
104
+
105
+ id: str
106
+ created: datetime.datetime
107
+
108
+ x: ta.Mapping[str, ta.Any] | None = None
109
+
110
+
111
+ def cli_inspect(ids: list[str]) -> list[Inspect]:
112
+ o = subprocess.check_output(['docker', 'inspect', *ids])
113
+ return msh.unmarshal(json.loads(o.decode()), list[Inspect])
114
+
115
+
116
+ class ComposeConfig:
117
+ def __init__(
118
+ self,
119
+ prefix: str,
120
+ *,
121
+ compose_path: str | None = None,
122
+ ) -> None:
123
+ super().__init__()
124
+
125
+ self._prefix = prefix
126
+ self._compose_path = compose_path
127
+
128
+ @lang.cached_function
129
+ def get_config(self) -> ta.Mapping[str, ta.Any]:
130
+ with open(check.not_none(self._compose_path), 'r') as f:
131
+ buf = f.read()
132
+ dct = yaml.safe_load(buf)
133
+
134
+ ret = {}
135
+ for n, c in dct['services'].items():
136
+ check.state(n.startswith(self._prefix))
137
+ ret[n[len(self._prefix):]] = c
138
+
139
+ return ret
140
+
141
+
142
+ def timebomb_payload(delay_s: int | float, name: str = 'omlish-timebomb') -> str:
143
+ return (
144
+ '('
145
+ f'echo {shlex.quote(name)} && '
146
+ f'sleep {delay_s:g} && '
147
+ 'sh -c \'killall5 -9 -o $PPID -o $$ ; kill 1\''
148
+ ') &'
149
+ )