omlish 0.0.0.dev6__py3-none-any.whl → 0.0.0.dev7__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 (106) hide show
  1. omlish/__about__.py +109 -5
  2. omlish/__init__.py +0 -8
  3. omlish/asyncs/__init__.py +0 -9
  4. omlish/asyncs/anyio.py +40 -0
  5. omlish/bootstrap.py +737 -0
  6. omlish/check.py +1 -1
  7. omlish/collections/__init__.py +4 -0
  8. omlish/collections/exceptions.py +2 -0
  9. omlish/collections/utils.py +38 -9
  10. omlish/configs/strings.py +2 -0
  11. omlish/dataclasses/__init__.py +7 -0
  12. omlish/dataclasses/impl/descriptors.py +95 -0
  13. omlish/dataclasses/impl/reflect.py +1 -1
  14. omlish/dataclasses/utils.py +23 -0
  15. omlish/{lang/datetimes.py → datetimes.py} +8 -4
  16. omlish/diag/procfs.py +1 -1
  17. omlish/diag/threads.py +131 -48
  18. omlish/docker.py +16 -1
  19. omlish/fnpairs.py +0 -4
  20. omlish/{serde → formats}/dotenv.py +3 -0
  21. omlish/{serde → formats}/yaml.py +2 -2
  22. omlish/graphs/trees.py +1 -1
  23. omlish/http/consts.py +6 -0
  24. omlish/http/sessions.py +2 -2
  25. omlish/inject/__init__.py +4 -0
  26. omlish/inject/binder.py +3 -3
  27. omlish/inject/elements.py +1 -1
  28. omlish/inject/impl/injector.py +57 -27
  29. omlish/inject/impl/origins.py +2 -0
  30. omlish/inject/origins.py +3 -0
  31. omlish/inject/utils.py +18 -0
  32. omlish/iterators.py +69 -2
  33. omlish/lang/__init__.py +16 -7
  34. omlish/lang/classes/restrict.py +10 -0
  35. omlish/lang/contextmanagers.py +1 -1
  36. omlish/lang/descriptors.py +3 -3
  37. omlish/lang/imports.py +67 -0
  38. omlish/lang/iterables.py +40 -0
  39. omlish/lang/maybes.py +3 -0
  40. omlish/lang/objects.py +38 -0
  41. omlish/lang/strings.py +25 -0
  42. omlish/lang/sys.py +9 -0
  43. omlish/lang/typing.py +37 -0
  44. omlish/lite/__init__.py +1 -0
  45. omlish/lite/cached.py +18 -0
  46. omlish/lite/check.py +29 -0
  47. omlish/lite/contextmanagers.py +18 -0
  48. omlish/lite/json.py +30 -0
  49. omlish/lite/logs.py +52 -0
  50. omlish/lite/marshal.py +316 -0
  51. omlish/lite/reflect.py +49 -0
  52. omlish/lite/runtime.py +18 -0
  53. omlish/lite/secrets.py +19 -0
  54. omlish/lite/strings.py +25 -0
  55. omlish/lite/subprocesses.py +112 -0
  56. omlish/logs/configs.py +15 -2
  57. omlish/logs/formatters.py +7 -2
  58. omlish/marshal/__init__.py +28 -0
  59. omlish/marshal/any.py +5 -5
  60. omlish/marshal/base.py +27 -11
  61. omlish/marshal/base64.py +24 -9
  62. omlish/marshal/dataclasses.py +34 -28
  63. omlish/marshal/datetimes.py +74 -18
  64. omlish/marshal/enums.py +14 -8
  65. omlish/marshal/exceptions.py +11 -1
  66. omlish/marshal/factories.py +59 -74
  67. omlish/marshal/forbidden.py +35 -0
  68. omlish/marshal/global_.py +11 -4
  69. omlish/marshal/iterables.py +21 -24
  70. omlish/marshal/mappings.py +23 -26
  71. omlish/marshal/numbers.py +51 -0
  72. omlish/marshal/optionals.py +11 -12
  73. omlish/marshal/polymorphism.py +86 -21
  74. omlish/marshal/primitives.py +4 -5
  75. omlish/marshal/standard.py +13 -8
  76. omlish/marshal/uuids.py +4 -5
  77. omlish/matchfns.py +218 -0
  78. omlish/os.py +64 -0
  79. omlish/reflect/__init__.py +39 -0
  80. omlish/reflect/isinstance.py +38 -0
  81. omlish/reflect/ops.py +84 -0
  82. omlish/reflect/subst.py +110 -0
  83. omlish/reflect/types.py +275 -0
  84. omlish/secrets/__init__.py +18 -2
  85. omlish/secrets/crypto.py +132 -0
  86. omlish/secrets/marshal.py +36 -7
  87. omlish/secrets/openssl.py +207 -0
  88. omlish/secrets/secrets.py +260 -8
  89. omlish/secrets/subprocesses.py +42 -0
  90. omlish/sql/dbs.py +6 -5
  91. omlish/sql/exprs.py +12 -0
  92. omlish/sql/secrets.py +10 -0
  93. omlish/term.py +1 -1
  94. omlish/testing/pytest/plugins/switches.py +54 -19
  95. omlish/text/glyphsplit.py +5 -0
  96. omlish-0.0.0.dev7.dist-info/METADATA +50 -0
  97. {omlish-0.0.0.dev6.dist-info → omlish-0.0.0.dev7.dist-info}/RECORD +104 -76
  98. {omlish-0.0.0.dev6.dist-info → omlish-0.0.0.dev7.dist-info}/WHEEL +1 -1
  99. omlish/reflect.py +0 -470
  100. omlish-0.0.0.dev6.dist-info/METADATA +0 -34
  101. /omlish/{asyncs/futures.py → concurrent.py} +0 -0
  102. /omlish/{serde → formats}/__init__.py +0 -0
  103. /omlish/{serde → formats}/json.py +0 -0
  104. /omlish/{serde → formats}/props.py +0 -0
  105. {omlish-0.0.0.dev6.dist-info → omlish-0.0.0.dev7.dist-info}/LICENSE +0 -0
  106. {omlish-0.0.0.dev6.dist-info → omlish-0.0.0.dev7.dist-info}/top_level.txt +0 -0
