ae-base 0.3.40__py3-none-any.whl → 0.3.42__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
@@ -31,6 +31,9 @@ sortable and compact string from a timestamp.
31
31
  base helper functions
32
32
  ---------------------
33
33
 
34
+ :func:`now_str` creates a timestamp string with the actual UTC date and time. the :func:`utc_datetime` provides the
35
+ actual UTC date and time as datetime object.
36
+
34
37
  to write more compact and readable code for the most common file I/O operations, the helper functions :func:`read_file`
35
38
  and :func:`write_file` are wrapping Python's built-in :func:`open` function and its context manager.
36
39
 
@@ -43,6 +46,9 @@ the function :func:`duplicates` returns the duplicates of an iterable type.
43
46
  to normalize a file path, in order to remove `.`, `..` placeholders, to resolve symbolic links or to make it relative or
44
47
  absolute, call the function :func:`norm_path`.
45
48
 
49
+ :func:`uri2filename` converts special characters of a URI/URL resulting in a string that can be used as a file name.
50
+ use the function :func:`filename2uri` to convert this string back to the corresponding URL/URI.
51
+
46
52
  :func:`camel_to_snake` and :func:`snake_to_camel` providing name conversions of class and method names.
47
53
 
48
54
  to encode Unicode strings to other codecs the functions :func:`force_encoding` and :func:`to_ascii` can be used.
@@ -137,6 +143,13 @@ to determine e.g. variable values of the callers of a function/method.
137
143
  :attr:`title <AppBase.app_title>` of an application, if these values are not specified in the instance initializer.
138
144
 
139
145
  another useful helper function provided by this portion to inspect and debug your code is :func:`full_stack_trace`.
146
+
147
+
148
+ os.path shortcuts
149
+ -----------------
150
+
151
+ the following data items are pointers to shortcut the lookup to their related functions in the
152
+ Python module :mod:`os.path`:
140
153
  """
141
154
  import datetime
142
155
  import getpass
@@ -160,7 +173,21 @@ from types import ModuleType
160
173
  from typing import Any, Callable, Dict, Generator, Iterable, List, Optional, Tuple, Union, cast
161
174
 
162
175
 
163
- __version__ = '0.3.40'
176
+ __version__ = '0.3.42'
177
+
178
+
179
+ os_path_abspath = os.path.abspath
180
+ os_path_basename = os.path.basename
181
+ os_path_dirname = os.path.dirname
182
+ os_path_expanduser = os.path.expanduser
183
+ os_path_isdir = os.path.isdir
184
+ os_path_isfile = os.path.isfile
185
+ os_path_join = os.path.join
186
+ os_path_normpath = os.path.normpath
187
+ os_path_realpath = os.path.realpath
188
+ os_path_relpath = os.path.relpath
189
+ os_path_sep = os.path.sep
190
+ os_path_splitext = os.path.splitext
164
191
 
165
192
 
166
193
  DOCS_FOLDER = 'docs' #: project documentation root folder name
@@ -250,10 +277,10 @@ def app_name_guess() -> str:
250
277
  if not app_name:
251
278
  unspecified_app_names = ('ae_base', 'app', '_jb_pytest_runner', 'main', '__main__', 'pydevconsole', 'src')
252
279
  path = sys.argv[0]
253
- app_name = os.path.splitext(os.path.basename(path))[0]
280
+ app_name = os_path_splitext(os_path_basename(path))[0]
254
281
  if app_name.lower() in unspecified_app_names:
255
282
  path = os.getcwd()
256
- app_name = os.path.basename(path)
283
+ app_name = os_path_basename(path)
257
284
  if app_name.lower() in unspecified_app_names:
258
285
  app_name = "unguessable"
259
286
  return app_name
@@ -267,7 +294,7 @@ def build_config_variable_values(*names_defaults: Tuple[str, Any], section: str
267
294
  :return: tuple of build config variable values (using the passed default value if not specified
268
295
  in the :data:`BUILD_CONFIG_FILE` spec file or if the spec file does not exist in cwd).
269
296
  """
270
- if not os.path.exists(BUILD_CONFIG_FILE):
297
+ if not os_path_isfile(BUILD_CONFIG_FILE):
271
298
  return tuple(def_val for name, def_val in names_defaults)
272
299
 
273
300
  config = instantiate_config_parser()
@@ -352,6 +379,17 @@ def env_str(name: str, convert_name: bool = False) -> Optional[str]:
352
379
  return os.environ.get(name)
353
380
 
354
381
 
