omlish 0.0.0.dev6__py3-none-any.whl → 0.0.0.dev8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. omlish/__about__.py +109 -5
  2. omlish/__init__.py +0 -8
  3. omlish/asyncs/__init__.py +0 -9
  4. omlish/asyncs/anyio.py +40 -0
  5. omlish/bootstrap.py +737 -0
  6. omlish/check.py +1 -1
  7. omlish/collections/__init__.py +4 -0
  8. omlish/collections/exceptions.py +2 -0
  9. omlish/collections/utils.py +38 -9
  10. omlish/configs/strings.py +2 -0
  11. omlish/dataclasses/__init__.py +7 -0
  12. omlish/dataclasses/impl/descriptors.py +95 -0
  13. omlish/dataclasses/impl/reflect.py +1 -1
  14. omlish/dataclasses/utils.py +23 -0
  15. omlish/{lang/datetimes.py → datetimes.py} +8 -4
  16. omlish/diag/procfs.py +1 -1
  17. omlish/diag/threads.py +131 -48
  18. omlish/docker.py +16 -1
  19. omlish/fnpairs.py +0 -4
  20. omlish/{serde → formats}/dotenv.py +3 -0
  21. omlish/{serde → formats}/yaml.py +2 -2
  22. omlish/graphs/trees.py +1 -1
  23. omlish/http/consts.py +6 -0
  24. omlish/http/sessions.py +2 -2
  25. omlish/inject/__init__.py +4 -0
  26. omlish/inject/binder.py +3 -3
  27. omlish/inject/elements.py +1 -1
  28. omlish/inject/impl/injector.py +57 -27
  29. omlish/inject/impl/origins.py +2 -0
  30. omlish/inject/origins.py +3 -0
  31. omlish/inject/utils.py +18 -0
  32. omlish/iterators.py +69 -2
  33. omlish/lang/__init__.py +16 -7
  34. omlish/lang/classes/restrict.py +10 -0
  35. omlish/lang/contextmanagers.py +1 -1
  36. omlish/lang/descriptors.py +3 -3
  37. omlish/lang/imports.py +67 -0
  38. omlish/lang/iterables.py +40 -0
  39. omlish/lang/maybes.py +3 -0
  40. omlish/lang/objects.py +38 -0
  41. omlish/lang/strings.py +25 -0
  42. omlish/lang/sys.py +9 -0
  43. omlish/lang/typing.py +37 -0
  44. omlish/lite/__init__.py +1 -0
  45. omlish/lite/cached.py +18 -0
  46. omlish/lite/check.py +29 -0
  47. omlish/lite/contextmanagers.py +18 -0
  48. omlish/lite/json.py +30 -0
  49. omlish/lite/logs.py +121 -0
  50. omlish/lite/marshal.py +318 -0
  51. omlish/lite/reflect.py +49 -0
  52. omlish/lite/runtime.py +18 -0
  53. omlish/lite/secrets.py +19 -0
  54. omlish/lite/strings.py +25 -0
  55. omlish/lite/subprocesses.py +112 -0
  56. omlish/logs/__init__.py +13 -9
  57. omlish/logs/configs.py +17 -22
  58. omlish/logs/formatters.py +3 -48
  59. omlish/marshal/__init__.py +28 -0
  60. omlish/marshal/any.py +5 -5
  61. omlish/marshal/base.py +27 -11
  62. omlish/marshal/base64.py +24 -9
  63. omlish/marshal/dataclasses.py +34 -28
  64. omlish/marshal/datetimes.py +74 -18
  65. omlish/marshal/enums.py +14 -8
  66. omlish/marshal/exceptions.py +11 -1
  67. omlish/marshal/factories.py +59 -74
  68. omlish/marshal/forbidden.py +35 -0
  69. omlish/marshal/global_.py +11 -4
  70. omlish/marshal/iterables.py +21 -24
  71. omlish/marshal/mappings.py +23 -26
  72. omlish/marshal/numbers.py +51 -0
  73. omlish/marshal/optionals.py +11 -12
  74. omlish/marshal/polymorphism.py +86 -21
  75. omlish/marshal/primitives.py +4 -5
  76. omlish/marshal/standard.py +13 -8
  77. omlish/marshal/uuids.py +4 -5
  78. omlish/matchfns.py +218 -0
  79. omlish/os.py +64 -0
  80. omlish/reflect/__init__.py +39 -0
  81. omlish/reflect/isinstance.py +38 -0
  82. omlish/reflect/ops.py +84 -0
  83. omlish/reflect/subst.py +110 -0
  84. omlish/reflect/types.py +275 -0
  85. omlish/secrets/__init__.py +18 -2
  86. omlish/secrets/crypto.py +132 -0
  87. omlish/secrets/marshal.py +36 -7
  88. omlish/secrets/openssl.py +207 -0
  89. omlish/secrets/secrets.py +260 -8
  90. omlish/secrets/subprocesses.py +42 -0
  91. omlish/sql/dbs.py +6 -5
  92. omlish/sql/exprs.py +12 -0
  93. omlish/sql/secrets.py +10 -0
  94. omlish/term.py +1 -1
  95. omlish/testing/pytest/plugins/switches.py +54 -19
  96. omlish/text/glyphsplit.py +5 -0
  97. omlish-0.0.0.dev8.dist-info/METADATA +50 -0
  98. {omlish-0.0.0.dev6.dist-info → omlish-0.0.0.dev8.dist-info}/RECORD +105 -78
  99. {omlish-0.0.0.dev6.dist-info → omlish-0.0.0.dev8.dist-info}/WHEEL +1 -1
  100. omlish/logs/filters.py +0 -11
  101. omlish/reflect.py +0 -470
  102. omlish-0.0.0.dev6.dist-info/METADATA +0 -34
  103. /omlish/{asyncs/futures.py → concurrent.py} +0 -0
  104. /omlish/{serde → formats}/__init__.py +0 -0
  105. /omlish/{serde → formats}/json.py +0 -0
  106. /omlish/{serde → formats}/props.py +0 -0
  107. {omlish-0.0.0.dev6.dist-info → omlish-0.0.0.dev8.dist-info}/LICENSE +0 -0
  108. {omlish-0.0.0.dev6.dist-info → omlish-0.0.0.dev8.dist-info}/top_level.txt +0 -0
omlish/reflect/ops.py ADDED
@@ -0,0 +1,84 @@
1
+ import typing as ta
2
+
3
+ from .types import Annotated
4
+ from .types import Any
5
+ from .types import Generic
6
+ from .types import NewType
7
+ from .types import Type
8
+ from .types import Union
9
+ from .types import type_
10
+
11
+
12
+ def strip_objs(ty: Type) -> Type:
13
+ if isinstance(ty, (type, ta.TypeVar, NewType, Any)):
14
+ return ty
15
+
16
+ if isinstance(ty, Union):
17
+ return Union(frozenset(map(strip_objs, ty.args)))
18
+
19
+ if isinstance(ty, Generic):
20
+ return Generic(ty.cls, tuple(map(strip_objs, ty.args)), ty.params, None)
21
+
22
+ if isinstance(ty, Annotated):
23
+ return Annotated(strip_objs(ty.ty), ty.md, None)
24
+
25
+ raise TypeError(ty)
26
+
27
+
28
+ def strip_annotations(ty: Type) -> Type:
29
+ if isinstance(ty, (type, ta.TypeVar, NewType, Any)):
30
+ return ty
31
+
32
+ if isinstance(ty, Union):
33
+ return Union(frozenset(map(strip_annotations, ty.args)))
34
+
35
+ if isinstance(ty, Generic):
36
+ return Generic(ty.cls, tuple(map(strip_annotations, ty.args)), ty.params, ty.obj)
37
+
38
+ if isinstance(ty, Annotated):
39
+ return strip_annotations(ty.ty)
40
+
41
+ raise TypeError(ty)
42
+
43
+
44
+ def types_equivalent(l: Type, r: Type) -> bool:
45
+ if isinstance(l, Generic) and isinstance(r, Generic):
46
+ return l.cls == r.cls and l.args == r.args
47
+
48
+ return l == r
49
+
50
+
51
+ def get_underlying(nt: NewType) -> Type:
52
+ return type_(nt.obj.__supertype__) # noqa
53
+
54
+
55
+ def get_concrete_type(ty: Type) -> type | None:
56
+ if isinstance(ty, type):
57
+ return ty
58
+
59
+ if isinstance(ty, Generic):
60
+ return ty.cls
61
+
62
+ if isinstance(ty, NewType):
63
+ return get_concrete_type(get_underlying(ty))
64
+
65
+ if isinstance(ty, (Union, ta.TypeVar, Any)):
66
+ return None
67
+
68
+ raise TypeError(ty)
69
+
70
+
71
+ def to_annotation(ty: Type) -> ta.Any:
72
+ if isinstance(ty, Generic):
73
+ return ty.obj if ty.obj is not None else ty.cls
74
+
75
+ if isinstance(ty, Union):
76
+ return ta.Union[*tuple(to_annotation(e) for e in ty.args)]
77
+
78
+ if isinstance(ty, (type, ta.TypeVar, NewType)):
79
+ return ty
80
+
81
+ if isinstance(ty, Any):
82
+ return ta.Any
83
+
84
+ raise TypeError(ty)
@@ -0,0 +1,110 @@
1
+ import dataclasses as dc
2
+ import types
3
+ import typing as ta
4
+
5
+ from .. import c3
6
+ from .. import lang
7
+ from .ops import get_concrete_type
8
+ from .ops import to_annotation
9
+ from .types import Any
10
+ from .types import Generic
11
+ from .types import NewType
12
+ from .types import Type
13
+ from .types import Union
14
+ from .types import get_params
15
+ from .types import type_
16
+
17
+
18
+ if ta.TYPE_CHECKING:
19
+ from ..collections import cache
20
+ else:
21
+ cache = lang.proxy_import('.collections.cache', __package__)
22
+
23
+
24
+ def get_type_var_replacements(ty: Type) -> ta.Mapping[ta.TypeVar, Type]:
25
+ if isinstance(ty, Generic):
26
+ return dict(zip(ty.params, ty.args))
27
+
28
+ return {}
29
+
30
+
31
+ def replace_type_vars(
32
+ ty: Type,
33
+ rpl: ta.Mapping[ta.TypeVar, Type],
34
+ *,
35
+ update_aliases: bool = False,
36
+ ) -> Type:
37
+ def rec(cur):
38
+ if isinstance(cur, (type, NewType, Any)):
39
+ return cur
40
+
41
+ if isinstance(cur, Generic):
42
+ args = tuple(rec(a) for a in cur.args)
43
+ if update_aliases:
44
+ obj = cur.obj
45
+ if (ops := get_params(obj)):
46
+ nargs = [to_annotation(rpl[p]) for p in ops]
47
+ if ta.get_origin(obj) is ta.Generic:
48
+ # FIXME: None? filter_typing_generic in get_generic_bases?
49
+ pass
50
+ else:
51
+ obj = cur.obj[*nargs]
52
+ else:
53
+ obj = None
54
+ return dc.replace(cur, args=args, obj=obj)
55
+
56
+ if isinstance(cur, Union):
57
+ return Union(frozenset(rec(e) for e in cur.args))
58
+
59
+ if isinstance(cur, ta.TypeVar):
60
+ return rpl[cur]
61
+
62
+ raise TypeError(cur)
63
+
64
+ return rec(ty)
65
+
66
+
67
+ class GenericSubstitution:
68
+ def __init__(
69
+ self,
70
+ *,
71
+ update_aliases: bool = False,
72
+ cache_size: int = 0, # FIXME: ta.Generic isn't weakrefable..
73
+ ) -> None:
74
+ super().__init__()
75
+
76
+ self._update_aliases = update_aliases
77
+
78
+ if cache_size > 0:
79
+ self.get_generic_bases = cache.cache(weak_keys=True, max_size=cache_size)(self.get_generic_bases) # type: ignore # noqa
80
+ self.generic_mro = cache.cache(weak_keys=True, max_size=cache_size)(self.generic_mro) # type: ignore
81
+
82
+ def get_generic_bases(self, ty: Type) -> tuple[Type, ...]:
83
+ if (cty := get_concrete_type(ty)) is not None:
84
+ rpl = get_type_var_replacements(ty)
85
+ ret: list[Type] = []
86
+ for b in types.get_original_bases(cty):
87
+ bty = type_(b)
88
+ if isinstance(bty, Generic) and isinstance(b, type):
89
+ # FIXME: throws away relative types, but can't use original vars as they're class-contextual
90
+ bty = type_(b[*((ta.Any,) * len(bty.params))]) # type: ignore
91
+ rty = replace_type_vars(bty, rpl, update_aliases=self._update_aliases)
92
+ ret.append(rty)
93
+ return tuple(ret)
94
+ return ()
95
+
96
+ def generic_mro(self, obj: ta.Any) -> list[Type]:
97
+ mro = c3.mro(
98
+ type_(obj),
99
+ get_bases=lambda t: self.get_generic_bases(t),
100
+ is_subclass=lambda l, r: issubclass(get_concrete_type(l), get_concrete_type(r)), # type: ignore
101
+ )
102
+ return [ty for ty in mro if get_concrete_type(ty) is not ta.Generic]
103
+
104
+
105
+ DEFAULT_GENERIC_SUBSTITUTION = GenericSubstitution()
106
+
107
+ get_generic_bases = DEFAULT_GENERIC_SUBSTITUTION.get_generic_bases
108
+ generic_mro = DEFAULT_GENERIC_SUBSTITUTION.generic_mro
109
+
110
+ ALIAS_UPDATING_GENERIC_SUBSTITUTION = GenericSubstitution(update_aliases=True)
@@ -0,0 +1,275 @@
1
+ """
2
+ TODO:
3
+ - visitor / transformer
4
+ - uniform collection isinstance - items() for mappings, iter() for other
5
+ - also check instance type in isinstance not just items lol
6
+ - ta.Generic in mro causing trouble - omit? no longer 1:1
7
+ - cache this shit, esp generic_mro shit
8
+ - cache __hash__ in Generic/Union
9
+ """
10
+ import dataclasses as dc
11
+ import types
12
+ import typing as ta
13
+
14
+
15
+ _NoneType = types.NoneType # type: ignore
16
+
17
+ _NONE_TYPE_FROZENSET: frozenset['Type'] = frozenset([_NoneType])
18
+
19
+
20
+ _GenericAlias = ta._GenericAlias # type: ignore # noqa
21
+ _CallableGenericAlias = ta._CallableGenericAlias # type: ignore # noqa
22
+ _SpecialGenericAlias = ta._SpecialGenericAlias # type: ignore # noqa
23
+ _UnionGenericAlias = ta._UnionGenericAlias # type: ignore # noqa
24
+ _AnnotatedAlias = ta._AnnotatedAlias # type: ignore # noqa
25
+
26
+
27
+ ##
28
+
29
+
30
+ @dc.dataclass(frozen=True)
31
+ class _Special:
32
+ name: str
33
+ alias: _SpecialGenericAlias # type: ignore
34
+ origin: type
35
+ nparams: int
36
+
37
+
38
+ _KNOWN_SPECIALS = [
39
+ _Special(
40
+ v._name, # noqa
41
+ v,
42
+ v.__origin__,
43
+ v._nparams, # noqa
44
+ )
45
+ for v in ta.__dict__.values()
46
+ if isinstance(v, _SpecialGenericAlias)
47
+ ]
48
+
49
+ _KNOWN_SPECIALS_BY_NAME = {s.name: s for s in _KNOWN_SPECIALS}
50
+ _KNOWN_SPECIALS_BY_ALIAS = {s.alias: s for s in _KNOWN_SPECIALS}
51
+ _KNOWN_SPECIALS_BY_ORIGIN = {s.origin: s for s in _KNOWN_SPECIALS}
52
+
53
+ _MAX_KNOWN_SPECIAL_TYPE_VARS = 16
54
+
55
+ _KNOWN_SPECIAL_TYPE_VARS = tuple(
56
+ ta.TypeVar(f'_{i}') # noqa
57
+ for i in range(_MAX_KNOWN_SPECIAL_TYPE_VARS)
58
+ )
59
+
60
+
61
+ ##
62
+
63
+
64
+ def get_params(obj: ta.Any) -> tuple[ta.TypeVar, ...]:
65
+ if isinstance(obj, type):
66
+ if issubclass(obj, ta.Generic): # type: ignore
67
+ return obj.__dict__.get('__parameters__', ()) # noqa
68
+
69
+ if (ks := _KNOWN_SPECIALS_BY_ORIGIN.get(obj)) is not None:
70
+ return _KNOWN_SPECIAL_TYPE_VARS[:ks.nparams]
71
+
72
+ oty = type(obj)
73
+
74
+ if (
75
+ oty is _GenericAlias or
76
+ oty is ta.GenericAlias # type: ignore # noqa
77
+ ):
78
+ return obj.__dict__.get('__parameters__', ()) # noqa
79
+
80
+ if oty is _CallableGenericAlias:
81
+ raise NotImplementedError('get_params not yet implemented for typing.Callable')
82
+
83
+ raise TypeError(obj)
84
+
85
+
86
+ def is_union_type(cls: ta.Any) -> bool:
87
+ if hasattr(ta, 'UnionType'):
88
+ return ta.get_origin(cls) in {ta.Union, getattr(ta, 'UnionType')}
89
+
90
+ else:
91
+ return ta.get_origin(cls) in {ta.Union}
92
+
93
+
94
+ def get_orig_class(obj: ta.Any) -> ta.Any:
95
+ return obj.__orig_class__ # noqa
96
+
97
+
98
+ ##
99
+
100
+
101
+ Type: ta.TypeAlias = ta.Union[
102
+ type,
103
+ ta.TypeVar,
104
+ 'Union',
105
+ 'Generic',
106
+ 'NewType',
107
+ 'Annotated',
108
+ 'Any',
109
+ ]
110
+
111
+
112
+ @dc.dataclass(frozen=True)
113
+ class Union:
114
+ args: frozenset[Type]
115
+
116
+ @property
117
+ def is_optional(self) -> bool:
118
+ return _NoneType in self.args
119
+
120
+ def without_none(self) -> Type:
121
+ if _NoneType not in self.args:
122
+ return self
123
+ rem = self.args - _NONE_TYPE_FROZENSET
124
+ if len(rem) == 1:
125
+ return next(iter(rem))
126
+ return Union(rem)
127
+
128
+
129
+ @dc.dataclass(frozen=True)
130
+ class Generic:
131
+ cls: type
132
+ args: tuple[Type, ...] # map[int, V] = (int, V) | map[T, T] = (T, T)
133
+
134
+ params: tuple[ta.TypeVar, ...] = dc.field(compare=False, repr=False) # map[int, V] = (_0, _1) | map[T, T] = (_0, _1) # noqa
135
+ # params2: tuple[ta.TypeVar, ...] # map[int, V] = (V,) | map[T, T] = (T,)
136
+
137
+ obj: ta.Any = dc.field(compare=False, repr=False)
138
+
139
+ # def __post_init__(self) -> None:
140
+ # if not isinstance(self.cls, type):
141
+ # raise TypeError(self.cls)
142
+
143
+ def full_eq(self, other: 'Generic') -> bool:
144
+ return (
145
+ self.cls == other.cls and
146
+ self.args == other.args and
147
+ self.params == other.params and
148
+ self.obj == other.obj
149
+ )
150
+
151
+
152
+ @dc.dataclass(frozen=True)
153
+ class NewType:
154
+ obj: ta.Any
155
+
156
+
157
+ @dc.dataclass(frozen=True)
158
+ class Annotated:
159
+ ty: Type
160
+ md: ta.Sequence[ta.Any]
161
+
162
+ obj: ta.Any = dc.field(compare=False, repr=False)
163
+
164
+
165
+ class Any:
166
+ pass
167
+
168
+
169
+ ANY = Any()
170
+
171
+
172
+ TYPES: tuple[type, ...] = (
173
+ type,
174
+ ta.TypeVar,
175
+ Union,
176
+ Generic,
177
+ NewType,
178
+ Annotated,
179
+ Any,
180
+ )
181
+
182
+
183
+ ##
184
+
185
+
186
+ def is_type(obj: ta.Any) -> bool:
187
+ if isinstance(obj, (Union, Generic, ta.TypeVar, NewType, Any)): # noqa
188
+ return True
189
+
190
+ oty = type(obj)
191
+
192
+ return (
193
+ oty is _UnionGenericAlias or oty is types.UnionType or # noqa
194
+
195
+ isinstance(obj, ta.NewType) or # noqa
196
+
197
+ (
198
+ oty is _GenericAlias or
199
+ oty is ta.GenericAlias or # type: ignore # noqa
200
+ oty is _CallableGenericAlias
201
+ ) or
202
+
203
+ isinstance(obj, type) or
204
+
205
+ isinstance(obj, _SpecialGenericAlias)
206
+ )
207
+
208
+
209
+ def type_(obj: ta.Any) -> Type:
210
+ if obj is ta.Any:
211
+ return ANY
212
+
213
+ if isinstance(obj, (Union, Generic, ta.TypeVar, NewType, Any)): # noqa
214
+ return obj
215
+
216
+ oty = type(obj)
217
+
218
+ if oty is _UnionGenericAlias or oty is types.UnionType:
219
+ return Union(frozenset(type_(a) for a in ta.get_args(obj)))
220
+
221
+ if isinstance(obj, ta.NewType): # noqa
222
+ return NewType(obj)
223
+
224
+ if (
225
+ oty is _GenericAlias or
226
+ oty is ta.GenericAlias or # type: ignore # noqa
227
+ oty is _CallableGenericAlias
228
+ ):
229
+ origin = ta.get_origin(obj)
230
+ args = ta.get_args(obj)
231
+ if oty is _CallableGenericAlias:
232
+ p, r = args
233
+ if p is Ellipsis or isinstance(p, ta.ParamSpec):
234
+ raise TypeError(f'Callable argument not yet supported for {obj=}')
235
+ args = (*p, r)
236
+ params = _KNOWN_SPECIAL_TYPE_VARS[:len(args)]
237
+ elif origin is ta.Generic:
238
+ params = args
239
+ else:
240
+ params = get_params(origin)
241
+ if len(args) != len(params):
242
+ raise TypeError(f'Mismatched {args=} and {params=} for {obj=}')
243
+ return Generic(
244
+ origin,
245
+ tuple(type_(a) for a in args),
246
+ params,
247
+ obj,
248
+ )
249
+
250
+ if isinstance(obj, type):
251
+ if issubclass(obj, ta.Generic): # type: ignore
252
+ params = get_params(obj)
253
+ return Generic(
254
+ obj,
255
+ params,
256
+ params,
257
+ obj,
258
+ )
259
+ return obj
260
+
261
+ if isinstance(obj, _SpecialGenericAlias):
262
+ if (ks := _KNOWN_SPECIALS_BY_ALIAS.get(obj)) is not None:
263
+ params = _KNOWN_SPECIAL_TYPE_VARS[:ks.nparams]
264
+ return Generic(
265
+ ks.origin,
266
+ params,
267
+ params,
268
+ obj,
269
+ )
270
+
271
+ if isinstance(obj, _AnnotatedAlias):
272
+ o = ta.get_args(obj)[0]
273
+ return Annotated(type_(o), md=obj.__metadata__, obj=obj)
274
+
275
+ raise TypeError(obj)
@@ -1,7 +1,23 @@
1
1
  from .secrets import ( # noqa
2
+ CachingSecrets,
3
+ CompositeSecrets,
2
4
  EMPTY_SECRETS,
3
5
  EmptySecrets,
4
- Secret,
6
+ EnvVarSecrets,
7
+ FnSecrets,
8
+ LoggingSecrets,
9
+ MappingSecrets,
10
+ SecretRef,
11
+ SecretRefOrStr,
5
12
  Secrets,
6
- SimpleSecrets,
13
+ secret_field,
14
+ secret_repr,
7
15
  )