omlish/secrets/secrets.py CHANGED
@@ -1,47 +1,299 @@
1
+ """
2
+ TODO:
3
+ - SqlFunctionSecrets (in .sql?)
4
+ - crypto is just Transformed, bound with a key
5
+ - crypto key in env + values in file?
6
+ - Secret:
7
+ - hold ref to Secret, and key
8
+ - time of retrieval
9
+ - logs accesses
10
+ - types? ssh / url / pw / basicauthtoken / tls / str
11
+ """
1
12
  import abc
13
+ import collections
14
+ import logging
15
+ import os
16
+ import sys
17
+ import time
18
+ import types # noqa
2
19
  import typing as ta
3
20
 
4
21
  from .. import dataclasses as dc
5
22
  from .. import lang
6
23
 
7
24
 
25
+ log = logging.getLogger(__name__)
26
+
27
+
28
+ ##
29
+
30
+
31
+ class Secret(lang.NotPicklable, lang.Final):
32
+ _VALUE_ATTR = '__secret_value__'
33
+
34
+ def __init__(self, *, key: str | None, value: str) -> None:
35
+ super().__init__()
36
+ self._key = key
37
+ setattr(self, self._VALUE_ATTR, lambda: value)
38
+
39
+ def __repr__(self) -> str:
40
+ return f'Secret<{self._key or ""}>'
41
+
42
+ def __str__(self) -> ta.NoReturn:
43
+ raise TypeError
44
+
45
+ def reveal(self) -> str:
46
+ return getattr(self, self._VALUE_ATTR)()
47
+
48
+
8
49
  ##
9
50
 
10
51
 
11
52
  @dc.dataclass(frozen=True)
12
- class Secret:
53
+ class SecretRef:
13
54
  key: str
14
55
 
15
56
 
