omlish 0.0.0.dev4__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 (143) hide show
  1. omlish/__about__.py +1 -1
  2. omlish/__init__.py +1 -1
  3. omlish/asyncs/__init__.py +10 -4
  4. omlish/asyncs/anyio.py +142 -12
  5. omlish/asyncs/asyncio.py +23 -0
  6. omlish/asyncs/asyncs.py +9 -6
  7. omlish/asyncs/bridge.py +316 -0
  8. omlish/asyncs/flavors.py +27 -1
  9. omlish/asyncs/trio_asyncio.py +28 -18
  10. omlish/c3.py +1 -1
  11. omlish/cached.py +1 -2
  12. omlish/collections/__init__.py +5 -1
  13. omlish/collections/cache/impl.py +1 -1
  14. omlish/collections/identity.py +7 -0
  15. omlish/collections/indexed.py +1 -1
  16. omlish/collections/utils.py +38 -6
  17. omlish/configs/__init__.py +5 -0
  18. omlish/configs/classes.py +53 -0
  19. omlish/configs/strings.py +94 -0
  20. omlish/dataclasses/__init__.py +9 -0
  21. omlish/dataclasses/impl/api.py +1 -1
  22. omlish/dataclasses/impl/as_.py +1 -1
  23. omlish/dataclasses/impl/copy.py +30 -0
  24. omlish/dataclasses/impl/exceptions.py +6 -0
  25. omlish/dataclasses/impl/fields.py +25 -25
  26. omlish/dataclasses/impl/init.py +5 -3
  27. omlish/dataclasses/impl/main.py +3 -0
  28. omlish/dataclasses/impl/metaclass.py +6 -1
  29. omlish/dataclasses/impl/order.py +1 -1
  30. omlish/dataclasses/impl/reflect.py +15 -2
  31. omlish/dataclasses/utils.py +44 -0
  32. omlish/defs.py +1 -1
  33. omlish/diag/__init__.py +4 -0
  34. omlish/diag/procfs.py +31 -3
  35. omlish/diag/procstats.py +32 -0
  36. omlish/{testing → diag}/pydevd.py +35 -0
  37. omlish/diag/replserver/console.py +3 -3
  38. omlish/diag/replserver/server.py +6 -5
  39. omlish/diag/threads.py +86 -0
  40. omlish/dispatch/_dispatch2.py +65 -0
  41. omlish/dispatch/_dispatch3.py +104 -0
  42. omlish/docker.py +20 -1
  43. omlish/fnpairs.py +37 -18
  44. omlish/graphs/dags.py +113 -0
  45. omlish/graphs/domination.py +268 -0
  46. omlish/graphs/trees.py +2 -2
  47. omlish/http/__init__.py +25 -0
  48. omlish/http/asgi.py +132 -0
  49. omlish/http/collections.py +15 -0
  50. omlish/http/consts.py +47 -5
  51. omlish/http/cookies.py +194 -0
  52. omlish/http/dates.py +70 -0
  53. omlish/http/encodings.py +6 -0
  54. omlish/http/json.py +273 -0
  55. omlish/http/sessions.py +204 -0
  56. omlish/inject/__init__.py +51 -17
  57. omlish/inject/binder.py +185 -5
  58. omlish/inject/bindings.py +3 -36
  59. omlish/inject/eagers.py +2 -8
  60. omlish/inject/elements.py +30 -9
  61. omlish/inject/exceptions.py +3 -3
  62. omlish/inject/impl/elements.py +65 -31
  63. omlish/inject/impl/injector.py +20 -2
  64. omlish/inject/impl/inspect.py +33 -5
  65. omlish/inject/impl/multis.py +74 -0
  66. omlish/inject/impl/origins.py +75 -0
  67. omlish/inject/impl/{private.py → privates.py} +2 -2
  68. omlish/inject/impl/providers.py +19 -39
  69. omlish/inject/{proxy.py → impl/proxy.py} +2 -2
  70. omlish/inject/impl/scopes.py +7 -2
  71. omlish/inject/injector.py +9 -4
  72. omlish/inject/inspect.py +18 -0
  73. omlish/inject/keys.py +11 -23
  74. omlish/inject/listeners.py +26 -0
  75. omlish/inject/managed.py +76 -10
  76. omlish/inject/multis.py +120 -0
  77. omlish/inject/origins.py +27 -0
  78. omlish/inject/overrides.py +5 -4
  79. omlish/inject/{private.py → privates.py} +6 -10
  80. omlish/inject/providers.py +12 -85
  81. omlish/inject/scopes.py +20 -9
  82. omlish/inject/types.py +2 -8
  83. omlish/iterators.py +13 -0
  84. omlish/lang/__init__.py +12 -2
  85. omlish/lang/cached.py +2 -2
  86. omlish/lang/classes/restrict.py +3 -2
  87. omlish/lang/classes/simple.py +18 -8
  88. omlish/lang/classes/virtual.py +2 -2
  89. omlish/lang/contextmanagers.py +75 -2
  90. omlish/lang/datetimes.py +6 -5
  91. omlish/lang/descriptors.py +131 -0
  92. omlish/lang/functions.py +18 -28
  93. omlish/lang/imports.py +11 -2
  94. omlish/lang/iterables.py +20 -1
  95. omlish/lang/typing.py +6 -0
  96. omlish/lifecycles/__init__.py +34 -0
  97. omlish/lifecycles/abstract.py +43 -0
  98. omlish/lifecycles/base.py +51 -0
  99. omlish/lifecycles/contextmanagers.py +74 -0
  100. omlish/lifecycles/controller.py +116 -0
  101. omlish/lifecycles/manager.py +161 -0
  102. omlish/lifecycles/states.py +43 -0
  103. omlish/lifecycles/transitions.py +64 -0
  104. omlish/logs/formatters.py +1 -1
  105. omlish/logs/utils.py +1 -1
  106. omlish/marshal/__init__.py +4 -0
  107. omlish/marshal/datetimes.py +1 -1
  108. omlish/marshal/naming.py +4 -0
  109. omlish/marshal/objects.py +1 -0
  110. omlish/marshal/polymorphism.py +4 -4
  111. omlish/reflect.py +139 -18
  112. omlish/secrets/__init__.py +7 -0
  113. omlish/secrets/marshal.py +41 -0
  114. omlish/secrets/passwords.py +120 -0
  115. omlish/secrets/secrets.py +47 -0
  116. omlish/serde/__init__.py +0 -0
  117. omlish/serde/dotenv.py +574 -0
  118. omlish/{json.py → serde/json.py} +4 -2
  119. omlish/serde/props.py +604 -0
  120. omlish/serde/yaml.py +223 -0
  121. omlish/sql/dbs.py +1 -1
  122. omlish/sql/duckdb.py +136 -0
  123. omlish/sql/sqlean.py +17 -0
  124. omlish/sync.py +70 -0
  125. omlish/term.py +7 -2
  126. omlish/testing/pytest/__init__.py +8 -2
  127. omlish/testing/pytest/helpers.py +0 -24
  128. omlish/testing/pytest/inject/harness.py +4 -4
  129. omlish/testing/pytest/marks.py +45 -0
  130. omlish/testing/pytest/plugins/__init__.py +3 -0
  131. omlish/testing/pytest/plugins/asyncs.py +136 -0
  132. omlish/testing/pytest/plugins/managermarks.py +60 -0
  133. omlish/testing/pytest/plugins/pydevd.py +1 -1
  134. omlish/testing/testing.py +10 -0
  135. omlish/text/delimit.py +4 -0
  136. omlish/text/glyphsplit.py +92 -0
  137. {omlish-0.0.0.dev4.dist-info → omlish-0.0.0.dev6.dist-info}/METADATA +1 -1
  138. omlish-0.0.0.dev6.dist-info/RECORD +240 -0
  139. {omlish-0.0.0.dev4.dist-info → omlish-0.0.0.dev6.dist-info}/WHEEL +1 -1
  140. omlish/configs/props.py +0 -64
  141. omlish-0.0.0.dev4.dist-info/RECORD +0 -195
  142. {omlish-0.0.0.dev4.dist-info → omlish-0.0.0.dev6.dist-info}/LICENSE +0 -0
  143. {omlish-0.0.0.dev4.dist-info → omlish-0.0.0.dev6.dist-info}/top_level.txt +0 -0