16
+
17
+
18
+ ##
19
+
20
+
21
+ from ..lang.imports import _register_conditional_import # noqa
22
+
23
+ _register_conditional_import('..marshal', '.marshal', __package__)
@@ -0,0 +1,132 @@
1
+ """
2
+ TODO:
3
+ - cryptography vs pycryptodome[x]
4
+ - standardize failure exception
5
+ - chains - take first
6
+ - keysets
7
+
8
+ See:
9
+ - https://soatok.blog/2020/05/13/why-aes-gcm-sucks/
10
+ - https://pycryptodome.readthedocs.io/en/latest/src/cipher/chacha20_poly1305.html
11
+
12
+ ==
13
+
14
+ https://www.tecmint.com/gpg-encrypt-decrypt-files/
15
+
16
+ ==
17
+
18
+ gpg --batch --passphrase '' --quick-gen-key wrmsr default default
19
+ gpg --batch --passphrase '' --quick-gen-key wrmsr2 default default
20
+ echo 'hi there' > secret.txt
21
+ gpg -e -u wrmsr -r wrmsr2 secret.txt
22
+ gpg -d -o secret2.txt secret.txt.gpg
23
+
24
+ gpg --batch -c --passphrase-file /var/secret.key -o some.gpg toencrypt.txt
25
+
26
+ openssl rand -rand /dev/urandom 128 > barf.key
27
+ openssl enc -in secret.txt -out secret.txt.enc -e -aes256 -pbkdf2 -kfile barf.key
28
+ openssl aes-256-cbc -d -pbkdf2 -in secret.txt.enc -out secret3.txt -kfile barf.key
29
+
30
+ https://wiki.openssl.org/index.php/Enc#Options
31
+ -pass 'file:...'
32
+ """
33
+ import abc
34
+ import secrets
35
+ import typing as ta
36
+
37
+ from .. import lang
38
+
39
+
40
+ if ta.TYPE_CHECKING:
41
+ from cryptography import exceptions as cry_exc
42
+ from cryptography import fernet as cry_fernet
43
+ from cryptography.hazmat.primitives.ciphers import aead as cry_aead
44
+ else:
45
+ cry_aead = lang.proxy_import('cryptography.hazmat.primitives.ciphers.aead')
46
+ cry_exc = lang.proxy_import('cryptography.exceptions')
47
+ cry_fernet = lang.proxy_import('cryptography.fernet')
48
+
49
+
50
+ ##
51
+
52
+
53
+ class EncryptionError(Exception):
54
+ pass
55
+
56
+
57
+ class DecryptionError(Exception):
58
+ pass
59
+
60
+
61
+ class Crypto(abc.ABC):
62
+ @abc.abstractmethod
63
+ def generate_key(self) -> bytes:
64
+ raise NotImplementedError
65
+
66
+ @abc.abstractmethod
67
+ def encrypt(self, data: bytes, key: bytes) -> bytes:
68
+ raise NotImplementedError
69
+
70
+ @abc.abstractmethod
71
+ def decrypt(self, data: bytes, key: bytes) -> bytes:
72
+ raise NotImplementedError
73
+
74
+
75
+ ##
76
+
77
+
78
+ class FernetCrypto(Crypto):
79
+
80
+ def generate_key(self) -> bytes:
81
+ return cry_fernet.Fernet.generate_key()
82
+
83
+ def encrypt(self, data: bytes, key: bytes) -> bytes:
84
+ try:
85
+ f = cry_fernet.Fernet(key)
86
+ except ValueError as e:
87
+ raise EncryptionError from e
88
+ return f.encrypt(data)
89
+
90
+ def decrypt(self, data: bytes, key: bytes) -> bytes:
91
+ try:
92
+ f = cry_fernet.Fernet(key)
93
+ except ValueError as e:
94
+ raise DecryptionError from e
95
+ try:
96
+ return f.decrypt(data)
97
+ except cry_fernet.InvalidToken as e:
98
+ raise DecryptionError from e
99
+
100
+
101
+ class AesgsmCrypto(Crypto):
102
+ """https://stackoverflow.com/a/59835994"""
103
+
104
+ def generate_key(self) -> bytes:
105
+ return secrets.token_bytes(32)
106
+
107
+ def encrypt(self, data: bytes, key: bytes) -> bytes:
108
+ nonce = secrets.token_bytes(12)
109
+ return nonce + cry_aead.AESGCM(key).encrypt(nonce, data, b'')
110
+
111
+ def decrypt(self, data: bytes, key: bytes) -> bytes:
112
+ try:
113
+ return cry_aead.AESGCM(key).decrypt(data[:12], data[12:], b'')
114
+ except cry_exc.InvalidTag as e:
115
+ raise DecryptionError from e
116
+
117
+
118
+ class Chacha20Poly1305Crypto(Crypto):
119
+ """https://cryptography.io/en/latest/hazmat/primitives/aead/#cryptography.hazmat.primitives.ciphers.aead.ChaCha20Poly1305""" # noqa
120
+
121
+ def generate_key(self) -> bytes:
122
+ return cry_aead.ChaCha20Poly1305.generate_key()
123
+
124
+ def encrypt(self, data: bytes, key: bytes) -> bytes:
125
+ nonce = secrets.token_bytes(12)
126
+ return nonce + cry_aead.ChaCha20Poly1305(key).encrypt(nonce, data, b'')
127
+
128
+ def decrypt(self, data: bytes, key: bytes) -> bytes:
129
+ try:
130
+ return cry_aead.ChaCha20Poly1305(key).decrypt(data[:12], data[12:], b'')
131
+ except cry_exc.InvalidTag as e:
132
+ raise DecryptionError from e
omlish/secrets/marshal.py CHANGED
@@ -1,23 +1,30 @@
1
+ """
2
+ TODO:
3
+ - ensure import order or at least warn or smth lol
4
+ - raise exception on ambiguous 'registered' impls
5
+ """
1
6
  import collections.abc
