encommon 0.7.6__py3-none-any.whl → 0.8.1__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.
Files changed (53) hide show
  1. encommon/config/__init__.py +6 -0
  2. encommon/config/common.py +18 -14
  3. encommon/config/config.py +21 -14
  4. encommon/config/files.py +13 -7
  5. encommon/config/logger.py +94 -88
  6. encommon/config/params.py +4 -4
  7. encommon/config/paths.py +16 -8
  8. encommon/config/test/test_common.py +27 -4
  9. encommon/config/test/test_config.py +48 -82
  10. encommon/config/test/test_files.py +58 -43
  11. encommon/config/test/test_logger.py +129 -82
  12. encommon/config/test/test_paths.py +70 -30
  13. encommon/conftest.py +52 -12
  14. encommon/crypts/__init__.py +2 -0
  15. encommon/crypts/crypts.py +3 -1
  16. encommon/crypts/hashes.py +2 -2
  17. encommon/crypts/test/test_crypts.py +50 -28
  18. encommon/crypts/test/test_hashes.py +20 -18
  19. encommon/times/__init__.py +2 -0
  20. encommon/times/common.py +99 -15
  21. encommon/times/duration.py +50 -36
  22. encommon/times/parse.py +13 -25
  23. encommon/times/test/test_common.py +47 -16
  24. encommon/times/test/test_duration.py +104 -79
  25. encommon/times/test/test_parse.py +53 -63
  26. encommon/times/test/test_timers.py +90 -36
  27. encommon/times/test/test_times.py +21 -30
  28. encommon/times/test/test_window.py +73 -21
  29. encommon/times/timers.py +91 -58
  30. encommon/times/times.py +36 -34
  31. encommon/times/window.py +4 -4
  32. encommon/types/dicts.py +10 -4
  33. encommon/types/empty.py +7 -2
  34. encommon/types/strings.py +10 -0
  35. encommon/types/test/test_dicts.py +5 -5
  36. encommon/types/test/test_empty.py +4 -1
  37. encommon/types/test/test_strings.py +1 -1
  38. encommon/utils/__init__.py +4 -0
  39. encommon/utils/common.py +51 -6
  40. encommon/utils/match.py +5 -4
  41. encommon/utils/paths.py +42 -23
  42. encommon/utils/sample.py +31 -27
  43. encommon/utils/stdout.py +28 -17
  44. encommon/utils/test/test_common.py +35 -0
  45. encommon/utils/test/test_paths.py +3 -2
  46. encommon/utils/test/test_sample.py +28 -12
  47. encommon/version.txt +1 -1
  48. {encommon-0.7.6.dist-info → encommon-0.8.1.dist-info}/METADATA +1 -1
  49. encommon-0.8.1.dist-info/RECORD +63 -0
  50. encommon-0.7.6.dist-info/RECORD +0 -62
  51. {encommon-0.7.6.dist-info → encommon-0.8.1.dist-info}/LICENSE +0 -0
  52. {encommon-0.7.6.dist-info → encommon-0.8.1.dist-info}/WHEEL +0 -0
  53. {encommon-0.7.6.dist-info → encommon-0.8.1.dist-info}/top_level.txt +0 -0
encommon/utils/common.py CHANGED
@@ -12,16 +12,61 @@ from typing import Union
12
12
 
13
13
 
14
14
 
15
- JOINABLE = (
16
- list # type: ignore
17
- | tuple # type: ignore
18
- | set) # type: ignore
15
+ JOINABLE = (list, tuple, set)
19
16
 
20
17
 
21
18
 
22
19
  PATHABLE = Union[
23
- str,
24
- Path,
20
+ str, Path,
25
21
  list[str | Path],
26
22
  tuple[str | Path],
27
23
  set[str]]
