pythonwrench 0.6.4__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 (52) hide show
  1. pythonwrench/__init__.py +490 -0
  2. pythonwrench/__main__.py +7 -0
  3. pythonwrench/_core.py +192 -0
  4. pythonwrench/abc.py +30 -0
  5. pythonwrench/argparse/__init__.py +81 -0
  6. pythonwrench/argparse/dataclass_.py +284 -0
  7. pythonwrench/argparse/parsers.py +619 -0
  8. pythonwrench/cast.py +247 -0
  9. pythonwrench/checksum.py +427 -0
  10. pythonwrench/collections/__init__.py +104 -0
  11. pythonwrench/collections/collections.py +900 -0
  12. pythonwrench/collections/prop.py +104 -0
  13. pythonwrench/collections/reducers.py +330 -0
  14. pythonwrench/concurrent.py +73 -0
  15. pythonwrench/csv.py +12 -0
  16. pythonwrench/dataclasses.py +117 -0
  17. pythonwrench/datetime.py +17 -0
  18. pythonwrench/difflib.py +39 -0
  19. pythonwrench/disk_cache.py +615 -0
  20. pythonwrench/entrypoints/info.py +44 -0
  21. pythonwrench/entrypoints/safe_rmdir.py +98 -0
  22. pythonwrench/entrypoints/tree.py +113 -0
  23. pythonwrench/enum.py +55 -0
  24. pythonwrench/functools.py +234 -0
  25. pythonwrench/hashlib.py +95 -0
  26. pythonwrench/importlib.py +243 -0
  27. pythonwrench/inspect.py +69 -0
  28. pythonwrench/json.py +12 -0
  29. pythonwrench/jsonl.py +12 -0
  30. pythonwrench/logging.py +252 -0
  31. pythonwrench/math.py +107 -0
  32. pythonwrench/os.py +226 -0
  33. pythonwrench/pickle.py +12 -0
  34. pythonwrench/random.py +60 -0
  35. pythonwrench/re.py +139 -0
  36. pythonwrench/semver.py +406 -0
  37. pythonwrench/serialization/__init__.py +70 -0
  38. pythonwrench/serialization/_core.py +70 -0
  39. pythonwrench/serialization/csv.py +493 -0
  40. pythonwrench/serialization/json.py +178 -0
  41. pythonwrench/serialization/jsonl.py +215 -0
  42. pythonwrench/serialization/pickle.py +186 -0
  43. pythonwrench/time.py +34 -0
  44. pythonwrench/typing/__init__.py +125 -0
  45. pythonwrench/typing/checks.py +551 -0
  46. pythonwrench/typing/classes.py +251 -0
  47. pythonwrench/warnings.py +118 -0
  48. pythonwrench-0.6.4.dist-info/METADATA +242 -0
  49. pythonwrench-0.6.4.dist-info/RECORD +52 -0
  50. pythonwrench-0.6.4.dist-info/WHEEL +4 -0
  51. pythonwrench-0.6.4.dist-info/entry_points.txt +10 -0
  52. pythonwrench-0.6.4.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ from typing import (
5
+ Any,
6
+ Callable,
7
+ Iterable,
8
+ Literal,
9
+ Optional,
10
+ TypeVar,
11
+ )
12
+
13
+ from pythonwrench.functools import function_alias
14
+
15
+ K = TypeVar("K", covariant=True)
16
+ T = TypeVar("T", covariant=True)
17
+ U = TypeVar("U", covariant=True)
18
+ V = TypeVar("V", covariant=True)
19
+ W = TypeVar("W", covariant=True)
20
+ X = TypeVar("X", covariant=True)
21
+ Y = TypeVar("Y", covariant=True)
22
+
23
+ KeyMode = Literal["intersect", "same", "union"]
24
+ Order = Literal["left", "right"]
25
+
26
+
27
+ def all_eq(it: Iterable[T], eq_fn: Optional[Callable[[T, T], bool]] = None) -> bool:
28
+ """Returns true if all elements in iterable are equal.
29
+
30
+ Note: This function returns True for iterable that contains 0 or 1 element.
31
+ """
32
+ it = list(it)
33
+ try:
34
+ first = next(iter(it))
35
+ except StopIteration:
36
+ return True
37
+
38
+ if eq_fn is None:
39
+ return all(first == elt for elt in it)
40
+ else:
41
+ return all(eq_fn(first, elt) for elt in it)
42
+
43
+
44
+ def all_ne(
45
+ it: Iterable[T],
46
+ ne_fn: Optional[Callable[[T, T], bool]] = None,
47
+ use_set: bool = False,
48
+ ) -> bool:
49
+ """Returns true if all elements in iterable are differents.
50
+
51
+ Note: This function returns True for iterable that contains 0 or 1 element.
52
+ """
53
+ if isinstance(it, (set, frozenset, dict)):
54
+ return True
55
+ if use_set and ne_fn is not None:
56
+ raise ValueError(f"Cannot use arguments {use_set=} with {ne_fn=}.")
57
+
58
+ it = list(it)
59
+ if use_set:
60
+ return len(it) == len(set(it))
61
+ elif ne_fn is None:
62
+ return all(
63
+ it[i] != it[j] for i in range(len(it)) for j in range(i + 1, len(it))
64
+ )
65
+ else:
66
+ return all(
67
+ ne_fn(it[i], it[j]) for i in range(len(it)) for j in range(i + 1, len(it))
68
+ )
69
+
70
+
71
+ @function_alias(all_eq)
72
+ def is_full(*args, **kwargs):
73
+ """Return whether full."""
74
+ ...
75
+
76
+
77
+ def is_sorted(
78
+ x: Iterable[Any],
79
+ *,
80
+ reverse: bool = False,
81
+ strict: bool = False,
82
+ ) -> bool:
83
+ """Return whether sorted."""
84
+ it = iter(x)
85
+ try:
86
+ prev = next(it)
87
+ except StopIteration:
88
+ return True
89
+
90
+ for xi in it:
91
+ if not reverse and prev > xi:
92
+ return False
93
+ if reverse and prev < xi:
94
+ return False
95
+ if strict and prev == xi:
96
+ return False
97
+ prev = xi
98
+ return True
99
+
100
+
101
+ @function_alias(all_ne)
102
+ def is_unique(*args, **kwargs):
103
+ """Return whether unique."""
104
+ ...
@@ -0,0 +1,330 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import operator
5
+ from typing import (
6
+ Any,
7
+ Callable,
8
+ Iterable,
9
+ Iterator,
10
+ Optional,
11
+ Tuple,
12
+ Type,
13
+ TypeVar,
14
+ overload,
15
+ )
16
+
17
+ from pythonwrench.functools import function_alias
18
+ from pythonwrench.typing.checks import isinstance_generic
19
+ from pythonwrench.typing.classes import (
20
+ SupportsAdd,
21
+ SupportsAnd,
22
+ SupportsMatmul,
23
+ SupportsMul,
24
+ SupportsOr,
25
+ )
26
+
27
+ T = TypeVar("T")
28
+ T_SupportsAdd = TypeVar("T_SupportsAdd", bound=SupportsAdd)
29
+ T_SupportsAnd = TypeVar("T_SupportsAnd", bound=SupportsAnd)
30
+ T_SupportsMul = TypeVar("T_SupportsMul", bound=SupportsMul)
31
+ T_SupportsOr = TypeVar("T_SupportsOr", bound=SupportsOr)
32
+ T_SupportsMatmul = TypeVar("T_SupportsMatmul", bound=SupportsMatmul)
33
+
34
+
35
+ @overload
36
+ def reduce_add(
37
+ args: Iterable[T_SupportsAdd],
38
+ /,
39
+ *,
40
+ start: T_SupportsAdd,
41
+ ) -> T_SupportsAdd:
42
+ """Perform the reduce add operation."""
43
+ ...
44
+
45
+
46
+ @overload
47
+ def reduce_add(
48
+ *args: T_SupportsAdd,
49
+ start: T_SupportsAdd,
50
+ ) -> T_SupportsAdd:
51
+ """Perform the reduce add operation."""
52
+ ...
53
+
54
+
55
+ @overload
56
+ def reduce_add(
57
+ arg0: T_SupportsAdd,
58
+ /,
59
+ *args: T_SupportsAdd,
60
+ start: Optional[T_SupportsAdd] = None,
61
+ ) -> T_SupportsAdd:
62
+ """Perform the reduce add operation."""
63
+ ...
64
+
65
+
66
+ def reduce_add(*args, start=None):
67
+ """Reduce elements using "add" operator (+)."""
68
+ return _reduce(*args, start=start, op_fn=operator.add, type_=SupportsAdd)
69
+
70
+
71
+ @overload
72
+ def reduce_and(
73
+ args: Iterable[T_SupportsAnd],
74
+ /,
75
+ *,
76
+ start: T_SupportsAnd,
77
+ ) -> T_SupportsAnd:
78
+ """Perform the reduce and operation."""
79
+ ...
80
+
81
+
82
+ @overload
83
+ def reduce_and(
84
+ *args: T_SupportsAnd,
85
+ start: T_SupportsAnd,
86
+ ) -> T_SupportsAnd:
87
+ """Perform the reduce and operation."""
88
+ ...
89
+
90
+
91
+ @overload
92
+ def reduce_and(
93
+ arg0: T_SupportsAnd,
94
+ /,
95
+ *args: T_SupportsAnd,
96
+ start: Optional[T_SupportsAnd] = None,
97
+ ) -> T_SupportsAnd:
98
+ """Perform the reduce and operation."""
99
+ ...
100
+
101
+
102
+ def reduce_and(*args, start=None):
103
+ """Reduce elements using "and" operator (&)."""
104
+ return _reduce(*args, start=start, op_fn=operator.and_, type_=SupportsAnd)
105
+
106
+
107
+ @overload
108
+ def reduce_matmul(
109
+ args: Iterable[T_SupportsMatmul],
110
+ /,
111
+ *,
112
+ start: T_SupportsMatmul,
113
+ ) -> T_SupportsMatmul:
114
+ """Perform the reduce matmul operation."""
115
+ ...
116
+
117
+
118
+ @overload
119
+ def reduce_matmul(
120
+ *args: T_SupportsMatmul,
121
+ start: T_SupportsMatmul,
122
+ ) -> T_SupportsMatmul:
123
+ """Perform the reduce matmul operation."""
124
+ ...
125
+
126
+
127
+ @overload
128
+ def reduce_matmul(
129
+ arg0: T_SupportsMatmul,
130
+ /,
131
+ *args: T_SupportsMatmul,
132
+ start: Optional[T_SupportsMatmul] = None,
133
+ ) -> T_SupportsMatmul:
134
+ """Perform the reduce matmul operation."""
135
+ ...
136
+
137
+
138
+ def reduce_matmul(*args, start=None):
139
+ """Reduce elements using "mul" operator (*)."""
140
+ return _reduce(*args, start=start, op_fn=operator.matmul, type_=SupportsMatmul)
141
+
142
+
143
+ @overload
144
+ def reduce_mul(
145
+ args: Iterable[T_SupportsMul],
146
+ /,
147
+ *,
148
+ start: T_SupportsMul,
149
+ ) -> T_SupportsMul:
150
+ """Perform the reduce mul operation."""
151
+ ...
152
+
153
+
154
+ @overload
155
+ def reduce_mul(
156
+ *args: T_SupportsMul,
157
+ start: T_SupportsMul,
158
+ ) -> T_SupportsMul:
159
+ """Perform the reduce mul operation."""
160
+ ...
161
+
162
+
163
+ @overload
164
+ def reduce_mul(
165
+ arg0: T_SupportsMul,
166
+ /,
167
+ *args: T_SupportsMul,
168
+ start: Optional[T_SupportsMul] = None,
169
+ ) -> T_SupportsMul:
170
+ """Perform the reduce mul operation."""
171
+ ...
172
+
173
+
174
+ def reduce_mul(*args, start=None):
175
+ """Reduce elements using "mul" operator (*)."""
176
+ return _reduce(*args, start=start, op_fn=operator.mul, type_=SupportsMul)
177
+
178
+
179
+ @overload
180
+ def reduce_or(
181
+ args: Iterable[T_SupportsOr],
182
+ /,
183
+ *,
184
+ start: T_SupportsOr,
185
+ ) -> T_SupportsOr:
186
+ """Perform the reduce or operation."""
187
+ ...
188
+
189
+
190
+ @overload
191
+ def reduce_or(
192
+ *args: T_SupportsOr,
193
+ start: T_SupportsOr,
194
+ ) -> T_SupportsOr:
195
+ """Perform the reduce or operation."""
196
+ ...
197
+
198
+
199
+ @overload
200
+ def reduce_or(
201
+ arg0: T_SupportsOr,
202
+ /,
203
+ *args: T_SupportsOr,
204
+ start: Optional[T_SupportsOr] = None,
205
+ ) -> T_SupportsOr:
206
+ """Perform the reduce or operation."""
207
+ ...
208
+
209
+
210
+ def reduce_or(*args, start=None):
211
+ """Reduce elements using "or" operator (|)."""
212
+ return _reduce(*args, start=start, op_fn=operator.or_, type_=SupportsOr)
213
+
214
+
215
+ def _reduce(
216
+ *args,
217
+ start: Optional[T] = None,
218
+ op_fn: Callable[[T, T], T],
219
+ type_: Type[T],
220
+ ) -> T:
221
+ """Perform the reduce operation."""
222
+ if isinstance_generic(args, Tuple[Iterable[type_]]):
223
+ it_or_args = args[0]
224
+ elif isinstance_generic(args, Tuple[type_, ...]):
225
+ it_or_args = args
226
+ else:
227
+ msg = f"Invalid positional arguments {args}. (expected {Tuple[type_, ...]} or {Tuple[Iterable[type_]]})"
228
+ raise TypeError(msg)
229
+
230
+ it: Iterator[T] = iter(it_or_args)
231
+
232
+ if isinstance(start, type_):
233
+ accumulator = start
234
+ elif start is None or start is ...:
235
+ try:
236
+ accumulator = next(it)
237
+ except StopIteration:
238
+ msg = f"Invalid combinaison of arguments {args=} and {start=}. (expected at least 1 non-empty argument or start object that supports operator.)"
239
+ raise ValueError(msg)
240
+ else:
241
+ msg = f"Invalid argument type {type(start)}."
242
+ raise TypeError(msg)
243
+
244
+ for arg in it:
245
+ accumulator = op_fn(accumulator, arg)
246
+ return accumulator
247
+
248
+
249
+ @overload
250
+ def sum(
251
+ args: Iterable[T_SupportsAdd],
252
+ /,
253
+ *,
254
+ start: T_SupportsAdd = 0,
255
+ ) -> T_SupportsAdd:
256
+ """Perform the sum operation."""
257
+ ...
258
+
259
+
260
+ @overload
261
+ def sum(
262
+ *args: T_SupportsAdd,
263
+ start: T_SupportsAdd = 0,
264
+ ) -> T_SupportsAdd:
265
+ """Perform the sum operation."""
266
+ ...
267
+
268
+
269
+ @overload
270
+ def sum(
271
+ arg0: T_SupportsAdd,
272
+ /,
273
+ *args: T_SupportsAdd,
274
+ start: Optional[T_SupportsAdd] = 0,
275
+ ) -> T_SupportsAdd:
276
+ """Perform the sum operation."""
277
+ ...
278
+
279
+
280
+ def sum(*args, start: Any = 0):
281
+ """Compute sum of elements."""
282
+ return reduce_add(*args, start=start)
283
+
284
+
285
+ @overload
286
+ def prod(
287
+ args: Iterable[T_SupportsMul],
288
+ /,
289
+ *,
290
+ start: T_SupportsMul = 1,
291
+ ) -> T_SupportsMul:
292
+ """Perform the prod operation."""
293
+ ...
294
+
295
+
296
+ @overload
297
+ def prod(
298
+ *args: T_SupportsMul,
299
+ start: T_SupportsMul = 1,
300
+ ) -> T_SupportsMul:
301
+ """Perform the prod operation."""
302
+ ...
303
+
304
+
305
+ @overload
306
+ def prod(
307
+ arg0: T_SupportsMul,
308
+ /,
309
+ *args: T_SupportsMul,
310
+ start: Optional[T_SupportsMul] = 1,
311
+ ) -> T_SupportsMul:
312
+ """Perform the prod operation."""
313
+ ...
314
+
315
+
316
+ def prod(*args, start: Any = 1):
317
+ """Compute product of elements."""
318
+ return reduce_mul(*args, start=start)
319
+
320
+
321
+ @function_alias(reduce_and)
322
+ def intersect(*args, **kwargs):
323
+ """Perform the intersect operation."""
324
+ ...
325
+
326
+
327
+ @function_alias(reduce_or)
328
+ def union(*args, **kwargs):
329
+ """Perform the union operation."""
330
+ ...
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import copy
5
+ import logging
6
+ from concurrent.futures import Future, ThreadPoolExecutor
7
+ from typing import Any, Callable, Dict, Generic, Iterable, List, Optional, TypeVar
8
+
9
+ from typing_extensions import ParamSpec
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ P = ParamSpec("P")
15
+ T = TypeVar("T")
16
+
17
+
18
+ class ThreadPoolExecutorHelper(Generic[P, T]):
19
+ # Note: use commas for typing because Future is not generic in older python versions
20
+
21
+ def __init__(
22
+ self,
23
+ fn: Callable[P, T],
24
+ *,
25
+ executor_kwds: Optional[Dict[str, Any]] = None,
26
+ executor: Optional[ThreadPoolExecutor] = None,
27
+ futures: "Iterable[Future[T]]" = (),
28
+ **default_fn_kwds,
29
+ ) -> None:
30
+ """Initialize the instance."""
31
+ futures = list(futures)
32
+
33
+ super().__init__()
34
+ self.fn = fn
35
+ self.executor_kwds = executor_kwds
36
+ self.executor = executor
37
+ self.futures = futures
38
+ self.default_kwargs = default_fn_kwds
39
+
40
+ def submit(self, *args: P.args, **kwargs: P.kwargs) -> "Future[T]":
41
+ """Perform the submit operation."""
42
+ if self.executor is None:
43
+ executor_kwds = self.executor_kwds
44
+ if executor_kwds is None:
45
+ executor_kwds = {}
46
+ self.executor = ThreadPoolExecutor(**executor_kwds)
47
+
48
+ default_kwargs = copy.copy(self.default_kwargs)
49
+ default_kwargs.update(kwargs)
50
+ del kwargs
51
+
52
+ future = self.executor.submit(self.fn, *args, **default_kwargs)
53
+ self.futures.append(future)
54
+ return future
55
+
56
+ def wait_all(self, shutdown: bool = True, verbose: bool = True) -> List[T]:
57
+ """Perform the wait all operation."""
58
+ futures = self.futures
59
+ if verbose:
60
+ try:
61
+ import tqdm # type: ignore
62
+
63
+ futures = tqdm.tqdm(futures, disable=not verbose)
64
+ except ImportError:
65
+ msg = "Cannot display verbose bar because tqdm is not installed."
66
+ logger.warning(msg)
67
+
68
+ results = [future.result() for future in futures]
69
+ self.futures.clear()
70
+ if shutdown and self.executor is not None:
71
+ self.executor.shutdown()
72
+ self.executor = None
73
+ return results
pythonwrench/csv.py ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ # for backward compatibility
5
+ from pythonwrench.serialization.csv import ( # noqa: F401
6
+ dump_csv,
7
+ dumps_csv,
8
+ load_csv,
9
+ loads_csv,
10
+ read_csv,
11
+ save_csv,
12
+ )
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ from dataclasses import MISSING, dataclass, is_dataclass # noqa: F401
5
+ from typing import Any, Dict, Type, TypeVar, cast
6
+
7
+ from typing_extensions import dataclass_transform
8
+
9
+ from pythonwrench.typing.checks import ( # noqa: F401
10
+ is_dataclass_instance,
11
+ is_dataclass_type,
12
+ )
13
+ from pythonwrench.typing.classes import Dataclass, DataclassInstance # noqa: F401
14
+
15
+ T = TypeVar("T")
16
+
17
+
18
+ @dataclass_transform()
19
+ def dataclassdict(cls: Type[T]) -> Type[T]:
20
+ """Decorate a class so it becomes both a dataclass and a dictionary."""
21
+ return add_dict_methods(dataclass(cls))
22
+
23
+
24
+ def add_dict_methods(cls: Type[T]) -> Type[T]:
25
+ """Return a dictionary subclass of an already-defined dataclass.
26
+
27
+ Field values are kept synchronized between attribute and mapping access.
28
+ """
29
+ if not is_dataclass_type(cls):
30
+ raise TypeError("add_dict_methods expects a dataclass type.")
31
+
32
+ dataclass_cls = cls
33
+ conflicting_fields = sorted(
34
+ set(dataclass_cls.__dataclass_fields__).intersection(dir(dict)) # type: ignore
35
+ )
36
+ if conflicting_fields:
37
+ names = ", ".join(repr(name) for name in conflicting_fields)
38
+ msg = f"Dataclass fields conflict with dict attributes: {names}."
39
+ raise RuntimeError(msg)
40
+
41
+ def setattr_(self: Any, name: str, value: Any) -> None:
42
+ """Perform the setattr operation."""
43
+ object.__setattr__(self, name, value)
44
+ if name in self.__dataclass_fields__:
45
+ dict.__setitem__(self, name, value)
46
+
47
+ def setitem(self: Any, key: Any, value: Any) -> None:
48
+ """Perform the setitem operation."""
49
+ dict.__setitem__(self, key, value)
50
+ if key in self.__dataclass_fields__:
51
+ object.__setattr__(self, key, value)
52
+
53
+ def delitem(self: Any, key: Any) -> None:
54
+ """Perform the delitem operation."""
55
+ dict.__delitem__(self, key)
56
+ if key in self.__dataclass_fields__ and hasattr(self, key):
57
+ object.__delattr__(self, key)
58
+
59
+ def update(self: Any, *args: Any, **kwargs: Any) -> None:
60
+ """Perform the update operation."""
61
+ for key, value in dict(*args, **kwargs).items():
62
+ setitem(self, key, value)
63
+
64
+ def setdefault(self: Any, key: Any, default: Any = None) -> Any:
65
+ """Perform the setdefault operation."""
66
+ if key not in self:
67
+ setitem(self, key, default)
68
+ return self[key]
69
+
70
+ def pop(self: Any, key: Any, *default: Any) -> Any:
71
+ """Perform the pop operation."""
72
+ if len(default) > 1:
73
+ raise TypeError("pop expected at most 2 arguments")
74
+ if key not in self:
75
+ if default:
76
+ return default[0]
77
+ raise KeyError(key)
78
+ value = self[key]
79
+ delitem(self, key)
80
+ return value
81
+
82
+ def clear(self: Any) -> None:
83
+ """Perform the clear operation."""
84
+ for key in list(self):
85
+ delitem(self, key)
86
+
87
+ namespace = {
88
+ "__module__": cls.__module__,
89
+ "__doc__": cls.__doc__,
90
+ "__setattr__": setattr_,
91
+ "__setitem__": setitem,
92
+ "__delitem__": delitem,
93
+ "update": update,
94
+ "setdefault": setdefault,
95
+ "pop": pop,
96
+ "clear": clear,
97
+ }
98
+ result = type(cls.__name__, (dataclass_cls, dict), namespace) # type: ignore
99
+ result.__qualname__ = cls.__qualname__
100
+ result = dataclass(result)
101
+ return cast(Type[T], result)
102
+
103
+
104
+ def get_defaults_values(obj: DataclassInstance) -> Dict[str, Any]:
105
+ """Return defaults values."""
106
+ defaults = {}
107
+
108
+ for field in obj.__dataclass_fields__.values():
109
+ if callable(field.default_factory):
110
+ default = field.default_factory()
111
+ else:
112
+ default = field.default
113
+
114
+ if default != MISSING:
115
+ defaults[field.name] = default
116
+
117
+ return defaults
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import datetime
5
+
6
+ ISO8601_DAY_FORMAT = r"%Y-%m-%d"
7
+ ISO8601_HOUR_FORMAT_DOUBLE_DOT = r"%Y-%m-%dT%H:%M:%S"
8
+
9
+
10
+ def get_now_iso8601() -> str:
11
+ """Returns current datetime as string with the ISO8601 format."""
12
+ return get_now(ISO8601_HOUR_FORMAT_DOUBLE_DOT)
13
+
14
+
15
+ def get_now(fmt: str = ISO8601_HOUR_FORMAT_DOUBLE_DOT) -> str:
16
+ """Returns current datetime as string with the specified format."""
17
+ return datetime.datetime.now().strftime(fmt)
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import math
5
+ from difflib import SequenceMatcher
6
+ from typing import Callable, Iterable, Optional
7
+
8
+
9
+ def find_closest_in_list(
10
+ x: str,
11
+ lst: Iterable[str],
12
+ sim_fn: Optional[Callable[[str, str], float]] = None,
13
+ higher_is_closer: bool = True,
14
+ ) -> Optional[str]:
15
+ """Find closest element in a list based on matches ratio."""
16
+ if sim_fn is None:
17
+ sim_fn = sequence_matcher_ratio
18
+
19
+ best_sim = -int(higher_is_closer) * math.inf
20
+ closest = None
21
+
22
+ for elt in lst:
23
+ sim = sim_fn(x, elt)
24
+ if (higher_is_closer and best_sim < sim) or (
25
+ not higher_is_closer and best_sim > sim
26
+ ):
27
+ best_sim = sim
28
+ closest = elt
29
+
30
+ if math.isinf(best_sim):
31
+ msg = f"Invalid argument {lst=}. (expected non-empty iterable of strings)"
32
+ raise ValueError(msg)
33
+
34
+ return closest
35
+
36
+
37
+ def sequence_matcher_ratio(a: str, b: str) -> float:
38
+ """Compute distance ratio of two strings."""
39
+ return SequenceMatcher(None, a, b).ratio()