ae-base 0.3.36__py3-none-any.whl → 0.3.38__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.
ae/base.py CHANGED
@@ -69,6 +69,9 @@ use :func:`env_str` to determine the value of an OS environment variable with au
69
69
  helper functions provided by this namespace portion to determine the values of the most important system environment
70
70
  variables for your application are :func:`sys_env_dict` and :func:`sys_env_text`.
71
71
 
72
+ to integrate system environment variables from ``.env`` files into :data:`os.environ` the helper functions
73
+ :func:parse_dotenv`, :func:`load_env_var_defaults` and :func:`load_dotenvs` are provided.
74
+
72
75
 
73
76
  android-specific constants and helper functions
74
77
  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -129,20 +132,21 @@ import importlib.abc
129
132
  import importlib.util
130
133
  import os
131
134
  import platform
135
+ import re
132
136
  import shutil
133
137
  import socket
134
138
  import sys
135
139
  import unicodedata
140
+ import warnings
136
141
 
137
142
  from configparser import ConfigParser, ExtendedInterpolation
138
143
  from contextlib import contextmanager
139
144
  from importlib.machinery import ModuleSpec
140
145
  from inspect import getinnerframes, getouterframes, getsourcefile
141
146
  from types import ModuleType
142
- from typing import Any, Callable, Dict, Generator, Iterable, List, Optional, Tuple, Union
143
-
147
+ from typing import Any, Callable, Dict, Generator, Iterable, List, Optional, Tuple, Union, cast
144
148
 
145
- __version__ = '0.3.36'
149
+ __version__ = '0.3.38'
146
150
 
147
151
 
148
152
  DOCS_FOLDER = 'docs' #: project documentation root folder name
@@ -169,6 +173,33 @@ DEF_ENCODING = 'ascii'
169
173
  """ encoding for :func:`force_encoding` that will always work independent from destination (console, file sys, ...).
170
174
  """
171
175
 
176
+ DOTENV_FILE_NAME = '.env' #: name of the file containing console/shell environment variables
177
+ _env_line = re.compile(r"""
178
+ ^
179
+ (?:export\s+)? # optional export
180
+ ([\w.]+) # key
181
+ (?:\s*=\s*|:\s+?) # separator
182
+ ( # optional value begin
183
+ '(?:\'|[^'])*' # single quoted value
184
+ | # or
185
+ "(?:\"|[^"])*" # double quoted value
186
+ | # or
187
+ [^#\n]+ # unquoted value
188
+ )? # value end
189
+ (?:\s*\#.*)? # optional comment
190
+ $
191
+ """, re.VERBOSE)
192
+ _env_variable = re.compile(r"""
193
+ (\\)? # is it escaped with a backslash?
194
+ (\$) # literal $
195
+ ( # collect braces with var for sub
196
+ \{? # allow brace wrapping
197
+ ([A-Z0-9_]+) # match the variable
198
+ }? # closing brace
199
+ ) # braces end
200
+ """, re.IGNORECASE | re.VERBOSE)
201
+
202
+
172
203
  NAME_PARTS_SEP = '_' #: name parts separator character, e.g. for :func:`norm_name`
173
204
 
174
205
  SKIPPED_MODULES = ('ae.base', 'ae.paths', 'ae.dynamicod', 'ae.core', 'ae.console', 'ae.gui_app', 'ae.gui_help',
@@ -248,7 +279,7 @@ def deep_dict_update(data: dict, update: dict):
248
279
  """ update the optionally nested data dict in-place with the items and sub-items from the update dict.
249
280
 
250
281
  :param data: dict to be updated/extended. non-existing keys of dict-sub-items will be added.
251
- :param update: dict with the [sub-]items to update in the :paramref:`.data` dict.
282
+ :param update: dict with the [sub-]items to update in the :paramref:`~deep_dict_update.data` dict.
252
283
 
253
284
  .. hint:: the module/portion :mod:`ae.deep` is providing more deep update helper functions.
254
285
 
@@ -272,7 +303,7 @@ def dummy_function(*_args, **_kwargs):
272
303
 
273
304
 
274
305
  def duplicates(values: Iterable) -> list:
275
- """ determine all duplicates in the iterable specified in the :paramref:`.values` argument.
306
+ """ determine all duplicates in the iterable specified in the :paramref:`~duplicates.values` argument.
276
307
 
277
308
  inspired by Ritesh Kumars answer to https://stackoverflow.com/questions/9835762.
278
309
 
@@ -398,6 +429,43 @@ def in_wd(new_cwd: str) -> Generator[None, None, None]:
398
429
  os.chdir(cur_dir)
399
430
 
400
431
 
432
+ def load_dotenvs():
433
+ """ detect and load multiple ``.env`` files in/above the current working directory and the calling module folder.
434
+
435
+ .. hint:: call from main module of project/app in order to also load ``.env`` files in/above the project folder.
436
+ """
437
+ load_env_var_defaults(os.getcwd())
438
+ if file_name := stack_var('__file__'):
439
+ load_env_var_defaults(os.path.dirname(os.path.abspath(file_name)))
440
+
441
+
442
+ def load_env_var_defaults(start_dir: str):
443
+ """ detect and load chain of ``.env`` files starting in the specified folder or one above.
444
+
445
+ :param start_dir: folder to start search of an ``.env`` file, if not found then checks the parent folder.
446
+ if a first ``.env`` file got found, then load their console/shell environment variables
447
+ into Python's :data:`os.environ`. after loading the first one, repeat to check for
448
+ further ``.env`` files in the parent folder to load them too, until either detecting
449
+ a folder without an ``.env`` file or until an ``.env`` got loaded from the root folder.
450
+
451
+ .. note::
452
+ only variables that are not declared in :data:`os.environ` will be added (with the
453
+ value specified in the ``.env`` file to be loaded).
454
+ """
455
+ file_path = os.path.abspath(os.path.join(start_dir, DOTENV_FILE_NAME))
456
+ if not os.path.isfile(file_path):
457
+ file_path = os.path.join(os.path.dirname(start_dir), DOTENV_FILE_NAME)
458
+
459
+ while os.path.isfile(file_path):
460
+ for var_nam, var_val in parse_dotenv(file_path).items():
461
+ if var_nam not in os.environ:
462
+ os.environ[var_nam] = var_val
463
+
464
+ if os.sep not in file_path:
465
+ break # pragma: no cover # prevent endless-loop for ``.env`` file in root dir (os.sep == '/')
466
+ file_path = os.path.join(os.path.dirname(os.path.dirname(file_path)), DOTENV_FILE_NAME)
467
+
468
+
401
469
  def main_file_paths_parts(portion_name: str) -> Tuple[Tuple[str, ...], ...]:
402
470
  """ determine tuple of supported main/version file name path part tuples.
403
471
 
@@ -445,7 +513,14 @@ def module_file_path(local_object: Optional[Callable] = None) -> str:
445
513
  if file_path:
446
514
  return norm_path(file_path)
447
515
 
448
- return stack_var('__file__', depth=2) or "" # or use sys._getframe().f_code.co_filename
516
+ file_path = stack_var('__file__')
517
+ if not file_path: # pragma: no cover
518
+ try:
519
+ # noinspection PyProtectedMember,PyUnresolvedReferences
520
+ file_path = sys._getframe().f_back.f_code.co_filename # type: ignore # pylint: disable=protected-access
521
+ except (AttributeError, Exception): # pylint: disable=broad-except # pragma: no cover
522
+ file_path = ""
523
+ return file_path
449
524
 
450
525
 
451
526
  def module_name(*skip_modules: str, depth: int = 0) -> Optional[str]:
@@ -531,13 +606,13 @@ def norm_path(path: str, make_absolute: bool = True, remove_base_path: str = "",
531
606
 
532
607
 
533
608
  def now_str(sep: str = "") -> str:
534
- """ return the current timestamp as string (to use as suffix for file and variable/attribute names).
609
+ """ return the current UTC timestamp as string (to use as suffix for file and variable/attribute names).
535
610
 
536
611
  :param sep: optional prefix and separator character (separating date from time and in time part
537
612
  the seconds from the microseconds).
538
- :return: timestamp as string (length=20 + 3 * len(sep)).
613
+ :return: UTC timestamp as string (length=20 + 3 * len(sep)).
539
614
  """
540
- return datetime.datetime.now().strftime("{sep}%Y%m%d{sep}%H%M%S{sep}%f".format(sep=sep))
615
+ return datetime.datetime.utcnow().strftime("{sep}%Y%m%d{sep}%H%M%S{sep}%f".format(sep=sep))
541
616
 
542
617
 
543
618
  def os_host_name() -> str:
@@ -612,6 +687,45 @@ def os_user_name() -> str:
612
687
  return getpass.getuser()
613
688
 
614
689
 
690
+ def parse_dotenv(file_path: str) -> Dict[str, str]:
691
+ """ parse ``.env`` file content and return environment variable names as dict keys and values as dict values.
692
+
693
+ :param file_path: string with the name/path of an existing ``.env``/:data:`DOTENV_FILE_NAME` file.
694
+ :return: dict with environment variable names and values
695
+ """
696
+ env_vars: Dict[str, str] = {}
697
+ for line in cast(str, read_file(file_path)).splitlines():
698
+ match = _env_line.search(line)
699
+ if not match:
700
+ if not re.search(r'^\s*(?:#.*)?$', line): # not comment or blank
701
+ warnings.warn(f"'{line!r}' in '{file_path}' doesn't match {DOTENV_FILE_NAME} format", SyntaxWarning)
702
+ continue
703
+
704
+ var_nam, var_val = match.groups()
705
+ var_val = "" if var_val is None else var_val.strip()
706
+
707
+ # remove surrounding quotes, unescape all chars except $ so variables can be escaped properly
708
+ match = re.match(r'^([\'"])(.*)\1$', var_val)
709
+ if match:
710
+ delimiter, var_val = match.groups()
711
+ if delimiter == '"':
712
+ var_val = re.sub(r'\\([^$])', r'\1', var_val)
713
+ else:
714
+ delimiter = None
715
+ if delimiter != "'":
716
+ for parts in _env_variable.findall(var_val):
717
+ if parts[0] == '\\':
718
+ replace = "".join(parts[1:-1]) # don't replace escaped variables
719
+ else:
720
+ # substitute variables in a value, replace it with the value from the environment
721
+ replace = env_vars.get(parts[-1], os.environ.get(parts[-1], ""))
722
+ var_val = var_val.replace("".join(parts[0:-1]), replace)
723
+
724
+ env_vars[var_nam] = var_val
725
+
726
+ return env_vars
727
+
728
+
615
729
  def project_main_file(import_name: str, project_path: str = "") -> str:
616
730
  """ determine the main module file path of a project package, containing the project __version__ module variable.
617
731
 
@@ -878,12 +992,12 @@ if os_platform == 'android': # pragma: no cov
878
992
  # 'android' (see # https://bugs.python.org/issue28141 and https://bugs.python.org/issue32073). these functions are
879
993
  # used by shutil.copy2/copy/copytree/move to copy OS-specific file attributes.
880
994
  # although shutil.copytree() and shutil.move() are copying/moving the files correctly when the copy_function
881
- # arg is set to :func:`shutil.copyfile`, they will finally also crash afterwards when they try to set the attributes
995
+ # arg is set to :func:`shutil.copyfile`, they will finally also crash afterward when they try to set the attributes
882
996
  # on the destination root directory.
883
997
  shutil.copymode = dummy_function
884
998
  shutil.copystat = dummy_function
885
999
 
886
- # import permissions module from python-for-android (pythonforandroid/recipes/android/src/android/permissions.py)
1000
+ # import permissions module from python-for-android (recipes/android/src/android/permissions.py)
887
1001
  # noinspection PyUnresolvedReferences
888
1002
  from android.permissions import request_permissions, Permission # type: ignore # pylint: disable=import-error
889
1003
  from jnius import autoclass # type: ignore
@@ -1,4 +1,4 @@
1
- <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.tpl_project V0.3.29 -->
1
+ <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.tpl_project V0.3.30 -->
2
2
  ### GNU GENERAL PUBLIC LICENSE
3
3
 
4
4
  Version 3, 29 June 2007
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ae_base
3
- Version: 0.3.36
3
+ Version: 0.3.38
4
4
  Summary: ae namespace module portion base: basic constants, helper functions and context manager
5
5
  Home-page: https://gitlab.com/ae-group/ae_base
6
6
  Author: AndiEcker
@@ -51,19 +51,19 @@ Requires-Dist: types-setuptools ; extra == 'tests'
51
51
  Requires-Dist: wheel ; extra == 'tests'
52
52
  Requires-Dist: twine ; extra == 'tests'
53
53
 
54
- <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae V0.3.92 -->
55
- <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.tpl_namespace_root V0.3.13 -->
56
- # base 0.3.36
54
+ <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae V0.3.94 -->
55
+ <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.tpl_namespace_root V0.3.14 -->
56
+ # base 0.3.38
57
57
 
58
58
  [![GitLab develop](https://img.shields.io/gitlab/pipeline/ae-group/ae_base/develop?logo=python)](
59
59
  https://gitlab.com/ae-group/ae_base)
60
60
  [![LatestPyPIrelease](
61
- https://img.shields.io/gitlab/pipeline/ae-group/ae_base/release0.3.35?logo=python)](
62
- https://gitlab.com/ae-group/ae_base/-/tree/release0.3.35)
61
+ https://img.shields.io/gitlab/pipeline/ae-group/ae_base/release0.3.37?logo=python)](
62
+ https://gitlab.com/ae-group/ae_base/-/tree/release0.3.37)
63
63
  [![PyPIVersions](https://img.shields.io/pypi/v/ae_base)](
64
64
  https://pypi.org/project/ae-base/#history)
65
65
 
66
- >ae_base module 0.3.36.
66
+ >ae_base module 0.3.38.
67
67
 
68
68
  [![Coverage](https://ae-group.gitlab.io/ae_base/coverage.svg)](
69
69
  https://ae-group.gitlab.io/ae_base/coverage/index.html)
@@ -0,0 +1,7 @@
1
+ ae/base.py,sha256=HAkWF2Ft6XPRF9DpPVHalbrsD7YwxqBNoihmJHLK-8E,52051
2
+ ae_base-0.3.38.dist-info/LICENSE.md,sha256=3X7IwvwQFt4PqRHb7mV8qoJjQ1E-HmcGioyT4Y6-6c8,35002
3
+ ae_base-0.3.38.dist-info/METADATA,sha256=CpX74Z8BWOU201lQrKvn5JjDX8IN1ihff65-F8nCpDw,5245
4
+ ae_base-0.3.38.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
5
+ ae_base-0.3.38.dist-info/top_level.txt,sha256=vUdgAslSmhZLXWU48fm8AG2BjVnkOWLco8rzuW-5zY0,3
6
+ ae_base-0.3.38.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
7
+ ae_base-0.3.38.dist-info/RECORD,,
@@ -1,7 +0,0 @@
1
- ae/base.py,sha256=QoYyYh-0uPdU0vk7SJzVdNliqJA1DcYK47YxJwcIT04,46681
2
- ae_base-0.3.36.dist-info/LICENSE.md,sha256=pPZRU-fDrvoxF_E3nHVTzTAM51abjOg_Nd4ONV0biHY,35002
3
- ae_base-0.3.36.dist-info/METADATA,sha256=4WhIN9tp_BfVKSG7zE-79DflghjqKJeRkYs2MUOdl_s,5245
4
- ae_base-0.3.36.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
5
- ae_base-0.3.36.dist-info/top_level.txt,sha256=vUdgAslSmhZLXWU48fm8AG2BjVnkOWLco8rzuW-5zY0,3
6
- ae_base-0.3.36.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
7
- ae_base-0.3.36.dist-info/RECORD,,