omlish 0.0.0.dev6__py3-none-any.whl → 0.0.0.dev8__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 (108) 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 +121 -0
  50. omlish/lite/marshal.py +318 -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/__init__.py +13 -9
  57. omlish/logs/configs.py +17 -22
  58. omlish/logs/formatters.py +3 -48
  59. omlish/marshal/__init__.py +28 -0
  60. omlish/marshal/any.py +5 -5
  61. omlish/marshal/base.py +27 -11
  62. omlish/marshal/base64.py +24 -9
  63. omlish/marshal/dataclasses.py +34 -28
  64. omlish/marshal/datetimes.py +74 -18
  65. omlish/marshal/enums.py +14 -8
  66. omlish/marshal/exceptions.py +11 -1
  67. omlish/marshal/factories.py +59 -74
  68. omlish/marshal/forbidden.py +35 -0
  69. omlish/marshal/global_.py +11 -4
  70. omlish/marshal/iterables.py +21 -24
  71. omlish/marshal/mappings.py +23 -26
  72. omlish/marshal/numbers.py +51 -0
  73. omlish/marshal/optionals.py +11 -12
  74. omlish/marshal/polymorphism.py +86 -21
  75. omlish/marshal/primitives.py +4 -5
  76. omlish/marshal/standard.py +13 -8
  77. omlish/marshal/uuids.py +4 -5
  78. omlish/matchfns.py +218 -0
  79. omlish/os.py +64 -0
  80. omlish/reflect/__init__.py +39 -0
  81. omlish/reflect/isinstance.py +38 -0
  82. omlish/reflect/ops.py +84 -0
  83. omlish/reflect/subst.py +110 -0
  84. omlish/reflect/types.py +275 -0
  85. omlish/secrets/__init__.py +18 -2
  86. omlish/secrets/crypto.py +132 -0
  87. omlish/secrets/marshal.py +36 -7
  88. omlish/secrets/openssl.py +207 -0
  89. omlish/secrets/secrets.py +260 -8
  90. omlish/secrets/subprocesses.py +42 -0
  91. omlish/sql/dbs.py +6 -5
  92. omlish/sql/exprs.py +12 -0
  93. omlish/sql/secrets.py +10 -0
  94. omlish/term.py +1 -1
  95. omlish/testing/pytest/plugins/switches.py +54 -19
  96. omlish/text/glyphsplit.py +5 -0
  97. omlish-0.0.0.dev8.dist-info/METADATA +50 -0
  98. {omlish-0.0.0.dev6.dist-info → omlish-0.0.0.dev8.dist-info}/RECORD +105 -78
  99. {omlish-0.0.0.dev6.dist-info → omlish-0.0.0.dev8.dist-info}/WHEEL +1 -1
  100. omlish/logs/filters.py +0 -11
  101. omlish/reflect.py +0 -470
  102. omlish-0.0.0.dev6.dist-info/METADATA +0 -34
  103. /omlish/{asyncs/futures.py → concurrent.py} +0 -0
  104. /omlish/{serde → formats}/__init__.py +0 -0
  105. /omlish/{serde → formats}/json.py +0 -0
  106. /omlish/{serde → formats}/props.py +0 -0
  107. {omlish-0.0.0.dev6.dist-info → omlish-0.0.0.dev8.dist-info}/LICENSE +0 -0
  108. {omlish-0.0.0.dev6.dist-info → omlish-0.0.0.dev8.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,207 @@
1
+ """
2
+ TODO:
3
+ - LibreSSL supports aes-256-gcm: https://crypto.stackexchange.com/a/76178
4
+ """
5
+ import hashlib
6
+ import secrets
7
+ import subprocess
8
+ import typing as ta
9
+
10
+ from .. import lang
11
+ from .crypto import Crypto
12
+ from .crypto import DecryptionError
13
+ from .crypto import EncryptionError
14
+ from .subprocesses import SubprocessFileInputMethod
15
+ from .subprocesses import pipe_fd_subprocess_file_input # noqa
16
+ from .subprocesses import temp_subprocess_file_input # noqa
17
+
18
+
19
+ if ta.TYPE_CHECKING:
20
+ from cryptography.hazmat.primitives import ciphers as cry_ciphs
21
+ from cryptography.hazmat.primitives.ciphers import algorithms as cry_algs
22
+ from cryptography.hazmat.primitives.ciphers import modes as cry_modes
23
+
24
+ else:
25
+ cry_ciphs = lang.proxy_import('cryptography.hazmat.primitives.ciphers')
26
+ cry_algs = lang.proxy_import('cryptography.hazmat.primitives.ciphers.algorithms')
27
+ cry_modes = lang.proxy_import('cryptography.hazmat.primitives.ciphers.modes')
28
+
29
+
30
+ ##
31
+
32
+
33
+ DEFAULT_KEY_SIZE = 64
34
+
35
+
36
+ def generate_key(self, sz: int = DEFAULT_KEY_SIZE) -> bytes:
37
+ # !! https://docs.openssl.org/3.0/man7/passphrase-encoding/
38
+ # Must not contain null bytes!
39
+ return secrets.token_hex(sz).encode('ascii')
40
+
41
+
42
+ ##
43
+
44
+
45
+ class OpensslAes265CbcCrypto(Crypto):
46
+ """
47
+ !!! https://docs.openssl.org/3.0/man7/passphrase-encoding/
48
+ https://cryptography.io/en/latest/hazmat/primitives/symmetric-encryption/#cryptography.hazmat.primitives.ciphers.Cipher
49
+ https://stackoverflow.com/questions/16761458/how-to-decrypt-openssl-aes-encrypted-files-in-python
50
+ https://github.com/openssl/openssl/blob/3c1713aeed4dc7d1ac25e9e365b8bd98afead638/apps/enc.c#L555-L573
51
+ """
52
+
53
+ def __init__(
54
+ self,
55
+ *,
56
+ iters: int = 10_000,
57
+ ) -> None:
58
+ super().__init__()
59
+ self._iters = iters
60
+
61
+ def generate_key(self, sz: int = DEFAULT_KEY_SIZE) -> bytes:
62
+ # This actually can handle null bytes, but we don't generate keys with them for compatibility.
63
+ return generate_key(sz)
64
+
65
+ block_size = 16
66
+ key_length = 32
67
+ iv_length = 16
68
+ salt_length = 8
69
+ prefix = b'Salted__'
70
+
71
+ def _make_cipher(self, key: bytes, salt: bytes) -> 'cry_ciphs.Cipher':
72
+ dk = hashlib.pbkdf2_hmac(
73
+ 'sha256',
74
+ key,
75
+ salt,
76
+ self._iters,
77
+ self.key_length + self.iv_length,
78
+ )
79
+
80
+ return cry_ciphs.Cipher(
81
+ cry_algs.AES256(dk[:self.key_length]),
82
+ cry_modes.CBC(dk[self.key_length:]),
83
+ )
84
+
85
+ def encrypt(self, data: bytes, key: bytes, *, salt: bytes | None = None) -> bytes:
86
+ if salt is None:
87
+ salt = secrets.token_bytes(self.salt_length)
88
+ elif len(salt) != self.salt_length:
89
+ raise EncryptionError('bad salt length')
90
+
91
+ last_byte = self.block_size - (len(data) % self.block_size)
92
+ raw = data + bytes([last_byte] * last_byte)
93
+
94
+ cipher = self._make_cipher(key, salt)
95
+
96
+ encryptor = cipher.encryptor()
97
+ enc = encryptor.update(raw) + encryptor.finalize()
98
+
99
+ return b''.join([
100
+ self.prefix,
101
+ salt,
102
+ enc,
103
+ ])
104
+
105
+ def decrypt(self, data: bytes, key: bytes) -> bytes:
106
+ if not data.startswith(self.prefix):
107
+ raise DecryptionError('bad prefix')
108
+
109
+ salt = data[len(self.prefix):self.block_size]
110
+ cipher = self._make_cipher(key, salt)
111
+
112
+ decryptor = cipher.decryptor()
113
+ dec = decryptor.update(data[self.block_size:]) + decryptor.finalize()
114
+
115
+ last_byte = dec[-1]
116
+ if last_byte > self.block_size:
117
+ raise DecryptionError('bad padding')
118
+
119
+ return dec[:-last_byte]
120
+
121
+
122
+ ##
123
+
124
+
125
+ class OpensslSubprocessAes256CbcCrypto(Crypto):
126
+ def __init__(
127
+ self,
128
+ *,
129
+ cmd: ta.Sequence[str] = ('openssl',),
130
+ timeout: float = 5.,
131
+ file_input: SubprocessFileInputMethod = temp_subprocess_file_input,
132
+ ) -> None:
133
+ super().__init__()
134
+ self._cmd = cmd
135
+ self._timeout = timeout
136
+ self._file_input = file_input
137
+
138
+ def generate_key(self, sz: int = DEFAULT_KEY_SIZE) -> bytes:
139
+ return generate_key(sz)
140
+
141
+ def encrypt(self, data: bytes, key: bytes) -> bytes:
142
+ # !! https://docs.openssl.org/3.0/man7/passphrase-encoding/
143
+ # Must not contain null bytes!
144
+ if 0 in key:
145
+ raise Exception('invalid key')
146
+ with self._file_input(key) as fi:
147
+ proc = subprocess.Popen(
148
+ [
149
+ *self._cmd,
150
+ 'aes-256-cbc',
151
+
152
+ '-e',
153
+
154
+ '-pbkdf2',
155
+ '-salt',
156
+ '-iter', '10000',
157
+
158
+ '-in', '-',
159
+ '-out', '-',
160
+ '-kfile', fi.file_path,
161
+ ],
162
+ stdin=subprocess.PIPE,
163
+ stdout=subprocess.PIPE,
164
+ stderr=subprocess.PIPE,
165
+ pass_fds=[*fi.pass_fds],
166
+ )
167
+ out, err = proc.communicate(
168
+ data,
169
+ timeout=self._timeout,
170
+ )
171
+ if proc.returncode != 0:
172
+ raise EncryptionError
173
+ return out
174
+
175
+ def decrypt(self, data: bytes, key: bytes) -> bytes:
176
+ # !! https://docs.openssl.org/3.0/man7/passphrase-encoding/
177
+ # Must not contain null bytes!
178
+ if 0 in key:
179
+ raise Exception('invalid key')
180
+ with self._file_input(key) as fi:
181
+ proc = subprocess.Popen(
182
+ [
183
+ *self._cmd,
184
+ 'aes-256-cbc',
185
+
186
+ '-d',
187
+
188
+ '-pbkdf2',
189
+ '-salt',
190
+ '-iter', '10000',
191
+
192
+ '-in', '-',
193
+ '-out', '-',
194
+ '-kfile', fi.file_path,
195
+ ],
196
+ stdin=subprocess.PIPE,
197
+ stdout=subprocess.PIPE,
198
+ stderr=subprocess.PIPE,
199
+ pass_fds=[*fi.pass_fds],
200
+ )
201
+ out, err = proc.communicate(
202
+ data,
203
+ timeout=self._timeout,
204
+ )
205
+ if proc.returncode != 0:
206
+ raise DecryptionError
207
+ return out
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: