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.

Files changed (163) hide show
  1. omlish/__about__.py +109 -5
  2. omlish/__init__.py +0 -8
  3. omlish/asyncs/__init__.py +9 -9
  4. omlish/asyncs/anyio.py +123 -19
  5. omlish/asyncs/asyncio.py +23 -0
  6. omlish/asyncs/asyncs.py +9 -6
  7. omlish/asyncs/bridge.py +316 -0
  8. omlish/asyncs/trio_asyncio.py +7 -3
  9. omlish/bootstrap.py +737 -0
  10. omlish/check.py +1 -1
  11. omlish/collections/__init__.py +5 -0
  12. omlish/collections/exceptions.py +2 -0
  13. omlish/collections/identity.py +7 -0
  14. omlish/collections/utils.py +38 -9
  15. omlish/configs/strings.py +96 -0
  16. omlish/dataclasses/__init__.py +16 -0
  17. omlish/dataclasses/impl/copy.py +30 -0
  18. omlish/dataclasses/impl/descriptors.py +95 -0
  19. omlish/dataclasses/impl/exceptions.py +6 -0
  20. omlish/dataclasses/impl/fields.py +24 -25
  21. omlish/dataclasses/impl/init.py +4 -2
  22. omlish/dataclasses/impl/main.py +2 -0
  23. omlish/dataclasses/impl/reflect.py +1 -1
  24. omlish/dataclasses/utils.py +67 -0
  25. omlish/{lang/datetimes.py → datetimes.py} +8 -4
  26. omlish/diag/__init__.py +4 -0
  27. omlish/diag/procfs.py +2 -2
  28. omlish/{testing → diag}/pydevd.py +35 -0
  29. omlish/diag/threads.py +131 -48
  30. omlish/dispatch/_dispatch2.py +65 -0
  31. omlish/dispatch/_dispatch3.py +104 -0
  32. omlish/docker.py +16 -1
  33. omlish/fnpairs.py +11 -4
  34. omlish/formats/__init__.py +0 -0
  35. omlish/{configs → formats}/dotenv.py +15 -24
  36. omlish/{json.py → formats/json.py} +2 -1
  37. omlish/formats/yaml.py +223 -0
  38. omlish/graphs/trees.py +1 -1
  39. omlish/http/asgi.py +2 -1
  40. omlish/http/collections.py +15 -0
  41. omlish/http/consts.py +22 -1
  42. omlish/http/sessions.py +10 -3
  43. omlish/inject/__init__.py +49 -17
  44. omlish/inject/binder.py +185 -5
  45. omlish/inject/bindings.py +3 -36
  46. omlish/inject/eagers.py +2 -8
  47. omlish/inject/elements.py +31 -10
  48. omlish/inject/exceptions.py +1 -1
  49. omlish/inject/impl/elements.py +37 -12
  50. omlish/inject/impl/injector.py +72 -25
  51. omlish/inject/impl/inspect.py +33 -5
  52. omlish/inject/impl/origins.py +77 -0
  53. omlish/inject/impl/{private.py → privates.py} +2 -2
  54. omlish/inject/impl/scopes.py +6 -2
  55. omlish/inject/injector.py +8 -4
  56. omlish/inject/inspect.py +18 -0
  57. omlish/inject/keys.py +8 -14
  58. omlish/inject/listeners.py +26 -0
  59. omlish/inject/managed.py +76 -10
  60. omlish/inject/multis.py +68 -18
  61. omlish/inject/origins.py +30 -0
  62. omlish/inject/overrides.py +5 -4
  63. omlish/inject/{private.py → privates.py} +6 -10
  64. omlish/inject/providers.py +12 -85
  65. omlish/inject/scopes.py +13 -6
  66. omlish/inject/types.py +3 -1
  67. omlish/inject/utils.py +18 -0
  68. omlish/iterators.py +69 -2
  69. omlish/lang/__init__.py +24 -9
  70. omlish/lang/cached.py +2 -2
  71. omlish/lang/classes/restrict.py +12 -1
  72. omlish/lang/classes/simple.py +18 -8
  73. omlish/lang/contextmanagers.py +13 -4
  74. omlish/lang/descriptors.py +132 -1
  75. omlish/lang/functions.py +8 -28
  76. omlish/lang/imports.py +67 -0
  77. omlish/lang/iterables.py +60 -1
  78. omlish/lang/maybes.py +3 -0
  79. omlish/lang/objects.py +38 -0
  80. omlish/lang/strings.py +25 -0
  81. omlish/lang/sys.py +9 -0
  82. omlish/lang/typing.py +42 -0
  83. omlish/lifecycles/__init__.py +34 -0
  84. omlish/lifecycles/abstract.py +43 -0
  85. omlish/lifecycles/base.py +51 -0
  86. omlish/lifecycles/contextmanagers.py +74 -0
  87. omlish/lifecycles/controller.py +116 -0
  88. omlish/lifecycles/manager.py +161 -0
  89. omlish/lifecycles/states.py +43 -0
  90. omlish/lifecycles/transitions.py +64 -0
  91. omlish/lite/__init__.py +1 -0
  92. omlish/lite/cached.py +18 -0
  93. omlish/lite/check.py +29 -0
  94. omlish/lite/contextmanagers.py +18 -0
  95. omlish/lite/json.py +30 -0
  96. omlish/lite/logs.py +52 -0
  97. omlish/lite/marshal.py +316 -0
  98. omlish/lite/reflect.py +49 -0
  99. omlish/lite/runtime.py +18 -0
  100. omlish/lite/secrets.py +19 -0
  101. omlish/lite/strings.py +25 -0
  102. omlish/lite/subprocesses.py +112 -0
  103. omlish/logs/configs.py +15 -2
  104. omlish/logs/formatters.py +7 -2
  105. omlish/marshal/__init__.py +32 -0
  106. omlish/marshal/any.py +5 -5
  107. omlish/marshal/base.py +27 -11
  108. omlish/marshal/base64.py +24 -9
  109. omlish/marshal/dataclasses.py +34 -28
  110. omlish/marshal/datetimes.py +74 -18
  111. omlish/marshal/enums.py +14 -8
  112. omlish/marshal/exceptions.py +11 -1
  113. omlish/marshal/factories.py +59 -74
  114. omlish/marshal/forbidden.py +35 -0
  115. omlish/marshal/global_.py +11 -4
  116. omlish/marshal/iterables.py +21 -24
  117. omlish/marshal/mappings.py +23 -26
  118. omlish/marshal/naming.py +4 -0
  119. omlish/marshal/numbers.py +51 -0
  120. omlish/marshal/objects.py +1 -0
  121. omlish/marshal/optionals.py +11 -12
  122. omlish/marshal/polymorphism.py +86 -21
  123. omlish/marshal/primitives.py +4 -5
  124. omlish/marshal/standard.py +13 -8
  125. omlish/marshal/uuids.py +4 -5
  126. omlish/matchfns.py +218 -0
  127. omlish/os.py +64 -0
  128. omlish/reflect/__init__.py +39 -0
  129. omlish/reflect/isinstance.py +38 -0
  130. omlish/reflect/ops.py +84 -0
  131. omlish/reflect/subst.py +110 -0
  132. omlish/reflect/types.py +275 -0
  133. omlish/secrets/__init__.py +23 -0
  134. omlish/secrets/crypto.py +132 -0
  135. omlish/secrets/marshal.py +70 -0
  136. omlish/secrets/openssl.py +207 -0
  137. omlish/secrets/passwords.py +120 -0
  138. omlish/secrets/secrets.py +299 -0
  139. omlish/secrets/subprocesses.py +42 -0
  140. omlish/sql/dbs.py +7 -6
  141. omlish/sql/duckdb.py +136 -0
  142. omlish/sql/exprs.py +12 -0
  143. omlish/sql/secrets.py +10 -0
  144. omlish/sql/sqlean.py +17 -0
  145. omlish/term.py +2 -2
  146. omlish/testing/pytest/__init__.py +3 -2
  147. omlish/testing/pytest/inject/harness.py +3 -3
  148. omlish/testing/pytest/marks.py +4 -7
  149. omlish/testing/pytest/plugins/__init__.py +1 -0
  150. omlish/testing/pytest/plugins/asyncs.py +136 -0
  151. omlish/testing/pytest/plugins/pydevd.py +1 -1
  152. omlish/testing/pytest/plugins/switches.py +54 -19
  153. omlish/text/glyphsplit.py +97 -0
  154. omlish-0.0.0.dev7.dist-info/METADATA +50 -0
  155. omlish-0.0.0.dev7.dist-info/RECORD +268 -0
  156. {omlish-0.0.0.dev5.dist-info → omlish-0.0.0.dev7.dist-info}/WHEEL +1 -1
  157. omlish/reflect.py +0 -355
  158. omlish-0.0.0.dev5.dist-info/METADATA +0 -34
  159. omlish-0.0.0.dev5.dist-info/RECORD +0 -212
  160. /omlish/{asyncs/futures.py → concurrent.py} +0 -0
  161. /omlish/{configs → formats}/props.py +0 -0
  162. {omlish-0.0.0.dev5.dist-info → omlish-0.0.0.dev7.dist-info}/LICENSE +0 -0
  163. {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: 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/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
@@ -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/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__)