oj-toolkit 0.2.0__tar.gz → 0.2.1__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.
Files changed (35) hide show
  1. {oj_toolkit-0.2.0 → oj_toolkit-0.2.1}/PKG-INFO +78 -16
  2. {oj_toolkit-0.2.0 → oj_toolkit-0.2.1}/README.md +77 -15
  3. {oj_toolkit-0.2.0 → oj_toolkit-0.2.1}/oj_toolkit/parsing/consts.py +4 -1
  4. {oj_toolkit-0.2.0 → oj_toolkit-0.2.1}/oj_toolkit/parsing/types.py +33 -8
  5. {oj_toolkit-0.2.0 → oj_toolkit-0.2.1}/oj_toolkit.egg-info/PKG-INFO +78 -16
  6. {oj_toolkit-0.2.0 → oj_toolkit-0.2.1}/pyproject.toml +1 -1
  7. {oj_toolkit-0.2.0 → oj_toolkit-0.2.1}/LICENSE +0 -0
  8. {oj_toolkit-0.2.0 → oj_toolkit-0.2.1}/oj_toolkit/__init__.py +0 -0
  9. {oj_toolkit-0.2.0 → oj_toolkit-0.2.1}/oj_toolkit/asynchronous/__init__.py +0 -0
  10. {oj_toolkit-0.2.0 → oj_toolkit-0.2.1}/oj_toolkit/asynchronous/async_chunks.py +0 -0
  11. {oj_toolkit-0.2.0 → oj_toolkit-0.2.1}/oj_toolkit/asynchronous/decorators.py +0 -0
  12. {oj_toolkit-0.2.0 → oj_toolkit-0.2.1}/oj_toolkit/console/__init__.py +0 -0
  13. {oj_toolkit-0.2.0 → oj_toolkit-0.2.1}/oj_toolkit/console/box.py +0 -0
  14. {oj_toolkit-0.2.0 → oj_toolkit-0.2.1}/oj_toolkit/console/colored_text.py +0 -0
  15. {oj_toolkit-0.2.0 → oj_toolkit-0.2.1}/oj_toolkit/console/colors.py +0 -0
  16. {oj_toolkit-0.2.0 → oj_toolkit-0.2.1}/oj_toolkit/console/status.py +0 -0
  17. {oj_toolkit-0.2.0 → oj_toolkit-0.2.1}/oj_toolkit/console/streams.py +0 -0
  18. {oj_toolkit-0.2.0 → oj_toolkit-0.2.1}/oj_toolkit/console/table.py +0 -0
  19. {oj_toolkit-0.2.0 → oj_toolkit-0.2.1}/oj_toolkit/console/terminal.py +0 -0
  20. {oj_toolkit-0.2.0 → oj_toolkit-0.2.1}/oj_toolkit/console/text.py +0 -0
  21. {oj_toolkit-0.2.0 → oj_toolkit-0.2.1}/oj_toolkit/data/__init__.py +0 -0
  22. {oj_toolkit-0.2.0 → oj_toolkit-0.2.1}/oj_toolkit/data/flex.py +0 -0
  23. {oj_toolkit-0.2.0 → oj_toolkit-0.2.1}/oj_toolkit/diagnostics.py +0 -0
  24. {oj_toolkit-0.2.0 → oj_toolkit-0.2.1}/oj_toolkit/logging/__init__.py +0 -0
  25. {oj_toolkit-0.2.0 → oj_toolkit-0.2.1}/oj_toolkit/logging/config.py +0 -0
  26. {oj_toolkit-0.2.0 → oj_toolkit-0.2.1}/oj_toolkit/logging/consts.py +0 -0
  27. {oj_toolkit-0.2.0 → oj_toolkit-0.2.1}/oj_toolkit/logging/decorators.py +0 -0
  28. {oj_toolkit-0.2.0 → oj_toolkit-0.2.1}/oj_toolkit/logging/formatters.py +0 -0
  29. {oj_toolkit-0.2.0 → oj_toolkit-0.2.1}/oj_toolkit/logging/handlers.py +0 -0
  30. {oj_toolkit-0.2.0 → oj_toolkit-0.2.1}/oj_toolkit/parsing/__init__.py +0 -0
  31. {oj_toolkit-0.2.0 → oj_toolkit-0.2.1}/oj_toolkit.egg-info/SOURCES.txt +0 -0
  32. {oj_toolkit-0.2.0 → oj_toolkit-0.2.1}/oj_toolkit.egg-info/dependency_links.txt +0 -0
  33. {oj_toolkit-0.2.0 → oj_toolkit-0.2.1}/oj_toolkit.egg-info/requires.txt +0 -0
  34. {oj_toolkit-0.2.0 → oj_toolkit-0.2.1}/oj_toolkit.egg-info/top_level.txt +0 -0
  35. {oj_toolkit-0.2.0 → oj_toolkit-0.2.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oj-toolkit
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Centralized shared utilities for ownjoo-org projects: standardized logging config, type validation, data parsing, console output, and async utilities
5
5
  Author-email: Speedy <speedy@ownjoo.org>
6
6
  License: MIT
@@ -33,7 +33,7 @@ Requires-Dist: mypy>=1.0; extra == "dev"
33
33
  Requires-Dist: pylint>=2.0; extra == "dev"
34
34
  Dynamic: license-file
35
35
 
36
- # ownjoo-toolkit
36
+ # oj-toolkit
37
37
 
38
38
  Centralized shared utilities library for all ownjoo-org projects. Provides battle-tested functions for type validation, data parsing, and progress logging.
39
39
 
@@ -187,11 +187,19 @@ fields = dig_many(
187
187
  # Bind a path once, reuse it against many objects
188
188
  get_name = Digger(path='name', exp=str, default='')
189
189
  [get_name(user) for user in data['users']] # Returns: ['Alice', 'Bob']
190
+
191
+ # Constrain a string result with a regex (checked via re.fullmatch after conversion)
192
+ mac = dig({'device': {'mac': 'AA:BB:CC:DD:EE:FF'}}, path='device.mac', exp=str,
193
+ pattern=r'([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}')
194
+ # Returns: 'AA:BB:CC:DD:EE:FF'
190
195
  ```
191
196
 
192
- > **Note:** `dig()`'s default `post_processor` (`validate()`) only accepts a value if you pass
193
- > `exp=<type>` -- without it, validation always fails and you get `default` back (`None` unless set).
194
- > Pass `exp=` for a real value, or `post_processor=None` to skip validation and get the raw match.
197
+ > **Note:** if the path resolves to nothing (missing key/index, or `src` doesn't contain it),
198
+ > `dig()`'s default `post_processor` (`validate()`) still runs and returns `default` (`None`
199
+ > unless set) -- a missing path never silently drops `default` in favor of a bare `None`.
200
+ > Passing no `exp=` means "accept any type, no isinstance check" -- not "always fail". Pass
201
+ > `exp=<type>` to add a type check, or `post_processor=None` to bypass validation entirely and
202
+ > get the raw match untouched.
195
203
 
196
204
  ### Logging Setup
197
205
 
@@ -713,16 +721,17 @@ Returns `True` if the terminal likely supports Unicode characters. Used internal
713
721
 
714
722
  ### `parsing` Module
715
723
 
716
- #### `validate(v, exp=None, default=None, converter=None, validator=None, **kwargs)`
724
+ #### `validate(v, exp=None, default=None, converter=None, validator=None, pattern=None, **kwargs)`
717
725
 
718
726
  Generic validation utility that converts and validates a value.
719
727
 
720
728
  - **Parameters:**
721
729
  - `v` (Any): The value to validate
722
- - `exp` (Type): Expected type. Enables automatic converter selection (list → str_to_list, datetime → get_datetime)
723
- - `default` (Any): Return this if validation fails. Default: None
724
- - `converter` (Callable): Custom converter function. Default: auto-selected based on exp
725
- - `validator` (Callable): Custom validator function(result, exp, **kwargs) → bool. Default: isinstance check
730
+ - `exp` (Type): Expected type. Enables automatic converter selection (list → str_to_list, datetime → get_datetime). If `None`, no isinstance check is performed -- any type passes (see `validator` below)
731
+ - `default` (Any): Return this if validation fails, or `v` is missing/`None`. Default: None
732
+ - `converter` (Callable): Custom converter function. Default: auto-selected based on exp. Auto-selection is limited to `list`/`datetime` -- `bool`/`int`/`float`/etc. are pass-through unless you supply your own `converter=`
733
+ - `validator` (Callable): Custom validator function(result, exp, **kwargs) → bool. Default: isinstance check (`exp=None` always passes; a non-type `exp` is rejected rather than raising)
734
+ - `pattern` (str | re.Pattern): Optional regex checked via `re.fullmatch` against the result, but only when the result is a `str` -- ignored for other types. Applied after conversion/validation succeed; a mismatch falls back to `default`
726
735
  - `**kwargs`: Passed to converter and validator
727
736
 
728
737
  - **Returns:** Converted and validated value, or default if validation fails
@@ -734,6 +743,16 @@ Generic validation utility that converts and validates a value.
734
743
  numbers = validate('1,2,3', exp=list) # Returns: ['1', '2', '3']
735
744
  dt = validate('2024-01-15T10:30:00', exp=datetime) # Returns: datetime object
736
745
 
746
+ # exp=bool/int/float do NOT auto-coerce strings -- pass converter= explicitly for that
747
+ validate('42', exp=int) # Returns: None ('42' is a str, not an int)
748
+ validate('42', exp=int, converter=int) # Returns: 42
749
+
750
+ # Regex-constrain a string result
751
+ validate('AA:BB:CC:DD:EE:FF', exp=str, pattern=r'([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}')
752
+ # Returns: 'AA:BB:CC:DD:EE:FF'
753
+ validate('not-a-mac', exp=str, pattern=r'([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}', default='invalid')
754
+ # Returns: 'invalid'
755
+
737
756
  # Custom converter and validator
738
757
  def to_upper(v, *args, **kwargs):
739
758
  return str(v).upper()
@@ -795,6 +814,12 @@ get_datetime('01/15/2024 10:30:00', format_str='%m/%d/%Y %H:%M:%S') # Custom
795
814
  Extract and post-process a value from a nested data structure, using [jmespath](https://jmespath.org/)
796
815
  under the hood (`jmespath` is a required dependency).
797
816
 
817
+ > **Compilation caching:** compiled jmespath expressions are cached in a process-global
818
+ > `lru_cache(maxsize=256)`, keyed by the expression string. A bare `dig()` call already avoids
819
+ > recompiling a repeated path -- this isn't a `Digger`-only optimization. The cache holds up to
820
+ > 256 distinct expressions across the whole process (shared by every `dig()`/`Digger` caller),
821
+ > LRU-evicted beyond that.
822
+
798
823
  - **Parameters:**
799
824
  - `src` (Mapping | Sequence): Data structure to navigate
800
825
  - `path` (int | str | list | None):
@@ -811,16 +836,18 @@ under the hood (`jmespath` is a required dependency).
811
836
  Pass `None` to skip post-processing and get the raw match.
812
837
  - `**kwargs`: Passed to `post_processor`
813
838
 
814
- - **Returns:** Post-processed value, or `None` if extraction fails, no candidate path matches, or
815
- no post-processor is specified and nothing was found
839
+ - **Returns:** Post-processed value. If extraction fails or no candidate path matches, the
840
+ post-processor (default `validate()`) still runs and returns `default` (`None` unless set) --
841
+ unless `post_processor=None`, in which case the raw (possibly missing/`None`) value is returned
842
+ untouched with no default substitution.
816
843
 
817
844
  > **Type checkers:** pass `exp=<type>` to get a precise `<type> | None` return type instead of `Any`
818
845
  > (`dig(data, path='...', exp=str)` type-checks as `str | None`). Passing `post_processor=None` types
819
846
  > as `Any` (raw, unvalidated value). A custom `post_processor` types as that callable's own return type.
820
847
 
821
- > **Passing no `exp`:** the default `validate()` post-processor only accepts a value when you give
822
- > it `exp=<type>` -- without one, validation always fails and you get `default` back (`None` unless
823
- > set). Either pass `exp=`, or `post_processor=None` to bypass validation entirely.
848
+ > **Passing no `exp`:** the default `validate()` post-processor treats a missing `exp` as "accept
849
+ > any type" -- it does not fail. Pass `exp=<type>` to add an isinstance check, or
850
+ > `post_processor=None` to bypass validation entirely and get the raw match.
824
851
 
825
852
  **Example:**
826
853
 
@@ -855,6 +882,39 @@ name = dig(data, path=['response.users[0].display_name', 'response.users[0].name
855
882
  # Returns: 'Alice'
856
883
  ```
857
884
 
885
+ ##### `dig()` Behavior Matrix
886
+
887
+ Every row below is a real `dig()` call, backed by a passing unit test in
888
+ `test/unit/parsing/test_parsing.py` -- the test name is the proof, not just a claim. This isn't
889
+ every possible combination of kwargs (that cross product is huge and mostly redundant); it's one
890
+ representative call per distinct decision point in `dig()`/`validate()`. `*(unset)*` means the
891
+ kwarg wasn't passed (the function's own default applies); `--` means it isn't relevant to that row.
892
+
893
+ | `src` | `path` | `exp` | `default` | `post_processor` | `pattern` | `validator` | `pop` | Result | Proven by |
894
+ |---|---|---|---|---|---|---|---|---|---|
895
+ | `{'first': 'a', 'second': ['blah']}` | `'second[0]'` (found) | `str` | *(unset)* | *(unset)* | *(unset)* | *(unset)* | `False` | `'blah'` | `test_should_get_value_from_dict` |
896
+ | `['', ['blah']]` | `'[1][0]'` (found) | `str` | `''` | *(unset)* | *(unset)* | *(unset)* | `False` | `'blah'` (default unused -- value was valid) | `test_should_get_value_from_list` |
897
+ | `{'a': {'b': 'value'}}` | `'x.y.z'` (missing) | `str` | `'default'` | *(unset)* | *(unset)* | *(unset)* | `False` | `'default'` | `test_should_return_default_on_missing_intermediate_key` |
898
+ | `{'items': ['a', 'b', 'c']}` | `'items.invalid_index'` (missing) | `str` | `'default'` | *(unset)* | *(unset)* | *(unset)* | `False` | `'default'` | `test_should_return_default_when_accessing_list_with_string_key` |
899
+ | `{'a': {'b': 'value'}}` | `'x.y.z'` (missing) | `str` | *(unset)* | *(unset)* | *(unset)* | *(unset)* | `False` | `None` (no default set) | `test_should_return_none_for_missing_path_with_exp_and_no_default` |
900
+ | `{'a': {'b': 'c'}}` | `'x.y.z'` (missing) | *(unset)* | *(unset)* | *(unset)* | *(unset)* | *(unset)* | `False` | `None` (`exp=None` means "anything passes"; the found value already is `None`) | `test_should_handle_get_value_with_invalid_path` |
901
+ | `{'key': 'blah'}` | `'key'` (found) | *(unset)* | *(unset)* | *(unset)* | *(unset)* | *(unset)* | `False` | `'blah'` (raw pass-through, no isinstance check) | `test_should_not_crash_when_exp_omitted_from_dig` |
902
+ | `{'a': 'not-an-int'}` | `'a'` (found, wrong type) | `int` | *(unset)* | *(unset)* | *(unset)* | *(unset)* | `False` | `None` | `test_should_return_none_for_dig_result_type_mismatch` |
903
+ | `{'a': 'not-an-int'}` | `'a'` (found, wrong type) | `int` | `-1` | *(unset)* | *(unset)* | *(unset)* | `False` | `-1` | `test_should_return_default_for_dig_result_type_mismatch` |
904
+ | `{'a': 'blah'}` | `'a'` (found, non-type `exp`) | `'str'` | `'fallback'` | *(unset)* | *(unset)* | *(unset)* | `False` | `'fallback'` (garbage `exp` rejected, not raised) | `test_should_return_default_for_dig_with_non_type_expected_type` |
905
+ | `{'key': 'raw_value'}` | `'key'` (found) | -- | -- | `None` | -- | -- | `False` | raw value, unvalidated | `test_should_return_raw_value_when_no_post_processor` |
906
+ | `{'a': 1}` | `'missing'` (missing) | -- | `'default'` (ignored) | `None` | -- | -- | `False` | `None` (`default` is never consulted -- it's `validate()`'s kwarg, and `validate()` never runs) | `test_should_return_none_for_missing_path_with_no_post_processor` |
907
+ | `{'response': {'users': [{'name': 'Alice'}, {'name': 'Bob'}]}}` | `'response.users'` (found) | -- | -- | `len` | -- | -- | `False` | `2` (custom post_processor gets the raw match) | `test_should_use_custom_post_processor_function` |
908
+ | `'blah'` | `None` (src is the value) | `str` | *(unset)* | *(unset)* | *(unset)* | custom lambda | `False` | `'blah'` (custom validator overrides isinstance check) | `test_should_get_value_with_passed_validator` |
909
+ | `'test_string'` | `None` (src is the value) | `str` | *(unset)* | *(unset)* | *(unset)* | *(unset)* | `False` | the src value itself | `test_should_post_process_source_when_path_is_none` |
910
+ | `{'device': {'mac': 'AA:BB:CC:DD:EE:FF'}}` | `'device.mac'` (found) | `str` | *(unset)* | *(unset)* | matching regex | *(unset)* | `False` | matched MAC string | `test_should_validate_dig_result_with_matching_pattern` |
911
+ | `{'device': {'mac': 'not-a-mac'}}` | `'device.mac'` (found) | `str` | `'invalid'` | *(unset)* | non-matching regex | *(unset)* | `False` | `'invalid'` | `test_should_return_default_for_dig_result_with_non_matching_pattern` |
912
+ | `{'a': 1, 'b': 2}` | `'a'` (found) | `int` | *(unset)* | *(unset)* | -- | -- | `True` | `1`, and `'a'` removed from `src` | `test_should_pop_key_from_dict` |
913
+ | `{'items': [{'id': 1}, {'id': 2}]}` | `'items[?id==\`2\`].id'` (found, ambiguous) | `list` | *(unset)* | *(unset)* | -- | -- | `True` (refused) | `[2]`, `src` left unmutated (pop refused + warning logged) | `test_should_refuse_pop_with_ambiguous_jmespath_filter` |
914
+ | `{'user': {'name': 'Alice'}}` | `['user.nickname', 'user.name']` (fallback) | `str` | *(unset)* | *(unset)* | -- | -- | `False` | `'Alice'` (first candidate wins) | `test_should_use_first_matching_path_in_fallback_list` |
915
+ | `{'user': {'name': 'Alice'}}` | `['user.nickname', 'user.alias']` (fallback, none match) | `str` | *(unset)* | *(unset)* | -- | -- | `False` | `None` | `test_should_return_none_when_no_fallback_path_matches` |
916
+ | `{'user': {'name': 'Alice', 'nickname': 'Ali'}}` | `['user.nickname', 'user.name']` (fallback) | `str` | *(unset)* | *(unset)* | -- | -- | `True` | `'Ali'`, only the winning candidate's key removed | `test_should_pop_only_the_winning_fallback_path` |
917
+
858
918
  #### `dig_many(src, paths, **common_kwargs)`
859
919
 
860
920
  Extract several named fields from `src` in one call.
@@ -889,7 +949,9 @@ fields = dig_many(
889
949
 
890
950
  A pre-bound, reusable `dig()` call. Validates/compiles the jmespath expression once at
891
951
  construction (failing fast on a bad expression instead of on first use), then can be called
892
- like a function against any number of `src` objects without repeating `path`/`exp`/kwargs.
952
+ like a function against any number of `src` objects without repeating `path`/`exp`/kwargs. A
953
+ string `pattern=` kwarg is likewise pre-compiled to a `re.Pattern` at construction time, so
954
+ repeated calls don't re-compile the regex.
893
955
 
894
956
  **Example:**
895
957
 
@@ -1,4 +1,4 @@
1
- # ownjoo-toolkit
1
+ # oj-toolkit
2
2
 
3
3
  Centralized shared utilities library for all ownjoo-org projects. Provides battle-tested functions for type validation, data parsing, and progress logging.
4
4
 
@@ -152,11 +152,19 @@ fields = dig_many(
152
152
  # Bind a path once, reuse it against many objects
153
153
  get_name = Digger(path='name', exp=str, default='')
154
154
  [get_name(user) for user in data['users']] # Returns: ['Alice', 'Bob']
155
+
156
+ # Constrain a string result with a regex (checked via re.fullmatch after conversion)
157
+ mac = dig({'device': {'mac': 'AA:BB:CC:DD:EE:FF'}}, path='device.mac', exp=str,
158
+ pattern=r'([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}')
159
+ # Returns: 'AA:BB:CC:DD:EE:FF'
155
160
  ```
156
161
 
157
- > **Note:** `dig()`'s default `post_processor` (`validate()`) only accepts a value if you pass
158
- > `exp=<type>` -- without it, validation always fails and you get `default` back (`None` unless set).
159
- > Pass `exp=` for a real value, or `post_processor=None` to skip validation and get the raw match.
162
+ > **Note:** if the path resolves to nothing (missing key/index, or `src` doesn't contain it),
163
+ > `dig()`'s default `post_processor` (`validate()`) still runs and returns `default` (`None`
164
+ > unless set) -- a missing path never silently drops `default` in favor of a bare `None`.
165
+ > Passing no `exp=` means "accept any type, no isinstance check" -- not "always fail". Pass
166
+ > `exp=<type>` to add a type check, or `post_processor=None` to bypass validation entirely and
167
+ > get the raw match untouched.
160
168
 
161
169
  ### Logging Setup
162
170
 
@@ -678,16 +686,17 @@ Returns `True` if the terminal likely supports Unicode characters. Used internal
678
686
 
679
687
  ### `parsing` Module
680
688
 
681
- #### `validate(v, exp=None, default=None, converter=None, validator=None, **kwargs)`
689
+ #### `validate(v, exp=None, default=None, converter=None, validator=None, pattern=None, **kwargs)`
682
690
 
683
691
  Generic validation utility that converts and validates a value.
684
692
 
685
693
  - **Parameters:**
686
694
  - `v` (Any): The value to validate
687
- - `exp` (Type): Expected type. Enables automatic converter selection (list → str_to_list, datetime → get_datetime)
688
- - `default` (Any): Return this if validation fails. Default: None
689
- - `converter` (Callable): Custom converter function. Default: auto-selected based on exp
690
- - `validator` (Callable): Custom validator function(result, exp, **kwargs) → bool. Default: isinstance check
695
+ - `exp` (Type): Expected type. Enables automatic converter selection (list → str_to_list, datetime → get_datetime). If `None`, no isinstance check is performed -- any type passes (see `validator` below)
696
+ - `default` (Any): Return this if validation fails, or `v` is missing/`None`. Default: None
697
+ - `converter` (Callable): Custom converter function. Default: auto-selected based on exp. Auto-selection is limited to `list`/`datetime` -- `bool`/`int`/`float`/etc. are pass-through unless you supply your own `converter=`
698
+ - `validator` (Callable): Custom validator function(result, exp, **kwargs) → bool. Default: isinstance check (`exp=None` always passes; a non-type `exp` is rejected rather than raising)
699
+ - `pattern` (str | re.Pattern): Optional regex checked via `re.fullmatch` against the result, but only when the result is a `str` -- ignored for other types. Applied after conversion/validation succeed; a mismatch falls back to `default`
691
700
  - `**kwargs`: Passed to converter and validator
692
701
 
693
702
  - **Returns:** Converted and validated value, or default if validation fails
@@ -699,6 +708,16 @@ Generic validation utility that converts and validates a value.
699
708
  numbers = validate('1,2,3', exp=list) # Returns: ['1', '2', '3']
700
709
  dt = validate('2024-01-15T10:30:00', exp=datetime) # Returns: datetime object
701
710
 
711
+ # exp=bool/int/float do NOT auto-coerce strings -- pass converter= explicitly for that
712
+ validate('42', exp=int) # Returns: None ('42' is a str, not an int)
713
+ validate('42', exp=int, converter=int) # Returns: 42
714
+
715
+ # Regex-constrain a string result
716
+ validate('AA:BB:CC:DD:EE:FF', exp=str, pattern=r'([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}')
717
+ # Returns: 'AA:BB:CC:DD:EE:FF'
718
+ validate('not-a-mac', exp=str, pattern=r'([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}', default='invalid')
719
+ # Returns: 'invalid'
720
+
702
721
  # Custom converter and validator
703
722
  def to_upper(v, *args, **kwargs):
704
723
  return str(v).upper()
@@ -760,6 +779,12 @@ get_datetime('01/15/2024 10:30:00', format_str='%m/%d/%Y %H:%M:%S') # Custom
760
779
  Extract and post-process a value from a nested data structure, using [jmespath](https://jmespath.org/)
761
780
  under the hood (`jmespath` is a required dependency).
762
781
 
782
+ > **Compilation caching:** compiled jmespath expressions are cached in a process-global
783
+ > `lru_cache(maxsize=256)`, keyed by the expression string. A bare `dig()` call already avoids
784
+ > recompiling a repeated path -- this isn't a `Digger`-only optimization. The cache holds up to
785
+ > 256 distinct expressions across the whole process (shared by every `dig()`/`Digger` caller),
786
+ > LRU-evicted beyond that.
787
+
763
788
  - **Parameters:**
764
789
  - `src` (Mapping | Sequence): Data structure to navigate
765
790
  - `path` (int | str | list | None):
@@ -776,16 +801,18 @@ under the hood (`jmespath` is a required dependency).
776
801
  Pass `None` to skip post-processing and get the raw match.
777
802
  - `**kwargs`: Passed to `post_processor`
778
803
 
779
- - **Returns:** Post-processed value, or `None` if extraction fails, no candidate path matches, or
780
- no post-processor is specified and nothing was found
804
+ - **Returns:** Post-processed value. If extraction fails or no candidate path matches, the
805
+ post-processor (default `validate()`) still runs and returns `default` (`None` unless set) --
806
+ unless `post_processor=None`, in which case the raw (possibly missing/`None`) value is returned
807
+ untouched with no default substitution.
781
808
 
782
809
  > **Type checkers:** pass `exp=<type>` to get a precise `<type> | None` return type instead of `Any`
783
810
  > (`dig(data, path='...', exp=str)` type-checks as `str | None`). Passing `post_processor=None` types
784
811
  > as `Any` (raw, unvalidated value). A custom `post_processor` types as that callable's own return type.
785
812
 
786
- > **Passing no `exp`:** the default `validate()` post-processor only accepts a value when you give
787
- > it `exp=<type>` -- without one, validation always fails and you get `default` back (`None` unless
788
- > set). Either pass `exp=`, or `post_processor=None` to bypass validation entirely.
813
+ > **Passing no `exp`:** the default `validate()` post-processor treats a missing `exp` as "accept
814
+ > any type" -- it does not fail. Pass `exp=<type>` to add an isinstance check, or
815
+ > `post_processor=None` to bypass validation entirely and get the raw match.
789
816
 
790
817
  **Example:**
791
818
 
@@ -820,6 +847,39 @@ name = dig(data, path=['response.users[0].display_name', 'response.users[0].name
820
847
  # Returns: 'Alice'
821
848
  ```
822
849
 
850
+ ##### `dig()` Behavior Matrix
851
+
852
+ Every row below is a real `dig()` call, backed by a passing unit test in
853
+ `test/unit/parsing/test_parsing.py` -- the test name is the proof, not just a claim. This isn't
854
+ every possible combination of kwargs (that cross product is huge and mostly redundant); it's one
855
+ representative call per distinct decision point in `dig()`/`validate()`. `*(unset)*` means the
856
+ kwarg wasn't passed (the function's own default applies); `--` means it isn't relevant to that row.
857
+
858
+ | `src` | `path` | `exp` | `default` | `post_processor` | `pattern` | `validator` | `pop` | Result | Proven by |
859
+ |---|---|---|---|---|---|---|---|---|---|
860
+ | `{'first': 'a', 'second': ['blah']}` | `'second[0]'` (found) | `str` | *(unset)* | *(unset)* | *(unset)* | *(unset)* | `False` | `'blah'` | `test_should_get_value_from_dict` |
861
+ | `['', ['blah']]` | `'[1][0]'` (found) | `str` | `''` | *(unset)* | *(unset)* | *(unset)* | `False` | `'blah'` (default unused -- value was valid) | `test_should_get_value_from_list` |
862
+ | `{'a': {'b': 'value'}}` | `'x.y.z'` (missing) | `str` | `'default'` | *(unset)* | *(unset)* | *(unset)* | `False` | `'default'` | `test_should_return_default_on_missing_intermediate_key` |
863
+ | `{'items': ['a', 'b', 'c']}` | `'items.invalid_index'` (missing) | `str` | `'default'` | *(unset)* | *(unset)* | *(unset)* | `False` | `'default'` | `test_should_return_default_when_accessing_list_with_string_key` |
864
+ | `{'a': {'b': 'value'}}` | `'x.y.z'` (missing) | `str` | *(unset)* | *(unset)* | *(unset)* | *(unset)* | `False` | `None` (no default set) | `test_should_return_none_for_missing_path_with_exp_and_no_default` |
865
+ | `{'a': {'b': 'c'}}` | `'x.y.z'` (missing) | *(unset)* | *(unset)* | *(unset)* | *(unset)* | *(unset)* | `False` | `None` (`exp=None` means "anything passes"; the found value already is `None`) | `test_should_handle_get_value_with_invalid_path` |
866
+ | `{'key': 'blah'}` | `'key'` (found) | *(unset)* | *(unset)* | *(unset)* | *(unset)* | *(unset)* | `False` | `'blah'` (raw pass-through, no isinstance check) | `test_should_not_crash_when_exp_omitted_from_dig` |
867
+ | `{'a': 'not-an-int'}` | `'a'` (found, wrong type) | `int` | *(unset)* | *(unset)* | *(unset)* | *(unset)* | `False` | `None` | `test_should_return_none_for_dig_result_type_mismatch` |
868
+ | `{'a': 'not-an-int'}` | `'a'` (found, wrong type) | `int` | `-1` | *(unset)* | *(unset)* | *(unset)* | `False` | `-1` | `test_should_return_default_for_dig_result_type_mismatch` |
869
+ | `{'a': 'blah'}` | `'a'` (found, non-type `exp`) | `'str'` | `'fallback'` | *(unset)* | *(unset)* | *(unset)* | `False` | `'fallback'` (garbage `exp` rejected, not raised) | `test_should_return_default_for_dig_with_non_type_expected_type` |
870
+ | `{'key': 'raw_value'}` | `'key'` (found) | -- | -- | `None` | -- | -- | `False` | raw value, unvalidated | `test_should_return_raw_value_when_no_post_processor` |
871
+ | `{'a': 1}` | `'missing'` (missing) | -- | `'default'` (ignored) | `None` | -- | -- | `False` | `None` (`default` is never consulted -- it's `validate()`'s kwarg, and `validate()` never runs) | `test_should_return_none_for_missing_path_with_no_post_processor` |
872
+ | `{'response': {'users': [{'name': 'Alice'}, {'name': 'Bob'}]}}` | `'response.users'` (found) | -- | -- | `len` | -- | -- | `False` | `2` (custom post_processor gets the raw match) | `test_should_use_custom_post_processor_function` |
873
+ | `'blah'` | `None` (src is the value) | `str` | *(unset)* | *(unset)* | *(unset)* | custom lambda | `False` | `'blah'` (custom validator overrides isinstance check) | `test_should_get_value_with_passed_validator` |
874
+ | `'test_string'` | `None` (src is the value) | `str` | *(unset)* | *(unset)* | *(unset)* | *(unset)* | `False` | the src value itself | `test_should_post_process_source_when_path_is_none` |
875
+ | `{'device': {'mac': 'AA:BB:CC:DD:EE:FF'}}` | `'device.mac'` (found) | `str` | *(unset)* | *(unset)* | matching regex | *(unset)* | `False` | matched MAC string | `test_should_validate_dig_result_with_matching_pattern` |
876
+ | `{'device': {'mac': 'not-a-mac'}}` | `'device.mac'` (found) | `str` | `'invalid'` | *(unset)* | non-matching regex | *(unset)* | `False` | `'invalid'` | `test_should_return_default_for_dig_result_with_non_matching_pattern` |
877
+ | `{'a': 1, 'b': 2}` | `'a'` (found) | `int` | *(unset)* | *(unset)* | -- | -- | `True` | `1`, and `'a'` removed from `src` | `test_should_pop_key_from_dict` |
878
+ | `{'items': [{'id': 1}, {'id': 2}]}` | `'items[?id==\`2\`].id'` (found, ambiguous) | `list` | *(unset)* | *(unset)* | -- | -- | `True` (refused) | `[2]`, `src` left unmutated (pop refused + warning logged) | `test_should_refuse_pop_with_ambiguous_jmespath_filter` |
879
+ | `{'user': {'name': 'Alice'}}` | `['user.nickname', 'user.name']` (fallback) | `str` | *(unset)* | *(unset)* | -- | -- | `False` | `'Alice'` (first candidate wins) | `test_should_use_first_matching_path_in_fallback_list` |
880
+ | `{'user': {'name': 'Alice'}}` | `['user.nickname', 'user.alias']` (fallback, none match) | `str` | *(unset)* | *(unset)* | -- | -- | `False` | `None` | `test_should_return_none_when_no_fallback_path_matches` |
881
+ | `{'user': {'name': 'Alice', 'nickname': 'Ali'}}` | `['user.nickname', 'user.name']` (fallback) | `str` | *(unset)* | *(unset)* | -- | -- | `True` | `'Ali'`, only the winning candidate's key removed | `test_should_pop_only_the_winning_fallback_path` |
882
+
823
883
  #### `dig_many(src, paths, **common_kwargs)`
824
884
 
825
885
  Extract several named fields from `src` in one call.
@@ -854,7 +914,9 @@ fields = dig_many(
854
914
 
855
915
  A pre-bound, reusable `dig()` call. Validates/compiles the jmespath expression once at
856
916
  construction (failing fast on a bad expression instead of on first use), then can be called
857
- like a function against any number of `src` objects without repeating `path`/`exp`/kwargs.
917
+ like a function against any number of `src` objects without repeating `path`/`exp`/kwargs. A
918
+ string `pattern=` kwarg is likewise pre-compiled to a `re.Pattern` at construction time, so
919
+ repeated calls don't re-compile the regex.
858
920
 
859
921
  **Example:**
860
922
 
@@ -6,7 +6,10 @@ from typing import Callable
6
6
 
7
7
  # pylint: disable=invalid-name
8
8
  DEFAULT_CONVERTER: Callable = lambda value, *args, **kwargs: value
9
- DEFAULT_VALIDATOR: Callable = lambda value, expected_type, *args, **kwargs: isinstance(value, expected_type)
9
+ DEFAULT_VALIDATOR: Callable = lambda value, expected_type, *args, **kwargs: (
10
+ expected_type is None
11
+ or (isinstance(expected_type, (type, tuple)) and isinstance(value, expected_type))
12
+ )
10
13
  DEFAULT_SEPARATOR: str = ','
11
14
 
12
15
 
@@ -104,12 +104,13 @@ def get_datetime(
104
104
  return result
105
105
 
106
106
 
107
- def validate(
107
+ def validate( # pylint: disable=too-many-arguments,too-many-positional-arguments
108
108
  v: Any,
109
109
  exp: Type[T] = None,
110
110
  default: T | None = None,
111
111
  converter: Callable = None,
112
112
  validator: Callable | None = DEFAULT_VALIDATOR,
113
+ pattern: str | re.Pattern | None = None,
113
114
  **kwargs
114
115
  ) -> T | None:
115
116
  """Validate and optionally convert a value with a custom converter and validator.
@@ -117,11 +118,13 @@ def validate(
117
118
  This is a generic validation utility that:
118
119
  1. Converts the value using a converter function (with automatic selection for common types)
119
120
  2. Validates the result using a validator function
120
- 3. Returns the result if valid, otherwise returns the default value
121
+ 3. Optionally checks the result against a regex pattern (str results only)
122
+ 4. Returns the result if valid, otherwise returns the default value
121
123
 
122
124
  Args:
123
125
  v: The value to validate.
124
- exp: Expected type of the value. If None, no type conversion is attempted.
126
+ exp: Expected type of the value. If None, no type conversion or isinstance check is
127
+ attempted (DEFAULT_VALIDATOR passes any value through unchanged).
125
128
  default: The value to return if validation fails or v is None. Default: None.
126
129
  converter: Custom converter function. If None, one is selected based on exp:
127
130
  - exp=list: uses str_to_list
@@ -129,6 +132,9 @@ def validate(
129
132
  - otherwise: uses DEFAULT_CONVERTER (pass-through)
130
133
  validator: Custom validator function(result, exp, **kwargs) -> bool.
131
134
  If None, uses DEFAULT_VALIDATOR (isinstance check).
135
+ pattern: Optional regex (string or compiled re.Pattern) the result must fully match
136
+ via re.fullmatch. Only applied when the (converted, validated) result is a str;
137
+ checked after conversion/validation succeed. On mismatch, returns default.
132
138
  **kwargs: Additional arguments passed to converter and validator functions.
133
139
 
134
140
  Returns:
@@ -142,6 +148,8 @@ def validate(
142
148
  0
143
149
  >>> validate('a,b,c', exp=list)
144
150
  ['a', 'b', 'c']
151
+ >>> validate('AA:BB:CC:DD:EE:FF', exp=str, pattern=r'([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}')
152
+ 'AA:BB:CC:DD:EE:FF'
145
153
  """
146
154
  result: Any = v
147
155
  is_valid_result: bool = False
@@ -173,6 +181,11 @@ def validate(
173
181
  except Exception as exc_validation:
174
182
  logger.debug(f'Failed validation: {validator=}: {exc_validation=}', exc_info=True)
175
183
 
184
+ if is_valid_result and pattern is not None and isinstance(result, str):
185
+ compiled_pattern = pattern if isinstance(pattern, re.Pattern) else re.compile(pattern)
186
+ if not compiled_pattern.fullmatch(result):
187
+ is_valid_result = False
188
+
176
189
  return result if is_valid_result else default
177
190
 
178
191
 
@@ -192,7 +205,13 @@ _JMESPATH_TERMINAL_RE = re.compile(
192
205
 
193
206
  @lru_cache(maxsize=256)
194
207
  def _compile_jmespath(expr: str) -> Any:
195
- """Compile (and cache) a jmespath expression string."""
208
+ """Compile (and cache) a jmespath expression string.
209
+
210
+ This cache is process-global and shared by every dig()/Digger call, keyed on the
211
+ expression string -- a bare dig() call already avoids recompiling a repeated path, not
212
+ just Digger. Capped at 256 distinct expressions (LRU-evicted); Digger's up-front compile
213
+ call at construction populates this same cache rather than maintaining its own.
214
+ """
196
215
  return jmespath.compile(expr)
197
216
 
198
217
 
@@ -337,10 +356,13 @@ def dig(
337
356
  else:
338
357
  result = _dig_expr(src, _path_to_expr(path), pop)
339
358
 
340
- # Apply post-processor if result was found (not None) or if no result
341
- if callable(post_processor) and result is not None:
359
+ # Always run the post-processor -- even on a missing/None result -- so it can apply
360
+ # its own default handling (validate() already treats v=None as failing an isinstance
361
+ # check and falls back to kwargs['default']). Skipping post_processor entirely is
362
+ # reserved for the explicit post_processor=None "give me the raw value" case.
363
+ if callable(post_processor):
342
364
  return post_processor(result, **kwargs)
343
- return result # return found value without post-processing
365
+ return result
344
366
 
345
367
 
346
368
  def dig_many(
@@ -382,7 +404,8 @@ class Digger:
382
404
 
383
405
  Validates/compiles the path up front (failing fast on a bad jmespath expression
384
406
  instead of on first use) and avoids re-passing the same path/exp/post_processor/kwargs
385
- on every call.
407
+ on every call. A string `pattern=` kwarg is likewise pre-compiled to a re.Pattern here
408
+ so repeated calls don't re-compile the regex on every invocation.
386
409
 
387
410
  Example:
388
411
  >>> get_name = Digger(path='user.name', exp=str, default='')
@@ -399,6 +422,8 @@ class Digger:
399
422
  self.path = path
400
423
  self.pop = pop
401
424
  self.post_processor = post_processor
425
+ if isinstance(kwargs.get('pattern'), str):
426
+ kwargs['pattern'] = re.compile(kwargs['pattern'])
402
427
  self.kwargs = kwargs
403
428
  for candidate in (path if isinstance(path, (list, tuple)) else [path]):
404
429
  if candidate is not None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oj-toolkit
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Centralized shared utilities for ownjoo-org projects: standardized logging config, type validation, data parsing, console output, and async utilities
5
5
  Author-email: Speedy <speedy@ownjoo.org>
6
6
  License: MIT
@@ -33,7 +33,7 @@ Requires-Dist: mypy>=1.0; extra == "dev"
33
33
  Requires-Dist: pylint>=2.0; extra == "dev"
34
34
  Dynamic: license-file
35
35
 
36
- # ownjoo-toolkit
36
+ # oj-toolkit
37
37
 
38
38
  Centralized shared utilities library for all ownjoo-org projects. Provides battle-tested functions for type validation, data parsing, and progress logging.
39
39
 
@@ -187,11 +187,19 @@ fields = dig_many(
187
187
  # Bind a path once, reuse it against many objects
188
188
  get_name = Digger(path='name', exp=str, default='')
189
189
  [get_name(user) for user in data['users']] # Returns: ['Alice', 'Bob']
190
+
191
+ # Constrain a string result with a regex (checked via re.fullmatch after conversion)
192
+ mac = dig({'device': {'mac': 'AA:BB:CC:DD:EE:FF'}}, path='device.mac', exp=str,
193
+ pattern=r'([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}')
194
+ # Returns: 'AA:BB:CC:DD:EE:FF'
190
195
  ```
191
196
 
192
- > **Note:** `dig()`'s default `post_processor` (`validate()`) only accepts a value if you pass
193
- > `exp=<type>` -- without it, validation always fails and you get `default` back (`None` unless set).
194
- > Pass `exp=` for a real value, or `post_processor=None` to skip validation and get the raw match.
197
+ > **Note:** if the path resolves to nothing (missing key/index, or `src` doesn't contain it),
198
+ > `dig()`'s default `post_processor` (`validate()`) still runs and returns `default` (`None`
199
+ > unless set) -- a missing path never silently drops `default` in favor of a bare `None`.
200
+ > Passing no `exp=` means "accept any type, no isinstance check" -- not "always fail". Pass
201
+ > `exp=<type>` to add a type check, or `post_processor=None` to bypass validation entirely and
202
+ > get the raw match untouched.
195
203
 
196
204
  ### Logging Setup
197
205
 
@@ -713,16 +721,17 @@ Returns `True` if the terminal likely supports Unicode characters. Used internal
713
721
 
714
722
  ### `parsing` Module
715
723
 
716
- #### `validate(v, exp=None, default=None, converter=None, validator=None, **kwargs)`
724
+ #### `validate(v, exp=None, default=None, converter=None, validator=None, pattern=None, **kwargs)`
717
725
 
718
726
  Generic validation utility that converts and validates a value.
719
727
 
720
728
  - **Parameters:**
721
729
  - `v` (Any): The value to validate
722
- - `exp` (Type): Expected type. Enables automatic converter selection (list → str_to_list, datetime → get_datetime)
723
- - `default` (Any): Return this if validation fails. Default: None
724
- - `converter` (Callable): Custom converter function. Default: auto-selected based on exp
725
- - `validator` (Callable): Custom validator function(result, exp, **kwargs) → bool. Default: isinstance check
730
+ - `exp` (Type): Expected type. Enables automatic converter selection (list → str_to_list, datetime → get_datetime). If `None`, no isinstance check is performed -- any type passes (see `validator` below)
731
+ - `default` (Any): Return this if validation fails, or `v` is missing/`None`. Default: None
732
+ - `converter` (Callable): Custom converter function. Default: auto-selected based on exp. Auto-selection is limited to `list`/`datetime` -- `bool`/`int`/`float`/etc. are pass-through unless you supply your own `converter=`
733
+ - `validator` (Callable): Custom validator function(result, exp, **kwargs) → bool. Default: isinstance check (`exp=None` always passes; a non-type `exp` is rejected rather than raising)
734
+ - `pattern` (str | re.Pattern): Optional regex checked via `re.fullmatch` against the result, but only when the result is a `str` -- ignored for other types. Applied after conversion/validation succeed; a mismatch falls back to `default`
726
735
  - `**kwargs`: Passed to converter and validator
727
736
 
728
737
  - **Returns:** Converted and validated value, or default if validation fails
@@ -734,6 +743,16 @@ Generic validation utility that converts and validates a value.
734
743
  numbers = validate('1,2,3', exp=list) # Returns: ['1', '2', '3']
735
744
  dt = validate('2024-01-15T10:30:00', exp=datetime) # Returns: datetime object
736
745
 
746
+ # exp=bool/int/float do NOT auto-coerce strings -- pass converter= explicitly for that
747
+ validate('42', exp=int) # Returns: None ('42' is a str, not an int)
748
+ validate('42', exp=int, converter=int) # Returns: 42
749
+
750
+ # Regex-constrain a string result
751
+ validate('AA:BB:CC:DD:EE:FF', exp=str, pattern=r'([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}')
752
+ # Returns: 'AA:BB:CC:DD:EE:FF'
753
+ validate('not-a-mac', exp=str, pattern=r'([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}', default='invalid')
754
+ # Returns: 'invalid'
755
+
737
756
  # Custom converter and validator
738
757
  def to_upper(v, *args, **kwargs):
739
758
  return str(v).upper()
@@ -795,6 +814,12 @@ get_datetime('01/15/2024 10:30:00', format_str='%m/%d/%Y %H:%M:%S') # Custom
795
814
  Extract and post-process a value from a nested data structure, using [jmespath](https://jmespath.org/)
796
815
  under the hood (`jmespath` is a required dependency).
797
816
 
817
+ > **Compilation caching:** compiled jmespath expressions are cached in a process-global
818
+ > `lru_cache(maxsize=256)`, keyed by the expression string. A bare `dig()` call already avoids
819
+ > recompiling a repeated path -- this isn't a `Digger`-only optimization. The cache holds up to
820
+ > 256 distinct expressions across the whole process (shared by every `dig()`/`Digger` caller),
821
+ > LRU-evicted beyond that.
822
+
798
823
  - **Parameters:**
799
824
  - `src` (Mapping | Sequence): Data structure to navigate
800
825
  - `path` (int | str | list | None):
@@ -811,16 +836,18 @@ under the hood (`jmespath` is a required dependency).
811
836
  Pass `None` to skip post-processing and get the raw match.
812
837
  - `**kwargs`: Passed to `post_processor`
813
838
 
814
- - **Returns:** Post-processed value, or `None` if extraction fails, no candidate path matches, or
815
- no post-processor is specified and nothing was found
839
+ - **Returns:** Post-processed value. If extraction fails or no candidate path matches, the
840
+ post-processor (default `validate()`) still runs and returns `default` (`None` unless set) --
841
+ unless `post_processor=None`, in which case the raw (possibly missing/`None`) value is returned
842
+ untouched with no default substitution.
816
843
 
817
844
  > **Type checkers:** pass `exp=<type>` to get a precise `<type> | None` return type instead of `Any`
818
845
  > (`dig(data, path='...', exp=str)` type-checks as `str | None`). Passing `post_processor=None` types
819
846
  > as `Any` (raw, unvalidated value). A custom `post_processor` types as that callable's own return type.
820
847
 
821
- > **Passing no `exp`:** the default `validate()` post-processor only accepts a value when you give
822
- > it `exp=<type>` -- without one, validation always fails and you get `default` back (`None` unless
823
- > set). Either pass `exp=`, or `post_processor=None` to bypass validation entirely.
848
+ > **Passing no `exp`:** the default `validate()` post-processor treats a missing `exp` as "accept
849
+ > any type" -- it does not fail. Pass `exp=<type>` to add an isinstance check, or
850
+ > `post_processor=None` to bypass validation entirely and get the raw match.
824
851
 
825
852
  **Example:**
826
853
 
@@ -855,6 +882,39 @@ name = dig(data, path=['response.users[0].display_name', 'response.users[0].name
855
882
  # Returns: 'Alice'
856
883
  ```
857
884
 
885
+ ##### `dig()` Behavior Matrix
886
+
887
+ Every row below is a real `dig()` call, backed by a passing unit test in
888
+ `test/unit/parsing/test_parsing.py` -- the test name is the proof, not just a claim. This isn't
889
+ every possible combination of kwargs (that cross product is huge and mostly redundant); it's one
890
+ representative call per distinct decision point in `dig()`/`validate()`. `*(unset)*` means the
891
+ kwarg wasn't passed (the function's own default applies); `--` means it isn't relevant to that row.
892
+
893
+ | `src` | `path` | `exp` | `default` | `post_processor` | `pattern` | `validator` | `pop` | Result | Proven by |
894
+ |---|---|---|---|---|---|---|---|---|---|
895
+ | `{'first': 'a', 'second': ['blah']}` | `'second[0]'` (found) | `str` | *(unset)* | *(unset)* | *(unset)* | *(unset)* | `False` | `'blah'` | `test_should_get_value_from_dict` |
896
+ | `['', ['blah']]` | `'[1][0]'` (found) | `str` | `''` | *(unset)* | *(unset)* | *(unset)* | `False` | `'blah'` (default unused -- value was valid) | `test_should_get_value_from_list` |
897
+ | `{'a': {'b': 'value'}}` | `'x.y.z'` (missing) | `str` | `'default'` | *(unset)* | *(unset)* | *(unset)* | `False` | `'default'` | `test_should_return_default_on_missing_intermediate_key` |
898
+ | `{'items': ['a', 'b', 'c']}` | `'items.invalid_index'` (missing) | `str` | `'default'` | *(unset)* | *(unset)* | *(unset)* | `False` | `'default'` | `test_should_return_default_when_accessing_list_with_string_key` |
899
+ | `{'a': {'b': 'value'}}` | `'x.y.z'` (missing) | `str` | *(unset)* | *(unset)* | *(unset)* | *(unset)* | `False` | `None` (no default set) | `test_should_return_none_for_missing_path_with_exp_and_no_default` |
900
+ | `{'a': {'b': 'c'}}` | `'x.y.z'` (missing) | *(unset)* | *(unset)* | *(unset)* | *(unset)* | *(unset)* | `False` | `None` (`exp=None` means "anything passes"; the found value already is `None`) | `test_should_handle_get_value_with_invalid_path` |
901
+ | `{'key': 'blah'}` | `'key'` (found) | *(unset)* | *(unset)* | *(unset)* | *(unset)* | *(unset)* | `False` | `'blah'` (raw pass-through, no isinstance check) | `test_should_not_crash_when_exp_omitted_from_dig` |
902
+ | `{'a': 'not-an-int'}` | `'a'` (found, wrong type) | `int` | *(unset)* | *(unset)* | *(unset)* | *(unset)* | `False` | `None` | `test_should_return_none_for_dig_result_type_mismatch` |
903
+ | `{'a': 'not-an-int'}` | `'a'` (found, wrong type) | `int` | `-1` | *(unset)* | *(unset)* | *(unset)* | `False` | `-1` | `test_should_return_default_for_dig_result_type_mismatch` |
904
+ | `{'a': 'blah'}` | `'a'` (found, non-type `exp`) | `'str'` | `'fallback'` | *(unset)* | *(unset)* | *(unset)* | `False` | `'fallback'` (garbage `exp` rejected, not raised) | `test_should_return_default_for_dig_with_non_type_expected_type` |
905
+ | `{'key': 'raw_value'}` | `'key'` (found) | -- | -- | `None` | -- | -- | `False` | raw value, unvalidated | `test_should_return_raw_value_when_no_post_processor` |
906
+ | `{'a': 1}` | `'missing'` (missing) | -- | `'default'` (ignored) | `None` | -- | -- | `False` | `None` (`default` is never consulted -- it's `validate()`'s kwarg, and `validate()` never runs) | `test_should_return_none_for_missing_path_with_no_post_processor` |
907
+ | `{'response': {'users': [{'name': 'Alice'}, {'name': 'Bob'}]}}` | `'response.users'` (found) | -- | -- | `len` | -- | -- | `False` | `2` (custom post_processor gets the raw match) | `test_should_use_custom_post_processor_function` |
908
+ | `'blah'` | `None` (src is the value) | `str` | *(unset)* | *(unset)* | *(unset)* | custom lambda | `False` | `'blah'` (custom validator overrides isinstance check) | `test_should_get_value_with_passed_validator` |
909
+ | `'test_string'` | `None` (src is the value) | `str` | *(unset)* | *(unset)* | *(unset)* | *(unset)* | `False` | the src value itself | `test_should_post_process_source_when_path_is_none` |
910
+ | `{'device': {'mac': 'AA:BB:CC:DD:EE:FF'}}` | `'device.mac'` (found) | `str` | *(unset)* | *(unset)* | matching regex | *(unset)* | `False` | matched MAC string | `test_should_validate_dig_result_with_matching_pattern` |
911
+ | `{'device': {'mac': 'not-a-mac'}}` | `'device.mac'` (found) | `str` | `'invalid'` | *(unset)* | non-matching regex | *(unset)* | `False` | `'invalid'` | `test_should_return_default_for_dig_result_with_non_matching_pattern` |
912
+ | `{'a': 1, 'b': 2}` | `'a'` (found) | `int` | *(unset)* | *(unset)* | -- | -- | `True` | `1`, and `'a'` removed from `src` | `test_should_pop_key_from_dict` |
913
+ | `{'items': [{'id': 1}, {'id': 2}]}` | `'items[?id==\`2\`].id'` (found, ambiguous) | `list` | *(unset)* | *(unset)* | -- | -- | `True` (refused) | `[2]`, `src` left unmutated (pop refused + warning logged) | `test_should_refuse_pop_with_ambiguous_jmespath_filter` |
914
+ | `{'user': {'name': 'Alice'}}` | `['user.nickname', 'user.name']` (fallback) | `str` | *(unset)* | *(unset)* | -- | -- | `False` | `'Alice'` (first candidate wins) | `test_should_use_first_matching_path_in_fallback_list` |
915
+ | `{'user': {'name': 'Alice'}}` | `['user.nickname', 'user.alias']` (fallback, none match) | `str` | *(unset)* | *(unset)* | -- | -- | `False` | `None` | `test_should_return_none_when_no_fallback_path_matches` |
916
+ | `{'user': {'name': 'Alice', 'nickname': 'Ali'}}` | `['user.nickname', 'user.name']` (fallback) | `str` | *(unset)* | *(unset)* | -- | -- | `True` | `'Ali'`, only the winning candidate's key removed | `test_should_pop_only_the_winning_fallback_path` |
917
+
858
918
  #### `dig_many(src, paths, **common_kwargs)`
859
919
 
860
920
  Extract several named fields from `src` in one call.
@@ -889,7 +949,9 @@ fields = dig_many(
889
949
 
890
950
  A pre-bound, reusable `dig()` call. Validates/compiles the jmespath expression once at
891
951
  construction (failing fast on a bad expression instead of on first use), then can be called
892
- like a function against any number of `src` objects without repeating `path`/`exp`/kwargs.
952
+ like a function against any number of `src` objects without repeating `path`/`exp`/kwargs. A
953
+ string `pattern=` kwarg is likewise pre-compiled to a `re.Pattern` at construction time, so
954
+ repeated calls don't re-compile the regex.
893
955
 
894
956
  **Example:**
895
957
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "oj-toolkit"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "Centralized shared utilities for ownjoo-org projects: standardized logging config, type validation, data parsing, console output, and async utilities"
9
9
  authors = [
10
10
  { name = "Speedy", email = "speedy@ownjoo.org" }
File without changes
File without changes