omlish 0.0.0.dev5__py3-none-any.whl → 0.0.0.dev6__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 (100) hide show
  1. omlish/__about__.py +1 -1
  2. omlish/asyncs/__init__.py +9 -0
  3. omlish/asyncs/anyio.py +83 -19
  4. omlish/asyncs/asyncio.py +23 -0
  5. omlish/asyncs/asyncs.py +9 -6
  6. omlish/asyncs/bridge.py +316 -0
  7. omlish/asyncs/trio_asyncio.py +7 -3
  8. omlish/collections/__init__.py +1 -0
  9. omlish/collections/identity.py +7 -0
  10. omlish/configs/strings.py +94 -0
  11. omlish/dataclasses/__init__.py +9 -0
  12. omlish/dataclasses/impl/copy.py +30 -0
  13. omlish/dataclasses/impl/exceptions.py +6 -0
  14. omlish/dataclasses/impl/fields.py +24 -25
  15. omlish/dataclasses/impl/init.py +4 -2
  16. omlish/dataclasses/impl/main.py +2 -0
  17. omlish/dataclasses/utils.py +44 -0
  18. omlish/diag/__init__.py +4 -0
  19. omlish/diag/procfs.py +2 -2
  20. omlish/{testing → diag}/pydevd.py +35 -0
  21. omlish/dispatch/_dispatch2.py +65 -0
  22. omlish/dispatch/_dispatch3.py +104 -0
  23. omlish/docker.py +1 -1
  24. omlish/fnpairs.py +11 -0
  25. omlish/http/asgi.py +2 -1
  26. omlish/http/collections.py +15 -0
  27. omlish/http/consts.py +16 -1
  28. omlish/http/sessions.py +10 -3
  29. omlish/inject/__init__.py +45 -17
  30. omlish/inject/binder.py +185 -5
  31. omlish/inject/bindings.py +3 -36
  32. omlish/inject/eagers.py +2 -8
  33. omlish/inject/elements.py +30 -9
  34. omlish/inject/exceptions.py +1 -1
  35. omlish/inject/impl/elements.py +37 -12
  36. omlish/inject/impl/injector.py +19 -2
  37. omlish/inject/impl/inspect.py +33 -5
  38. omlish/inject/impl/origins.py +75 -0
  39. omlish/inject/impl/{private.py → privates.py} +2 -2
  40. omlish/inject/impl/scopes.py +6 -2
  41. omlish/inject/injector.py +8 -4
  42. omlish/inject/inspect.py +18 -0
  43. omlish/inject/keys.py +8 -14
  44. omlish/inject/listeners.py +26 -0
  45. omlish/inject/managed.py +76 -10
  46. omlish/inject/multis.py +68 -18
  47. omlish/inject/origins.py +27 -0
  48. omlish/inject/overrides.py +5 -4
  49. omlish/inject/{private.py → privates.py} +6 -10
  50. omlish/inject/providers.py +12 -85
  51. omlish/inject/scopes.py +13 -6
  52. omlish/inject/types.py +3 -1
  53. omlish/lang/__init__.py +8 -2
  54. omlish/lang/cached.py +2 -2
  55. omlish/lang/classes/restrict.py +2 -1
  56. omlish/lang/classes/simple.py +18 -8
  57. omlish/lang/contextmanagers.py +12 -3
  58. omlish/lang/descriptors.py +131 -0
  59. omlish/lang/functions.py +8 -28
  60. omlish/lang/iterables.py +20 -1
  61. omlish/lang/typing.py +5 -0
  62. omlish/lifecycles/__init__.py +34 -0
  63. omlish/lifecycles/abstract.py +43 -0
  64. omlish/lifecycles/base.py +51 -0
  65. omlish/lifecycles/contextmanagers.py +74 -0
  66. omlish/lifecycles/controller.py +116 -0
  67. omlish/lifecycles/manager.py +161 -0
  68. omlish/lifecycles/states.py +43 -0
  69. omlish/lifecycles/transitions.py +64 -0
  70. omlish/logs/formatters.py +1 -1
  71. omlish/marshal/__init__.py +4 -0
  72. omlish/marshal/naming.py +4 -0
  73. omlish/marshal/objects.py +1 -0
  74. omlish/marshal/polymorphism.py +4 -4
  75. omlish/reflect.py +134 -19
  76. omlish/secrets/__init__.py +7 -0
  77. omlish/secrets/marshal.py +41 -0
  78. omlish/secrets/passwords.py +120 -0
  79. omlish/secrets/secrets.py +47 -0
  80. omlish/serde/__init__.py +0 -0
  81. omlish/{configs → serde}/dotenv.py +12 -24
  82. omlish/{json.py → serde/json.py} +2 -1
  83. omlish/serde/yaml.py +223 -0
  84. omlish/sql/dbs.py +1 -1
  85. omlish/sql/duckdb.py +136 -0
  86. omlish/sql/sqlean.py +17 -0
  87. omlish/term.py +1 -1
  88. omlish/testing/pytest/__init__.py +3 -2
  89. omlish/testing/pytest/inject/harness.py +3 -3
  90. omlish/testing/pytest/marks.py +4 -7
  91. omlish/testing/pytest/plugins/__init__.py +1 -0
  92. omlish/testing/pytest/plugins/asyncs.py +136 -0
  93. omlish/testing/pytest/plugins/pydevd.py +1 -1
  94. omlish/text/glyphsplit.py +92 -0
  95. {omlish-0.0.0.dev5.dist-info → omlish-0.0.0.dev6.dist-info}/METADATA +1 -1
  96. {omlish-0.0.0.dev5.dist-info → omlish-0.0.0.dev6.dist-info}/RECORD +100 -72
  97. /omlish/{configs → serde}/props.py +0 -0
  98. {omlish-0.0.0.dev5.dist-info → omlish-0.0.0.dev6.dist-info}/LICENSE +0 -0
  99. {omlish-0.0.0.dev5.dist-info → omlish-0.0.0.dev6.dist-info}/WHEEL +0 -0
  100. {omlish-0.0.0.dev5.dist-info → omlish-0.0.0.dev6.dist-info}/top_level.txt +0 -0