2
7
  import typing as ta
3
8
 
4
9
  from .. import check
5
10
  from .. import dataclasses as dc
11
+ from .. import lang
6
12
  from .. import marshal as msh
13
+ from .. import reflect as rfl
7
14
  from .secrets import Secret
15
+ from .secrets import SecretRef
16
+ from .secrets import SecretRefOrStr
8
17
 
9
18
 
10
- class StrOrSecretMarshaler(msh.Marshaler):
19
+ class StrOrSecretRefMarshalerUnmarshaler(msh.Marshaler, msh.Unmarshaler):
11
20
  def marshal(self, ctx: msh.MarshalContext, o: ta.Any) -> msh.Value:
12
21
  if isinstance(o, str):
13
22
  return o
14
- elif isinstance(o, Secret):
23
+ elif isinstance(o, SecretRef):
15
24
  return {'secret': o.key}
16
25
  else:
17
26
  raise TypeError(o)
18
27
 
19
-
20
- class StrOrSecretUnmarshaler(msh.Unmarshaler):
21
28
  def unmarshal(self, ctx: msh.UnmarshalContext, v: msh.Value) -> ta.Any:
22
29
  if isinstance(v, str):
23
30
  return v
@@ -25,17 +32,39 @@ class StrOrSecretUnmarshaler(msh.Unmarshaler):
25
32
  [(mk, mv)] = v.items()