382
+ def filename2uri(file_name: str) -> str:
383
+ """ convert a file name converted by :func:`uri2filename` back to its representation as a URI
384
+
385
+ :param file_name: name of the file/folder to convert back to its URI representation.
386
+ :return: URI string.
387
+
388
+ .. hint:: to ensure proper conversion the specified file name has to be created by :func:`uri2filename`.
389
+ """
390
+ return urllib.parse.unquote(file_name)
391
+
392
+
355
393
  def force_encoding(text: Union[str, bytes], encoding: str = DEF_ENCODING, errors: str = DEF_ENCODE_ERRORS) -> str:
356
394
  """ force/ensure the encoding of text (str or bytes) without any UnicodeDecodeError/UnicodeEncodeError.
357
395
 
@@ -400,8 +438,8 @@ def import_module(import_name: str, path: Optional[Union[str, UnsetType]] = UNSE
400
438
  :return: a reference to the loaded module or ``None`` if module could not be imported.
401
439
  """
402
440
  if path is UNSET:
403
- path = import_name.replace('.', os.path.sep)
404
- path += PY_EXT if os.path.isfile(path + PY_EXT) else os.path.sep + PY_INIT
441
+ path = import_name.replace('.', os_path_sep)
442
+ path += PY_EXT if os_path_isfile(path + PY_EXT) else os_path_sep + PY_INIT
405
443
  mod_ref = None
406
444
 
407
445
  spec = importlib.util.spec_from_file_location(import_name, path) # type: ignore # silly mypy
@@ -452,7 +490,7 @@ def load_dotenvs():
452
490
  """
453
491
  load_env_var_defaults(os.getcwd())
454
492
  if file_name := stack_var('__file__'):
455
- load_env_var_defaults(os.path.dirname(os.path.abspath(file_name)))
493
+ load_env_var_defaults(os_path_dirname(os_path_abspath(file_name)))
456
494
 
457
495
 
458
496
  def load_env_var_defaults(start_dir: str):
@@ -468,18 +506,18 @@ def load_env_var_defaults(start_dir: str):
468
506
  only variables that are not declared in :data:`os.environ` will be added (with the
469
507
  value specified in the ``.env`` file to be loaded).
470
508
  """
471
- file_path = os.path.abspath(os.path.join(start_dir, DOTENV_FILE_NAME))
472
- if not os.path.isfile(file_path):
473
- file_path = os.path.join(os.path.dirname(start_dir), DOTENV_FILE_NAME)
509
+ file_path = os_path_abspath(os_path_join(start_dir, DOTENV_FILE_NAME))
510
+ if not os_path_isfile(file_path):
511
+ file_path = os_path_join(os_path_dirname(start_dir), DOTENV_FILE_NAME)
474
512
 
475
- while os.path.isfile(file_path):
513
+ while os_path_isfile(file_path):
476
514
  for var_nam, var_val in parse_dotenv(file_path).items():
477
515
  if var_nam not in os.environ:
478
516
  os.environ[var_nam] = var_val
479
517
 
480
518
  if os.sep not in file_path:
481
519
  break # pragma: no cover # prevent endless-loop for ``.env`` file in root dir (os.sep == '/')
482
- file_path = os.path.join(os.path.dirname(os.path.dirname(file_path)), DOTENV_FILE_NAME)
520
+ file_path = os_path_join(os_path_dirname(os_path_dirname(file_path)), DOTENV_FILE_NAME)
483
521
 
484
522
 
485
523
  def main_file_paths_parts(portion_name: str) -> Tuple[Tuple[str, ...], ...]:
@@ -603,20 +641,20 @@ def norm_path(path: str, make_absolute: bool = True, remove_base_path: str = "",
603
641
  """
604
642
  path = path or "."
605
643
  if path[0] == "~":
606
- path = os.path.expanduser(path)
644
+ path = os_path_expanduser(path)
607
645
 
608
646
  if remove_dots:
609
- path = os.path.normpath(path)
647
+ path = os_path_normpath(path)
610
648
 
611
649
  if resolve_sym_links:
612
- path = os.path.realpath(path)
650
+ path = os_path_realpath(path)
613
651
  elif make_absolute:
614
- path = os.path.abspath(path)
652
+ path = os_path_abspath(path)
615
653
 
616
654
  if remove_base_path:
617
655
  if remove_base_path[0] == "~":
618
- remove_base_path = os.path.expanduser(remove_base_path)
619
- path = os.path.relpath(path, remove_base_path)
656
+ remove_base_path = os_path_expanduser(remove_base_path)
657
+ path = os_path_relpath(path, remove_base_path)
620
658
 
621
659
  return path
622
660
 
@@ -628,10 +666,7 @@ def now_str(sep: str = "") -> str:
628
666
  the seconds from the microseconds).
629
667
  :return: naive UTC timestamp (without timezone info) as string (length=20 + 3 * len(sep)).
630
668
  """
631
- # if sys.version_info >= (3, 12):
632
- return datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None).strftime(NOW_STR_FORMAT.format(sep=sep))
633
- # else:
634
- # return datetime.datetime.utcnow().strftime(NOW_STR_FORMAT.format(sep=sep))
669
+ return utc_datetime().strftime(NOW_STR_FORMAT.format(sep=sep))
635
670
 