57
+ SecretRefOrStr: ta.TypeAlias = SecretRef | str
58
+
59
+
60
+ def secret_repr(o: SecretRefOrStr | None) -> str | None:
61
+ if isinstance(o, str):
62
+ return '...'
63
+ elif isinstance(o, SecretRef):
64
+ return repr(o)
65
+ elif o is None:
66
+ return None
67
+ else:
68
+ raise TypeError(o)
69
+
70
+
71
+ @dc.field_modifier
72
+ def secret_field(f: dc.Field) -> dc.Field:
73
+ return dc.update_field_extras(
74
+ f,
75
+ repr_fn=secret_repr,
76
+ unless_non_default=True,
77
+ )
78
+
79
+
16
80
  ##
17
81
 
18
82
 
19
83
  class Secrets(lang.Abstract):
20
- def fix(self, obj: str | Secret) -> str:
21
- if isinstance(obj, str):
84
+ def fix(self, obj: str | SecretRef | Secret) -> Secret:
85
+ if isinstance(obj, Secret):
22
86
  return obj
23
- elif isinstance(obj, Secret):
87
+ elif isinstance(obj, str):
88
+ return Secret(key=None, value=obj)
89
+ elif isinstance(obj, SecretRef):
24
90
  return self.get(obj.key)
25
91
  else:
26
92
  raise TypeError(obj)
27
93
 
94
+ def get(self, key: str) -> Secret:
95
+ try:
96
+ raw = self._get_raw(key) # noqa
97
+ except KeyError: # noqa
98
+ raise
99
+ else:
100
+ return Secret(key=key, value=raw)
101
+
28
102
  @abc.abstractmethod
29
- def get(self, key: str) -> str:
103
+ def _get_raw(self, key: str) -> str:
30
104
  raise NotImplementedError
31
105
 
32
106
 
107
+ ##
108
+
109
+
33
110
  class EmptySecrets(Secrets):
34
- def get(self, key: str) -> str:
111
+ def _get_raw(self, key: str) -> str:
35
112
  raise KeyError(key)
36
113
 
37
114
 
38
115
  EMPTY_SECRETS = EmptySecrets()
39
116
 
40
117
 
41
- class SimpleSecrets(Secrets):
118
+ ##
119
+
120
+
121
+ class MappingSecrets(Secrets):
42
122
  def __init__(self, dct: ta.Mapping[str, str]) -> None:
43
123
  super().__init__()
44
124
  self._dct = dct
45
125
 
46
- def get(self, key: str) -> str:
126
+ def __repr__(self) -> str:
127
+ return f'{self.__class__.__name__}({{{", ".join(map(repr, self._dct.keys()))}}})'
128
+
129
+ def _get_raw(self, key: str) -> str:
47
130
  return self._dct[key]
