encommon 0.13.1__py3-none-any.whl → 0.15.0__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 (63) hide show
  1. encommon/__init__.py +2 -7
  2. encommon/colors/__init__.py +14 -0
  3. encommon/colors/color.py +518 -0
  4. encommon/colors/test/__init__.py +6 -0
  5. encommon/colors/test/test_color.py +189 -0
  6. encommon/config/config.py +83 -30
  7. encommon/config/files.py +16 -13
  8. encommon/config/logger.py +10 -5
  9. encommon/config/params.py +47 -31
  10. encommon/config/paths.py +15 -12
  11. encommon/config/test/__init__.py +1 -1
  12. encommon/config/test/test_config.py +15 -17
  13. encommon/config/test/test_files.py +8 -7
  14. encommon/config/test/test_logger.py +21 -15
  15. encommon/config/test/test_paths.py +12 -11
  16. encommon/config/utils.py +9 -3
  17. encommon/conftest.py +33 -22
  18. encommon/crypts/params.py +46 -11
  19. encommon/crypts/test/test_crypts.py +5 -5
  20. encommon/crypts/test/test_hashes.py +2 -1
  21. encommon/times/__init__.py +5 -3
  22. encommon/times/common.py +4 -3
  23. encommon/times/params.py +103 -45
  24. encommon/times/parse.py +39 -12
  25. encommon/times/test/test_duration.py +3 -2
  26. encommon/times/test/test_parse.py +16 -9
  27. encommon/times/test/test_time.py +123 -0
  28. encommon/times/test/test_timer.py +5 -4
  29. encommon/times/test/test_timers.py +10 -9
  30. encommon/times/test/test_unitime.py +23 -0
  31. encommon/times/test/test_window.py +4 -3
  32. encommon/times/test/test_windows.py +7 -6
  33. encommon/times/{times.py → time.py} +129 -22
  34. encommon/times/timer.py +10 -10
  35. encommon/times/timers.py +3 -3
  36. encommon/times/unitime.py +57 -0
  37. encommon/times/window.py +31 -31
  38. encommon/times/windows.py +10 -10
  39. encommon/types/__init__.py +20 -2
  40. encommon/types/classes.py +84 -0
  41. encommon/types/lists.py +33 -0
  42. encommon/types/notate.py +2 -1
  43. encommon/types/strings.py +34 -4
  44. encommon/types/test/test_classes.py +74 -0
  45. encommon/types/test/test_empty.py +2 -1
  46. encommon/types/test/test_lists.py +23 -0
  47. encommon/types/test/test_strings.py +15 -3
  48. encommon/types/types.py +20 -0
  49. encommon/utils/__init__.py +4 -0
  50. encommon/utils/paths.py +5 -6
  51. encommon/utils/sample.py +118 -41
  52. encommon/utils/stdout.py +53 -7
  53. encommon/utils/test/test_paths.py +3 -3
  54. encommon/utils/test/test_sample.py +128 -29
  55. encommon/utils/test/test_stdout.py +92 -28
  56. encommon/version.txt +1 -1
  57. {encommon-0.13.1.dist-info → encommon-0.15.0.dist-info}/METADATA +1 -1
  58. encommon-0.15.0.dist-info/RECORD +84 -0
  59. {encommon-0.13.1.dist-info → encommon-0.15.0.dist-info}/WHEEL +1 -1
  60. encommon/times/test/test_times.py +0 -89
  61. encommon-0.13.1.dist-info/RECORD +0 -73
  62. {encommon-0.13.1.dist-info → encommon-0.15.0.dist-info}/LICENSE +0 -0
  63. {encommon-0.13.1.dist-info → encommon-0.15.0.dist-info}/top_level.txt +0 -0
encommon/utils/sample.py CHANGED
@@ -7,54 +7,80 @@ is permitted, for more information consult the project license file.
7
7
 
8
8
 
9
9
 
10
+ from dataclasses import asdict
11
+ from dataclasses import is_dataclass
10
12
  from json import dumps
11
- from json import loads
13
+ from os import environ
12
14
  from pathlib import Path
13
15
  from typing import Any
14
16
  from typing import Callable
15
17
  from typing import Optional
16
- from typing import TYPE_CHECKING
18
+
19
+ from pydantic import BaseModel
17
20
 
18
21
  from .files import read_text
19
22
  from .files import save_text
23
+ from ..types import DictStrAny
24
+ from ..types import rplstr
25
+
26
+
27
+
28
+ PREFIX = 'encommon_sample'
20
29
 