636
671
 
637
672
  def os_host_name() -> str:
@@ -754,23 +789,22 @@ def project_main_file(import_name: str, project_path: str = "") -> str:
754
789
  sister project (under the same project parent folder).
755
790
  :return: absolute file path/name of main module or empty string if no main/version file found.
756
791
  """
757
- join = os.path.join
758
792
  *namespace_dirs, portion_name = import_name.split('.')
759
793
  project_name = ('_'.join(namespace_dirs) + '_' if namespace_dirs else "") + portion_name
760
794
  paths_parts = main_file_paths_parts(portion_name)
761
795
 
762
796
  project_path = norm_path(project_path)
763
797
  module_paths = []
764
- if os.path.basename(project_path) != project_name:
765
- module_paths.append(join(os.path.dirname(project_path), project_name, *namespace_dirs))
798
+ if os_path_basename(project_path) != project_name:
799
+ module_paths.append(os_path_join(os_path_dirname(project_path), project_name, *namespace_dirs))
766
800
  if namespace_dirs:
767
- module_paths.append(join(project_path, *namespace_dirs))
801
+ module_paths.append(os_path_join(project_path, *namespace_dirs))
768
802
  module_paths.append(project_path)
769
803
 
770
804
  for module_path in module_paths:
771
805
  for path_parts in paths_parts:
772
- main_file = join(module_path, *path_parts)
773
- if os.path.isfile(main_file):
806
+ main_file = os_path_join(module_path, *path_parts)
807
+ if os_path_isfile(main_file):
774
808
  return main_file
775
809
  return ""
776
810
 
@@ -885,7 +919,7 @@ def stack_vars(*skip_modules: str,
885
919
  :param max_depth: the maximum depth in the call stack from which to return the variables. if the specified
886
920
  argument is not zero and no :paramref:`~stack_vars.skip_modules` are specified then the
887
921
  first deeper stack frame that is not within the default :data:`SKIPPED_MODULES` will be
888
- returned. if this argument and :paramref:`~stack_var.find_name` get not passed then the
922
+ returned. if this argument and :paramref:`~stack_vars.find_name` get not passed then the
889
923
  variables of the top stack frame will be returned.
890
924
  :return: tuple of the global and local variable dicts and the depth in the call stack.
891
925
  """
@@ -986,33 +1020,38 @@ def uri2filename(uri: str) -> str:
986
1020
 
987
1021
  more on allowed characters in file names in the answers of RedGrittyBrick on https://superuser.com/questions/358855
988
1022
  and of Christopher Oezbek on https://stackoverflow.com/questions/1976007.
1023
+
1024
+ .. hint:: use :func:`filename2uri` to convert the resulting file name back to the corresponding URO
989
1025
  """
990
1026
  # using urllib.parse.quote(uri, safe="") instead would convert also any non-ascii (e.g. umlaut) characters into hex
991
1027
  # added [] to str.join() argument because List comprehensions are faster than generator expressions
992
1028
  return "".join([f"%{hex(ord(_))[2:].upper()}" if _ in '/|\\:*?"<>%' else _ for _ in uri])
993
1029
 
994
1030
 
995
- def filename2uri(file_name: str) -> str:
996
- """ convert a file name converted by :func:`uri2filename` back to its representation as a URI
1031
+ def utc_datetime() -> datetime.datetime:
1032
+ """ return the current UTC timestamp as string (to use as suffix for file and variable/attribute names).
997
1033
 
998
- :param file_name: name of the file/folder to convert back to its URI representation.
999
- :return: URI string.
1034
+ :return: timestamp string of the actual UTC date and time.
1000
1035
  """
1001
- return urllib.parse.unquote(file_name)
1036
+ return datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None)
1002
1037
 
1003
1038
 