omlish/reflect.py CHANGED
@@ -1,5 +1,6 @@
1
1
  """
2
2
  TODO:
3
+ - visitor / transformer
3
4
  - uniform collection isinstance - items() for mappings, iter() for other
4
5
  - also check instance type in isinstance not just items lol
5
6
  - ta.Generic in mro causing trouble - omit? no longer 1:1
@@ -7,12 +8,14 @@ TODO:
7
8
  - cache __hash__ in Generic/Union
8
9
  """
9
10
  import collections.abc
10
- import typing as ta
11
+ import dataclasses as dc
11
12
  import types
13
+ import typing as ta
12
14
 
13
15
  from . import c3
14
16
  from . import lang
15
17
 
18
+
16
19
  if ta.TYPE_CHECKING:
17
20
  from .collections import cache
18
21
  else:
@@ -25,8 +28,10 @@ _NONE_TYPE_FROZENSET: frozenset['Type'] = frozenset([_NoneType])
25
28
 
26
29
 
27
30
  _GenericAlias = ta._GenericAlias # type: ignore # noqa
31
+ _CallableGenericAlias = ta._CallableGenericAlias # type: ignore # noqa
28
32
  _SpecialGenericAlias = ta._SpecialGenericAlias # type: ignore # noqa
29
33
  _UnionGenericAlias = ta._UnionGenericAlias # type: ignore # noqa
