omlish 0.0.0.dev5__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 +9 -9
- omlish/asyncs/anyio.py +123 -19
- omlish/asyncs/asyncio.py +23 -0
- omlish/asyncs/asyncs.py +9 -6
- omlish/asyncs/bridge.py +316 -0
- omlish/asyncs/trio_asyncio.py +7 -3
- omlish/bootstrap.py +737 -0
- omlish/check.py +1 -1
- omlish/collections/__init__.py +5 -0
- omlish/collections/exceptions.py +2 -0
- omlish/collections/identity.py +7 -0
- omlish/collections/utils.py +38 -9
- omlish/configs/strings.py +96 -0
- omlish/dataclasses/__init__.py +16 -0
- omlish/dataclasses/impl/copy.py +30 -0
- omlish/dataclasses/impl/descriptors.py +95 -0
- omlish/dataclasses/impl/exceptions.py +6 -0
- omlish/dataclasses/impl/fields.py +24 -25
- omlish/dataclasses/impl/init.py +4 -2
- omlish/dataclasses/impl/main.py +2 -0
- omlish/dataclasses/impl/reflect.py +1 -1
- omlish/dataclasses/utils.py +67 -0
- omlish/{lang/datetimes.py → datetimes.py} +8 -4
- omlish/diag/__init__.py +4 -0
- omlish/diag/procfs.py +2 -2
- omlish/{testing → diag}/pydevd.py +35 -0
- omlish/diag/threads.py +131 -48
- omlish/dispatch/_dispatch2.py +65 -0
- omlish/dispatch/_dispatch3.py +104 -0
- omlish/docker.py +16 -1
- omlish/fnpairs.py +11 -4
- omlish/formats/__init__.py +0 -0
- omlish/{configs → formats}/dotenv.py +15 -24
- omlish/{json.py → formats/json.py} +2 -1
- omlish/formats/yaml.py +223 -0
- omlish/graphs/trees.py +1 -1
- omlish/http/asgi.py +2 -1
- omlish/http/collections.py +15 -0
- omlish/http/consts.py +22 -1
- omlish/http/sessions.py +10 -3
- omlish/inject/__init__.py +49 -17
- omlish/inject/binder.py +185 -5
- omlish/inject/bindings.py +3 -36
- omlish/inject/eagers.py +2 -8
- omlish/inject/elements.py +31 -10
- omlish/inject/exceptions.py +1 -1
- omlish/inject/impl/elements.py +37 -12
- omlish/inject/impl/injector.py +72 -25
- omlish/inject/impl/inspect.py +33 -5
- omlish/inject/impl/origins.py +77 -0
- omlish/inject/impl/{private.py → privates.py} +2 -2
- omlish/inject/impl/scopes.py +6 -2
- omlish/inject/injector.py +8 -4
- omlish/inject/inspect.py +18 -0
- omlish/inject/keys.py +8 -14
- omlish/inject/listeners.py +26 -0
- omlish/inject/managed.py +76 -10
- omlish/inject/multis.py +68 -18
- omlish/inject/origins.py +30 -0
- omlish/inject/overrides.py +5 -4
- omlish/inject/{private.py → privates.py} +6 -10
- omlish/inject/providers.py +12 -85
- omlish/inject/scopes.py +13 -6
- omlish/inject/types.py +3 -1
- omlish/inject/utils.py +18 -0
- omlish/iterators.py +69 -2
- omlish/lang/__init__.py +24 -9
- omlish/lang/cached.py +2 -2
- omlish/lang/classes/restrict.py +12 -1
- omlish/lang/classes/simple.py +18 -8
- omlish/lang/contextmanagers.py +13 -4
- omlish/lang/descriptors.py +132 -1
- omlish/lang/functions.py +8 -28
- omlish/lang/imports.py +67 -0
- omlish/lang/iterables.py +60 -1
- 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 +42 -0
- omlish/lifecycles/__init__.py +34 -0
- omlish/lifecycles/abstract.py +43 -0
- omlish/lifecycles/base.py +51 -0
- omlish/lifecycles/contextmanagers.py +74 -0
- omlish/lifecycles/controller.py +116 -0
- omlish/lifecycles/manager.py +161 -0
- omlish/lifecycles/states.py +43 -0
- omlish/lifecycles/transitions.py +64 -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 +32 -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/naming.py +4 -0
- omlish/marshal/numbers.py +51 -0
- omlish/marshal/objects.py +1 -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 +23 -0
- omlish/secrets/crypto.py +132 -0
- omlish/secrets/marshal.py +70 -0
- omlish/secrets/openssl.py +207 -0
- omlish/secrets/passwords.py +120 -0
- omlish/secrets/secrets.py +299 -0
- omlish/secrets/subprocesses.py +42 -0
- omlish/sql/dbs.py +7 -6
- omlish/sql/duckdb.py +136 -0
- omlish/sql/exprs.py +12 -0
- omlish/sql/secrets.py +10 -0
- omlish/sql/sqlean.py +17 -0
- omlish/term.py +2 -2
- omlish/testing/pytest/__init__.py +3 -2
- omlish/testing/pytest/inject/harness.py +3 -3
- omlish/testing/pytest/marks.py +4 -7
- omlish/testing/pytest/plugins/__init__.py +1 -0
- omlish/testing/pytest/plugins/asyncs.py +136 -0
- omlish/testing/pytest/plugins/pydevd.py +1 -1
- omlish/testing/pytest/plugins/switches.py +54 -19
- omlish/text/glyphsplit.py +97 -0
- omlish-0.0.0.dev7.dist-info/METADATA +50 -0
- omlish-0.0.0.dev7.dist-info/RECORD +268 -0
- {omlish-0.0.0.dev5.dist-info → omlish-0.0.0.dev7.dist-info}/WHEEL +1 -1
- omlish/reflect.py +0 -355
- omlish-0.0.0.dev5.dist-info/METADATA +0 -34
- omlish-0.0.0.dev5.dist-info/RECORD +0 -212
- /omlish/{asyncs/futures.py → concurrent.py} +0 -0
- /omlish/{configs → formats}/props.py +0 -0
- {omlish-0.0.0.dev5.dist-info → omlish-0.0.0.dev7.dist-info}/LICENSE +0 -0
- {omlish-0.0.0.dev5.dist-info → omlish-0.0.0.dev7.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""
|
|
2
|
+
~> https://github.com/pallets/werkzeug/blob/7a76170c473c26685bdfa2774d083ba2386fc60f/src/werkzeug/security.py
|
|
3
|
+
"""
|
|
4
|
+
# Copyright 2007 Pallets
|
|
5
|
+
#
|
|
6
|
+
# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
|
|
7
|
+
# following conditions are met:
|
|
8
|
+
#
|
|
9
|
+
# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
|
|
10
|
+
# disclaimer.
|
|
11
|
+
#
|
|
12
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
|
|
13
|
+
# following disclaimer in the documentation and/or other materials provided with the distribution.
|
|
14
|
+
#
|
|
15
|
+
# 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
|
|
16
|
+
# products derived from this software without specific prior written permission.
|
|
17
|
+
#
|
|
18
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
|
19
|
+
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
20
|
+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
21
|
+
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
22
|
+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
|
23
|
+
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
24
|
+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
25
|
+
import hashlib
|
|
26
|
+
import hmac
|
|
27
|
+
import secrets
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
SALT_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
|
31
|
+
DEFAULT_PBKDF2_ITERATIONS = 600000
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def gen_salt(length: int) -> str:
|
|
35
|
+
if length <= 0:
|
|
36
|
+
raise ValueError('Salt length must be at least 1.')
|
|
37
|
+
|
|
38
|
+
return ''.join(secrets.choice(SALT_CHARS) for _ in range(length))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _hash_internal(
|
|
42
|
+
method: str,
|
|
43
|
+
salt: str,
|
|
44
|
+
password: str,
|
|
45
|
+
) -> tuple[str, str]:
|
|
46
|
+
method, *args = method.split(':')
|
|
47
|
+
salt_bytes = salt.encode()
|
|
48
|
+
password_bytes = password.encode()
|
|
49
|
+
|
|
50
|
+
if method == 'scrypt':
|
|
51
|
+
if not args:
|
|
52
|
+
n = 2 ** 15
|
|
53
|
+
r = 8
|
|
54
|
+
p = 1
|
|
55
|
+
else:
|
|
56
|
+
try:
|
|
57
|
+
n, r, p = map(int, args)
|
|
58
|
+
except ValueError:
|
|
59
|
+
raise ValueError("'scrypt' takes 3 arguments.") from None
|
|
60
|
+
|
|
61
|
+
maxmem = 132 * n * r * p # ideally 128, but some extra seems needed
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
hashlib.scrypt(
|
|
65
|
+
password_bytes,
|
|
66
|
+
salt=salt_bytes,
|
|
67
|
+
n=n,
|
|
68
|
+
r=r,
|
|
69
|
+
p=p,
|
|
70
|
+
maxmem=maxmem,
|
|
71
|
+
).hex(),
|
|
72
|
+
f'scrypt:{n}:{r}:{p}',
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
elif method == 'pbkdf2':
|
|
76
|
+
len_args = len(args)
|
|
77
|
+
|
|
78
|
+
if len_args == 0:
|
|
79
|
+
hash_name = 'sha256'
|
|
80
|
+
iterations = DEFAULT_PBKDF2_ITERATIONS
|
|
81
|
+
elif len_args == 1:
|
|
82
|
+
hash_name = args[0]
|
|
83
|
+
iterations = DEFAULT_PBKDF2_ITERATIONS
|
|
84
|
+
elif len_args == 2:
|
|
85
|
+
hash_name = args[0]
|
|
86
|
+
iterations = int(args[1])
|
|
87
|
+
else:
|
|
88
|
+
raise ValueError("'pbkdf2' takes 2 arguments.")
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
hashlib.pbkdf2_hmac(
|
|
92
|
+
hash_name,
|
|
93
|
+
password_bytes,
|
|
94
|
+
salt_bytes,
|
|
95
|
+
iterations,
|
|
96
|
+
).hex(),
|
|
97
|
+
f'pbkdf2:{hash_name}:{iterations}',
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
else:
|
|
101
|
+
raise ValueError(f"Invalid hash method '{method}'.")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def generate_password_hash(
|
|
105
|
+
password: str,
|
|
106
|
+
method: str = 'scrypt',
|
|
107
|
+
salt_length: int = 16,
|
|
108
|
+
) -> str:
|
|
109
|
+
salt = gen_salt(salt_length)
|
|
110
|
+
h, actual_method = _hash_internal(method, salt, password)
|
|
111
|
+
return f'{actual_method}${salt}${h}'
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def check_password_hash(pwhash: str, password: str) -> bool:
|
|
115
|
+
try:
|
|
116
|
+
method, salt, hashval = pwhash.split('$', 2)
|
|
117
|
+
except ValueError:
|
|
118
|
+
return False
|
|
119
|
+
|
|
120
|
+
return hmac.compare_digest(_hash_internal(method, salt, password)[0], hashval)
|
|
@@ -0,0 +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
|
+
"""
|
|
12
|
+
import abc
|
|
13
|
+
import collections
|
|
14
|
+
import logging
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
import time
|
|
18
|
+
import types # noqa
|
|
19
|
+
import typing as ta
|
|
20
|
+
|
|
21
|
+
from .. import dataclasses as dc
|
|
22
|
+
from .. import lang
|
|
23
|
+
|
|
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
|
+
|
|
49
|
+
##
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dc.dataclass(frozen=True)
|
|
53
|
+
class SecretRef:
|
|
54
|
+
key: str
|
|
55
|
+
|
|
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
|
+
|
|
80
|
+
##
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class Secrets(lang.Abstract):
|
|
84
|
+
def fix(self, obj: str | SecretRef | Secret) -> Secret:
|
|
85
|
+
if isinstance(obj, Secret):
|
|
86
|
+
return obj
|
|
87
|
+
elif isinstance(obj, str):
|
|
88
|
+
return Secret(key=None, value=obj)
|
|
89
|
+
elif isinstance(obj, SecretRef):
|
|
90
|
+
return self.get(obj.key)
|
|
91
|
+
else:
|
|
92
|
+
raise TypeError(obj)
|
|
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
|
+
|
|
102
|
+
@abc.abstractmethod
|
|
103
|
+
def _get_raw(self, key: str) -> str:
|
|
104
|
+
raise NotImplementedError
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
##
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class EmptySecrets(Secrets):
|
|
111
|
+
def _get_raw(self, key: str) -> str:
|
|
112
|
+
raise KeyError(key)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
EMPTY_SECRETS = EmptySecrets()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
##
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class MappingSecrets(Secrets):
|
|
122
|
+
def __init__(self, dct: ta.Mapping[str, str]) -> None:
|
|
123
|
+
super().__init__()
|
|
124
|
+
self._dct = dct
|
|
125
|
+
|
|
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:
|
|
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,20 +3,21 @@ 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
|
|
|
16
17
|
default_port: int | None = None
|
|
17
18
|
|
|
18
19
|
|
|
19
|
-
class DbTypes(lang.Namespace):
|
|
20
|
+
class DbTypes(lang.Namespace, lang.Final):
|
|
20
21
|
MYSQL = DbType(
|
|
21
22
|
name='mysql',
|
|
22
23
|
dialect_name='mysql',
|
|
@@ -38,13 +39,13 @@ class DbTypes(lang.Namespace):
|
|
|
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/duckdb.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""
|
|
2
|
+
See:
|
|
3
|
+
- https://duckdb.org/docs/api/python/overview.html
|
|
4
|
+
- https://github.com/Mause/duckdb_engine/blob/0e3ea0107f81c66d50b444011d31fce22a9b902c/duckdb_engine/__init__.py
|
|
5
|
+
"""
|
|
6
|
+
import typing as ta
|
|
7
|
+
|
|
8
|
+
import sqlalchemy as sa
|
|
9
|
+
from sqlalchemy.dialects import postgresql as sap
|
|
10
|
+
|
|
11
|
+
from .. import lang
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
if ta.TYPE_CHECKING:
|
|
15
|
+
import duckdb
|
|
16
|
+
else:
|
|
17
|
+
duckdb = lang.proxy_import('duckdb')
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ConnectionWrapper:
|
|
21
|
+
def __init__(self, c: 'duckdb.DuckDBPyConnection') -> None:
|
|
22
|
+
super().__init__()
|
|
23
|
+
self.__c = c
|
|
24
|
+
self.autocommit = None
|
|
25
|
+
self.closed = False
|
|
26
|
+
|
|
27
|
+
def cursor(self) -> 'CursorWrapper':
|
|
28
|
+
return CursorWrapper(self.__c, self)
|
|
29
|
+
|
|
30
|
+
def __getattr__(self, name: str) -> ta.Any:
|
|
31
|
+
return getattr(self.__c, name)
|
|
32
|
+
|
|
33
|
+
def close(self) -> None:
|
|
34
|
+
self.__c.close()
|
|
35
|
+
self.closed = True
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _is_transaction_context_message(e: Exception) -> bool:
|
|
39
|
+
return e.args[0] == 'TransactionContext Error: cannot rollback - no transaction is active'
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class CursorWrapper:
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
c: 'duckdb.DuckDBPyConnection',
|
|
46
|
+
connection_wrapper: 'ConnectionWrapper',
|
|
47
|
+
) -> None:
|
|
48
|
+
super().__init__()
|
|
49
|
+
self.__c = c
|
|
50
|
+
self.__connection_wrapper = connection_wrapper
|
|
51
|
+
|
|
52
|
+
def executemany(
|
|
53
|
+
self,
|
|
54
|
+
statement: str,
|
|
55
|
+
parameters: ta.Sequence[ta.Mapping[str, ta.Any]] | None = None,
|
|
56
|
+
context: ta.Any | None = None,
|
|
57
|
+
) -> None:
|
|
58
|
+
self.__c.executemany(statement, list(parameters) if parameters else [])
|
|
59
|
+
|
|
60
|
+
def execute(
|
|
61
|
+
self,
|
|
62
|
+
statement: str,
|
|
63
|
+
parameters: ta.Sequence[ta.Any] | None = None,
|
|
64
|
+
context: ta.Any | None = None,
|
|
65
|
+
) -> None:
|
|
66
|
+
try:
|
|
67
|
+
if statement.lower() == 'commit': # this is largely for ipython-sql
|
|
68
|
+
self.__c.commit()
|
|
69
|
+
elif parameters is None:
|
|
70
|
+
self.__c.execute(statement)
|
|
71
|
+
else:
|
|
72
|
+
self.__c.execute(statement, parameters)
|
|
73
|
+
except RuntimeError as e:
|
|
74
|
+
if e.args[0].startswith('Not implemented Error'):
|
|
75
|
+
raise NotImplementedError(*e.args) from e
|
|
76
|
+
elif _is_transaction_context_message(e):
|
|
77
|
+
return
|
|
78
|
+
else:
|
|
79
|
+
raise
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def connection(self):
|
|
83
|
+
return self.__connection_wrapper
|
|
84
|
+
|
|
85
|
+
def close(self) -> None:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
def __getattr__(self, name: str) -> ta.Any:
|
|
89
|
+
return getattr(self.__c, name)
|
|
90
|
+
|
|
91
|
+
def fetchmany(self, size: int | None = None) -> list:
|
|
92
|
+
if size is None:
|
|
93
|
+
return self.__c.fetchmany()
|
|
94
|
+
else:
|
|
95
|
+
return self.__c.fetchmany(size)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class DuckdbDialect(sap.base.PGDialect):
|
|
99
|
+
name = 'postgres__duckdb'
|
|
100
|
+
driver = 'duckdb_engine'
|
|
101
|
+
|
|
102
|
+
supports_statement_cache = False
|
|
103
|
+
|
|
104
|
+
@classmethod
|
|
105
|
+
def import_dbapi(cls):
|
|
106
|
+
return duckdb
|
|
107
|
+
|
|
108
|
+
def connect(self, *cargs, **cparams):
|
|
109
|
+
conn = self.loaded_dbapi.connect(*cargs, **cparams)
|
|
110
|
+
|
|
111
|
+
return ConnectionWrapper(conn)
|
|
112
|
+
|
|
113
|
+
def do_rollback(self, connection) -> None:
|
|
114
|
+
try:
|
|
115
|
+
super().do_rollback(connection)
|
|
116
|
+
except duckdb.TransactionException as e:
|
|
117
|
+
if _is_transaction_context_message(e):
|
|
118
|
+
return
|
|
119
|
+
else:
|
|
120
|
+
raise
|
|
121
|
+
|
|
122
|
+
def do_begin(self, connection) -> None:
|
|
123
|
+
connection.begin()
|
|
124
|
+
|
|
125
|
+
def get_default_isolation_level(self, connection) -> ta.Any:
|
|
126
|
+
raise NotImplementedError
|
|
127
|
+
|
|
128
|
+
def _get_server_version_info(self, connection) -> tuple[int, int]:
|
|
129
|
+
connection.execute(sa.text('select version()')).fetchone()
|
|
130
|
+
return (8, 0)
|
|
131
|
+
|
|
132
|
+
def _set_backslash_escapes(self, connection):
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
sa.dialects.registry.register(DuckdbDialect.name, __name__, DuckdbDialect.__name__)
|
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/sql/sqlean.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import sqlalchemy as sa
|
|
2
|
+
from sqlalchemy.dialects import sqlite as sal
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class SqleanDialect(sal.dialect): # type: ignore
|
|
6
|
+
name = 'sqlite__sqlean'
|
|
7
|
+
driver = 'sqlean_engine'
|
|
8
|
+
|
|
9
|
+
supports_statement_cache = True
|
|
10
|
+
|
|
11
|
+
@classmethod
|
|
12
|
+
def import_dbapi(cls):
|
|
13
|
+
from sqlean import dbapi2
|
|
14
|
+
return dbapi2
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
sa.dialects.registry.register(SqleanDialect.name, __name__, SqleanDialect.__name__)
|