21
- if TYPE_CHECKING:
22
- from .common import REPLACE
30
+ ENPYRWS = (
31
+ environ.get('ENPYRWS') == '1')
23
32
 
24
33
 
25
34
 
26
- def prep_sample(
35
+ def prep_sample( # noqa: CFQ004
27
36
  content: Any,
28
37
  *,
29
38
  default: Callable[[Any], str] = str,
30
- replace: Optional['REPLACE'] = None,
31
- ) -> Any:
32
- """
33
- Return the content after processing using JSON functions.
34
-
35
- .. testsetup::
36
- >>> from ..types import Empty
39
+ replace: Optional[DictStrAny] = None,
40
+ indent: Optional[int] = 2,
41
+ ) -> str:
42
+ r"""
43
+ Return the content after processing as the sample value.
37
44
 
38
45
  Example
39
46
  -------
40
47
  >>> prep_sample(['one', 'two'])
41
- ['one', 'two']
48
+ '[\n "one",\n "two"\n]'
42
49
 
43
50
  Example
44
51
  -------
52
+ >>> from ..types import Empty
45
53
  >>> prep_sample({'one': Empty})
46
- {'one': 'Empty'}
54
+ '{\n "one": "Empty"\n}'
47
55
 
48
56
  :param content: Content that will be processed as JSON.
49
57
  :param default: Callable used when stringifying values.
50
58
  :param replace: Optional values to replace in the file.
51
- :returns: Content after processing using JSON functions.
59
+ :returns: Content after processing as the sample value.
52
60
  """
53
61
 
54
- content = dumps(
55
- content, default=default)
56
62
 
57
- prefix = 'encommon_sample'
63
+ def _default(
64
+ value: Any, # noqa: ANN401
65
+ ) -> DictStrAny | str:
66
+
67
+ if is_dataclass(value):
68
+
69
+ assert not isinstance(
70
+ value, type)
71
+
72
+ return asdict(value)
73
+
74
+ if isinstance(value, BaseModel):
75
+ return value.model_dump()
76
+
77
+ return str(value)
78
+
79
+
80
+ content = dumps(
81
+ content,
82
+ default=_default,
83
+ indent=indent)
58
84
 
59
85
  replace = replace or {}
60
86
 
@@ -62,16 +88,14 @@ def prep_sample(
62
88
 
63
89
  for old, new in items:
64
90
 
65
- if isinstance(old, Path):
66
- old = str(old)
91
+ new = str(new)
67
92
 
68
- if isinstance(new, Path):
69
- new = str(new)
93
+ old = f'_/{PREFIX}/{old}/_'
70
94
 
71
- content = content.replace(
72
- new, f'_/{prefix}/{old}/_')
95
+ content = rplstr(
96
+ content, new, old)
73
97
 
74
- return loads(content)
98
+ return str(content)
75
99
 
76
100
 
77
101
 
@@ -81,9 +105,9 @@ def load_sample(
81
105
  update: bool = False,
82
106
  *,
83
107
  default: Callable[[Any], str] = str,
84
- replace: Optional['REPLACE'] = None,
85
- ) -> Any:
86
- """
108
+ replace: Optional[DictStrAny] = None,
109
+ ) -> str:
110
+ r"""
87
111
  Load the sample file and compare using provided content.
88
112
 
89
113
  .. testsetup::
@@ -96,14 +120,14 @@ def load_sample(
96
120
  -------
97
121
  >>> content = {'one': 'two'}
98
122
  >>> load_sample(sample, content)
99
- {'one': 'two'}
123
+ '{\n "one": "two"\n}'
100
124
 
101
125
  Example
102
126
  -------
103
127
  >>> load_sample(sample)
104
- {'one': 'two'}
128
+ '{\n "one": "two"\n}'
105
129
 
106
- :param path: Complete or relative path for the sample.
130
+ :param path: Complete or relative path for sample file.
107
131
  :param update: Determine whether the sample is updated.
108
132
  :param content: Content that will be processed as JSON.
109
133
  :param default: Callable used when stringifying values.
@@ -111,6 +135,7 @@ def load_sample(
111
135
  :returns: Content after processing using JSON functions.
112
136
  """
113
137
 
138
+
114
139
  path = Path(path).resolve()
115
140
 
116
141
  loaded: Optional[Any] = None