131
+
132
+
133
+ ##
134
+
135
+
136
+ @dc.dataclass(frozen=True)
137
+ class FnSecrets(Secrets):
138
+ fn: ta.Callable[[str], str]
139
+
140
+ def _get_raw(self, key: str) -> str:
141
+ return self.fn(key)
142
+
143
+
144
+ ##
145
+
146
+
147
+ @dc.dataclass(frozen=True)
148
+ class TransformedSecrets(Secrets):
149
+ fn: ta.Callable[[str], str]
150
+ child: Secrets
151
+
152
+ def _get_raw(self, key: str) -> str:
153
+ # FIXME: hm..
154
+ return self.fn(self.child._get_raw(key)) # noqa
155
+
156
+
157
+ ##
158
+
159
+
160
+ class CachingSecrets(Secrets):
161
+ def __init__(
162
+ self,
163
+ child: Secrets,
164
+ *,
165
+ ttl_s: float | None = None,
166
+ clock: ta.Callable[..., float] = time.time,
167
+ ) -> None:
168
+ super().__init__()
169
+ self._child = child
170
+ self._dct: dict[str, str] = {}
171
+ self._ttl_s = ttl_s
172
+ self._clock = clock
173
+ self._deque: collections.deque[tuple[str, float]] = collections.deque()
174
+
175
+ def __repr__(self) -> str:
176
+ return f'{self.__class__.__name__}({{{", ".join(map(repr, self._dct.keys()))}}})'
177
+
178
+ def evict(self) -> None:
179
+ now = self._clock()
180
+ while self._deque:
181
+ k, dl = self._deque[0]
182
+ if now < dl:
183
+ break
184
+ del self._dct[k]
185
+ self._deque.popleft()
186
+
187
+ def _get_raw(self, key: str) -> str:
188
+ self.evict()
189
+ try:
190
+ return self._dct[key]
191
+ except KeyError:
192
+ pass
193
+ out = self._child._get_raw(key) # noqa
194
+ self._dct[key] = out
195
+ if self._ttl_s is not None:
196
+ dl = self._clock() + self._ttl_s
197
+ self._deque.append((key, dl))
198
+ return out
199
+
200
+
201
+ ##
202
+
203
+
204
+ class CompositeSecrets(Secrets):
205
+ def __init__(self, *children: Secrets) -> None:
206
+ super().__init__()
207
+ self._children = children
208
+
209
+ def _get_raw(self, key: str) -> str:
210
+ for c in self._children:
211
+ try:
212
+ return c._get_raw(key) # noqa
213
+ except KeyError:
214
+ pass
215
+ raise KeyError(key)
216
+
217
+
218
+ ##
219
+
220
+
221
+ class LoggingSecrets(Secrets):
222
+ def __init__(
223
+ self,
224
+ child: Secrets,
225
+ *,
226
+ log: logging.Logger | None = None, # noqa
227
+ ) -> None:
228
+ super().__init__()
229
+ self._child = child
230
+ self._log = log if log is not None else globals()['log']
231
+
232
+ IGNORE_PACKAGES: ta.ClassVar[ta.AbstractSet[str]] = {
233
+ __package__,
234
+ }
235
+
236
+ def _get_caller_str(self, n: int = 3) -> str:
237
+ l: list[str] = []
238
+ f: types.FrameType | None = sys._getframe(2) # noqa
239
+ while f is not None and len(l) < n:
240
+ try:
241
+ pkg = f.f_globals['__package__']
242
+ except KeyError:
243
+ pkg = None
244
+ else:
245
+ if pkg in self.IGNORE_PACKAGES:
246
+ f = f.f_back
247
+ continue
248
+ if (fn := f.f_code.co_filename):
249
+ l.append(f'{fn}:{f.f_lineno}')
250
+ else:
251
+ l.append(pkg)
252
+ f = f.f_back
253
+ return ', '.join(l)
254
+
255
+ def _get_raw(self, key: str) -> str:
256
+ cs = self._get_caller_str()
257
+ self._log.info('Attempting to access secret: %s, %s', key, cs)
258
+ try:
259
+ ret = self._child._get_raw(key) # noqa
260
+ except KeyError:
261
+ self._log.info('Failed to access secret: %s, %s', key, cs)
262
+ raise
263
+ else:
264
+ self._log.info('Successfully accessed secret: %s, %s', key, cs)
265
+ return ret
266
+
267
+
268
+ ##
269
+
270
+
271
+ class EnvVarSecrets(Secrets):
272
+ def __init__(
273
+ self,
274
+ *,
275
+ env: ta.MutableMapping[str, str] | None = None,
276
+ upcase: bool = False,
277
+ prefix: str | None = None,
278
+ pop: bool = False,
279
+ ) -> None:
280
+ super().__init__()
281
+ self._env = env
282
+ self._upcase = upcase
283
+ self._prefix = prefix
284
+ self._pop = pop
285
+
286
+ def _get_raw(self, key: str) -> str:
287
+ ekey = key
288
+ if self._upcase:
289
+ ekey = ekey.upper()
290
+ if self._prefix is not None:
291
+ ekey = self._prefix + ekey
292
+ if self._env is not None:
293
+ dct = self._env
294
+ else:
295
+ dct = os.environ
296
+ if self._pop:
297
+ return dct.pop(ekey)
298
+ else:
299
+ return dct[ekey]
@@ -0,0 +1,42 @@
1
+ """
2
+ FIXME:
3
+ - macos pipe size lol, and just like checking at all
4
+ """
5
+ import contextlib
6
+ import fcntl
7
+ import os
8
+ import tempfile
9
+ import typing as ta
10
+
11
+
12
+ class SubprocessFileInput(ta.NamedTuple):
13
+ file_path: str
14
+ pass_fds: ta.Sequence[int]
15
+
16
+
17
+ SubprocessFileInputMethod: ta.TypeAlias = ta.Callable[[bytes], ta.ContextManager[SubprocessFileInput]]
18
+
19
+
20
+ @contextlib.contextmanager
21
+ def temp_subprocess_file_input(buf: bytes) -> ta.Iterator[SubprocessFileInput]:
22
+ with tempfile.NamedTemporaryFile(delete=True) as kf:
23
+ kf.write(buf)
24
+ kf.flush()
25
+ yield SubprocessFileInput(kf.name, [])
26
+
27
+
28
+ @contextlib.contextmanager
29
+ def pipe_fd_subprocess_file_input(buf: bytes) -> ta.Iterator[SubprocessFileInput]:
30
+ rfd, wfd = os.pipe()
31
+ closed_wfd = False
32
+ try:
33
+ if hasattr(fcntl, 'F_SETPIPE_SZ'):
34
+ fcntl.fcntl(wfd, fcntl.F_SETPIPE_SZ, max(len(buf), 0x1000))
35
+ os.write(wfd, buf)
36
+ os.close(wfd)
37
+ closed_wfd = True
38
+ yield SubprocessFileInput(f'/dev/fd/{rfd}', [rfd])
39
+ finally:
40
+ if not closed_wfd:
41
+ os.close(wfd)
42
+ os.close(rfd)
omlish/sql/dbs.py CHANGED
@@ -3,13 +3,14 @@ import urllib.parse
3
3
 
