ae-base 0.3.36__py3-none-any.whl → 0.3.37__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.37'
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,42 @@ 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
+ load_env_var_defaults(os.path.dirname(stack_var('__file__', depth=2)))
439
+
440
+
441
+ def load_env_var_defaults(start_dir: str):
442
+ """ detect and load chain of ``.env`` files starting in the specified folder or one above.
443
+
444
+ :param start_dir: folder to start search of an ``.env`` file, if not found then checks the parent folder.
445
+ if a first ``.env`` file got found, then load their console/shell environment variables
446
+ into Python's :data:`os.environ`. after loading the first one, repeat to check for
447
+ further ``.env`` files in the parent folder to load them too, until either detecting
448
+ a folder without an ``.env`` file or until an ``.env`` got loaded from the root folder.
449
+
450
+ .. note::
451
+ only variables that are not declared in :data:`os.environ` will be added (with the
452
+ value specified in the ``.env`` file to be loaded).
453
+ """
454
+ file_path = os.path.abspath(os.path.join(start_dir, DOTENV_FILE_NAME))
455
+ if not os.path.isfile(file_path):
456
+ file_path = os.path.join(os.path.dirname(start_dir), DOTENV_FILE_NAME)
457
+
458
+ while os.path.isfile(file_path):
459
+ for var_nam, var_val in parse_dotenv(file_path).items():
460
+ if var_nam not in os.environ:
461
+ os.environ[var_nam] = var_val
462
+
463
+ if os.sep not in file_path:
464
+ break # pragma: no cover # prevent endless-loop for ``.env`` file in root dir (os.sep == '/')
465
+ file_path = os.path.join(os.path.dirname(os.path.dirname(file_path)), DOTENV_FILE_NAME)
466
+
467
+
401
468
  def main_file_paths_parts(portion_name: str) -> Tuple[Tuple[str, ...], ...]:
402
469
  """ determine tuple of supported main/version file name path part tuples.
403
470
 
@@ -445,7 +512,14 @@ def module_file_path(local_object: Optional[Callable] = None) -> str:
445
512
  if file_path:
446
513
  return norm_path(file_path)
447
514
 
448
- return stack_var('__file__', depth=2) or "" # or use sys._getframe().f_code.co_filename
515
+ file_path = stack_var('__file__')
516
+ if not file_path: # pragma: no cover
517
+ try:
518
+ # noinspection PyProtectedMember,PyUnresolvedReferences
519
+ file_path = sys._getframe().f_back.f_code.co_filename # type: ignore # pylint: disable=protected-access
520
+ except (AttributeError, Exception): # pylint: disable=broad-except # pragma: no cover
521
+ file_path = ""
522
+ return file_path
449
523
 
450
524
 
451
525
  def module_name(*skip_modules: str, depth: int = 0) -> Optional[str]:
@@ -531,13 +605,13 @@ def norm_path(path: str, make_absolute: bool = True, remove_base_path: str = "",
531
605
 
532
606
 
533
607
  def now_str(sep: str = "") -> str:
534
- """ return the current timestamp as string (to use as suffix for file and variable/attribute names).
608
+ """ return the current UTC timestamp as string (to use as suffix for file and variable/attribute names).
535
609
 
536
610
  :param sep: optional prefix and separator character (separating date from time and in time part
537
611
  the seconds from the microseconds).
538
- :return: timestamp as string (length=20 + 3 * len(sep)).
612
+ :return: UTC timestamp as string (length=20 + 3 * len(sep)).
539
613
  """
540
- return datetime.datetime.now().strftime("{sep}%Y%m%d{sep}%H%M%S{sep}%f".format(sep=sep))
614
+ return datetime.datetime.utcnow().strftime("{sep}%Y%m%d{sep}%H%M%S{sep}%f".format(sep=sep))
541
615
 
542
616
 
543
617
  def os_host_name() -> str:
@@ -612,6 +686,45 @@ def os_user_name() -> str:
612
686
  return getpass.getuser()
613
687
 
614
688
 