@@ -123,18 +148,11 @@ def load_sample(
123
148
 
124
149
 
125
150
  def _save_sample() -> None:
126
-
127
- dumped = dumps(
128
- content, indent=2)
129
-
130
- save_text(path, dumped)
151
+ save_text(path, content)
131
152
 
132
153
 
133
- def _load_sample() -> Any:
134
-
135
- loaded = read_text(path)
136
-
137
- return loads(loaded)
154
+ def _load_sample() -> str:
155
+ return read_text(path)
138
156
 
139
157
 
140
158
  if path.exists():
@@ -150,3 +168,62 @@ def load_sample(
150
168
 
151
169
 
152
170
  return _load_sample()
171
+
172
+
173
+
174
+ def read_sample(
175
+ sample: str,
176
+ *,
177
+ replace: Optional[DictStrAny] = None,
178
+ prefix: bool = True,
179
+ ) -> str:
180
+ """
181
+ Return the content after processing as the sample value.
182
+
183
+ :param sample: Content that will be processed as sample.
184
+ :param replace: Optional values to replace in the file.
185
+ :param prefix: Determine whether or not prefix is added.
186
+ :returns: Content after processing as the sample value.
187
+ """
188
+
189
+ replace = replace or {}
190
+
191
+ items = replace.items()
192
+
193
+ for new, old in items:
194
+
195
+ if prefix is True:
196
+ old = f'_/{PREFIX}/{old}/_'
197
+
198
+ sample = rplstr(
199
+ sample, new, old)
200
+
201
+ return str(sample)
202
+
203
+
204
+
205
+ def rvrt_sample(
206
+ sample: str,
207
+ *,
208
+ replace: Optional[DictStrAny] = None,
209
+ ) -> str:
210
+ """
211
+ Return the content after processing as the sample value.
212
+
213
+ :param sample: Content that will be processed as sample.
214
+ :param replace: Optional values to replace in the file.
215
+ :returns: Content after processing as the sample value.
216
+ """
217
+
218
+ replace = replace or {}
219
+
220
+ items = replace.items()
221
+
222
+ for new, old in items:
223
+
224
+ new = f'_/{PREFIX}/{new}/_'
225
+
226
+ sample = rplstr(
227
+ sample, new, old)
228
+
229
+ return str(sample)
encommon/utils/stdout.py CHANGED
@@ -7,23 +7,32 @@ is permitted, for more information consult the project license file.
7
7
 
8
8
 
9
9
 
10
+ from dataclasses import asdict
10
11
  from dataclasses import dataclass
12
+ from dataclasses import is_dataclass
11
13
  from re import compile
12
14
  from re import sub as re_sub
13
15
  from sys import stdout
14
16
  from typing import Any
15
17
  from typing import Literal
16
18
  from typing import Optional
19
+ from typing import TYPE_CHECKING
17
20
  from typing import Union
18
21
 
22
+ from pydantic import BaseModel
23
+
19
24
  from .common import JOINABLE
20
25
  from ..times import Duration
21
- from ..times import Times
26
+ from ..times import Time
22
27
  from ..types import Empty
28
+ from ..types import clsname
23
29
  from ..types.strings import COMMAD
24
30
  from ..types.strings import NEWLINE
25
31
  from ..types.strings import SEMPTY
26
32
 
33
+ if TYPE_CHECKING:
34
+ from _typeshed import DataclassInstance
35
+
27
36
 
28
37
 
29
38
  ANSICODE = compile(
@@ -34,7 +43,10 @@ ANSIARRAL = Union[
34
43
  tuple[Any, ...],
35
44
  set[Any]]
36
45
 
37
- ANSIARRAD = dict[Any, Any]
46
+ ANSIARRAD = Union[
47
+ dict[Any, Any],
48
+ BaseModel,
49
+ 'DataclassInstance']
38
50
 
39
51
  ANSIARRAY = Union[
40
52
  ANSIARRAL,
@@ -220,14 +232,21 @@ def array_ansi( # noqa: CFQ001, CFQ004
220
232
  'list': list,
221
233
  'tuple': tuple,
222
234
  'dict': dict,
235
+ 'BaseModel': BaseModel,
223
236
  'frozenset': frozenset,
224
237
  'set': set}
225
238
 
226
239
  items = typing.items()
227
240
 
228
- for name, _type in items:
241
+ for name, typed in items:
242
+
243
+ if isinstance(value, BaseModel):
244
+ name = clsname(value)
229
245
 
230
- if not isinstance(value, _type):
246
+ elif is_dataclass(value):
247
+ name = clsname(value)
248
+
249
+ elif not isinstance(value, typed):
231
250
  continue
232
251
 
233
252
  output.append(
@@ -268,7 +287,7 @@ def array_ansi( # noqa: CFQ001, CFQ004
268
287
  elif isinstance(source, Duration):
269
288
  color = colors.times
270
289
 
271
- elif isinstance(source, Times):
290
+ elif isinstance(source, Time):
272
291
  color = colors.times
273
292
 
274
293
  elif source is Empty:
@@ -288,6 +307,12 @@ def array_ansi( # noqa: CFQ001, CFQ004
288
307
  refers: Optional[set[int]] = None,
289
308
  ) -> None:
290
309
 
310
+ if isinstance(source, BaseModel):
311
+ source = source.model_dump()
312
+
313
+ if is_dataclass(source):
314
+ source = asdict(source)
315
+
291
316
  assert isinstance(source, dict)
292
317
 
293
318
  items = source.items()
@@ -325,19 +350,40 @@ def array_ansi( # noqa: CFQ001, CFQ004
325
350
 
326
351
 
327
352
  def _process(
328
- source: ANSIARRAD | ANSIARRAL,
353
+ source: ANSIARRAY,
329
354
  **kwargs: Any,
330
355
  ) -> None:
331
356
 
332
357
  if isinstance(source, dict):
333
358
  return _dict(source, **kwargs)
334
359
 
335
- return _list(source, **kwargs)
360
+ if isinstance(source, BaseModel):
361
+ return _dict(source, **kwargs)
362
+
363
+ if is_dataclass(source):
364
+ return _dict(source, **kwargs)
365
+
366
+ assert isinstance(
367
+ source,
368
+ set | list | tuple)
369
+
370
+ _list(source, **kwargs)
371
+
372
+
373
+ if is_dataclass(source):
374
+
375
+ assert not isinstance(
376
+ source, type)
377
+
378
+ source = asdict(source)
336
379
 
337
380
 
338
381
  if isinstance(source, dict):
339
382
  _dict(source, indent)
340
383
 
384
+ elif isinstance(source, BaseModel):
385
+ _dict(source, indent)
386
+
341
387
  elif isinstance(source, list):
342
388
  _list(source, indent)
343
389
 
@@ -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 import Times
16
+ from ...times import Time
17
17
 
18
18
 
19
19
 
@@ -67,9 +67,9 @@ def test_stats_path() -> None:
67
67
  stat = stats['/utils/paths.py']
68
68
 
69
69
  assert stat.st_ctime >= (
70
- Times('2023-01-01').epoch)
70
+ Time('2023-01-01').epoch)
71
71
  assert stat.st_mtime >= (
72
- Times('2023-01-01').epoch)
72
+ Time('2023-01-01').epoch)
73
73
 
74
74
  assert stat.st_size >= 2000
75
75
 
@@ -7,12 +7,41 @@ is permitted, for more information consult the project license file.
7
7
 
8
8
 
9
9
 
10
+ from copy import deepcopy
11
+ from json import dumps
12
+ from json import loads
10
13
  from pathlib import Path
11
14
 
12
15
  from ..sample import load_sample
13
16
  from ..sample import prep_sample
14
- from ... import ENPYRWS
17
+ from ..sample import read_sample
18
+ from ..sample import rvrt_sample
19
+ from ..stdout import ArrayColors
15
20
  from ... import PROJECT
21
+ from ...config import LoggerParams
22
+ from ...utils.sample import ENPYRWS
23
+
24
+
25
+
26
+ _PREFIX = 'encommon_sample'
27
+
28
+ _SOURCE = {
29
+ 'list': ['bar', 'baz'],
30
+ 'tuple': (1, 2),
31
+ 'project': PROJECT,
32
+ 'other': '/pat/h',
33
+ 'devnull': '/dev/null'}
34
+
35
+ _EXPECT = {
36
+ 'list': ['bar', 'baz'],
37
+ 'tuple': [1, 2],
38
+ 'project': f'_/{_PREFIX}/PROJECT/_',
39
+ 'other': f'_/{_PREFIX}/pytemp/_',
40
+ 'devnull': '/dev/null'}
41
+
42
+ _REPLACES = {
43
+ 'PROJECT': str(PROJECT),
44
+ 'pytemp': '/pat/h'}
16
45
 
17
46
 
18
47
 
@@ -21,7 +50,35 @@ def test_prep_sample() -> None:
21
50
  Perform various tests associated with relevant routines.
22
51
  """
23
52
 
24
- assert prep_sample((1, '2')) == [1, '2']
53
+ colors = ArrayColors()
54
+
55
+ params = LoggerParams()
56
+
57
+ source = {
58
+ 'colors': colors,
59
+ 'params': params}
60
+
61
+ sample = prep_sample(
62
+ content=source,
63
+ indent=None)
64
+
65
+ assert loads(sample) == {
66
+ 'colors': {
67
+ 'bool': 93,
68
+ 'colon': 37,
69
+ 'empty': 36,
70
+ 'hyphen': 37,
71
+ 'key': 97,
72
+ 'label': 37,
73
+ 'none': 33,
74
+ 'num': 93,
75
+ 'other': 91,
76
+ 'str': 92,
77
+ 'times': 96},
78
+ 'params': {
79
+ 'file_level': None,
80
+ 'file_path': None,
81
+ 'stdo_level': None}}
25
82
 
26
83
 
27
84
 
@@ -34,41 +91,24 @@ def test_load_sample(
34
91
  :param tmp_path: pytest object for temporal filesystem.
35
92
  """
36
93
 
37
- prefix = 'encommon_sample'
38
-
39
- source = {
40
- 'list': ['bar', 'baz'],
41
- 'tuple': (1, 2),
42
- 'project': PROJECT,
43
- 'other': '/pat/h',
44
- 'devnull': '/dev/null'}
45
-
46
- expect = {
47
- 'list': ['bar', 'baz'],
48
- 'tuple': [1, 2],
49
- 'project': f'_/{prefix}/PROJECT/_',
50
- 'other': f'_/{prefix}/pytemp/_',
51
- 'devnull': '/dev/null'}
52
-
94
+ source = deepcopy(_SOURCE)
53
95
 
54
- devnull = Path('/dev/null')
55
-
56
- replaces = {
57
- devnull: 'nothing here',
58
- 'PROJECT': str(PROJECT),
59
- 'pytemp': '/pat/h'}
96
+ expect = deepcopy(_EXPECT)
60
97
 
61
98
 
62
99
  sample_path = (
63
- f'{tmp_path}/samples.json')
100
+ tmp_path / 'samples.json')
64
101
 
65
102
  sample = load_sample(
66
103
  path=sample_path,
67
104
  update=ENPYRWS,
68
105
  content=source,
69
- replace=replaces) # type: ignore
106
+ replace=_REPLACES)
107
+
108
+ _expect = dumps(
109
+ expect, indent=2)
70
110
 
71
- assert sample == expect
111
+ assert _expect == sample
72
112
 
73
113
 
74
114
  source |= {'list': [1, 3, 2]}
@@ -78,6 +118,65 @@ def test_load_sample(
78
118
  path=sample_path,
79
119
  content=source,
80
120
  update=True,
81
- replace=replaces) # type: ignore
121
+ replace=_REPLACES)
122
+
123
+ _expect = dumps(
124
+ expect, indent=2)
125
+
126
+ assert _expect == sample
127
+
128
+
129
+
130
+ def test_read_sample(
131
+ tmp_path: Path,
132
+ ) -> None:
133
+ """
134
+ Perform various tests associated with relevant routines.
135
+
136
+ :param tmp_path: pytest object for temporal filesystem.
137
+ """
138
+
139
+ source = prep_sample(
140
+ content=_SOURCE,
141
+ indent=None)
142
+
143
+ sample = read_sample(
144
+ sample=source,
145
+ replace=_REPLACES)
146
+
147
+ sample = prep_sample(
148
+ content=loads(sample),
149
+ replace=_REPLACES,
150
+ indent=None)
151
+
152
+ expect = dumps(_EXPECT)
153
+
154
+ assert expect == sample
155
+
156
+
157
+
158
+ def test_rvrt_sample(
159
+ tmp_path: Path,
160
+ ) -> None:
161
+ """
162
+ Perform various tests associated with relevant routines.
163
+
164
+ :param tmp_path: pytest object for temporal filesystem.
165
+ """
166
+
167
+ source = prep_sample(
168
+ content=_SOURCE,
169
+ indent=None)
170
+
171
+ sample = rvrt_sample(
172
+ sample=source,
173
+ replace=_REPLACES)
174
+
175
+ sample = prep_sample(
176
+ content=loads(sample),
177
+ replace=_REPLACES,
178
+ indent=None)
179
+
180
+ expect = dumps(_EXPECT)
82
181
 
83
- assert sample == expect
182
+ assert expect == sample