34
+ _AnnotatedAlias = ta._AnnotatedAlias # type: ignore # noqa
30
35
 
31
36
 
32
37
  ##
@@ -54,9 +59,11 @@ _KNOWN_SPECIALS_BY_NAME = {s.name: s for s in _KNOWN_SPECIALS}
54
59
  _KNOWN_SPECIALS_BY_ALIAS = {s.alias: s for s in _KNOWN_SPECIALS}
55
60
  _KNOWN_SPECIALS_BY_ORIGIN = {s.origin: s for s in _KNOWN_SPECIALS}
56
61
 
62
+ _MAX_KNOWN_SPECIAL_TYPE_VARS = 16
63
+
57
64
  _KNOWN_SPECIAL_TYPE_VARS = tuple(
58
65
  ta.TypeVar(f'_{i}') # noqa
59
- for i in range(max(s.nparams for s in _KNOWN_SPECIALS) + 1)
66
+ for i in range(_MAX_KNOWN_SPECIAL_TYPE_VARS)
60
67
  )
61
68
 
62
69
 
@@ -73,28 +80,40 @@ def get_params(obj: ta.Any) -> tuple[ta.TypeVar, ...]:
73
80
 
74
81
  oty = type(obj)
75
82
 
76
- 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
+ ):
77
87
  return obj.__dict__.get('__parameters__', ()) # noqa
78
88
 
89
+ if oty is _CallableGenericAlias:
90
+ raise NotImplementedError('get_params not yet implemented for typing.Callable')
91
+
79
92
  raise TypeError(obj)
80
93
 
81
94
 
82
95
  def is_union_type(cls: ta.Any) -> bool:
83
96
  if hasattr(ta, 'UnionType'):
84
97
  return ta.get_origin(cls) in {ta.Union, getattr(ta, 'UnionType')}
98
+
85
99
  else:
86
100
  return ta.get_origin(cls) in {ta.Union}
87
101
 
88
102
 
103
+ def get_orig_class(obj: ta.Any) -> ta.Any:
104
+ return obj.__orig_class__ # noqa
105
+
106
+
89
107
  ##
90
108
 
91
109
 
92
110
  Type = ta.Union[
93
111
  type,
94
112
  ta.TypeVar,
95
- 'NewType',
96
113
  'Union',
97
114
  'Generic',
115
+ 'NewType',
116
+ 'Annotated',
98
117
  ]
99
118
 
100
119
 
@@ -114,19 +133,26 @@ class Union(ta.NamedTuple):
114
133
  return Union(rem)
115
134
 
116
135
 
117
- class Generic(ta.NamedTuple):
136
+ @dc.dataclass(frozen=True)
137
+ class Generic:
118
138
  cls: type
119
- args: tuple[Type, ...] # map[int, V] = (int, V) | map[T, T] = (T, T)
120
- params: tuple[ta.TypeVar, ...] # map[int, V] = (_0, _1) | map[T, T] = (_0, _1)
121
- # params2: tuple[ta.TypeVar, ...] # map[int, V] = (V,) | map[T, T] = (T,)
122
- 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)
123
145
 
124
- 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:
125
151
  return (
126
- f'{self.__class__.__name__}('
127
- f'cls={self.cls.__name__}, '
128
- f'args={self.args!r}, '
129
- 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
130
156
  )
131
157
 
132
158
 
@@ -134,15 +160,50 @@ class NewType(ta.NamedTuple):
134
160
  obj: ta.Any
135
161
 
