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.
- 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 +121 -0
- omlish/lite/marshal.py +318 -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/__init__.py +13 -9
- omlish/logs/configs.py +17 -22
- omlish/logs/formatters.py +3 -48
- 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.dev8.dist-info/METADATA +50 -0
- {omlish-0.0.0.dev6.dist-info → omlish-0.0.0.dev8.dist-info}/RECORD +105 -78
- {omlish-0.0.0.dev6.dist-info → omlish-0.0.0.dev8.dist-info}/WHEEL +1 -1
- omlish/logs/filters.py +0 -11
- 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.dev8.dist-info}/LICENSE +0 -0
- {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
|
|
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