1004
1039
  def write_file(file_path: str, content: Union[str, bytes], extra_mode: str = "", encoding: Optional[str] = None):
1005
1040
  """ (over)write the file specified by :paramref:`~write_file.file_path` with text or binary/bytes content.
1006
1041
 
1007
1042
  :param file_path: file path/name to write the passed content into (overwriting any previous content!).
1008
- :param content: new file content either passed as string or list of line strings (will be
1009
- concatenated with the line separator of the current OS: os.linesep).
1010
- :param extra_mode: open mode flag characters. passed unchanged to the `mode` argument of :func:`open` if
1011
- this argument starts with 'a', else this argument value will be appended to 'w'.
1043
+ :param content: new file content passed either as string or bytes array. if a bytes array get passed
1044
+ then this method will automatically write the content as binary.
1045
+ :param extra_mode: additional open mode flag characters. passed to the `mode` argument of :func:`open` if
1046
+ this argument starts with 'a' or 'w', else this argument value will be appended to 'w'
1047
+ before it get passed to the `mode` argument of :func:`open`.
1048
+ if the :paramref:`~write_file.content` is a bytes array, then a 'b' character will
1049
+ be automatically added to the `mode` argument of :func:`open` (if not already specified
1050
+ in this argument).
1012
1051
  :param encoding: encoding used to write/convert/interpret the file content to write.
1013
1052
  :raises FileExistsError: if file exists already and is write-protected.
1014
1053
  :raises FileNotFoundError: if parts of the file path do not exist.
1015
- :raises OSError: if :paramref:`~read_file.file_path` is misspelled or contains invalid characters.
1054
+ :raises OSError: if :paramref:`~write_file.file_path` is misspelled or contains invalid characters.
1016
1055
  :raises PermissionError: if current OS user account lacks permissions to read the file content.
1017
1056
  :raises ValueError: on decoding errors.
1018
1057
 
@@ -1025,7 +1064,13 @@ def write_file(file_path: str, content: Union[str, bytes], extra_mode: str = "",
1025
1064
  for an intent result.
1026
1065
  Related german docs: https://developer.android.com/training/data-storage/shared/media?hl=de
1027
1066
  """
1028
- with open(file_path, ('' if extra_mode.startswith('a') else 'w') + extra_mode, encoding=encoding) as file_handle:
1067
+ if isinstance(content, bytes) and 'b' not in extra_mode:
1068
+ extra_mode += 'b'
1069
+
1070
+ if extra_mode == '' or extra_mode[0] not in ('a', 'w'):
1071
+ extra_mode = 'w' + extra_mode
1072
+
1073
+ with open(file_path, mode=extra_mode, encoding=encoding) as file_handle:
1029
1074
  file_handle.write(content)
1030
1075
 
1031
1076
 
@@ -1054,16 +1099,16 @@ class ErrorMsgMixin:
1054
1099
  PACKAGE_NAME = stack_var('__name__') or 'unspecified_package'
1055
1100
  PACKAGE_DOMAIN = 'org.test'
1056
1101
  PERMISSIONS = "INTERNET, VIBRATE, READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE"
1057
- if os.path.exists(BUILD_CONFIG_FILE): # pragma: no cover
1102
+ if os_path_isfile(BUILD_CONFIG_FILE): # pragma: no cover
1058
1103
  PACKAGE_NAME, PACKAGE_DOMAIN, PERMISSIONS = build_config_variable_values(
1059
1104
  ('package.name', PACKAGE_NAME),
1060
1105
  ('package.domain', PACKAGE_DOMAIN),
1061
1106
  ('android.permissions', PERMISSIONS))
1062
1107
  elif os_platform == 'android': # pragma: no cover
1063
1108
  _importing_package = norm_path(stack_var('__file__') or 'empty_package' + PY_EXT)
1064
- if os.path.basename(_importing_package) in (PY_INIT, PY_MAIN):
1065
- _importing_package = os.path.dirname(_importing_package)
1066
- _importing_package = os.path.splitext(os.path.basename(_importing_package))[0]
1109
+ if os_path_basename(_importing_package) in (PY_INIT, PY_MAIN):
1110
+ _importing_package = os_path_dirname(_importing_package)
1111
+ _importing_package = os_path_splitext(os_path_basename(_importing_package))[0]
1067
1112
  write_file(f'{_importing_package}_debug.log', f"{BUILD_CONFIG_FILE} not bundled - using defaults\n", extra_mode='a')
1068
1113
 
1069
1114
 