689
+ def parse_dotenv(file_path: str) -> Dict[str, str]:
690
+ """ parse ``.env`` file content and return environment variable names as dict keys and values as dict values.
691
+
692
+ :param file_path: string with the name/path of an existing ``.env``/:data:`DOTENV_FILE_NAME` file.
693
+ :return: dict with environment variable names and values
694
+ """
695
+ env_vars: Dict[str, str] = {}
696
+ for line in cast(str, read_file(file_path)).splitlines():
697
+ match = _env_line.search(line)
698
+ if not match:
699
+ if not re.search(r'^\s*(?:#.*)?$', line): # not comment or blank
700
+ warnings.warn(f"'{line!r}' in '{file_path}' doesn't match {DOTENV_FILE_NAME} format", SyntaxWarning)
701
+ continue
702
+
703
+ var_nam, var_val = match.groups()
704
+ var_val = "" if var_val is None else var_val.strip()
705
+
706
+ # remove surrounding quotes, unescape all chars except $ so variables can be escaped properly
707
+ match = re.match(r'^([\'"])(.*)\1$', var_val)
708
+ if match:
709
+ delimiter, var_val = match.groups()
710
+ if delimiter == '"':
711
+ var_val = re.sub(r'\\([^$])', r'\1', var_val)
712
+ else:
713
+ delimiter = None
714
+ if delimiter != "'":
715
+ for parts in _env_variable.findall(var_val):
716
+ if parts[0] == '\\':
717
+ replace = "".join(parts[1:-1]) # don't replace escaped variables
718
+ else:
719
+ # substitute variables in a value, replace it with the value from the environment
720
+ replace = env_vars.get(parts[-1], os.environ.get(parts[-1], ""))
721
+ var_val = var_val.replace("".join(parts[0:-1]), replace)
722
+
723
+ env_vars[var_nam] = var_val
724
+
725
+ return env_vars
726
+
727
+
615
728
  def project_main_file(import_name: str, project_path: str = "") -> str:
616
729
  """ determine the main module file path of a project package, containing the project __version__ module variable.
617
730
 
@@ -878,12 +991,12 @@ if os_platform == 'android': # pragma: no cov
878
991
  # 'android' (see # https://bugs.python.org/issue28141 and https://bugs.python.org/issue32073). these functions are
879
992
  # used by shutil.copy2/copy/copytree/move to copy OS-specific file attributes.
880
993
  # 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
994
+ # arg is set to :func:`shutil.copyfile`, they will finally also crash afterward when they try to set the attributes
882
995
  # on the destination root directory.
883
996
  shutil.copymode = dummy_function
884
997
  shutil.copystat = dummy_function
885
998
 
886
- # import permissions module from python-for-android (pythonforandroid/recipes/android/src/android/permissions.py)
999
+ # import permissions module from python-for-android (recipes/android/src/android/permissions.py)
887
1000
  # noinspection PyUnresolvedReferences
888
1001
  from android.permissions import request_permissions, Permission # type: ignore # pylint: disable=import-error
889
1002
  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.37
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.37
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.36?logo=python)](
62
+ https://gitlab.com/ae-group/ae_base/-/tree/release0.3.36)
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.37.
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=Jb4C0-uyu40_vK0dQZnHHR2hTwDILzaGpS7ecp-0BQs,52008
2
+ ae_base-0.3.37.dist-info/LICENSE.md,sha256=3X7IwvwQFt4PqRHb7mV8qoJjQ1E-HmcGioyT4Y6-6c8,35002
3
+ ae_base-0.3.37.dist-info/METADATA,sha256=2wTlS7GMWnV1vuGdsuMb1z_KWr-y2KwUsLcesuWYVMw,5245
4
+ ae_base-0.3.37.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
5
+ ae_base-0.3.37.dist-info/top_level.txt,sha256=vUdgAslSmhZLXWU48fm8AG2BjVnkOWLco8rzuW-5zY0,3
6
+ ae_base-0.3.37.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
7
+ ae_base-0.3.37.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,,