24
+
25
+
26
+
27
+ REPLACE = Union[
28
+ dict[str, str],
29
+ dict[str, str | Path],
30
+ dict[str, Path],
31
+ dict[str, str],
32
+ dict[str | Path, str],
33
+ dict[Path, str]]
34
+
35
+
36
+
37
+ def read_text(
38
+ path: str | Path,
39
+ ) -> str:
40
+ """
41
+ Read the text content from within the provided file path.
42
+
43
+ :param path: Complete or relative path to the text file.
44
+ :returns: Text content that was read from the file path.
45
+ """
46
+
47
+ path = Path(path).resolve()
48
+
49
+ return path.read_text(
50
+ encoding='utf-8')
51
+
52
+
53
+
54
+ def save_text(
55
+ path: str | Path,
56
+ content: str,
57
+ ) -> str:
58
+ """
59
+ Save the provided text content to the provided file path.
60
+
61
+ :param path: Complete or relative path to the text file.
62
+ :param content: Content that will be written to the file.
63
+ :returns: Text content that was read from the file path.
64
+ """
65
+
66
+ path = Path(path).resolve()
67
+
68
+ path.write_text(
69
+ data=content,
70
+ encoding='utf-8')
71
+
72
+ return read_text(path)
encommon/utils/match.py CHANGED
@@ -23,12 +23,12 @@ def rgxp_match(
23
23
 
24
24
  Example
25
25
  -------
26
- >>> rgxp_match('one', ['one', 'two'])
26
+ >>> rgxp_match('one', ['on*', 'two'])
27
27
  True
28
28
 
29
29
  Example
30
30
  -------
31
- >>> rgxp_match('uno', ['one', 'two'])
31
+ >>> rgxp_match('uno', ['on*', 'two'])
32
32
  False
33
33
 
34
34
  :param values: Value or values to enumerate for matching.
@@ -51,6 +51,7 @@ def rgxp_match(
51
51
 
52
52
 
53
53
  def _matches() -> bool:
54
+
54
55
  return bool(
55
56
  function(pattern, value))
56
57
 
@@ -85,12 +86,12 @@ def fuzz_match(
85
86
 
86
87
  Example
87
88
  -------
88
- >>> rgxp_match('one', ['one', 'two'])
89
+ >>> rgxp_match('one', ['on[a-z]', 'two'])
89
90
  True
90
91
 
91
92
  Example
92
93
  -------
93
- >>> rgxp_match('uno', ['one', 'two'])
94
+ >>> rgxp_match('uno', ['on[a-z]', 'two'])
94
95
  False
95
96
 
96
97
  :param values: Value or values to enumerate for matching.
encommon/utils/paths.py CHANGED
@@ -10,22 +10,27 @@ is permitted, for more information consult the project license file.
10
10
  from os import stat_result
11
11
  from pathlib import Path
12
12
  from typing import Optional
13
+ from typing import TYPE_CHECKING
13
14
 
14
- from .common import PATHABLE
15
15
  from .match import rgxp_match
16
+ from ..types import sort_dict
16
17
 
18
+ if TYPE_CHECKING:
19
+ from .common import PATHABLE
20
+ from .common import REPLACE
17
21
 
18
22
 
19
- _REPLACE = dict[str, str]
23
+
24
+ STATS_PATH = dict[str, stat_result]
20
25
 
21
26
 
22
27
 
23
28
  def resolve_path(
24
29
  path: str | Path,
25
- replace: Optional[_REPLACE] = None,
30
+ replace: Optional['REPLACE'] = None,
26
31
  ) -> Path:
27
32
  """
28
- Resolve the provided path and replace the magic keywords.
33
+ Resolve the provided path replacing the magic keywords.
29
34
 
30
35
  Example
31
36
  -------
@@ -33,14 +38,24 @@ def resolve_path(
33
38
  PosixPath('/foo/bar')
34
39
 
35
40
  :param path: Complete or relative path for processing.
36
- :param replace: Optional string values to replace in path.
41
+ :param replace: Optional values to replace in the path.
37
42
  :returns: New resolved filesystem path object instance.
38
43
  """
39
44
 
40
45
  path = str(path).strip()
41
46
 
42
47
  if replace is not None:
43
- for old, new in replace.items():
48
+
49
+ items = replace.items()
50
+
51
+ for old, new in items:
52
+
53
+ if isinstance(old, Path):
54
+ old = str(old)
55
+
56
+ if isinstance(new, Path):
57
+ new = str(new)
58
+
44
59
  path = path.replace(old, new)
45
60
 
46
61
  return Path(path).resolve()
@@ -48,11 +63,11 @@ def resolve_path(
48
63
 
49
64
 
50
65
  def resolve_paths(
51
- paths: PATHABLE,
52
- replace: Optional[_REPLACE] = None,
66
+ paths: 'PATHABLE',
67
+ replace: Optional['REPLACE'] = None,
53
68
  ) -> tuple[Path, ...]:
54
69
  """
55
- Resolve the provided paths and replace the magic keywords.
70
+ Resolve the provided paths replacing the magic keywords.
56
71
 
57
72
  .. note::
58
73
  This will remove duplicative paths from the returned.
@@ -63,7 +78,7 @@ def resolve_paths(
63
78
  (PosixPath('/foo/bar'),)
64
79
 
65
80
  :param paths: Complete or relative paths for processing.
66
- :param replace: Optional string values to replace in path.
81
+ :param replace: Optional values to replace in the path.
67
82
  :returns: New resolved filesystem path object instances.
68
83
  """
69
84
 
@@ -74,13 +89,13 @@ def resolve_paths(
74
89
 
75
90
  for path in paths:
76
91
 
77
- _path = resolve_path(
78
- str(path), replace)
92
+ resolved = resolve_path(
93
+ path, replace)
79
94
 
80
- if _path in returned:
95
+ if resolved in returned:
81
96
  continue
82
97
 
83
- returned.append(_path)
98
+ returned.append(resolved)
84
99
 
85
100
  return tuple(returned)
86
101
 
@@ -88,17 +103,18 @@ def resolve_paths(
88
103
 
89
104
  def stats_path(
90
105
  path: str | Path,
91
- replace: Optional[_REPLACE] = None,
106
+ replace: Optional['REPLACE'] = None,
92
107
  ignore: Optional[list[str]] = None,
93
- ) -> dict[str, stat_result]:
108
+ ) -> STATS_PATH:
94
109
  """
95
110
  Collect stats object for the complete or relative path.
96
111
 
97
112
  .. testsetup::
113
+ >>> from . import save_text
98
114
  >>> path = Path(getfixture('tmpdir'))
99
115
  >>> file = path.joinpath('hello.txt')
100
- >>> file.write_text('Hello world!')
101
- 12
116
+ >>> save_text(file, 'Hello world!')
117
+ 'Hello world!'
102
118
 
103
119
  Example
104
120
  -------
@@ -108,18 +124,20 @@ def stats_path(
108
124
  12
109
125
 
110
126
  :param path: Complete or relative path for enumeration.
111
- :param replace: Optional string values to replace in path.
127
+ :param replace: Optional values to replace in the path.
112
128
  :param ignore: Paths matching these patterns are ignored.
113
129
  :returns: Metadata for files recursively found in path.
114
130
  """
115
131
 
116
132
  path = Path(path).resolve()
117
133
 
118
- returned: dict[str, stat_result] = {}
134
+ returned: STATS_PATH = {}
119
135
 
120
136
 
121
137
  def _ignore() -> bool:
138
+
122
139
  assert ignore is not None
140
+
123
141
  return rgxp_match(
124
142
  str(item), ignore)
125
143
 
@@ -141,8 +159,9 @@ def stats_path(
141
159
  key = resolve_path(
142
160
  item, replace)
143
161
 
144
- returned[str(key)] = (
145
- item.stat())
162
+ stat = item.stat()
163
+
164
+ returned[str(key)] = stat
146
165
 
147
166
 
148
- return dict(sorted(returned.items()))
167
+ return sort_dict(returned)
encommon/utils/sample.py CHANGED
@@ -13,17 +13,13 @@ from pathlib import Path
13
13
  from typing import Any
14
14
  from typing import Callable
15
15
  from typing import Optional
16
- from typing import Union
16
+ from typing import TYPE_CHECKING
17
17
 
18
- from .. import PROJECT
19
- from .. import WORKSPACE
18
+ from . import read_text
19
+ from . import save_text
20
20
 
21
-
22
-
23
- _REPLACE = Union[
24
- dict[str, str],
25
- dict[str, str | Path],
26
- dict[str, Path]]
21
+ if TYPE_CHECKING:
22
+ from .common import REPLACE
27
23
 
28
24
 
29
25
 
@@ -31,7 +27,7 @@ def prep_sample(
31
27
  content: Any,
32
28
  *,
33
29
  default: Callable[[Any], str] = str,
34
- replace: Optional[_REPLACE] = None,
30
+ replace: Optional['REPLACE'] = None,
35
31
  ) -> Any:
36
32
  """
37
33
  Return the content after processing using JSON functions.
@@ -49,9 +45,9 @@ def prep_sample(
49
45
  >>> prep_sample({'one': Empty})
50
46
  {'one': 'Empty'}
51
47
 
52
- :param content: Content which will be processed for JSON.
48
+ :param content: Content that will be processed as JSON.
53
49
  :param default: Callable used when stringifying values.
54
- :param replace: Optional string values to replace in path.
50
+ :param replace: Optional values to replace in the file.
55
51
  :returns: Content after processing using JSON functions.
56
52
  """
57
53
 
@@ -60,13 +56,14 @@ def prep_sample(
60
56
 
61
57
  prefix = 'encommon_sample'
62
58
 
63
- replace = dict(replace or {})
59
+ replace = replace or {}
60
+
61
+ items = replace.items()
64
62
 
65
- replace |= {
66
- 'PROJECT': PROJECT,
67
- 'WORKSPACE': WORKSPACE}
63
+ for old, new in items:
68
64
 
69
- for old, new in replace.items():
65
+ if isinstance(old, Path):
66
+ old = str(old)
70
67
 
71
68
  if isinstance(new, Path):
72
69
  new = str(new)
@@ -79,12 +76,12 @@ def prep_sample(
79
76
 
80
77
 
81
78
  def load_sample(
82
- path: Path,
79
+ path: str | Path,
83
80
  content: Optional[Any] = None,
84
81
  update: bool = False,
85
82
  *,
86
83
  default: Callable[[Any], str] = str,
87
- replace: Optional[_REPLACE] = None,
84
+ replace: Optional['REPLACE'] = None,
88
85
  ) -> Any:
89
86
  """
90
87
  Load the sample file and compare using provided content.
@@ -106,16 +103,19 @@ def load_sample(
106
103
  >>> load_sample(sample)
107
104
  {'one': 'two'}
108
105
 
109
- :param path: Complete or relative path to the sample file.
106
+ :param path: Complete or relative path for the sample.
110
107
  :param update: Determine whether the sample is updated.
111
- :param content: Content which will be processed for JSON.
108
+ :param content: Content that will be processed as JSON.
112
109
  :param default: Callable used when stringifying values.
113
- :param replace: Optional string values to replace in file.
110
+ :param replace: Optional values to replace in the file.
114
111
  :returns: Content after processing using JSON functions.
115
112
  """
116
113
 
114
+ path = Path(path).resolve()
115
+
117
116
  loaded: Optional[Any] = None
118
117
 
118
+
119
119
  content = prep_sample(
120
120
  content=content,
121
121
  default=default,
@@ -123,14 +123,18 @@ def load_sample(
123
123
 
124
124
 
125
125
  def _save_sample() -> None:
126
- path.write_text(
127
- dumps(content, indent=2))
126
+
127
+ dumped = dumps(
128
+ content, indent=2)
129
+
130
+ save_text(path, dumped)
128
131
 
129
132
 
130
133
  def _load_sample() -> Any:
131
- return loads(
132
- path.read_text(
133
- encoding='utf-8'))
134
+
135
+ loaded = read_text(path)
136
+
137
+ return loads(loaded)
134
138
 
135
139
 
136
140
  if path.exists():
encommon/utils/stdout.py CHANGED
@@ -16,11 +16,13 @@ from typing import Literal
16
16
  from typing import Optional
17
17
  from typing import Union
18
18
 
19
- from encommon.times import Duration
20
- from encommon.times import Times
21
- from encommon.types import Empty
22
-
23
19
  from .common import JOINABLE
20
+ from ..times import Duration
21
+ from ..times import Times
22
+ from ..types import Empty
23
+ from ..types.strings import COMMAD
24
+ from ..types.strings import NEWLINE
25
+ from ..types.strings import SEMPTY
24
26
 
25
27
 
26
28
 
@@ -40,6 +42,10 @@ ANSIARRAY = Union[
40
42
 
41
43
 
42
44
 
45
+ _REPEAT = (list, tuple, dict)
46
+
47
+
48
+
43
49
  @dataclass(frozen=True)
44
50
  class ArrayColors:
45
51
  """
@@ -49,6 +55,9 @@ class ArrayColors:
49
55
  label: int = 37
50
56
  key: int = 97
51
57
 
58
+ colon: int = 37
59
+ hyphen: int = 37
60
+
52
61
  bool: int = 93
53
62
  none: int = 33
54
63
  str: int = 92
@@ -58,13 +67,10 @@ class ArrayColors:
58
67
  empty: int = 36
59
68
  other: int = 91
60
69
 
61
- colon: int = 37
62
- hyphen: int = 37
63
-
64
70
 
65
71
 
66
72
  def print_ansi(
67
- string: str = '',
73
+ string: str = SEMPTY,
68
74
  method: Literal['stdout', 'print'] = 'stdout',
69
75
  output: bool = True,
70
76
  ) -> str:
@@ -133,8 +139,8 @@ def kvpair_ansi(
133
139
  :returns: ANSI colorized string using inline directives.
134
140
  """ # noqa: D301 LIT102
135
141
 
136
- if isinstance(value, JOINABLE): # type: ignore
137
- value = ','.join([
142
+ if isinstance(value, JOINABLE):
143
+ value = COMMAD.join([
138
144
  str(x) for x in value])
139
145
 
140
146
  elif not isinstance(value, str):
@@ -161,7 +167,7 @@ def strip_ansi(
161
167
  :returns: Provided string with the ANSI codes removed.
162
168
  """ # noqa: D301 LIT102
163
169
 
164
- return re_sub(ANSICODE, '', string)
170
+ return re_sub(ANSICODE, SEMPTY, string)
165
171
 
166
172
 
167
173
 
@@ -206,25 +212,28 @@ def array_ansi( # noqa: CFQ001, CFQ004
206
212
  f'{prefix} {repeat}')
207
213
 
208
214
 
209
- if isinstance(value, list | tuple | dict):
215
+ if isinstance(value, _REPEAT):
210
216
  refers.add(id(value))
211
217
 
212
218
 
213
- types = {
219
+ typing = {
214
220
  'list': list,
215
221
  'tuple': tuple,
216
222
  'dict': dict,
217
223
  'frozenset': frozenset,
218
224
  'set': set}
219
225
 
220
- for name, _type in types.items():
226
+ items = typing.items()
227
+
228
+ for name, _type in items:
221
229
 
222
230
  if not isinstance(value, _type):
223
231
  continue
224
232
 
225
233
  output.append(
226
234
  f'{prefix} '
227
- f'<c{colors.label}>{name}<c0>')
235
+ f'<c{colors.label}>'
236
+ f'{name}<c0>')
228
237
 
229
238
  return _process(
230
239
  source=value,
@@ -281,7 +290,9 @@ def array_ansi( # noqa: CFQ001, CFQ004
281
290
 
282
291
  assert isinstance(source, dict)
283
292
 
284
- for key, value in source.items():
293
+ items = source.items()
294
+
295
+ for key, value in items:
285
296
 
286
297
  prefix = (
287
298
  f'{" " * indent}'
@@ -340,4 +351,4 @@ def array_ansi( # noqa: CFQ001, CFQ004
340
351
  _output = [
341
352
  make_ansi(x) for x in output]
342
353
 
343
- return '\n'.join(_output)
354
+ return NEWLINE.join(_output)
@@ -0,0 +1,35 @@
1
+ """
2
+ Functions and routines associated with Enasis Network Common Library.
3
+
4
+ This file is part of Enasis Network software eco-system. Distribution
5
+ is permitted, for more information consult the project license file.
6
+ """
7
+
8
+
9
+
10
+ from pathlib import Path
11
+
12
+ from ..common import read_text
13
+ from ..common import save_text
14
+
15
+
16
+
17
+ def test_readsave_text(
18
+ tmp_path: Path,
19
+ ) -> None:
20
+ """
21
+ Perform various tests associated with relevant routines.
22
+
23
+ :param tmp_path: pytest object for temporal filesystem.
24
+ """
25
+
26
+ content = 'pytest'
27
+
28
+ save_text(
29
+ f'{tmp_path}/test.txt',
30
+ content)
31
+
32
+ loaded = read_text(
33
+ f'{tmp_path}/test.txt')
34
+
35
+ assert loaded == content
@@ -13,7 +13,7 @@ from ..paths import resolve_path
13
13
  from ..paths import resolve_paths
14
14
  from ..paths import stats_path
15
15
  from ... import PROJECT
16
- from ...times.times import Times
16
+ from ...times import Times
17
17
 
18
18
 
19
19
 
@@ -61,7 +61,7 @@ def test_stats_path() -> None:
61
61
 
62
62
  stats = stats_path(
63
63
  f'{PROJECT}/utils',
64
- replace={str(PROJECT): '/'},
64
+ replace={PROJECT: '/'},
65
65
  ignore=[r'\S+\.pyc'])
66
66
 
67
67
  stat = stats['/utils/paths.py']
@@ -81,6 +81,7 @@ def test_stats_path() -> None:
81
81
  '/utils/sample.py',
82
82
  '/utils/stdout.py',
83
83
  '/utils/test/__init__.py',
84
+ '/utils/test/test_common.py',
84
85
  '/utils/test/test_match.py',
85
86
  '/utils/test/test_paths.py',
86
87
  '/utils/test/test_sample.py',
@@ -34,34 +34,50 @@ def test_load_sample(
34
34
  :param tmp_path: pytest object for temporal filesystem.
35
35
  """
36
36
 
37
- path = (
38
- Path(tmp_path)
39
- .joinpath('samples.json'))
37
+ prefix = 'encommon_sample'
40
38
 
41
39
  source = {
42
40
  'list': ['bar', 'baz'],
43
41
  'tuple': (1, 2),
44
42
  'project': PROJECT,
45
- 'other': '/path/to/other'}
43
+ 'other': '/pat/h',
44
+ 'devnull': '/dev/null'}
46
45
 
47
46
  expect = {
48
47
  'list': ['bar', 'baz'],
49
48
  'tuple': [1, 2],
50
- 'project': '_/encommon_sample/PROJECT/_',
51
- 'other': '_/encommon_sample/tmp_path/_'}
49
+ 'project': f'_/{prefix}/PROJECT/_',
50
+ 'other': f'_/{prefix}/pytemp/_',
51
+ 'devnull': '/dev/null'}
52
+
53
+
54
+ devnull = Path('/dev/null')
55
+
56
+ replaces = {
57
+ devnull: 'nothing here',
58
+ 'PROJECT': str(PROJECT),
59
+ 'pytemp': '/pat/h'}
60
+
61
+
62
+ sample_path = (
63
+ f'{tmp_path}/samples.json')
52
64
 
53
65
  sample = load_sample(
54
- path=path,
66
+ path=sample_path,
55
67
  update=ENPYRWS,
56
68
  content=source,
57
- replace={'tmp_path': '/path/to/other'})
69
+ replace=replaces) # type: ignore
58
70
 
59
71
  assert sample == expect
60
72
 
73
+
74
+ source |= {'list': [1, 3, 2]}
75
+ expect |= {'list': [1, 3, 2]}
76
+
61
77
  sample = load_sample(
62
- path=path,
63
- content=source | {'list': [1]},
78
+ path=sample_path,
79
+ content=source,
64
80
  update=True,
65
- replace={'tmp_path': '/path/to/other'})
81
+ replace=replaces) # type: ignore
66
82
 
67
- assert sample == expect | {'list': [1]}
83
+ assert sample == expect
encommon/version.txt CHANGED
@@ -1 +1 @@
1
- 0.7.6
1
+ 0.8.1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: encommon
3
- Version: 0.7.6
3
+ Version: 0.8.1
4
4
  Summary: Enasis Network Common Library
5
5
  License: MIT
6
6
  Classifier: Programming Language :: Python :: 3