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.
- omlish/__about__.py +109 -5
- omlish/__init__.py +0 -8
- omlish/asyncs/__init__.py +0 -9
- omlish/asyncs/anyio.py +40 -0
- omlish/bootstrap.py +737 -0
- omlish/check.py +1 -1
- omlish/collections/__init__.py +4 -0
- omlish/collections/exceptions.py +2 -0
- omlish/collections/utils.py +38 -9
- omlish/configs/strings.py +2 -0
- omlish/dataclasses/__init__.py +7 -0
- omlish/dataclasses/impl/descriptors.py +95 -0
- omlish/dataclasses/impl/reflect.py +1 -1
- omlish/dataclasses/utils.py +23 -0
- omlish/{lang/datetimes.py → datetimes.py} +8 -4
- omlish/diag/procfs.py +1 -1
- omlish/diag/threads.py +131 -48
- omlish/docker.py +16 -1
- omlish/fnpairs.py +0 -4
- omlish/{serde → formats}/dotenv.py +3 -0
- omlish/{serde → formats}/yaml.py +2 -2
- omlish/graphs/trees.py +1 -1
- omlish/http/consts.py +6 -0
- omlish/http/sessions.py +2 -2
- omlish/inject/__init__.py +4 -0
- omlish/inject/binder.py +3 -3
- omlish/inject/elements.py +1 -1
- omlish/inject/impl/injector.py +57 -27
- omlish/inject/impl/origins.py +2 -0
- omlish/inject/origins.py +3 -0
- omlish/inject/utils.py +18 -0
- omlish/iterators.py +69 -2
- omlish/lang/__init__.py +16 -7
- omlish/lang/classes/restrict.py +10 -0
- omlish/lang/contextmanagers.py +1 -1
- omlish/lang/descriptors.py +3 -3
- omlish/lang/imports.py +67 -0
- omlish/lang/iterables.py +40 -0
- omlish/lang/maybes.py +3 -0
- omlish/lang/objects.py +38 -0
- omlish/lang/strings.py +25 -0
- omlish/lang/sys.py +9 -0
- omlish/lang/typing.py +37 -0
- omlish/lite/__init__.py +1 -0
- omlish/lite/cached.py +18 -0
- omlish/lite/check.py +29 -0
- omlish/lite/contextmanagers.py +18 -0
- omlish/lite/json.py +30 -0
- omlish/lite/logs.py +52 -0
- omlish/lite/marshal.py +316 -0
- omlish/lite/reflect.py +49 -0
- omlish/lite/runtime.py +18 -0
- omlish/lite/secrets.py +19 -0
- omlish/lite/strings.py +25 -0
- omlish/lite/subprocesses.py +112 -0
- omlish/logs/configs.py +15 -2
- omlish/logs/formatters.py +7 -2
- omlish/marshal/__init__.py +28 -0
- omlish/marshal/any.py +5 -5
- omlish/marshal/base.py +27 -11
- omlish/marshal/base64.py +24 -9
- omlish/marshal/dataclasses.py +34 -28
- omlish/marshal/datetimes.py +74 -18
- omlish/marshal/enums.py +14 -8
- omlish/marshal/exceptions.py +11 -1
- omlish/marshal/factories.py +59 -74
- omlish/marshal/forbidden.py +35 -0
- omlish/marshal/global_.py +11 -4
- omlish/marshal/iterables.py +21 -24
- omlish/marshal/mappings.py +23 -26
- omlish/marshal/numbers.py +51 -0
- omlish/marshal/optionals.py +11 -12
- omlish/marshal/polymorphism.py +86 -21
- omlish/marshal/primitives.py +4 -5
- omlish/marshal/standard.py +13 -8
- omlish/marshal/uuids.py +4 -5
- omlish/matchfns.py +218 -0
- omlish/os.py +64 -0
- omlish/reflect/__init__.py +39 -0
- omlish/reflect/isinstance.py +38 -0
- omlish/reflect/ops.py +84 -0
- omlish/reflect/subst.py +110 -0
- omlish/reflect/types.py +275 -0
- omlish/secrets/__init__.py +18 -2
- omlish/secrets/crypto.py +132 -0
- omlish/secrets/marshal.py +36 -7
- omlish/secrets/openssl.py +207 -0
- omlish/secrets/secrets.py +260 -8
- omlish/secrets/subprocesses.py +42 -0
- omlish/sql/dbs.py +6 -5
- omlish/sql/exprs.py +12 -0
- omlish/sql/secrets.py +10 -0
- omlish/term.py +1 -1
- omlish/testing/pytest/plugins/switches.py +54 -19
- omlish/text/glyphsplit.py +5 -0
- omlish-0.0.0.dev7.dist-info/METADATA +50 -0
- {omlish-0.0.0.dev6.dist-info → omlish-0.0.0.dev7.dist-info}/RECORD +104 -76
- {omlish-0.0.0.dev6.dist-info → omlish-0.0.0.dev7.dist-info}/WHEEL +1 -1
- omlish/reflect.py +0 -470
- omlish-0.0.0.dev6.dist-info/METADATA +0 -34
- /omlish/{asyncs/futures.py → concurrent.py} +0 -0
- /omlish/{serde → formats}/__init__.py +0 -0
- /omlish/{serde → formats}/json.py +0 -0
- /omlish/{serde → formats}/props.py +0 -0
- {omlish-0.0.0.dev6.dist-info → omlish-0.0.0.dev7.dist-info}/LICENSE +0 -0
- {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
|
|
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) ->
|
|
21
|
-
if isinstance(obj,
|
|
84
|
+
def fix(self, obj: str | SecretRef | Secret) -> Secret:
|
|
85
|
+
if isinstance(obj, Secret):
|
|
22
86
|
return obj
|
|
23
|
-
elif isinstance(obj,
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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
omlish/term.py
CHANGED
|
@@ -16,10 +16,21 @@ from ._registry import register
|
|
|
16
16
|
Configable = pytest.FixtureRequest | pytest.Config
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
SWITCHES =
|
|
20
|
-
'
|
|
21
|
-
'
|
|
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,
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
+
|