136
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
+
137
171
  TYPES: tuple[type, ...] = (
138
172
  type,
139
173
  ta.TypeVar,
140
174
  Union,
141
175
  Generic,
142
176
  NewType,
177
+ Annotated,
143
178
  )
144
179
 
145
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
+
146
207
  def type_(obj: ta.Any) -> Type:
147
208
  if isinstance(obj, (Union, Generic, ta.TypeVar, NewType)): # noqa
148
209
  return obj
@@ -153,12 +214,22 @@ def type_(obj: ta.Any) -> Type:
153
214
  return Union(frozenset(type_(a) for a in ta.get_args(obj)))
154
215
 
155
216
  if isinstance(obj, ta.NewType): # noqa
156
- return NewType(oty)
217
+ return NewType(obj)
157
218
 
158
- if oty is _GenericAlias or oty is ta.GenericAlias: # type: ignore # noqa
219
+ if (
220
+ oty is _GenericAlias or
221
+ oty is ta.GenericAlias or # type: ignore # noqa
222
+ oty is _CallableGenericAlias
223
+ ):
159
224
  origin = ta.get_origin(obj)
160
225
  args = ta.get_args(obj)
161
- 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:
162
233
  params = args
163
234
  else:
164
235
  params = get_params(origin)
@@ -192,15 +263,52 @@ def type_(obj: ta.Any) -> Type:
192
263
  obj,
193
264
  )
194
265
 
266
+ if isinstance(obj, _AnnotatedAlias):
267
+ o = ta.get_args(obj)[0]
268
+ return Annotated(type_(o), md=obj.__metadata__, obj=obj)
269
+
195
270
  raise TypeError(obj)
196
271
 
197
272
 
198
273
  ##
199
274
 
200
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
+
201
308
  def types_equivalent(l: Type, r: Type) -> bool:
202
309
  if isinstance(l, Generic) and isinstance(r, Generic):
203
310
  return l.cls == r.cls and l.args == r.args
311
+
204
312
  return l == r
205
313
 
206
314
 
@@ -211,28 +319,36 @@ def get_underlying(nt: NewType) -> Type:
211
319
  def get_concrete_type(ty: Type) -> type | None:
212
320
  if isinstance(ty, type):
213
321
  return ty
322
+
214
323
  if isinstance(ty, Generic):
215
324
  return ty.cls
325
+
216
326
  if isinstance(ty, NewType):
217
327
  return get_concrete_type(get_underlying(ty))
328
+
218
329
  if isinstance(ty, (Union, ta.TypeVar)):
219
330
  return None
331
+
220
332
  raise TypeError(ty)
221
333
 
222
334
 
223
335
  def get_type_var_replacements(ty: Type) -> ta.Mapping[ta.TypeVar, Type]:
224
336
  if isinstance(ty, Generic):
225
337
  return dict(zip(ty.params, ty.args))
338
+
226
339
  return {}
227
340
 
228
341
 
229
342
  def to_annotation(ty: Type) -> ta.Any:
230
343
  if isinstance(ty, Generic):
231
344
  return ty.obj if ty.obj is not None else ty.cls
345
+
232
346
  if isinstance(ty, Union):
233
347
  return ta.Union[*tuple(to_annotation(e) for e in ty.args)]
348
+
234
349
  if isinstance(ty, (type, ta.TypeVar, NewType)):
235
350
  return ty
351
+
236
352
  raise TypeError(ty)
237
353
 
238
354
 
@@ -245,8 +361,10 @@ def replace_type_vars(
245
361
  def rec(cur):
246
362
  if isinstance(cur, type):
247
363
  return cur
364
+
248
365
  if isinstance(cur, NewType):
249
366
  return cur
367
+
250
368
  if isinstance(cur, Generic):
251
369
  args = tuple(rec(a) for a in cur.args)
252
370
  if update_aliases:
@@ -260,11 +378,14 @@ def replace_type_vars(
260
378
  obj = cur.obj[*nargs]
261
379
  else:
262
380
  obj = None
263
- return cur._replace(args=args, obj=obj)
381
+ return dc.replace(cur, args=args, obj=obj)
382
+
264
383
  if isinstance(cur, Union):
265
384
  return Union(frozenset(rec(e) for e in cur.args))
385
+
266
386
  if isinstance(cur, ta.TypeVar):
267
387
  return rpl[cur]
388
+
268
389
  raise TypeError(cur)
269
390
 
270
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