iker-python-common 1.0.59__py3-none-any.whl → 1.0.60__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.
@@ -1,8 +1,12 @@
1
1
  import functools
2
2
  from collections.abc import Callable
3
- from typing import Protocol
3
+ from typing import Any, Protocol
4
4
 
5
5
  __all__ = [
6
+ "const",
7
+ "first",
8
+ "second",
9
+ "packed",
6
10
  "identity",
7
11
  "composable",
8
12
  "singleton",
@@ -12,6 +16,70 @@ __all__ = [
12
16
  ]
13
17
 
14
18
 
19
+ def const[T](value: T) -> Callable[..., T]:
20
+ """
21
+ Returns a function that always returns the specified ``value``, regardless of the input arguments.
22
+
23
+ :param value: The constant value to return.
24
+ :return: A function that takes any arguments and returns ``value``.
25
+ """
26
+
27
+ def getter(*args: Any, **kwargs: Any) -> T:
28
+ return value
29
+
30
+ return getter
31
+
32
+
33
+ def first[K]() -> Callable[[tuple[K, Any]], K]:
34
+ """
35
+ Returns a function that extracts the first element (key) from a 2-tuple.
36
+
37
+ :return: A function that takes a 2-tuple and returns its first element.
38
+ """
39
+
40
+ def getter(item: tuple[K, Any]) -> K:
41
+ if not isinstance(item, tuple) or len(item) != 2:
42
+ raise ValueError("item must be a 2-tuple")
43
+ return item[0]
44
+
45
+ return getter
46
+
47
+
48
+ def second[V]() -> Callable[[tuple[Any, V]], V]:
49
+ """
50
+ Returns a function that extracts the second element (value) from a 2-tuple.
51
+
52
+ :return: A function that takes a 2-tuple and returns its second element.
53
+ """
54
+
55
+ def getter(item: tuple[Any, V]) -> V:
56
+ if not isinstance(item, tuple) or len(item) != 2:
57
+ raise ValueError("item must be a 2-tuple")
58
+ return item[1]
59
+
60
+ return getter
61
+
62
+
63
+ def packed[R](func: Callable[..., R]) -> Callable[[tuple[Any, ...]], R]:
64
+ """
65
+ Wraps a function to accept its arguments as a single tuple, unpacking them when called. This is useful for
66
+ scenarios where arguments are naturally grouped in tuples, such as when working with data structures like maps or
67
+ lists of tuples, or when interfacing with APIs that provide arguments in tuple form.
68
+
69
+ >>> data = [(1, 2), (3, 4), (5, 6)]
70
+ >>> sums = map(packed(lambda x, y: x + y), data)
71
+
72
+ :param func: The function to wrap.
73
+ :return: A function that takes a tuple of arguments and calls the original function with them unpacked.
74
+ """
75
+
76
+ @functools.wraps(func)
77
+ def wrapper(args: tuple[Any, ...]) -> R:
78
+ return func(*args)
79
+
80
+ return wrapper
81
+
82
+
15
83
  def identity[T](instance: T) -> T:
16
84
  """
17
85
  Returns the input ``instance`` unchanged. This is a utility function often used as a default or placeholder.
@@ -1,5 +1,5 @@
1
1
  import math
2
- from collections.abc import Callable, Mapping, MutableMapping, MutableSequence, Sequence, Set
2
+ from collections.abc import Callable, Generator, Mapping, MutableMapping, MutableSequence, Sequence, Set
3
3
  from typing import Any, SupportsFloat, SupportsInt
4
4
 
5
5
  from iker.common.utils.numutils import is_normal_real
@@ -24,6 +24,8 @@ __all__ = [
24
24
  "json_traverse",
25
25
  "json_reformat",
26
26
  "json_sanitize",
27
+ "json_difference",
28
+ "json_equals",
27
29
  "json_compare",
28
30
  ]
29
31
 
@@ -347,79 +349,134 @@ def json_sanitize(obj: Any, *, str_inf_nan: bool = True, str_unregistered: bool
347
349
  unregistered_formatter=unregistered_formatter)
348
350
 
349
351
 
350
- def json_compare(
352
+ def json_difference(
351
353
  a: JsonTypeCompatible,
352
354
  b: JsonTypeCompatible,
355
+ node_path: NodePath | None = None,
353
356
  *,
354
357
  int_strict: bool = False,
355
358
  float_tol: float = 1e-5,
356
359
  list_order: bool = True,
357
360
  dict_extra: bool = False,
358
- ) -> bool:
361
+ ) -> Generator[tuple[NodePath, str], None, None]:
359
362
  """
360
- Compares two JSON-like structures for equality, with options for integer strictness, float tolerance, list order,
361
- and dictionary key matching.
363
+ Compares two JSON-like structures and yields differences found, with options for integer strictness, float
364
+ tolerance, list order, and dictionary key matching.
362
365
 
363
366
  :param a: The first JSON-compatible object to compare.
364
367
  :param b: The second JSON-compatible object to compare.
368
+ :param node_path: The current node path during recursion (used internally).
365
369
  :param int_strict: Whether to require strict integer type matching.
366
370
  :param float_tol: The tolerance for comparing float values.
367
371
  :param list_order: Whether to require list order to match.
368
372
  :param dict_extra: Whether to allow extra keys in dictionaries.
369
- :return: ``True`` if the structures are considered equal, ``False`` otherwise.
373
+ :return: Tuples of node paths and difference descriptions.
370
374
  """
371
375
  if a is None or b is None:
372
- return a is None and b is None
376
+ if not (a is None and b is None):
377
+ yield node_path, "one value is None while the other is not"
378
+ return
373
379
 
374
380
  if isinstance(a, (str, bool)):
375
381
  if type(a) != type(b):
376
- return False
377
- return a == b
382
+ yield node_path, f"type mismatch: '{type(a)}' vs '{type(b)}'"
383
+ elif a != b:
384
+ yield node_path, f"value mismatch: '{a}' vs '{b}'"
385
+ return
378
386
 
379
387
  if isinstance(a, (SupportsFloat, SupportsInt)) and isinstance(b, (SupportsFloat, SupportsInt)):
380
388
  isint_a = isinstance(a, int) or not isinstance(a, SupportsFloat)
381
389
  isint_b = isinstance(b, int) or not isinstance(b, SupportsFloat)
382
390
  if isint_a and isint_b:
383
- return int(a) == int(b)
391
+ if int(a) != int(b):
392
+ yield node_path, f"integer value mismatch: '{int(a)}' vs '{int(b)}'"
393
+ return
384
394
  if int_strict and (isint_a or isint_b):
385
- return False
395
+ yield node_path, "integer type mismatch under strict mode"
396
+ return
386
397
  va = int(a) if isint_a else float(a)
387
398
  vb = int(b) if isint_b else float(b)
388
- if math.isnan(va) and math.isnan(vb):
389
- return True
399
+ if math.isnan(va) or math.isnan(vb):
400
+ if not (math.isnan(va) and math.isnan(vb)):
401
+ yield node_path, "NaN mismatch"
402
+ return
390
403
  if math.isinf(va) and math.isinf(vb):
391
- return va == vb
392
- return abs(va - vb) <= float_tol
404
+ if va != vb:
405
+ yield node_path, "infinity sign mismatch"
406
+ return
407
+ if abs(va - vb) > float_tol:
408
+ yield node_path, f"float value mismatch: '{va}' vs '{vb}' with tolerance '{float_tol}'"
409
+ return
393
410
 
394
411
  if isinstance(a, Mapping) and isinstance(b, Mapping):
395
412
  if not dict_extra and set(a.keys()) != set(b.keys()):
396
- return False
397
- return all(json_compare(a[k],
398
- b[k],
399
- int_strict=int_strict,
400
- float_tol=float_tol,
401
- list_order=list_order,
402
- dict_extra=dict_extra)
403
- for k in set(a.keys()) & set(b.keys()))
413
+ yield node_path, f"dictionary key mismatch: '{set(a.keys())}' vs '{set(b.keys())}'"
414
+ return
415
+ for k in set(a.keys()) & set(b.keys()):
416
+ yield from json_difference(a[k],
417
+ b[k],
418
+ (node_path or []) + [k],
419
+ int_strict=int_strict,
420
+ float_tol=float_tol,
421
+ list_order=list_order,
422
+ dict_extra=dict_extra)
423
+ return
404
424
 
405
425
  if isinstance(a, Sequence) and isinstance(b, Sequence):
406
426
  if len(a) != len(b):
407
- return False
427
+ yield node_path, f"list length mismatch: '{len(a)}' vs '{len(b)}'"
428
+ return
408
429
  if list_order:
409
- return all(json_compare(va,
410
- vb,
411
- int_strict=int_strict,
412
- float_tol=float_tol,
413
- list_order=list_order,
414
- dict_extra=dict_extra)
415
- for va, vb in zip(a, b))
430
+ for i, (va, vb) in enumerate(zip(a, b)):
431
+ yield from json_difference(va,
432
+ vb,
433
+ (node_path or []) + [i],
434
+ int_strict=int_strict,
435
+ float_tol=float_tol,
436
+ list_order=list_order,
437
+ dict_extra=dict_extra)
416
438
  else:
417
- return all(json_compare(va,
418
- vb,
419
- int_strict=int_strict,
420
- float_tol=float_tol,
421
- list_order=list_order,
422
- dict_extra=dict_extra)
423
- for va, vb in zip(sorted(a), sorted(b)))
424
-
425
- raise ValueError(f"incompatible type '{type(a)}' and '{type(b)}'")
439
+ for i, (va, vb) in enumerate(zip(sorted(a), sorted(b))):
440
+ yield from json_difference(va,
441
+ vb,
442
+ (node_path or []) + [i],
443
+ int_strict=int_strict,
444
+ float_tol=float_tol,
445
+ list_order=list_order,
446
+ dict_extra=dict_extra)
447
+ return
448
+
449
+ yield node_path, f"type mismatch: '{type(a)}' vs '{type(b)}'"
450
+
451
+
452
+ def json_equals(
453
+ a: JsonTypeCompatible,
454
+ b: JsonTypeCompatible,
455
+ *,
456
+ int_strict: bool = False,
457
+ float_tol: float = 1e-5,
458
+ list_order: bool = True,
459
+ dict_extra: bool = False,
460
+ ) -> bool:
461
+ """
462
+ Compares two JSON-like structures for equality based on specified criteria.
463
+
464
+ :param a: The first JSON-compatible object to compare.
465
+ :param b: The second JSON-compatible object to compare.
466
+ :param int_strict: Whether to require strict integer type matching.
467
+ :param float_tol: The tolerance for comparing float values.
468
+ :param list_order: Whether to require list order to match.
469
+ :param dict_extra: Whether to allow extra keys in dictionaries.
470
+ :return: ``True`` if the structures are considered equal, ``False`` otherwise.
471
+ """
472
+ return next(json_difference(a,
473
+ b,
474
+ node_path=[],
475
+ int_strict=int_strict,
476
+ float_tol=float_tol,
477
+ list_order=list_order,
478
+ dict_extra=dict_extra),
479
+ None) is None
480
+
481
+
482
+ json_compare = json_equals
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iker-python-common
3
- Version: 1.0.59
3
+ Version: 1.0.60
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: 3.12
6
6
  Classifier: Programming Language :: Python :: 3.13
@@ -6,8 +6,8 @@ iker/common/utils/csv.py,sha256=_V9OUrKcojec2L-hWagEIVnL2uvGjyJAFTrD7tHNr48,7573
6
6
  iker/common/utils/dbutils.py,sha256=zXZVJCz7HZPityFRF7sHRRMpMraegV_hyYnzApUUPhY,11852
7
7
  iker/common/utils/dockerutils.py,sha256=n2WuzXaZB6_WocSljvPOnfExSIjIHRUbuWp2oBbaPKQ,8004
8
8
  iker/common/utils/dtutils.py,sha256=86vbaa4pgcBWERZvTfJ92PKB3IimxP6tf0O11ho2Ffk,12554
9
- iker/common/utils/funcutils.py,sha256=hHk7UHI-h92F8CFff-SZGvfhSlZ-CkO2dI8GWa6V81Q,5734
10
- iker/common/utils/jsonutils.py,sha256=Df4uxEgIejEhZdrfmiT4DX2aewEX0Pqbl1n4OCN4YdI,15970
9
+ iker/common/utils/funcutils.py,sha256=4AkkvK9_Z2tgk1-Sp6-vLLVhI15cIgN9xW58QqL5QL4,7780
10
+ iker/common/utils/jsonutils.py,sha256=AkziMAYVQDODHRqZC-c1x7VqI2hHY3Kxrw7gmoss8mU,18527
11
11
  iker/common/utils/logger.py,sha256=FJaai6Sbchy4wKHcUMUCrrkBcXvIxq4qByERZ_TJBps,3881
12
12
  iker/common/utils/numutils.py,sha256=p6Rz1qyCcUru3v1zDy2PM-nds2NWJdL5A_vLmG-kswk,4294
13
13
  iker/common/utils/randutils.py,sha256=Sxf852B18CJ-MfrEDsv1ROO_brmz79dRZ4jpJiH65v4,12843
@@ -18,7 +18,7 @@ iker/common/utils/span.py,sha256=u_KuWi2U7QDMUotl4AeW2_57ItL3YhVDSeCwaOiFDvs,596
18
18
  iker/common/utils/strutils.py,sha256=Tu_qFeH3K-SfwvMxdrZAc9iLPV8ZmtX4ntyyFGNslf8,5094
19
19
  iker/common/utils/testutils.py,sha256=2VieV5yeCDntSKQSpIeyqRT8BZmZYE_ArMeQz3g7fXY,5568
20
20
  iker/common/utils/typeutils.py,sha256=RVkYkFRgDrx77OHFH7PavMV0AIB0S8ly40rs4g7JWE4,8220
21
- iker_python_common-1.0.59.dist-info/METADATA,sha256=C3LhE0abnoE8e5KeGHrKj2DmN8RCL8fP7M9aWY6KVE0,813
22
- iker_python_common-1.0.59.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
- iker_python_common-1.0.59.dist-info/top_level.txt,sha256=4_B8Prfc_lxFafFYTQThIU1ZqOYQ4pHHHnJ_fQ_oHs8,5
24
- iker_python_common-1.0.59.dist-info/RECORD,,
21
+ iker_python_common-1.0.60.dist-info/METADATA,sha256=2B3f-_-H83ceea3JraxM602M47-BcdClWBtZIZLF_I0,813
22
+ iker_python_common-1.0.60.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
+ iker_python_common-1.0.60.dist-info/top_level.txt,sha256=4_B8Prfc_lxFafFYTQThIU1ZqOYQ4pHHHnJ_fQ_oHs8,5
24
+ iker_python_common-1.0.60.dist-info/RECORD,,