26
33
  if mk != 'secret':
27
34
  raise TypeError(v)
28
- return Secret(check.isinstance(mv, str))
35
+ return SecretRef(check.isinstance(mv, str))
29
36
  else:
30
37
  raise TypeError(v)
31
38
 
32
39
 
33
40
  @dc.field_modifier
34
41
  def marshal_secret_field(f: dc.Field) -> dc.Field:
42
+ """Mostly obsolete with auto-registration below."""
43
+
35
44
  return dc.update_field_metadata(f, {
36
45
  msh.FieldMetadata: dc.replace(
37
46
  f.metadata.get(msh.FieldMetadata, msh.FieldMetadata()),
38
- marshaler=StrOrSecretMarshaler(),
39
- unmarshaler=StrOrSecretUnmarshaler(),
47
+ marshaler=StrOrSecretRefMarshalerUnmarshaler(),
48
+ unmarshaler=StrOrSecretRefMarshalerUnmarshaler(),
40
49
  ),
41
50
  })
51
+
52
+
53
+ @lang.cached_function
54
+ def _install_standard_marshalling() -> None:
55
+ msh.STANDARD_MARSHALER_FACTORIES[0:0] = [
56
+ msh.ForbiddenTypeMarshalerFactory({Secret}),
57
+ msh.TypeMapMarshalerFactory({
58
+ rfl.type_(SecretRefOrStr): StrOrSecretRefMarshalerUnmarshaler(),
59
+ }),
60
+ ]
61
+
62
+ msh.STANDARD_UNMARSHALER_FACTORIES[0:0] = [
63
+ msh.ForbiddenTypeUnmarshalerFactory({Secret}),
64
+ msh.TypeMapUnmarshalerFactory({
65
+ rfl.type_(SecretRefOrStr): StrOrSecretRefMarshalerUnmarshaler(),
66
+ }),
67
+ ]
68
+
69
+
70
+ _install_standard_marshalling()