4
4
  from .. import dataclasses as dc
5
5
  from .. import lang
6
+ from .. import secrets as sec
6
7
 
7
8
 
8
9
  ##
9
10
 
10
11
 
11
12
  @dc.dataclass(frozen=True, kw_only=True)
12
- class DbType:
13
+ class DbType(lang.Final):
13
14
  name: str
14
15
  dialect_name: str
15
16
 
@@ -38,13 +39,13 @@ class DbTypes(lang.Namespace, lang.Final):
38
39
  ##
39
40
 
40
41
 
41
- class DbLoc(lang.Abstract):
42
+ class DbLoc(lang.Abstract, lang.Sealed):
42
43
  pass
43
44
 
44
45
 
45
46
  @dc.dataclass(frozen=True)
46
47
  class UrlDbLoc(DbLoc, lang.Final):
47
- url: str
48
+ url: sec.SecretRefOrStr = dc.xfield() | sec.secret_field
48
49
 
49
50
 
50
51
  @dc.dataclass(frozen=True)
@@ -53,14 +54,14 @@ class HostDbLoc(DbLoc, lang.Final):
53
54
  port: int | None = None
54
55
 
55
56
  username: str | None = None
56
- password: str | None = dc.xfield(default=None, repr_fn=lambda pw: '...' if pw is not None else None)
57
+ password: sec.SecretRefOrStr | None = dc.xfield(default=None) | sec.secret_field
57
58
 
58
59
 
59
60
  ##
60
61
 
61
62
 
62
63
  @dc.dataclass(frozen=True)
63
- class DbSpec:
64
+ class DbSpec(lang.Final):
64
65
  name: str
65
66
  type: DbType
66
67
  loc: DbLoc
omlish/sql/exprs.py ADDED
@@ -0,0 +1,12 @@
1
+ import sqlalchemy as sa
2
+ import sqlalchemy.ext.compiler
3
+
4
+
5
+ class paren(sa.sql.expression.UnaryExpression): # noqa
6
+ __visit_name__ = 'paren'
7
+ inherit_cache = True
8
+
9
+
10
+ @sa.ext.compiler.compiles(paren)
11
+ def _compile_paren(element, compiler, **kw):
12
+ return '(%s)' % (element.element._compiler_dispatch(compiler),) # noqa
omlish/sql/secrets.py ADDED
@@ -0,0 +1,10 @@
1
+ """
2
+ TODO:
3
+ - sync/async...
4
+ """
5
+ from .. import secrets as sec
6
+
7
+
8
+ class SqlFunctionSecrets(sec.Secrets):
9
+ def _get_raw(self, key: str) -> str:
10
+ raise NotImplementedError
omlish/term.py CHANGED
@@ -20,7 +20,7 @@ def set_title(title: str) -> str:
20
20
 
