ae-base 0.3.38__tar.gz → 0.3.40__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ae_base
3
- Version: 0.3.38
3
+ Version: 0.3.40
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
@@ -53,17 +53,17 @@ Requires-Dist: twine; extra == "tests"
53
53
 
54
54
  <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae V0.3.94 -->
55
55
  <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.tpl_namespace_root V0.3.14 -->
56
- # base 0.3.38
56
+ # base 0.3.40
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.37?logo=python)](
62
- https://gitlab.com/ae-group/ae_base/-/tree/release0.3.37)
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
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.38.
66
+ >ae_base module 0.3.40.
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)
@@ -1,16 +1,16 @@
1
1
  <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae V0.3.94 -->
2
2
  <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.tpl_namespace_root V0.3.14 -->
3
- # base 0.3.38
3
+ # base 0.3.40
4
4
 
5
5
  [![GitLab develop](https://img.shields.io/gitlab/pipeline/ae-group/ae_base/develop?logo=python)](
6
6
  https://gitlab.com/ae-group/ae_base)
7
7
  [![LatestPyPIrelease](
8
- https://img.shields.io/gitlab/pipeline/ae-group/ae_base/release0.3.37?logo=python)](
9
- https://gitlab.com/ae-group/ae_base/-/tree/release0.3.37)
8
+ https://img.shields.io/gitlab/pipeline/ae-group/ae_base/release0.3.39?logo=python)](
9
+ https://gitlab.com/ae-group/ae_base/-/tree/release0.3.39)
10
10
  [![PyPIVersions](https://img.shields.io/pypi/v/ae_base)](
11
11
  https://pypi.org/project/ae-base/#history)
12
12
 
13
- >ae_base module 0.3.38.
13
+ >ae_base module 0.3.40.
14
14
 
15
15
  [![Coverage](https://ae-group.gitlab.io/ae_base/coverage.svg)](
16
16
  https://ae-group.gitlab.io/ae_base/coverage/index.html)
@@ -3,7 +3,7 @@ basic constants, helper functions and context manager
3
3
  =====================================================
4
4
 
5
5
  this module is pure python, has no external dependencies, and is providing base constants, common helper
6
- functions and context managers.
6
+ functions, useful classes and context managers.
7
7
 
8
8
 
9
9
  base constants
@@ -24,6 +24,9 @@ the constants :data:`PACKAGE_NAME`, :data:`PACKAGE_DOMAIN` and :data:`PERMISSION
24
24
  on mobile devices. to avoid redundancies, these values get loaded from the
25
25
  :data:`build config file <BUILD_CONFIG_FILE>` - if it exists in the current working directory.
26
26
 
27
+ with the help of the format string constant :data:`NOW_STR_FORMAT` and the function :func:`now_str` you can create a
28
+ sortable and compact string from a timestamp.
29
+
27
30
 
28
31
  base helper functions
29
32
  ---------------------
@@ -42,7 +45,7 @@ absolute, call the function :func:`norm_path`.
42
45
 
43
46
  :func:`camel_to_snake` and :func:`snake_to_camel` providing name conversions of class and method names.
44
47
 
45
- to encode unicode strings to other codecs the functions :func:`force_encoding` and :func:`to_ascii` can be used.
48
+ to encode Unicode strings to other codecs the functions :func:`force_encoding` and :func:`to_ascii` can be used.
46
49
 
47
50
  the :func:`round_traditional` function get provided by this module for traditional rounding of float values. the
48
51
  function signature is fully compatible to Python's :func:`round` function.
@@ -99,6 +102,15 @@ code and build Kivy apps for the Android OS, and to `Gabriel Pettier <https://gi
99
102
  osc example.
100
103
 
101
104
 
105
+ types, classes and mixins
106
+ -------------------------
107
+
108
+ the :class:`UnsetType` class can be used e.g. for the declaration of optional function and method parameters,
109
+ allowing also `None` is an accepted argument value.
110
+
111
+ to extend any class with an intelligent error message property, add the mixin :class:`ErrorMsgMixin` to it.
112
+
113
+
102
114
  generic context manager
103
115
  -----------------------
104
116
 
@@ -137,6 +149,7 @@ import shutil
137
149
  import socket
138
150
  import sys
139
151
  import unicodedata
152
+ import urllib.parse
140
153
  import warnings
141
154
 
142
155
  from configparser import ConfigParser, ExtendedInterpolation
@@ -146,7 +159,8 @@ from inspect import getinnerframes, getouterframes, getsourcefile
146
159
  from types import ModuleType
147
160
  from typing import Any, Callable, Dict, Generator, Iterable, List, Optional, Tuple, Union, cast
148
161
 
149
- __version__ = '0.3.38'
162
+
163
+ __version__ = '0.3.40'
150
164
 
151
165
 
152
166
  DOCS_FOLDER = 'docs' #: project documentation root folder name
@@ -200,7 +214,9 @@ _env_variable = re.compile(r"""
200
214
  """, re.IGNORECASE | re.VERBOSE)
201
215
 
202
216
 
203
- NAME_PARTS_SEP = '_' #: name parts separator character, e.g. for :func:`norm_name`
217
+ NAME_PARTS_SEP = '_' #: name parts separator character, e.g. for :func:`norm_name`
218
+
219
+ NOW_STR_FORMAT = "{sep}%Y%m%d{sep}%H%M%S{sep}%f" #: timestamp format of :func:`now_str`
204
220
 
205
221
  SKIPPED_MODULES = ('ae.base', 'ae.paths', 'ae.dynamicod', 'ae.core', 'ae.console', 'ae.gui_app', 'ae.gui_help',
206
222
  'ae.kivy', 'ae.kivy.apps', 'ae.kivy.behaviors', 'ae.kivy.i18n', 'ae.kivy.tours', 'ae.kivy.widgets',
@@ -610,9 +626,12 @@ def now_str(sep: str = "") -> str:
610
626
 
611
627
  :param sep: optional prefix and separator character (separating date from time and in time part
612
628
  the seconds from the microseconds).
613
- :return: UTC timestamp as string (length=20 + 3 * len(sep)).
629
+ :return: naive UTC timestamp (without timezone info) as string (length=20 + 3 * len(sep)).
614
630
  """
615
- return datetime.datetime.utcnow().strftime("{sep}%Y%m%d{sep}%H%M%S{sep}%f".format(sep=sep))
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))
616
635
 
617
636
 
618
637
  def os_host_name() -> str:
@@ -921,7 +940,7 @@ def sys_env_text(ind_ch: str = " ", ind_len: int = 12, key_ch: str = "=", key_le
921
940
  extra_sys_env_dict: Optional[Dict[str, str]] = None) -> str:
922
941
  """ compile formatted text block with system environment info.
923
942
 
924
- :param ind_ch: indent character (default=" ").
943
+ :param ind_ch: indent character (defaults to " ").
925
944
  :param ind_len: indent depths (default=12 characters).
926
945
  :param key_ch: key-value separator character (default="=").
927
946
  :param key_len: key-name minimum length (default=15 characters).
@@ -940,7 +959,7 @@ def sys_env_text(ind_ch: str = " ", ind_len: int = 12, key_ch: str = "=", key_le
940
959
 
941
960
 
942
961
  def to_ascii(unicode_str: str) -> str:
943
- """ converts unicode string into ascii representation.
962
+ """ converts Unicode string into ascii representation.
944
963
 
945
964
  useful for fuzzy string compare; inspired by MiniQuark's answer
946
965
  in: https://stackoverflow.com/questions/517923/what-is-the-best-way-to-remove-accents-in-a-python-unicode-string
@@ -952,6 +971,36 @@ def to_ascii(unicode_str: str) -> str:
952
971
  return "".join([c for c in nfkd_form if not unicodedata.combining(c)]).replace('ß', "ss").replace('€', "Euro")
953
972
 
954
973
 
974
+ def uri2filename(uri: str) -> str:
975
+ """ convert a URI to be usable as name of a file or folder
976
+
977
+ :param uri: URI to convert to a corresponding file name, that will be revertible back to this URI.
978
+ :return: name of a file/folder representing the specified URI.
979
+
980
+ in *nix only / and \0 are not allowed characters in file names.
981
+ in MS Windows are not allowed: ASCII 0...31): / \\ : * ? ” < > | (). some blogs recommend to also not allow
982
+ (convert) the characters # and '.
983
+ only old POSIX seems to be even more restricted (only allowing alphanumeric characters plus . - and _).
984
+
985
+ file name length is not restricted/shortened by this function, although the maximum is 255 characters on most OSs.
986
+
987
+ more on allowed characters in file names in the answers of RedGrittyBrick on https://superuser.com/questions/358855
988
+ and of Christopher Oezbek on https://stackoverflow.com/questions/1976007.
989
+ """
990
+ # using urllib.parse.quote(uri, safe="") instead would convert also any non-ascii (e.g. umlaut) characters into hex
991
+ # added [] to str.join() argument because List comprehensions are faster than generator expressions
992
+ return "".join([f"%{hex(ord(_))[2:].upper()}" if _ in '/|\\:*?"<>%' else _ for _ in uri])
993
+
994
+
995
+ def filename2uri(file_name: str) -> str:
996
+ """ convert a file name converted by :func:`uri2filename` back to its representation as a URI
997
+
998
+ :param file_name: name of the file/folder to convert back to its URI representation.
999
+ :return: URI string.
1000
+ """
1001
+ return urllib.parse.unquote(file_name)
1002
+
1003
+
955
1004
  def write_file(file_path: str, content: Union[str, bytes], extra_mode: str = "", encoding: Optional[str] = None):
956
1005
  """ (over)write the file specified by :paramref:`~write_file.file_path` with text or binary/bytes content.
957
1006
 
@@ -966,11 +1015,42 @@ def write_file(file_path: str, content: Union[str, bytes], extra_mode: str = "",
966
1015
  :raises OSError: if :paramref:`~read_file.file_path` is misspelled or contains invalid characters.
967
1016
  :raises PermissionError: if current OS user account lacks permissions to read the file content.
968
1017
  :raises ValueError: on decoding errors.
1018
+
1019
+ to extend this function for Android 14+ see https://github.com/beeware/toga/pull/1158#issuecomment-2254564657
1020
+ and https://gist.github.com/neonankiti/05922cf0a44108a2e2732671ed9ef386
1021
+ Yes, to use ACTION_CREATE_DOCUMENT, you don't supply a URI in the intent. You wait for the intent result, and that
1022
+ will contain a URI which you can write to.
1023
+ See #1158 (comment - https://github.com/beeware/toga/pull/1158#issuecomment-2254564657) for a link to a Java
1024
+ example, and #1158 (comment - https://github.com/beeware/toga/pull/1158#issuecomment-1446196973) for how to wait
1025
+ for an intent result.
1026
+ Related german docs: https://developer.android.com/training/data-storage/shared/media?hl=de
969
1027
  """
970
1028
  with open(file_path, ('' if extra_mode.startswith('a') else 'w') + extra_mode, encoding=encoding) as file_handle:
971
1029
  file_handle.write(content)
972
1030
 
973
1031
 
1032
+ class ErrorMsgMixin:
1033
+ """ mixin class providing error message """
1034
+ _err_msg: str = ""
1035
+
1036
+ @property
1037
+ def error_message(self) -> str:
1038
+ """ error message string if an error occurred or an empty string if not.
1039
+
1040
+ :getter: return the accumulated error message of the recently occurred error(s).
1041
+ :setter: any assigned error message will be accumulated to recent error messages.
1042
+ pass an empty string to reset the error message.
1043
+ """
1044
+ return self._err_msg
1045
+
1046
+ @error_message.setter
1047
+ def error_message(self, next_err_msg: str):
1048
+ if next_err_msg:
1049
+ self._err_msg += ("\n\n" if self._err_msg else "") + next_err_msg
1050
+ else:
1051
+ self._err_msg = ""
1052
+
1053
+
974
1054
  PACKAGE_NAME = stack_var('__name__') or 'unspecified_package'
975
1055
  PACKAGE_DOMAIN = 'org.test'
976
1056
  PERMISSIONS = "INTERNET, VIBRATE, READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ae_base
3
- Version: 0.3.38
3
+ Version: 0.3.40
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
@@ -53,17 +53,17 @@ Requires-Dist: twine; extra == "tests"
53
53
 
54
54
  <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae V0.3.94 -->
55
55
  <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.tpl_namespace_root V0.3.14 -->
56
- # base 0.3.38
56
+ # base 0.3.40
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.37?logo=python)](
62
- https://gitlab.com/ae-group/ae_base/-/tree/release0.3.37)
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
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.38.
66
+ >ae_base module 0.3.40.
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)
@@ -20,9 +20,12 @@ from ae.base import (
20
20
  load_env_var_defaults, load_dotenvs, main_file_paths_parts, module_attr, module_file_path, module_name,
21
21
  norm_line_sep, norm_name, norm_path, now_str, os_host_name, os_local_ip, _os_platform, os_user_name,
22
22
  parse_dotenv, project_main_file, read_file, round_traditional, snake_to_camel, stack_frames, stack_var, stack_vars,
23
- sys_env_dict, sys_env_text, to_ascii, write_file)
23
+ sys_env_dict, sys_env_text, to_ascii, filename2uri, uri2filename, write_file, ErrorMsgMixin)
24
24
 
25
25
 
26
+ tst_uri1 = "schema://user:pwd@domain/path_root/path_sub\\path+file% Üml?ä|ït.path_ext*\"<>"
27
+ tst_fna1 = "schema%3A%2F%2Fuser%3Apwd@domain%2Fpath_root%2Fpath_sub%5Cpath+file%25 Üml%3Fä%7Cït.path_ext%2A%22%3C%3E"
28
+
26
29
  env_var_name = 'env_var_nam1'
27
30
  env_var_val = 'value of env var'
28
31
  folder_name = 'fdr'
@@ -60,6 +63,27 @@ def test_unset_null_length():
60
63
  assert len(UNSET) == 0
61
64
 
62
65
 
66
+ class TestErrorMsgMixin:
67
+ def test_instantiation(self):
68
+ assert ErrorMsgMixin()
69
+
70
+ def test_error_message_property(self):
71
+ ins = ErrorMsgMixin()
72
+ assert ins.error_message == ""
73
+
74
+ err_msg = "set new error message"
75
+ ins.error_message = err_msg
76
+ assert ins.error_message == err_msg
77
+
78
+ err_msg2 = "added error message"
79
+ ins.error_message = err_msg2
80
+ assert err_msg in ins.error_message
81
+ assert err_msg2 in ins.error_message
82
+
83
+ ins.error_message = ""
84
+ assert ins.error_message == ""
85
+
86
+
63
87
  class TestBaseHelpers:
64
88
  def test_app_name_guess(self):
65
89
  assert app_name_guess() # app.exe name in pytest returning '_jb_pytest_runner'(PyCharm)/'__main__'(console)
@@ -678,6 +702,21 @@ class TestBaseHelpers:
678
702
  assert to_ascii('ß') == 'ss'
679
703
  assert to_ascii('€') == 'Euro'
680
704
 
705
+ def test_filename2uri(self):
706
+ assert filename2uri(tst_fna1) == tst_uri1
707
+ assert uri2filename(filename2uri(tst_fna1)) == tst_fna1
708
+
709
+ def test_uri2filename(self):
710
+ assert uri2filename(tst_uri1) == tst_fna1
711
+ assert filename2uri(uri2filename(tst_uri1)) == tst_uri1
712
+
713
+ def test_uri_file_name(self):
714
+ try:
715
+ write_file(tst_fna1, "tst uri file content")
716
+ finally:
717
+ if os.path.exists(tst_fna1):
718
+ os.remove(tst_fna1)
719
+
681
720
  def test_write_file_as_text(self):
682
721
  test_file = os.path.join(TESTS_FOLDER, 'tst_file_written.ext')
683
722
  content = "any content"
File without changes
File without changes
File without changes