ae-base 0.3.42__tar.gz → 0.3.44__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.42
3
+ Version: 0.3.44
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
@@ -55,17 +55,17 @@ Requires-Dist: twine; extra == "tests"
55
55
 
56
56
  <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae V0.3.94 -->
57
57
  <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.tpl_namespace_root V0.3.14 -->
58
- # base 0.3.42
58
+ # base 0.3.44
59
59
 
60
60
  [![GitLab develop](https://img.shields.io/gitlab/pipeline/ae-group/ae_base/develop?logo=python)](
61
61
  https://gitlab.com/ae-group/ae_base)
62
62
  [![LatestPyPIrelease](
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
+ https://img.shields.io/gitlab/pipeline/ae-group/ae_base/release0.3.43?logo=python)](
64
+ https://gitlab.com/ae-group/ae_base/-/tree/release0.3.43)
65
65
  [![PyPIVersions](https://img.shields.io/pypi/v/ae_base)](
66
66
  https://pypi.org/project/ae-base/#history)
67
67
 
68
- >ae_base module 0.3.42.
68
+ >ae_base module 0.3.44.
69
69
 
70
70
  [![Coverage](https://ae-group.gitlab.io/ae_base/coverage.svg)](
71
71
  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.42
3
+ # base 0.3.44
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.41?logo=python)](
9
- https://gitlab.com/ae-group/ae_base/-/tree/release0.3.41)
8
+ https://img.shields.io/gitlab/pipeline/ae-group/ae_base/release0.3.43?logo=python)](
9
+ https://gitlab.com/ae-group/ae_base/-/tree/release0.3.43)
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.42.
13
+ >ae_base module 0.3.44.
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)
@@ -46,8 +46,9 @@ the function :func:`duplicates` returns the duplicates of an iterable type.
46
46
  to normalize a file path, in order to remove `.`, `..` placeholders, to resolve symbolic links or to make it relative or
47
47
  absolute, call the function :func:`norm_path`.
48
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.
49
+ :func:`defuse` converts special characters of a URI/URL or a file path string, resulting in a string that can be used
50
+ either as a URL slug or as a file name. use the function :func:`dedefuse` to convert this string back to the
51
+ corresponding URL/URI or file path.
51
52
 
52
53
  :func:`camel_to_snake` and :func:`snake_to_camel` providing name conversions of class and method names.
53
54
 
@@ -162,7 +163,6 @@ import shutil
162
163
  import socket
163
164
  import sys
164
165
  import unicodedata
165
- import urllib.parse
166
166
  import warnings
167
167
 
168
168
  from configparser import ConfigParser, ExtendedInterpolation
@@ -173,7 +173,7 @@ from types import ModuleType
173
173
  from typing import Any, Callable, Dict, Generator, Iterable, List, Optional, Tuple, Union, cast
174
174
 
175
175
 
176
- __version__ = '0.3.42'
176
+ __version__ = '0.3.44'
177
177
 
178
178
 
179
179
  os_path_abspath = os.path.abspath
@@ -336,6 +336,96 @@ def deep_dict_update(data: dict, update: dict):
336
336
  data[upd_key] = upd_val
337
337
 
338
338
 
339
+ URI_SEP_CHAR = '⫻' # U+2AFB: TRIPLE SOLIDUS BINARY RELATION
340
+ ASCII_UNICODE = (
341
+ ('/', '⁄'), # U+2044: Fraction Slash; '∕' U+2215: Division Slash; '⧸' U+29F8: Big Solidus
342
+ # ; '╱' U+FF0F: Fullwidth Solidus; '╱' U+2571: Box Drawings Light Diagonal Upper Right to Lower Left
343
+ ('|', '।'), # U+0964: Devanagari Danda
344
+ ('\\', '﹨'), # U+FE68: SMALL REVERSE SOLIDUS; '⑊' U+244A OCR DOUBLE BACKSLASH; '⧵' U+29F5 REV. SOLIDUS OPERATOR
345
+ (':', '﹕'), # U+FE55: Small Colon
346
+ ('*', '﹡'), # U+FE61: Small Asterisk
347
+ ('?', '﹖'), # U+FE56: Small Question Mark
348
+ ('"', '"'), # U+FF02: Fullwidth Quotation Mark
349
+ ("'", '‘'), # U+2018: Left Single; '’' U+2019: Right Single; '‛' U+201B: Single High-Reversed-9 Quotation Mark
350
+ ('<', '⟨'), # U+27E8: LEFT ANGLE BRACKET; '‹' U+2039: Single Left-Pointing Angle Quotation Mark
351
+ ('>', '⟩'), # U+27E9: RIGHT ANGLE BRACKET; '›' U+203A: Single Right-Pointing Angle Quotation Mark
352
+ ('(', '⟮'), # U+27EE: MATHEMATICAL LEFT FLATTENED PARENTHESIS
353
+ (')', '⟯'), # U+27EF: MATHEMATICAL RIGHT FLATTENED PARENTHESIS
354
+ ('[', '⟦'), # U+27E6: MATHEMATICAL LEFT WHITE SQUARE BRACKET
355
+ (']', '⟧'), # U+27E7: MATHEMATICAL RIGHT WHITE SQUARE BRACKET
356
+ ('#', '﹟'), # U+FE5F: Small Number Sign
357
+ (';', '﹔'), # U+FE54: Small Semicolon
358
+ ('@', '﹫'), # U+FE6B: Small Commercial At
359
+ ('&', '﹠'), # U+FE60: Small Ampersand
360
+ ('=', '﹦'), # U+FE66: Small Equals Sign
361
+ ('+', '﹢'), # U+FE62: Small Plus Sign
362
+ ('$', '﹩'), # U+FE69: Small Dollar Sign
363
+ ('%', '﹪'), # U+FE6A: Small Percent Sign
364
+ ('^', '^'), # U+FF3E: Fullwidth Circumflex Accent
365
+ (',', '﹐'), # U+FE50: Small Comma
366
+ (' ', ' '), # U+3000: Ideographic Space; ' ' U+200A Hair Space; ' ' U+2007 Figure Space;
367
+ # ' ' U+2009 Thin; ' ' U+2003 Em Space; ' ' U+2002 En Space; ' ' U+2008 Punctuation Space
368
+ # ' ' U+00A0: No-Break Space (NBSP); ' ' U+202F: Narrow No-Break Space (NNBSP)
369
+ (chr(127), '␡'), # U+2421: DELETE SYMBOL
370
+ # ('_', '𛲖'), # U+1BC96: Duployan Affix Low Line; '_' U+FF3F Fullwidth Low Line
371
+ )
372
+ """ transformation table of special ASCII to Unicode alternative character,
373
+ see https://www.compart.com/en/unicode/category/Po and https://xahlee.info/comp/unicode_naming_slash.html (http!) """
374
+
375
+ ASCII_TO_UNICODE = dict(ASCII_UNICODE) #: map to convert ASCII to an alternative defused Unicode character
376
+ UNICODE_TO_ASCII = {unicode_char: ascii_char for ascii_char, unicode_char in ASCII_UNICODE} #: Unicode to ASCII map
377
+
378
+
379
+ def dedefuse(value: str) -> str:
380
+ """ convert a string that got defused with :func:`defuse` back to its original form.
381
+
382
+ :param value: string defused with the function :func:`defuse`.
383
+ :return: re-activated form of the string (with all ASCII special characters recovered).
384
+ """
385
+ original = ""
386
+ for char in value:
387
+ if char in UNICODE_TO_ASCII:
388
+ char = UNICODE_TO_ASCII[char]
389
+ elif 0x2400 <= (code := ord(char)) <= 0x241F:
390
+ char = chr(code - 0x2400)
391
+ original += char
392
+
393
+ return original.replace(URI_SEP_CHAR, '://')
394
+
395
+
396
+ def defuse(value: str) -> str:
397
+ """ convert a file path or a URI into a defused/presentational form to be usable as URL slug or file/folder name.
398
+
399
+ :param value: any string to defuse (replace special chars with Unicode alternatives).
400
+ :return: string with its special characters replaced by its pure presentational alternatives.
401
+
402
+ the ASCII character range 0..31 gets converted to the Unicode range U+2400 + ord(char): 0==U+2400 ... 31==U+241F.
403
+
404
+ in *nix only / and \0 are not allowed characters in file names.
405
+
406
+ in MS Windows are not allowed: ASCII 0...31): / | \\ : * ? ” % < > ( ). some blogs recommend to also not allow
407
+ (convert) the characters # and '.
408
+ only old POSIX seems to be even more restricted (only allowing alphanumeric characters plus . - and _).
409
+
410
+ more on allowed characters in file names in the answers of RedGrittyBrick on https://superuser.com/questions/358855
411
+ and of Christopher Oezbek on https://stackoverflow.com/questions/1976007.
412
+
413
+ file name length is not restricted/shortened by this function, although the maximum is 255 characters on most OSs.
414
+
415
+ .. hint:: use :func:`dedefuse` to convert the defused string back to the corresponding URI/file-path.
416
+
417
+ """
418
+ defused = ""
419
+ value = value.replace('://', URI_SEP_CHAR) # make URIs shorter
420
+ for char in value:
421
+ if char in ASCII_TO_UNICODE:
422
+ char = ASCII_TO_UNICODE[char]
423
+ elif (code := ord(char)) <= 31:
424
+ char = chr(0x2400 + code)
425
+ defused += char
426
+ return defused
427
+
428
+
339
429
  def dummy_function(*_args, **_kwargs):
340
430
  """ null function accepting any arguments and returning None.
341
431
 
@@ -379,17 +469,6 @@ def env_str(name: str, convert_name: bool = False) -> Optional[str]:
379
469
  return os.environ.get(name)
380
470
 
381
471
 
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
-
393
472
  def force_encoding(text: Union[str, bytes], encoding: str = DEF_ENCODING, errors: str = DEF_ENCODE_ERRORS) -> str:
394
473
  """ force/ensure the encoding of text (str or bytes) without any UnicodeDecodeError/UnicodeEncodeError.
395
474
 
@@ -1005,29 +1084,6 @@ def to_ascii(unicode_str: str) -> str:
1005
1084
  return "".join([c for c in nfkd_form if not unicodedata.combining(c)]).replace('ß', "ss").replace('€', "Euro")
1006
1085
 
1007
1086
 
1008
- def uri2filename(uri: str) -> str:
1009
- """ convert a URI to be usable as name of a file or folder
1010
-
1011
- :param uri: URI to convert to a corresponding file name, that will be revertible back to this URI.
1012
- :return: name of a file/folder representing the specified URI.
1013
-
1014
- in *nix only / and \0 are not allowed characters in file names.
1015
- in MS Windows are not allowed: ASCII 0...31): / \\ : * ? ” < > | (). some blogs recommend to also not allow
1016
- (convert) the characters # and '.
1017
- only old POSIX seems to be even more restricted (only allowing alphanumeric characters plus . - and _).
1018
-
1019
- file name length is not restricted/shortened by this function, although the maximum is 255 characters on most OSs.
1020
-
1021
- more on allowed characters in file names in the answers of RedGrittyBrick on https://superuser.com/questions/358855
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
1025
- """
1026
- # using urllib.parse.quote(uri, safe="") instead would convert also any non-ascii (e.g. umlaut) characters into hex
1027
- # added [] to str.join() argument because List comprehensions are faster than generator expressions
1028
- return "".join([f"%{hex(ord(_))[2:].upper()}" if _ in '/|\\:*?"<>%' else _ for _ in uri])
1029
-
1030
-
1031
1087
  def utc_datetime() -> datetime.datetime:
1032
1088
  """ return the current UTC timestamp as string (to use as suffix for file and variable/attribute names).
1033
1089
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ae_base
3
- Version: 0.3.42
3
+ Version: 0.3.44
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
@@ -55,17 +55,17 @@ Requires-Dist: twine; extra == "tests"
55
55
 
56
56
  <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae V0.3.94 -->
57
57
  <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.tpl_namespace_root V0.3.14 -->
58
- # base 0.3.42
58
+ # base 0.3.44
59
59
 
60
60
  [![GitLab develop](https://img.shields.io/gitlab/pipeline/ae-group/ae_base/develop?logo=python)](
61
61
  https://gitlab.com/ae-group/ae_base)
62
62
  [![LatestPyPIrelease](
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
+ https://img.shields.io/gitlab/pipeline/ae-group/ae_base/release0.3.43?logo=python)](
64
+ https://gitlab.com/ae-group/ae_base/-/tree/release0.3.43)
65
65
  [![PyPIVersions](https://img.shields.io/pypi/v/ae_base)](
66
66
  https://pypi.org/project/ae-base/#history)
67
67
 
68
- >ae_base module 0.3.42.
68
+ >ae_base module 0.3.44.
69
69
 
70
70
  [![Coverage](https://ae-group.gitlab.io/ae_base/coverage.svg)](
71
71
  https://ae-group.gitlab.io/ae_base/coverage/index.html)
@@ -1,6 +1,7 @@
1
1
  """ ae.base unit tests """
2
2
  import datetime
3
3
  import os
4
+ import string
4
5
  import tempfile
5
6
 
6
7
  import pytest
@@ -15,17 +16,21 @@ from typing import cast
15
16
 
16
17
  # noinspection PyProtectedMember
17
18
  from ae.base import (
18
- BUILD_CONFIG_FILE, DOTENV_FILE_NAME, PY_EXT, PY_INIT, PY_MAIN, TESTS_FOLDER, UNSET,
19
- app_name_guess, build_config_variable_values, camel_to_snake, deep_dict_update, dummy_function, duplicates, env_str,
20
- filename2uri, force_encoding, full_stack_trace, import_module, instantiate_config_parser, in_wd,
19
+ ASCII_TO_UNICODE, BUILD_CONFIG_FILE, DOTENV_FILE_NAME, PY_EXT, PY_INIT, PY_MAIN, TESTS_FOLDER, UNICODE_TO_ASCII,
20
+ UNSET,
21
+ URI_SEP_CHAR, app_name_guess, build_config_variable_values, camel_to_snake, deep_dict_update, dummy_function,
22
+ duplicates, env_str,
23
+ dedefuse, force_encoding, full_stack_trace, import_module, instantiate_config_parser, in_wd,
21
24
  load_env_var_defaults, load_dotenvs, main_file_paths_parts, module_attr, module_file_path, module_name,
22
25
  norm_line_sep, norm_name, norm_path, now_str, os_host_name, os_local_ip, _os_platform, os_user_name,
23
26
  parse_dotenv, project_main_file, read_file, round_traditional, snake_to_camel, stack_frames, stack_var, stack_vars,
24
- sys_env_dict, sys_env_text, to_ascii, uri2filename, utc_datetime, write_file, ErrorMsgMixin)
27
+ sys_env_dict, sys_env_text, to_ascii, defuse, utc_datetime, write_file, ErrorMsgMixin)
25
28
 
26
29
 
27
- tst_uri1 = "schema://user:pwd@domain/path_root/path_sub\\path+file% Üml?ä|ït.path_ext*\"<>"
28
- 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"
30
+ tst_uri1 = "schema://user:pwd@domain/path_root/path_sub\\path+file% Üml?ä|ït.path_ext*\"<>|*'()[]{}#^;&=$,~" + chr(127)
31
+ tst_fna1 = "schema⫻user﹕pwd﹫domain⁄path_root⁄path_sub﹨path﹢file﹪ Üml﹖ä।ït.path_ext﹡"⟨⟩।﹡‘⟮⟯⟦⟧{}﹟^﹔﹠﹦﹩﹐~␡"
32
+ tst_uri2 = "test control chars" + "".join(chr(_) for _ in range(1, 32))
33
+ tst_fna2 = "test\u3000control\u3000chars␁␂␃␄␅␆␇␈␉␊␋␌␍␎␏␐␑␒␓␔␕␖␗␘␙␚␛␜␝␞␟"
29
34
 
30
35
  env_var_name = 'env_var_nam1'
31
36
  env_var_val = 'value of env var'
@@ -171,6 +176,44 @@ class TestBaseHelpers:
171
176
  assert pev['setup_kwargs']['entry_points']['console_scripts'] == lst_val
172
177
  assert pev['setup_kwargs']['entry_points']['console_scripts'][0] == str_new
173
178
 
179
+ def test_dedefuse_file_name(self):
180
+ assert dedefuse(tst_fna1) == tst_uri1
181
+ assert dedefuse(tst_fna2) == tst_uri2
182
+
183
+ assert dedefuse(defuse(tst_uri1)) == tst_uri1
184
+ assert dedefuse(defuse(tst_uri2)) == tst_uri2
185
+
186
+ def test_defuse_file_name(self):
187
+ assert defuse(tst_uri1) == tst_fna1
188
+ assert defuse(tst_uri2) == tst_fna2
189
+
190
+ assert defuse(dedefuse(tst_fna1)) == tst_fna1
191
+ assert defuse(dedefuse(tst_fna2)) == tst_fna2
192
+
193
+ def test_defuse_os_file_name(self):
194
+ try:
195
+ write_file(tst_fna1, "tst uri file content1")
196
+ assert os.path.exists(tst_fna1)
197
+ write_file(tst_fna2, "tst uri file content2")
198
+ assert os.path.exists(tst_fna2)
199
+ finally:
200
+ if os.path.exists(tst_fna1):
201
+ os.remove(tst_fna1)
202
+ if os.path.exists(tst_fna2):
203
+ os.remove(tst_fna2)
204
+
205
+ def test_defuse_maps_integrity(self):
206
+ assert URI_SEP_CHAR not in UNICODE_TO_ASCII
207
+ assert len(UNICODE_TO_ASCII) == len(ASCII_TO_UNICODE) # check for duplicates in the ASCII_UNICODE map
208
+
209
+ def test_defuse_maps_not_touching_chars_allowed_as_slug_and_filename(self):
210
+ assert '-' not in ASCII_TO_UNICODE
211
+ assert '_' not in ASCII_TO_UNICODE
212
+ assert '.' not in ASCII_TO_UNICODE
213
+ assert '~' not in ASCII_TO_UNICODE
214
+ for char in string.ascii_letters + string.digits:
215
+ assert char not in ASCII_TO_UNICODE
216
+
174
217
  def test_dummy_function(self):
175
218
  assert dummy_function() is None
176
219
  assert dummy_function(999, "any_args") is None
@@ -204,10 +247,6 @@ class TestBaseHelpers:
204
247
  os.environ['NON_ALPHA_NUM_CHARS_69'] = vv
205
248
  assert env_str(ev, convert_name=True) == vv
206
249
 
207
- def test_filename2uri(self):
208
- assert filename2uri(tst_fna1) == tst_uri1
209
- assert uri2filename(filename2uri(tst_fna1)) == tst_fna1
210
-
211
250
  def test_force_encoding_bytes(self):
212
251
  s = 'äöü'
213
252
 
@@ -708,22 +747,11 @@ class TestBaseHelpers:
708
747
  assert to_ascii('ß') == 'ss'
709
748
  assert to_ascii('€') == 'Euro'
710
749
 
711
- def test_uri2filename(self):
712
- assert uri2filename(tst_uri1) == tst_fna1
713
- assert filename2uri(uri2filename(tst_uri1)) == tst_uri1
714
-
715
750
  def test_utc_datetime(self):
716
751
  dt1 = utc_datetime()
717
752
  dt2 = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None)
718
753
  assert dt2 - dt1 < datetime.timedelta(seconds=1)
719
754
 
720
- def test_uri_file_name(self):
721
- try:
722
- write_file(tst_fna1, "tst uri file content")
723
- finally:
724
- if os.path.exists(tst_fna1):
725
- os.remove(tst_fna1)
726
-
727
755
  def test_write_file_as_text(self):
728
756
  test_file = os.path.join(TESTS_FOLDER, 'tst_file_written.ext')
729
757
  content = "any content"
File without changes
File without changes
File without changes