21
21
 
22
22
  def strip_ansi_codes(s: str) -> str:
23
- return re.sub(r'\033\\[([0-9]+)(;[0-9]+)*m', '', s)
23
+ return re.sub(r'\033\[([0-9]+)(;[0-9]+)*m', '', s)
24
24
 
25
25
 
26
26
  class ControlSequence:
@@ -16,10 +16,21 @@ from ._registry import register
16
16
  Configable = pytest.FixtureRequest | pytest.Config
17
17
 
18
18
 
19
- SWITCHES = col.OrderedSet([
20
- 'online',
21
- 'slow',
22
- ])
19
+ SWITCHES = {
20
+ 'docker': True,
21
+ 'online': True,
22
+ 'integration': True,
23
+ 'slow': False,
24
+ }
25
+
26
+
27
+ SwitchState: ta.TypeAlias = bool | ta.Literal['only']
28
+
29
+ SWITCH_STATE_OPT_PREFIXES: ta.Mapping[SwitchState, str] = {
30
+ True: '--',
31
+ False: '--no-',
32
+ 'only': '--only-',
33
+ }
23
34
 
24
35
 
25
36
  def _get_obj_config(obj: Configable) -> pytest.Config:
@@ -42,29 +53,53 @@ def skip_if_disabled(obj: Configable | None, name: str) -> None:
42
53
  pytest.skip(f'{name} disabled')
43
54
 
44
55
 
45
- def get_switches(obj: Configable) -> ta.Mapping[str, bool]:
46
- return {
47
- sw: _get_obj_config(obj).getoption(f'--no-{sw}')
48
- for sw in SWITCHES
49
- }
56
+ def get_switches(obj: Configable) -> ta.Mapping[str, SwitchState]:
57
+ ret: dict[str, SwitchState] = {}
58
+ for sw, d in SWITCHES.items():
59
+ sts = {
60
+ st
61
+ for st, pfx in SWITCH_STATE_OPT_PREFIXES.items()
62
+ if _get_obj_config(obj).getoption(pfx + sw)
63
+ }
64
+ if sts:
65
+ if len(sts) > 1:
66
+ raise Exception(f'Multiple switches specified for {sw}')
67
+ ret[sw] = check.single(sts)
68
+ else:
69
+ ret[sw] = d
70
+ return ret
50
71
 
51
72
 
52
73
  @register
53
74
  class SwitchesPlugin:
54
75
 
76
+ def pytest_configure(self, config):
77
+ for sw in SWITCHES:
78
+ config.addinivalue_line('markers', f'{sw}: mark test as {sw}')
79
+
55
80
  def pytest_addoption(self, parser):
56
81
  for sw in SWITCHES:
57
82
  parser.addoption(f'--no-{sw}', action='store_true', default=False, help=f'disable {sw} tests')
83
+ parser.addoption(f'--{sw}', action='store_true', default=False, help=f'enables {sw} tests')
84
+ parser.addoption(f'--only-{sw}', action='store_true', default=False, help=f'enables only {sw} tests')
58
85
 
59
86
  def pytest_collection_modifyitems(self, config, items):
60
- for sw in SWITCHES:
61
- if not config.getoption(f'--no-{sw}'):
62
- continue
63
- skip = pytest.mark.skip(reason=f'omit --no-{sw} to run')
64
- for item in items:
65
- if sw in item.keywords:
66
- item.add_marker(skip)
87
+ sts = get_switches(config)
88
+ stx = col.multi_map(map(reversed, sts.items())) # type: ignore
89
+ ts, fs, onlys = (stx.get(k, ()) for k in (True, False, 'only'))
67
90
 
