omlish 0.0.0.dev4__py3-none-any.whl → 0.0.0.dev5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of omlish might be problematic. Click here for more details.
- omlish/__about__.py +1 -1
- omlish/__init__.py +1 -1
- omlish/asyncs/__init__.py +1 -4
- omlish/asyncs/anyio.py +66 -0
- omlish/asyncs/flavors.py +27 -1
- omlish/asyncs/trio_asyncio.py +24 -18
- omlish/c3.py +1 -1
- omlish/cached.py +1 -2
- omlish/collections/__init__.py +4 -1
- omlish/collections/cache/impl.py +1 -1
- omlish/collections/indexed.py +1 -1
- omlish/collections/utils.py +38 -6
- omlish/configs/__init__.py +5 -0
- omlish/configs/classes.py +53 -0
- omlish/configs/dotenv.py +586 -0
- omlish/configs/props.py +589 -49
- omlish/dataclasses/impl/api.py +1 -1
- omlish/dataclasses/impl/as_.py +1 -1
- omlish/dataclasses/impl/fields.py +1 -0
- omlish/dataclasses/impl/init.py +1 -1
- omlish/dataclasses/impl/main.py +1 -0
- omlish/dataclasses/impl/metaclass.py +6 -1
- omlish/dataclasses/impl/order.py +1 -1
- omlish/dataclasses/impl/reflect.py +15 -2
- omlish/defs.py +1 -1
- omlish/diag/procfs.py +29 -1
- omlish/diag/procstats.py +32 -0
- omlish/diag/replserver/console.py +3 -3
- omlish/diag/replserver/server.py +6 -5
- omlish/diag/threads.py +86 -0
- omlish/docker.py +19 -0
- omlish/fnpairs.py +26 -18
- omlish/graphs/dags.py +113 -0
- omlish/graphs/domination.py +268 -0
- omlish/graphs/trees.py +2 -2
- omlish/http/__init__.py +25 -0
- omlish/http/asgi.py +131 -0
- omlish/http/consts.py +31 -4
- omlish/http/cookies.py +194 -0
- omlish/http/dates.py +70 -0
- omlish/http/encodings.py +6 -0
- omlish/http/json.py +273 -0
- omlish/http/sessions.py +197 -0
- omlish/inject/__init__.py +8 -2
- omlish/inject/bindings.py +3 -3
- omlish/inject/exceptions.py +3 -3
- omlish/inject/impl/elements.py +33 -24
- omlish/inject/impl/injector.py +1 -0
- omlish/inject/impl/multis.py +74 -0
- omlish/inject/impl/providers.py +19 -39
- omlish/inject/{proxy.py → impl/proxy.py} +2 -2
- omlish/inject/impl/scopes.py +1 -0
- omlish/inject/injector.py +1 -0
- omlish/inject/keys.py +3 -9
- omlish/inject/multis.py +70 -0
- omlish/inject/providers.py +23 -23
- omlish/inject/scopes.py +7 -3
- omlish/inject/types.py +0 -8
- omlish/iterators.py +13 -0
- omlish/json.py +2 -1
- omlish/lang/__init__.py +4 -0
- omlish/lang/classes/restrict.py +1 -1
- omlish/lang/classes/virtual.py +2 -2
- omlish/lang/contextmanagers.py +64 -0
- omlish/lang/datetimes.py +6 -5
- omlish/lang/functions.py +10 -0
- omlish/lang/imports.py +11 -2
- omlish/lang/typing.py +1 -0
- omlish/logs/utils.py +1 -1
- omlish/marshal/datetimes.py +1 -1
- omlish/reflect.py +8 -2
- omlish/sync.py +70 -0
- omlish/term.py +6 -1
- omlish/testing/pytest/__init__.py +5 -0
- omlish/testing/pytest/helpers.py +0 -24
- omlish/testing/pytest/inject/harness.py +1 -1
- omlish/testing/pytest/marks.py +48 -0
- omlish/testing/pytest/plugins/__init__.py +2 -0
- omlish/testing/pytest/plugins/managermarks.py +60 -0
- omlish/testing/testing.py +10 -0
- omlish/text/delimit.py +4 -0
- {omlish-0.0.0.dev4.dist-info → omlish-0.0.0.dev5.dist-info}/METADATA +1 -1
- {omlish-0.0.0.dev4.dist-info → omlish-0.0.0.dev5.dist-info}/RECORD +86 -69
- {omlish-0.0.0.dev4.dist-info → omlish-0.0.0.dev5.dist-info}/WHEEL +1 -1
- {omlish-0.0.0.dev4.dist-info → omlish-0.0.0.dev5.dist-info}/LICENSE +0 -0
- {omlish-0.0.0.dev4.dist-info → omlish-0.0.0.dev5.dist-info}/top_level.txt +0 -0
omlish/configs/dotenv.py
ADDED
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
# Copyright (c) 2014, Saurabh Kumar (python-dotenv), 2013, Ted Tieken (django-dotenv-rw), 2013, Jacob Kaplan-Moss
|
|
2
|
+
# (django-dotenv)
|
|
3
|
+
#
|
|
4
|
+
# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
|
|
5
|
+
# following conditions are met:
|
|
6
|
+
#
|
|
7
|
+
# - Redistributions of source code must retain the above copyright notice, this list of conditions and the following
|
|
8
|
+
# disclaimer.
|
|
9
|
+
#
|
|
10
|
+
# - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
|
|
11
|
+
# disclaimer in the documentation and/or other materials provided with the distribution.
|
|
12
|
+
#
|
|
13
|
+
# - Neither the name of django-dotenv nor the names of its contributors may be used to endorse or promote products
|
|
14
|
+
# derived from this software without specific prior written permission.
|
|
15
|
+
#
|
|
16
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
|
17
|
+
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
18
|
+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
19
|
+
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
20
|
+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
|
21
|
+
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
22
|
+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
23
|
+
import abc
|
|
24
|
+
import codecs
|
|
25
|
+
import contextlib
|
|
26
|
+
import io
|
|
27
|
+
import logging
|
|
28
|
+
import os
|
|
29
|
+
import pathlib
|
|
30
|
+
import re
|
|
31
|
+
import shutil
|
|
32
|
+
import tempfile
|
|
33
|
+
import typing as ta
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
##
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
log = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
##
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
_posix_variable: ta.Pattern[str] = re.compile(
|
|
46
|
+
r"""
|
|
47
|
+
\$\{
|
|
48
|
+
(?P<name>[^}:]*)
|
|
49
|
+
(?::-
|
|
50
|
+
(?P<default>[^}]*)
|
|
51
|
+
)?
|
|
52
|
+
}
|
|
53
|
+
""",
|
|
54
|
+
re.VERBOSE,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Atom(metaclass=abc.ABCMeta):
|
|
59
|
+
def __ne__(self, other: object) -> bool:
|
|
60
|
+
result = self.__eq__(other)
|
|
61
|
+
if result is NotImplemented:
|
|
62
|
+
return NotImplemented
|
|
63
|
+
return not result
|
|
64
|
+
|
|
65
|
+
@abc.abstractmethod
|
|
66
|
+
def resolve(self, env: ta.Mapping[str, str | None]) -> str: ...
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Literal(Atom):
|
|
70
|
+
def __init__(self, value: str) -> None:
|
|
71
|
+
super().__init__()
|
|
72
|
+
self.value = value
|
|
73
|
+
|
|
74
|
+
def __repr__(self) -> str:
|
|
75
|
+
return f'Literal(value={self.value})'
|
|
76
|
+
|
|
77
|
+
def __eq__(self, other: object) -> bool:
|
|
78
|
+
if not isinstance(other, self.__class__):
|
|
79
|
+
return NotImplemented
|
|
80
|
+
return self.value == other.value
|
|
81
|
+
|
|
82
|
+
def __hash__(self) -> int:
|
|
83
|
+
return hash((self.__class__, self.value))
|
|
84
|
+
|
|
85
|
+
def resolve(self, env: ta.Mapping[str, str | None]) -> str:
|
|
86
|
+
return self.value
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class Variable(Atom):
|
|
90
|
+
def __init__(self, name: str, default: str | None) -> None:
|
|
91
|
+
super().__init__()
|
|
92
|
+
self.name = name
|
|
93
|
+
self.default = default
|
|
94
|
+
|
|
95
|
+
def __repr__(self) -> str:
|
|
96
|
+
return f'Variable(name={self.name}, default={self.default})'
|
|
97
|
+
|
|
98
|
+
def __eq__(self, other: object) -> bool:
|
|
99
|
+
if not isinstance(other, self.__class__):
|
|
100
|
+
return NotImplemented
|
|
101
|
+
return (self.name, self.default) == (other.name, other.default)
|
|
102
|
+
|
|
103
|
+
def __hash__(self) -> int:
|
|
104
|
+
return hash((self.__class__, self.name, self.default))
|
|
105
|
+
|
|
106
|
+
def resolve(self, env: ta.Mapping[str, str | None]) -> str:
|
|
107
|
+
default = self.default if self.default is not None else ''
|
|
108
|
+
result = env.get(self.name, default)
|
|
109
|
+
return result if result is not None else ''
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def parse_variables(value: str) -> ta.Iterator[Atom]:
|
|
113
|
+
cursor = 0
|
|
114
|
+
|
|
115
|
+
for match in _posix_variable.finditer(value):
|
|
116
|
+
(start, end) = match.span()
|
|
117
|
+
name = match['name']
|
|
118
|
+
default = match['default']
|
|
119
|
+
|
|
120
|
+
if start > cursor:
|
|
121
|
+
yield Literal(value=value[cursor:start])
|
|
122
|
+
|
|
123
|
+
yield Variable(name=name, default=default)
|
|
124
|
+
cursor = end
|
|
125
|
+
|
|
126
|
+
length = len(value)
|
|
127
|
+
if cursor < length:
|
|
128
|
+
yield Literal(value=value[cursor:length])
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
##
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _make_regex(string: str, extra_flags: int = 0) -> ta.Pattern[str]:
|
|
135
|
+
return re.compile(string, re.UNICODE | extra_flags)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
_newline = _make_regex(r'(\r\n|\n|\r)')
|
|
139
|
+
_multiline_whitespace = _make_regex(r'\s*', extra_flags=re.MULTILINE)
|
|
140
|
+
_whitespace = _make_regex(r'[^\S\r\n]*')
|
|
141
|
+
_export = _make_regex(r'(?:export[^\S\r\n]+)?')
|
|
142
|
+
_single_quoted_key = _make_regex(r"'([^']+)'")
|
|
143
|
+
_unquoted_key = _make_regex(r'([^=\#\s]+)')
|
|
144
|
+
_equal_sign = _make_regex(r'(=[^\S\r\n]*)')
|
|
145
|
+
_single_quoted_value = _make_regex(r"'((?:\\'|[^'])*)'")
|
|
146
|
+
_double_quoted_value = _make_regex(r'"((?:\\"|[^"])*)"')
|
|
147
|
+
_unquoted_value = _make_regex(r'([^\r\n]*)')
|
|
148
|
+
_comment = _make_regex(r'(?:[^\S\r\n]*#[^\r\n]*)?')
|
|
149
|
+
_end_of_line = _make_regex(r'[^\S\r\n]*(?:\r\n|\n|\r|$)')
|
|
150
|
+
_rest_of_line = _make_regex(r'[^\r\n]*(?:\r|\n|\r\n)?')
|
|
151
|
+
_double_quote_escapes = _make_regex(r"\\[\\'\"abfnrtv]")
|
|
152
|
+
_single_quote_escapes = _make_regex(r"\\[\\']")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class Original(ta.NamedTuple):
|
|
156
|
+
string: str
|
|
157
|
+
line: int
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class Binding(ta.NamedTuple):
|
|
161
|
+
key: str | None
|
|
162
|
+
value: str | None
|
|
163
|
+
original: Original
|
|
164
|
+
error: bool
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class _Position:
|
|
168
|
+
def __init__(self, chars: int, line: int) -> None:
|
|
169
|
+
super().__init__()
|
|
170
|
+
self.chars = chars
|
|
171
|
+
self.line = line
|
|
172
|
+
|
|
173
|
+
@classmethod
|
|
174
|
+
def start(cls) -> '_Position':
|
|
175
|
+
return cls(chars=0, line=1)
|
|
176
|
+
|
|
177
|
+
def set(self, other: '_Position') -> None:
|
|
178
|
+
self.chars = other.chars
|
|
179
|
+
self.line = other.line
|
|
180
|
+
|
|
181
|
+
def advance(self, string: str) -> None:
|
|
182
|
+
self.chars += len(string)
|
|
183
|
+
self.line += len(re.findall(_newline, string))
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class Error(Exception):
|
|
187
|
+
pass
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class _Reader:
|
|
191
|
+
def __init__(self, stream: ta.IO[str]) -> None:
|
|
192
|
+
super().__init__()
|
|
193
|
+
self.string = stream.read()
|
|
194
|
+
self.position = _Position.start()
|
|
195
|
+
self.mark = _Position.start()
|
|
196
|
+
|
|
197
|
+
def has_next(self) -> bool:
|
|
198
|
+
return self.position.chars < len(self.string)
|
|
199
|
+
|
|
200
|
+
def set_mark(self) -> None:
|
|
201
|
+
self.mark.set(self.position)
|
|
202
|
+
|
|
203
|
+
def get_marked(self) -> Original:
|
|
204
|
+
return Original(
|
|
205
|
+
string=self.string[self.mark.chars:self.position.chars],
|
|
206
|
+
line=self.mark.line,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
def peek(self, count: int) -> str:
|
|
210
|
+
return self.string[self.position.chars:self.position.chars + count]
|
|
211
|
+
|
|
212
|
+
def read(self, count: int) -> str:
|
|
213
|
+
result = self.string[self.position.chars:self.position.chars + count]
|
|
214
|
+
if len(result) < count:
|
|
215
|
+
raise Error('read: End of string')
|
|
216
|
+
self.position.advance(result)
|
|
217
|
+
return result
|
|
218
|
+
|
|
219
|
+
def read_regex(self, regex: ta.Pattern[str]) -> ta.Sequence[str]:
|
|
220
|
+
match = regex.match(self.string, self.position.chars)
|
|
221
|
+
if match is None:
|
|
222
|
+
raise Error('read_regex: Pattern not found')
|
|
223
|
+
self.position.advance(self.string[match.start():match.end()])
|
|
224
|
+
return match.groups()
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _decode_escapes(regex: ta.Pattern[str], string: str) -> str:
|
|
228
|
+
def decode_match(match: ta.Match[str]) -> str:
|
|
229
|
+
return codecs.decode(match.group(0), 'unicode-escape')
|
|
230
|
+
|
|
231
|
+
return regex.sub(decode_match, string)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _parse_key(reader: _Reader) -> str | None:
|
|
235
|
+
char = reader.peek(1)
|
|
236
|
+
if char == '#':
|
|
237
|
+
return None
|
|
238
|
+
elif char == "'":
|
|
239
|
+
(key,) = reader.read_regex(_single_quoted_key)
|
|
240
|
+
else:
|
|
241
|
+
(key,) = reader.read_regex(_unquoted_key)
|
|
242
|
+
return key
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _parse_unquoted_value(reader: _Reader) -> str:
|
|
246
|
+
(part,) = reader.read_regex(_unquoted_value)
|
|
247
|
+
return re.sub(r'\s+#.*', '', part).rstrip()
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _parse_value(reader: _Reader) -> str:
|
|
251
|
+
char = reader.peek(1)
|
|
252
|
+
if char == "'":
|
|
253
|
+
(value,) = reader.read_regex(_single_quoted_value)
|
|
254
|
+
return _decode_escapes(_single_quote_escapes, value)
|
|
255
|
+
elif char == '"':
|
|
256
|
+
(value,) = reader.read_regex(_double_quoted_value)
|
|
257
|
+
return _decode_escapes(_double_quote_escapes, value)
|
|
258
|
+
elif char in ('', '\n', '\r'):
|
|
259
|
+
return ''
|
|
260
|
+
else:
|
|
261
|
+
return _parse_unquoted_value(reader)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _parse_binding(reader: _Reader) -> Binding:
|
|
265
|
+
reader.set_mark()
|
|
266
|
+
try:
|
|
267
|
+
reader.read_regex(_multiline_whitespace)
|
|
268
|
+
if not reader.has_next():
|
|
269
|
+
return Binding(
|
|
270
|
+
key=None,
|
|
271
|
+
value=None,
|
|
272
|
+
original=reader.get_marked(),
|
|
273
|
+
error=False,
|
|
274
|
+
)
|
|
275
|
+
reader.read_regex(_export)
|
|
276
|
+
key = _parse_key(reader)
|
|
277
|
+
reader.read_regex(_whitespace)
|
|
278
|
+
if reader.peek(1) == '=':
|
|
279
|
+
reader.read_regex(_equal_sign)
|
|
280
|
+
value: str | None = _parse_value(reader)
|
|
281
|
+
else:
|
|
282
|
+
value = None
|
|
283
|
+
reader.read_regex(_comment)
|
|
284
|
+
reader.read_regex(_end_of_line)
|
|
285
|
+
return Binding(
|
|
286
|
+
key=key,
|
|
287
|
+
value=value,
|
|
288
|
+
original=reader.get_marked(),
|
|
289
|
+
error=False,
|
|
290
|
+
)
|
|
291
|
+
except Error:
|
|
292
|
+
reader.read_regex(_rest_of_line)
|
|
293
|
+
return Binding(
|
|
294
|
+
key=None,
|
|
295
|
+
value=None,
|
|
296
|
+
original=reader.get_marked(),
|
|
297
|
+
error=True,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def parse_stream(stream: ta.IO[str]) -> ta.Iterator[Binding]:
|
|
302
|
+
reader = _Reader(stream)
|
|
303
|
+
while reader.has_next():
|
|
304
|
+
yield _parse_binding(reader)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
##
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
# A type alias for a string path to be used for the paths in this file. These paths may flow to `open()` and
|
|
311
|
+
# `shutil.move()`; `shutil.move()` only accepts string paths, not byte paths or file descriptors. See
|
|
312
|
+
# https://github.com/python/typeshed/pull/6832.
|
|
313
|
+
StrPath: ta.TypeAlias = ta.Union[str, 'os.PathLike[str]']
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _with_warn_for_invalid_lines(mappings: ta.Iterator[Binding]) -> ta.Iterator[Binding]:
|
|
317
|
+
for mapping in mappings:
|
|
318
|
+
if mapping.error:
|
|
319
|
+
log.warning(
|
|
320
|
+
'dotenv could not parse statement starting at line %s',
|
|
321
|
+
mapping.original.line,
|
|
322
|
+
)
|
|
323
|
+
yield mapping
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class DotEnv:
|
|
327
|
+
def __init__(
|
|
328
|
+
self,
|
|
329
|
+
path: StrPath | None,
|
|
330
|
+
stream: ta.IO[str] | None = None,
|
|
331
|
+
verbose: bool = False,
|
|
332
|
+
encoding: str | None = None,
|
|
333
|
+
interpolate: bool = True,
|
|
334
|
+
override: bool = True,
|
|
335
|
+
) -> None:
|
|
336
|
+
super().__init__()
|
|
337
|
+
self.path: StrPath | None = path
|
|
338
|
+
self.stream: ta.IO[str] | None = stream
|
|
339
|
+
self._dict: dict[str, str | None] | None = None
|
|
340
|
+
self.verbose: bool = verbose
|
|
341
|
+
self.encoding: str | None = encoding
|
|
342
|
+
self.interpolate: bool = interpolate
|
|
343
|
+
self.override: bool = override
|
|
344
|
+
|
|
345
|
+
@contextlib.contextmanager
|
|
346
|
+
def _get_stream(self) -> ta.Iterator[ta.IO[str]]:
|
|
347
|
+
if self.path and os.path.isfile(self.path):
|
|
348
|
+
with open(self.path, encoding=self.encoding) as stream:
|
|
349
|
+
yield stream
|
|
350
|
+
elif self.stream is not None:
|
|
351
|
+
yield self.stream
|
|
352
|
+
else:
|
|
353
|
+
if self.verbose:
|
|
354
|
+
log.info(
|
|
355
|
+
'dotenv could not find configuration file %s.',
|
|
356
|
+
self.path or '.env',
|
|
357
|
+
)
|
|
358
|
+
yield io.StringIO('')
|
|
359
|
+
|
|
360
|
+
def dict(self) -> dict[str, str | None]:
|
|
361
|
+
if self._dict:
|
|
362
|
+
return self._dict
|
|
363
|
+
|
|
364
|
+
raw_values = self.parse()
|
|
365
|
+
|
|
366
|
+
if self.interpolate:
|
|
367
|
+
self._dict = resolve_variables(raw_values, override=self.override)
|
|
368
|
+
else:
|
|
369
|
+
self._dict = dict(raw_values)
|
|
370
|
+
|
|
371
|
+
return self._dict
|
|
372
|
+
|
|
373
|
+
def parse(self) -> ta.Iterator[tuple[str, str | None]]:
|
|
374
|
+
with self._get_stream() as stream:
|
|
375
|
+
for mapping in _with_warn_for_invalid_lines(parse_stream(stream)):
|
|
376
|
+
if mapping.key is not None:
|
|
377
|
+
yield mapping.key, mapping.value
|
|
378
|
+
|
|
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
|
+
def get(self, key: str) -> str | None:
|
|
393
|
+
"""
|
|
394
|
+
"""
|
|
395
|
+
data = self.dict()
|
|
396
|
+
|
|
397
|
+
if key in data:
|
|
398
|
+
return data[key]
|
|
399
|
+
|
|
400
|
+
if self.verbose:
|
|
401
|
+
log.warning('Key %s not found in %s.', key, self.path)
|
|
402
|
+
|
|
403
|
+
return None
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
##
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def get_key(
|
|
410
|
+
path: StrPath,
|
|
411
|
+
key_to_get: str,
|
|
412
|
+
*,
|
|
413
|
+
encoding: str | None = 'utf-8',
|
|
414
|
+
) -> str | None:
|
|
415
|
+
"""
|
|
416
|
+
Get the value of a given key from the given .env.
|
|
417
|
+
|
|
418
|
+
Returns `None` if the key isn't found or doesn't have a value.
|
|
419
|
+
"""
|
|
420
|
+
return DotEnv(path, verbose=True, encoding=encoding).get(key_to_get)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
@contextlib.contextmanager
|
|
424
|
+
def _rewrite(
|
|
425
|
+
path: StrPath,
|
|
426
|
+
encoding: str | None,
|
|
427
|
+
) -> ta.Iterator[tuple[ta.IO[str], ta.IO[str]]]:
|
|
428
|
+
pathlib.Path(path).touch()
|
|
429
|
+
|
|
430
|
+
with tempfile.NamedTemporaryFile(mode='w', encoding=encoding, delete=False) as dest:
|
|
431
|
+
error = None
|
|
432
|
+
try:
|
|
433
|
+
with open(path, encoding=encoding) as source:
|
|
434
|
+
yield (source, dest)
|
|
435
|
+
except BaseException as err: # noqa
|
|
436
|
+
error = err
|
|
437
|
+
|
|
438
|
+
if error is None:
|
|
439
|
+
shutil.move(dest.name, path)
|
|
440
|
+
else:
|
|
441
|
+
os.unlink(dest.name)
|
|
442
|
+
raise error from None
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def set_key(
|
|
446
|
+
path: StrPath,
|
|
447
|
+
key_to_set: str,
|
|
448
|
+
value_to_set: str,
|
|
449
|
+
*,
|
|
450
|
+
quote_mode: str = 'always',
|
|
451
|
+
export: bool = False,
|
|
452
|
+
encoding: str | None = 'utf-8',
|
|
453
|
+
) -> tuple[bool | None, str, str]:
|
|
454
|
+
"""
|
|
455
|
+
Adds or Updates a key/value to the given .env
|
|
456
|
+
|
|
457
|
+
If the .env path given doesn't exist, fails instead of risking creating
|
|
458
|
+
an orphan .env somewhere in the filesystem
|
|
459
|
+
"""
|
|
460
|
+
if quote_mode not in ('always', 'auto', 'never'):
|
|
461
|
+
raise ValueError(f'Unknown quote_mode: {quote_mode}')
|
|
462
|
+
|
|
463
|
+
quote = (
|
|
464
|
+
quote_mode == 'always'
|
|
465
|
+
or (quote_mode == 'auto' and not value_to_set.isalnum())
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
if quote:
|
|
469
|
+
value_out = "'{}'".format(value_to_set.replace("'", "\\'"))
|
|
470
|
+
else:
|
|
471
|
+
value_out = value_to_set
|
|
472
|
+
if export:
|
|
473
|
+
line_out = f'export {key_to_set}={value_out}\n'
|
|
474
|
+
else:
|
|
475
|
+
line_out = f'{key_to_set}={value_out}\n'
|
|
476
|
+
|
|
477
|
+
with _rewrite(path, encoding=encoding) as (source, dest):
|
|
478
|
+
replaced = False
|
|
479
|
+
missing_newline = False
|
|
480
|
+
for mapping in _with_warn_for_invalid_lines(parse_stream(source)):
|
|
481
|
+
if mapping.key == key_to_set:
|
|
482
|
+
dest.write(line_out)
|
|
483
|
+
replaced = True
|
|
484
|
+
else:
|
|
485
|
+
dest.write(mapping.original.string)
|
|
486
|
+
missing_newline = not mapping.original.string.endswith('\n')
|
|
487
|
+
if not replaced:
|
|
488
|
+
if missing_newline:
|
|
489
|
+
dest.write('\n')
|
|
490
|
+
dest.write(line_out)
|
|
491
|
+
|
|
492
|
+
return True, key_to_set, value_to_set
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def unset_key(
|
|
496
|
+
path: StrPath,
|
|
497
|
+
key_to_unset: str,
|
|
498
|
+
*,
|
|
499
|
+
quote_mode: str = 'always',
|
|
500
|
+
encoding: str | None = 'utf-8',
|
|
501
|
+
) -> tuple[bool | None, str]:
|
|
502
|
+
"""
|
|
503
|
+
Removes a given key from the given `.env` file.
|
|
504
|
+
|
|
505
|
+
If the .env path given doesn't exist, fails.
|
|
506
|
+
If the given key doesn't exist in the .env, fails.
|
|
507
|
+
"""
|
|
508
|
+
if not os.path.exists(path):
|
|
509
|
+
log.warning("Can't delete from %s - it doesn't exist.", path)
|
|
510
|
+
return None, key_to_unset
|
|
511
|
+
|
|
512
|
+
removed = False
|
|
513
|
+
with _rewrite(path, encoding=encoding) as (source, dest):
|
|
514
|
+
for mapping in _with_warn_for_invalid_lines(parse_stream(source)):
|
|
515
|
+
if mapping.key == key_to_unset:
|
|
516
|
+
removed = True
|
|
517
|
+
else:
|
|
518
|
+
dest.write(mapping.original.string)
|
|
519
|
+
|
|
520
|
+
if not removed:
|
|
521
|
+
log.warning("Key %s not removed from %s - key doesn't exist.", key_to_unset, path)
|
|
522
|
+
return None, key_to_unset
|
|
523
|
+
|
|
524
|
+
return removed, key_to_unset
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def resolve_variables(
|
|
528
|
+
values: ta.Iterable[tuple[str, str | None]],
|
|
529
|
+
override: bool,
|
|
530
|
+
) -> dict[str, str | None]:
|
|
531
|
+
new_values: dict[str, str | None] = {}
|
|
532
|
+
|
|
533
|
+
for (name, value) in values:
|
|
534
|
+
if value is None:
|
|
535
|
+
result = None
|
|
536
|
+
else:
|
|
537
|
+
atoms = parse_variables(value)
|
|
538
|
+
env: dict[str, str | None] = {}
|
|
539
|
+
if override:
|
|
540
|
+
env.update(os.environ)
|
|
541
|
+
env.update(new_values)
|
|
542
|
+
else:
|
|
543
|
+
env.update(new_values)
|
|
544
|
+
env.update(os.environ)
|
|
545
|
+
result = ''.join(atom.resolve(env) for atom in atoms)
|
|
546
|
+
|
|
547
|
+
new_values[name] = result
|
|
548
|
+
|
|
549
|
+
return new_values
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def dotenv_values(
|
|
553
|
+
path: StrPath | None = None,
|
|
554
|
+
stream: ta.IO[str] | None = None,
|
|
555
|
+
*,
|
|
556
|
+
verbose: bool = False,
|
|
557
|
+
interpolate: bool = True,
|
|
558
|
+
encoding: str | None = 'utf-8',
|
|
559
|
+
) -> dict[str, str | None]:
|
|
560
|
+
"""
|
|
561
|
+
Parse a .env file and return its content as a dict.
|
|
562
|
+
|
|
563
|
+
The returned dict will have `None` values for keys without values in the .env file.
|
|
564
|
+
For example, `foo=bar` results in `{"foo": "bar"}` whereas `foo` alone results in
|
|
565
|
+
`{"foo": None}`
|
|
566
|
+
|
|
567
|
+
Parameters:
|
|
568
|
+
path: Absolute or relative path to the .env file.
|
|
569
|
+
stream: `StringIO` object with .env content, used if `path` is `None`.
|
|
570
|
+
verbose: Whether to output a warning if the .env file is missing.
|
|
571
|
+
encoding: Encoding to be used to read the file.
|
|
572
|
+
|
|
573
|
+
If both `path` and `stream` are `None`, `find_dotenv()` is used to find the
|
|
574
|
+
.env file.
|
|
575
|
+
"""
|
|
576
|
+
if path is None and stream is None:
|
|
577
|
+
raise ValueError('must set path or stream')
|
|
578
|
+
|
|
579
|
+
return DotEnv(
|
|
580
|
+
path=path,
|
|
581
|
+
stream=stream,
|
|
582
|
+
verbose=verbose,
|
|
583
|
+
interpolate=interpolate,
|
|
584
|
+
override=True,
|
|
585
|
+
encoding=encoding,
|
|
586
|
+
).dict()
|