omlish/reflect.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """
2
2
  TODO:
3
- - callables..
3
+ - visitor / transformer
4
4
  - uniform collection isinstance - items() for mappings, iter() for other
5
5
  - also check instance type in isinstance not just items lol
6
6
  - ta.Generic in mro causing trouble - omit? no longer 1:1
@@ -8,6 +8,7 @@ TODO:
8
8
  - cache __hash__ in Generic/Union
9
9
  """
10
10
  import collections.abc
11
+ import dataclasses as dc
11
12
  import types
12
13
  import typing as ta
13
14
 
@@ -27,9 +28,10 @@ _NONE_TYPE_FROZENSET: frozenset['Type'] = frozenset([_NoneType])
27
28
 
28
29
 
29
30
  _GenericAlias = ta._GenericAlias # type: ignore # noqa
30
- # _CallableGenericAlias = ta._CallableGenericAlias # type: ignore # noqa
31
+ _CallableGenericAlias = ta._CallableGenericAlias # type: ignore # noqa
31
32
  _SpecialGenericAlias = ta._SpecialGenericAlias # type: ignore # noqa
32
33
  _UnionGenericAlias = ta._UnionGenericAlias # type: ignore # noqa
34
+ _AnnotatedAlias = ta._AnnotatedAlias # type: ignore # noqa
33
35
 
34
36
 
35
37
  ##
@@ -57,9 +59,11 @@ _KNOWN_SPECIALS_BY_NAME = {s.name: s for s in _KNOWN_SPECIALS}
57
59
  _KNOWN_SPECIALS_BY_ALIAS = {s.alias: s for s in _KNOWN_SPECIALS}
58
60
  _KNOWN_SPECIALS_BY_ORIGIN = {s.origin: s for s in _KNOWN_SPECIALS}
59
61
 
62
+ _MAX_KNOWN_SPECIAL_TYPE_VARS = 16
63
+
60
64
  _KNOWN_SPECIAL_TYPE_VARS = tuple(
61
65
  ta.TypeVar(f'_{i}') # noqa
62
- for i in range(max(s.nparams for s in _KNOWN_SPECIALS) + 1)
66
+ for i in range(_MAX_KNOWN_SPECIAL_TYPE_VARS)
63
67
  )
64
68
 
65
69
 
@@ -76,28 +80,40 @@ def get_params(obj: ta.Any) -> tuple[ta.TypeVar, ...]:
76
80
 
77
81
  oty = type(obj)
78
82
 