@@ -1,4 +1,4 @@
1
- <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.tpl_project V0.3.30 -->
1
+ <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.tpl_project V0.3.31 -->
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.40
3
+ Version: 0.3.42
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
@@ -32,6 +32,7 @@ Requires-Dist: mypy; extra == "dev"
32
32
  Requires-Dist: pylint; extra == "dev"
33
33
  Requires-Dist: pytest; extra == "dev"
34
34
  Requires-Dist: pytest-cov; extra == "dev"
35
+ Requires-Dist: pytest-django; extra == "dev"
35
36
  Requires-Dist: typing; extra == "dev"
36
37
  Requires-Dist: types-setuptools; extra == "dev"
37
38
  Requires-Dist: wheel; extra == "dev"
@@ -46,6 +47,7 @@ Requires-Dist: mypy; extra == "tests"
46
47
  Requires-Dist: pylint; extra == "tests"
47
48
  Requires-Dist: pytest; extra == "tests"
48
49
  Requires-Dist: pytest-cov; extra == "tests"
50
+ Requires-Dist: pytest-django; extra == "tests"
49
51
  Requires-Dist: typing; extra == "tests"
50
52
  Requires-Dist: types-setuptools; extra == "tests"
51
53
  Requires-Dist: wheel; extra == "tests"
@@ -53,17 +55,17 @@ Requires-Dist: twine; extra == "tests"
53
55
 
54
56
  <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae V0.3.94 -->
55
57
  <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.tpl_namespace_root V0.3.14 -->
56
- # base 0.3.40
58
+ # base 0.3.42
57
59
 
58
60
  [![GitLab develop](https://img.shields.io/gitlab/pipeline/ae-group/ae_base/develop?logo=python)](
59
61
  https://gitlab.com/ae-group/ae_base)
60
62
  [![LatestPyPIrelease](
61
- https://img.shields.io/gitlab/pipeline/ae-group/ae_base/release0.3.39?logo=python)](
62
- https://gitlab.com/ae-group/ae_base/-/tree/release0.3.39)
63
+ https://img.shields.io/gitlab/pipeline/ae-group/ae_base/release0.3.41?logo=python)](
64
+ https://gitlab.com/ae-group/ae_base/-/tree/release0.3.41)
63
65
  [![PyPIVersions](https://img.shields.io/pypi/v/ae_base)](
64
66
  https://pypi.org/project/ae-base/#history)
65
67
 
66
- >ae_base module 0.3.40.
68
+ >ae_base module 0.3.42.
67
69
 
68
70
  [![Coverage](https://ae-group.gitlab.io/ae_base/coverage.svg)](
69
71
  https://ae-group.gitlab.io/ae_base/coverage/index.html)
@@ -0,0 +1,7 @@
1
+ ae/base.py,sha256=cvkGRht-0pQXaokHocJ0h6P5mZOpkUR3JjG357rxyJU,57805
2
+ ae_base-0.3.42.dist-info/LICENSE.md,sha256=eHVr16LWo8bsm2NbdFOBcQp-F_3MUXo9I922sABpsMQ,35002
3
+ ae_base-0.3.42.dist-info/METADATA,sha256=ceQT7h6abqzZYIuaNnIY3EREiZ8rOlpR4qFiX-bn2EI,5311
4
+ ae_base-0.3.42.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
5
+ ae_base-0.3.42.dist-info/top_level.txt,sha256=vUdgAslSmhZLXWU48fm8AG2BjVnkOWLco8rzuW-5zY0,3
6
+ ae_base-0.3.42.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
7
+ ae_base-0.3.42.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.1.0)
2
+ Generator: setuptools (75.2.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,7 +0,0 @@
1
- ae/base.py,sha256=-_eBIIgBgO1opcu_GlqDB1qQOU0qzuUvKUd1YM0hT5c,55971
2
- ae_base-0.3.40.dist-info/LICENSE.md,sha256=3X7IwvwQFt4PqRHb7mV8qoJjQ1E-HmcGioyT4Y6-6c8,35002
3
- ae_base-0.3.40.dist-info/METADATA,sha256=Mvak-VG2CUOXlP0lNF5wi2UuQSSkdPm8WdS5FsUwL5g,5219
4
- ae_base-0.3.40.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
5
- ae_base-0.3.40.dist-info/top_level.txt,sha256=vUdgAslSmhZLXWU48fm8AG2BjVnkOWLco8rzuW-5zY0,3
6
- ae_base-0.3.40.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
7
- ae_base-0.3.40.dist-info/RECORD,,