68
- def pytest_configure(self, config):
69
- for sw in SWITCHES:
70
- config.addinivalue_line('markers', f'{sw}: mark test as {sw}')
91
+ def process(item):
92
+ sws = {sw for sw in SWITCHES if sw in item.keywords}
93
+
94
+ if onlys:
95
+ if not any(sw in onlys for sw in sws):
96
+ item.add_marker(pytest.mark.skip(reason=f'skipping switches {sws}'))
97
+ return
98
+
99
+ else:
100
+ for sw in sws:
101
+ if sw in fs:
102
+ item.add_marker(pytest.mark.skip(reason=f'skipping switches {sw}'))
103
+
104
+ for item in items:
105
+ process(item)
omlish/text/glyphsplit.py CHANGED
@@ -1,3 +1,8 @@
1
+ """
2
+ Note: string.Formatter (and string.Template) shouldn't be ignored - if they can be used they probably should be.
3
+ - https://docs.python.org/3/library/string.html#custom-string-formatting
4
+ - https://docs.python.org/3/library/string.html#template-strings
5
+ """
1
6
  import dataclasses as dc
2
7
  import re
3
8
 
@@ -0,0 +1,50 @@
1
+ Metadata-Version: 2.1
2
+ Name: omlish
3
+ Version: 0.0.0.dev7
4
+ Summary: omlish
5
+ Author: wrmsr
6
+ License: BSD-3-Clause
7
+ Project-URL: source, https://github.com/wrmsr/omlish
8
+ Classifier: License :: OSI Approved :: BSD License
9
+ Classifier: Development Status :: 2 - Pre-Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Operating System :: POSIX
13
+ Requires-Python: >=3.12
14
+ License-File: LICENSE
15
+ Provides-Extra: async
16
+ Requires-Dist: anyio >=4.4 ; extra == 'async'
17
+ Requires-Dist: sniffio >=1.3 ; extra == 'async'
18
+ Requires-Dist: trio >=0.26 ; extra == 'async'
19
+ Requires-Dist: greenlet >=3 ; (python_version < "3.13") and extra == 'async'
20
+ Requires-Dist: trio-asyncio >=0.15 ; (python_version < "3.13") and extra == 'async'
21
+ Provides-Extra: compression
22
+ Requires-Dist: lz4 >=4 ; extra == 'compression'
23
+ Requires-Dist: zstd >=1.5 ; extra == 'compression'
24
+ Requires-Dist: python-snappy >=0.7 ; (python_version < "3.13") and extra == 'compression'
25
+ Provides-Extra: formats
26
+ Requires-Dist: orjson >3.10 ; extra == 'formats'
27
+ Requires-Dist: cloudpickle >=3 ; extra == 'formats'
28
+ Requires-Dist: pyyaml >=5 ; extra == 'formats'
29
+ Provides-Extra: http
30
+ Requires-Dist: httpx[http2] >=0.27 ; extra == 'http'
31
+ Provides-Extra: misc
32
+ Requires-Dist: jinja2 >=3.1 ; extra == 'misc'
33
+ Requires-Dist: psutil >=6 ; extra == 'misc'
34
+ Requires-Dist: wrapt >=1.14 ; extra == 'misc'
35
+ Provides-Extra: secrets
36
+ Requires-Dist: cryptography >=43 ; extra == 'secrets'
37
+ Provides-Extra: sql
38
+ Requires-Dist: pg8000 >=1.31 ; extra == 'sql'
39
+ Requires-Dist: pymysql >=1.1 ; extra == 'sql'
40
+ Requires-Dist: aiomysql >=0.2 ; extra == 'sql'
41
+ Requires-Dist: aiosqlite >=0.20 ; extra == 'sql'
42
+ Requires-Dist: sqlalchemy[asyncio] >=2 ; (python_version < "3.13") and extra == 'sql'
43
+ Requires-Dist: asyncpg >=0.29 ; (python_version < "3.13") and extra == 'sql'
44
+ Requires-Dist: sqlalchemy >=2 ; (python_version >= "3.13") and extra == 'sql'
45
+ Provides-Extra: sqlx
46
+ Requires-Dist: duckdb >=1 ; extra == 'sqlx'
47
+ Requires-Dist: sqlean.py >=3.45 ; (python_version < "3.13") and extra == 'sqlx'
48
+ Provides-Extra: testing
49
+ Requires-Dist: pytest >=8 ; extra == 'testing'
50
+