79
- if oty is _GenericAlias or oty is ta.GenericAlias: # type: ignore # noqa
83
+ if (
84
+ oty is _GenericAlias or
85
+ oty is ta.GenericAlias # type: ignore # noqa
86
+ ):
80
87
  return obj.__dict__.get('__parameters__', ()) # noqa
81
88
 
89
+ if oty is _CallableGenericAlias:
90
+ raise NotImplementedError('get_params not yet implemented for typing.Callable')
91
+
82
92
  raise TypeError(obj)
83
93
 
84
94
 
85
95
  def is_union_type(cls: ta.Any) -> bool:
86
96
  if hasattr(ta, 'UnionType'):
87
97
  return ta.get_origin(cls) in {ta.Union, getattr(ta, 'UnionType')}
98
+
88
99
  else:
89
100
  return ta.get_origin(cls) in {ta.Union}
90
101
 
91
102
 
103
+ def get_orig_class(obj: ta.Any) -> ta.Any:
104
+ return obj.__orig_class__ # noqa
105
+
106
+
92
107
  ##
93
108
 
94
109
 
95
110
  Type = ta.Union[
96
111
  type,
97
112
  ta.TypeVar,
98
- 'NewType',
99
113
  'Union',
100
114
  'Generic',
115
+ 'NewType',
116
+ 'Annotated',
101
117
  ]
102
118
 
103
119
 
@@ -117,19 +133,26 @@ class Union(ta.NamedTuple):
117
133
  return Union(rem)
118
134
 
119
135
 
120
- class Generic(ta.NamedTuple):
136
+ @dc.dataclass(frozen=True)
137
+ class Generic:
121
138
  cls: type
122
- args: tuple[Type, ...] # map[int, V] = (int, V) | map[T, T] = (T, T)
123
- params: tuple[ta.TypeVar, ...] # map[int, V] = (_0, _1) | map[T, T] = (_0, _1)
124
- # params2: tuple[ta.TypeVar, ...] # map[int, V] = (V,) | map[T, T] = (T,)
125
- obj: ta.Any
139
+ args: tuple[Type, ...] # map[int, V] = (int, V) | map[T, T] = (T, T)
140
+
141
+ params: tuple[ta.TypeVar, ...] = dc.field(compare=False, repr=False) # map[int, V] = (_0, _1) | map[T, T] = (_0, _1) # noqa
142
+ # params2: tuple[ta.TypeVar, ...] # map[int, V] = (V,) | map[T, T] = (T,)
143
+
144
+ obj: ta.Any = dc.field(compare=False, repr=False)
126
145
 
127
- def __repr__(self) -> str:
146
+ # def __post_init__(self) -> None:
147
+ # if not isinstance(self.cls, type):
148
+ # raise TypeError(self.cls)
149
+
150
+ def full_eq(self, other: 'Generic') -> bool:
128
151
  return (
129
- f'{self.__class__.__name__}('
130
- f'cls={self.cls.__name__}, '
131
- f'args={self.args!r}, '
132
- f'params={self.params!r})'
152
+ self.cls == other.cls and
153
+ self.args == other.args and
154
+ self.params == other.params and
155
+ self.obj == other.obj
133
156
  )
134
157
 
135
158
 
@@ -137,15 +160,50 @@ class NewType(ta.NamedTuple):
137
160
  obj: ta.Any
138
161
 
139
162
 
163
+ @dc.dataclass(frozen=True)
164
+ class Annotated:
165
+ ty: Type
166
+ md: ta.Sequence[ta.Any]
167
+
168
+ obj: ta.Any = dc.field(compare=False, repr=False)
169
+
170
+
140
171
  TYPES: tuple[type, ...] = (
141
172
  type,
142
173
  ta.TypeVar,
143
174
  Union,
144
175
  Generic,
145
176
  NewType,
177
+ Annotated,
146
178
  )
147
179
 
148
180
 
181
+ ##
182
+
183
+
184
+ def is_type(obj: ta.Any) -> bool:
185
+ if isinstance(obj, (Union, Generic, ta.TypeVar, NewType)): # noqa
186
+ return True
187
+
188
+ oty = type(obj)
189
+
190
+ return (
191
+ oty is _UnionGenericAlias or oty is types.UnionType or # noqa
192
+
193
+ isinstance(obj, ta.NewType) or # noqa
194
+
195
+ (
196
+ oty is _GenericAlias or
197
+ oty is ta.GenericAlias or # type: ignore # noqa
198
+ oty is _CallableGenericAlias
199
+ ) or
200
+
201
+ isinstance(obj, type) or
202
+
203
+ isinstance(obj, _SpecialGenericAlias)
204
+ )
205
+
206
+
149
207
  def type_(obj: ta.Any) -> Type:
150
208
  if isinstance(obj, (Union, Generic, ta.TypeVar, NewType)): # noqa
151
209
  return obj
@@ -156,15 +214,22 @@ def type_(obj: ta.Any) -> Type:
156
214
  return Union(frozenset(type_(a) for a in ta.get_args(obj)))
157
215
 
158
216
  if isinstance(obj, ta.NewType): # noqa
159
- return NewType(oty)
217
+ return NewType(obj)
160
218
 
161
219
  if (
162
220
  oty is _GenericAlias or
163
- oty is ta.GenericAlias # type: ignore # noqa
221
+ oty is ta.GenericAlias or # type: ignore # noqa
222
+ oty is _CallableGenericAlias
164
223
  ):
165
224
  origin = ta.get_origin(obj)
166
225
  args = ta.get_args(obj)
167
- if origin is ta.Generic:
226
+ if oty is _CallableGenericAlias:
227
+ p, r = args
228
+ if p is Ellipsis or isinstance(p, ta.ParamSpec):
229
+ raise TypeError(f'Callable argument not yet supported for {obj=}')
230
+ args = (*p, r)
231
+ params = _KNOWN_SPECIAL_TYPE_VARS[:len(args)]
232
+ elif origin is ta.Generic:
168
233
  params = args
169
234
  else:
170
235
  params = get_params(origin)
@@ -198,15 +263,52 @@ def type_(obj: ta.Any) -> Type:
198
263
  obj,
199
264
  )
200
265
 
266
+ if isinstance(obj, _AnnotatedAlias):
267
+ o = ta.get_args(obj)[0]
268
+ return Annotated(type_(o), md=obj.__metadata__, obj=obj)
269
+
201
270
  raise TypeError(obj)
202
271
 
203
272
 
204
273
  ##
205
274
 
206
275
 
276
+ def strip_objs(ty: Type) -> Type:
277
+ if isinstance(ty, (type, ta.TypeVar, NewType)):
278
+ return ty
279
+
280
+ if isinstance(ty, Union):
281
+ return Union(frozenset(map(strip_objs, ty.args)))
282
+
283
+ if isinstance(ty, Generic):
284
+ return Generic(ty.cls, tuple(map(strip_objs, ty.args)), ty.params, None)
285
+
286
+ if isinstance(ty, Annotated):
287
+ return Annotated(strip_objs(ty.ty), ty.md, None)
288
+
289
+ raise TypeError(ty)
290
+
291
+
292
+ def strip_annotations(ty: Type) -> Type:
293
+ if isinstance(ty, (type, ta.TypeVar, NewType)):
294
+ return ty
295
+
296
+ if isinstance(ty, Union):
297
+ return Union(frozenset(map(strip_annotations, ty.args)))
298
+
299
+ if isinstance(ty, Generic):
300
+ return Generic(ty.cls, tuple(map(strip_annotations, ty.args)), ty.params, ty.obj)
301
+
302
+ if isinstance(ty, Annotated):
303
+ return strip_annotations(ty.ty)
304
+
305
+ raise TypeError(ty)
306
+
307
+
207
308
  def types_equivalent(l: Type, r: Type) -> bool:
208
309
  if isinstance(l, Generic) and isinstance(r, Generic):
209
310
  return l.cls == r.cls and l.args == r.args
311
+
210
312
  return l == r
211
313
 
212
314
 
@@ -217,28 +319,36 @@ def get_underlying(nt: NewType) -> Type:
217
319
  def get_concrete_type(ty: Type) -> type | None:
218
320
  if isinstance(ty, type):
219
321
  return ty
322
+
220
323
  if isinstance(ty, Generic):
221
324
  return ty.cls
325
+
222
326
  if isinstance(ty, NewType):
223
327
  return get_concrete_type(get_underlying(ty))
328
+
224
329
  if isinstance(ty, (Union, ta.TypeVar)):
225
330
  return None
331
+
226
332
  raise TypeError(ty)
227
333
 
228
334
 
229
335
  def get_type_var_replacements(ty: Type) -> ta.Mapping[ta.TypeVar, Type]:
230
336
  if isinstance(ty, Generic):
231
337
  return dict(zip(ty.params, ty.args))
338
+
232
339
  return {}
233
340
 
234
341
 
235
342
  def to_annotation(ty: Type) -> ta.Any:
236
343
  if isinstance(ty, Generic):
237
344
  return ty.obj if ty.obj is not None else ty.cls
345
+
238
346
  if isinstance(ty, Union):
239
347
  return ta.Union[*tuple(to_annotation(e) for e in ty.args)]
348
+
240
349
  if isinstance(ty, (type, ta.TypeVar, NewType)):
241
350
  return ty
351
+
242
352
  raise TypeError(ty)
243
353
 
244
354
 
@@ -251,8 +361,10 @@ def replace_type_vars(
251
361
  def rec(cur):
252
362
  if isinstance(cur, type):
253
363
  return cur
364
+
254
365
  if isinstance(cur, NewType):
255
366
  return cur
367
+
256
368
  if isinstance(cur, Generic):
257
369
  args = tuple(rec(a) for a in cur.args)
258
370
  if update_aliases:
@@ -266,11 +378,14 @@ def replace_type_vars(
266
378
  obj = cur.obj[*nargs]
267
379
  else:
268
380
  obj = None
269
- return cur._replace(args=args, obj=obj)
381
+ return dc.replace(cur, args=args, obj=obj)
382
+
270
383
  if isinstance(cur, Union):
271
384
  return Union(frozenset(rec(e) for e in cur.args))
385
+
272
386
  if isinstance(cur, ta.TypeVar):
273
387
  return rpl[cur]
388
+
274
389
  raise TypeError(cur)
275
390
 
276
391
  return rec(ty)
@@ -0,0 +1,7 @@
1
+ from .secrets import ( # noqa
2
+ EMPTY_SECRETS,
3
+ EmptySecrets,
4
+ Secret,
5
+ Secrets,
6
+ SimpleSecrets,
7
+ )
@@ -0,0 +1,41 @@
1
+ import collections.abc
2
+ import typing as ta
3
+
4
+ from .. import check
5
+ from .. import dataclasses as dc
6
+ from .. import marshal as msh
7
+ from .secrets import Secret
8
+
9
+
10
+ class StrOrSecretMarshaler(msh.Marshaler):
11
+ def marshal(self, ctx: msh.MarshalContext, o: ta.Any) -> msh.Value:
12
+ if isinstance(o, str):
13
+ return o
14
+ elif isinstance(o, Secret):
15
+ return {'secret': o.key}
16
+ else:
17
+ raise TypeError(o)
18
+
19
+
20
+ class StrOrSecretUnmarshaler(msh.Unmarshaler):
21
+ def unmarshal(self, ctx: msh.UnmarshalContext, v: msh.Value) -> ta.Any:
22
+ if isinstance(v, str):
23
+ return v
24
+ elif isinstance(v, collections.abc.Mapping):
25
+ [(mk, mv)] = v.items()
26
+ if mk != 'secret':
27
+ raise TypeError(v)
28
+ return Secret(check.isinstance(mv, str))
29
+ else:
30
+ raise TypeError(v)
31
+
32
+
33
+ @dc.field_modifier
34
+ def marshal_secret_field(f: dc.Field) -> dc.Field:
35
+ return dc.update_field_metadata(f, {
36
+ msh.FieldMetadata: dc.replace(
37
+ f.metadata.get(msh.FieldMetadata, msh.FieldMetadata()),
38
+ marshaler=StrOrSecretMarshaler(),
39
+ unmarshaler=StrOrSecretUnmarshaler(),
40
+ ),
41
+ })
@@ -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,47 @@
1
+ import abc
2
+ import typing as ta
3
+
4
+ from .. import dataclasses as dc
5
+ from .. import lang
6
+
7
+
8
+ ##
9
+
10
+
11
+ @dc.dataclass(frozen=True)
12
+ class Secret:
13
+ key: str
14
+
15
+
16
+ ##
17
+
18
+
19
+ class Secrets(lang.Abstract):
20
+ def fix(self, obj: str | Secret) -> str:
21
+ if isinstance(obj, str):
22
+ return obj
23
+ elif isinstance(obj, Secret):
24
+ return self.get(obj.key)
25
+ else:
26
+ raise TypeError(obj)
27
+
28
+ @abc.abstractmethod
29
+ def get(self, key: str) -> str:
30
+ raise NotImplementedError
31
+
32
+
33
+ class EmptySecrets(Secrets):
34
+ def get(self, key: str) -> str:
35
+ raise KeyError(key)
36
+
37
+
38
+ EMPTY_SECRETS = EmptySecrets()
39
+
40
+
41
+ class SimpleSecrets(Secrets):
42
+ def __init__(self, dct: ta.Mapping[str, str]) -> None:
43
+ super().__init__()
44
+ self._dct = dct
45
+
46
+ def get(self, key: str) -> str:
47
+ return self._dct[key]
File without changes
@@ -332,6 +332,7 @@ class DotEnv:
332
332
  encoding: str | None = None,
333
333
  interpolate: bool = True,
334
334
  override: bool = True,
335
+ env: ta.Mapping[str, str] | None = None,
335
336
  ) -> None:
336
337
  super().__init__()
337
338
  self.path: StrPath | None = path
@@ -341,6 +342,7 @@ class DotEnv:
341
342
  self.encoding: str | None = encoding
342
343
  self.interpolate: bool = interpolate
343
344
  self.override: bool = override
345
+ self.env = env or {}
344
346
 
345
347
  @contextlib.contextmanager
346
348
  def _get_stream(self) -> ta.Iterator[ta.IO[str]]:
@@ -364,7 +366,7 @@ class DotEnv:
364
366
  raw_values = self.parse()
365
367
 
366
368
  if self.interpolate:
367
- self._dict = resolve_variables(raw_values, override=self.override)
369
+ self._dict = resolve_variables(raw_values, override=self.override, env=self.env)
368
370
  else:
369
371
  self._dict = dict(raw_values)
370
372
 
@@ -376,22 +378,7 @@ class DotEnv:
376
378
  if mapping.key is not None:
377
379
  yield mapping.key, mapping.value
378
380
 
379
- def set_as_environment_variables(self) -> bool:
380
- """Load the current dotenv as system environment variable."""
381
- if not self.dict():
382
- return False
383
-
384
- for k, v in self.dict().items():
385
- if k in os.environ and not self.override:
386
- continue
387
- if v is not None:
388
- os.environ[k] = v
389
-
390
- return True
391
-
392
381
  def get(self, key: str) -> str | None:
393
- """
394
- """
395
382
  data = self.dict()
396
383
 
397
384
  if key in data:
@@ -525,8 +512,9 @@ def unset_key(
525
512
 
526
513
 
527
514
  def resolve_variables(
528
- values: ta.Iterable[tuple[str, str | None]],
529
- override: bool,
515
+ values: ta.Iterable[tuple[str, str | None]],
516
+ override: bool,
517
+ env: ta.Mapping[str, str],
530
518
  ) -> dict[str, str | None]:
531
519
  new_values: dict[str, str | None] = {}
532
520
 
@@ -535,14 +523,14 @@ def resolve_variables(
535
523
  result = None
536
524
  else:
537
525
  atoms = parse_variables(value)
538
- env: dict[str, str | None] = {}
526
+ aenv: dict[str, str | None] = {}
539
527
  if override:
540
- env.update(os.environ)
541
- env.update(new_values)
528
+ aenv.update(env)
529
+ aenv.update(new_values)
542
530
  else:
543
- env.update(new_values)
544
- env.update(os.environ)
545
- result = ''.join(atom.resolve(env) for atom in atoms)
531
+ aenv.update(new_values)
532
+ aenv.update(env)
533
+ result = ''.join(atom.resolve(aenv) for atom in atoms)
546
534
 
547
535
  new_values[name] = result
548
536
 
@@ -125,7 +125,7 @@ import functools
125
125
  import json as _json
126
126
  import typing as ta
127
127
 
128
- from . import lang
128
+ from .. import lang
129
129
 
130
130
 
131
131
  if ta.TYPE_CHECKING:
@@ -160,6 +160,7 @@ PRETTY_KWARGS: ta.Mapping[str, ta.Any] = dict(
160
160
  dump_pretty: ta.Callable[..., bytes] = functools.partial(dump, **PRETTY_KWARGS) # type: ignore
161
161
  dumps_pretty: ta.Callable[..., str] = functools.partial(dumps, **PRETTY_KWARGS)
162
162
 
163
+
163
164
  ##
164
165
 
165
166