mappingtools 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,226 @@
1
+ Metadata-Version: 2.3
2
+ Name: mappingtools
3
+ Version: 0.0.1
4
+ Summary: mappingtools. Do stuff with Mappings
5
+ Project-URL: Homepage, https://erivlis.github.io/mappingtools
6
+ Project-URL: Bug Tracker, https://github.com/erivlis/mappingtools/issues
7
+ Project-URL: Source, https://github.com/erivlis/mappingtools
8
+ Author-email: Eran Rivlis <eran@rivlis.info>
9
+ License-File: LICENSE
10
+ Keywords: Mapping,manipulate,mutate,transform
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Information Technology
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Natural Language :: English
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: Implementation :: CPython
23
+ Classifier: Topic :: Software Development :: Libraries
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.10
26
+ Provides-Extra: dev
27
+ Requires-Dist: ruff; extra == 'dev'
28
+ Provides-Extra: docs
29
+ Requires-Dist: mkdocs-gen-files; extra == 'docs'
30
+ Requires-Dist: mkdocs-git-revision-date-localized-plugin; extra == 'docs'
31
+ Requires-Dist: mkdocs-glightbox; extra == 'docs'
32
+ Requires-Dist: mkdocs-literate-nav; extra == 'docs'
33
+ Requires-Dist: mkdocs-material; extra == 'docs'
34
+ Requires-Dist: mkdocs-section-index; extra == 'docs'
35
+ Requires-Dist: mkdocstrings-python; extra == 'docs'
36
+ Provides-Extra: test
37
+ Requires-Dist: pytest; extra == 'test'
38
+ Requires-Dist: pytest-cov; extra == 'test'
39
+ Requires-Dist: pytest-randomly; extra == 'test'
40
+ Requires-Dist: pytest-xdist; extra == 'test'
41
+ Description-Content-Type: text/markdown
42
+
43
+ # MappingTools
44
+
45
+ > This library provides utility functions for manipulating and transforming data structures which have or include
46
+ > Mapping-like characteristics.
47
+ > Including inverting dictionaries, converting class like objects to dictionaries, creating nested defaultdicts,
48
+ > and unwrapping complex objects.
49
+
50
+ <table>
51
+ <tr style="vertical-align: middle;">
52
+ <td>Package</td>
53
+ <td>
54
+ <img alt="PyPI - version" src="https://img.shields.io/pypi/v/mappingtools">
55
+ <img alt="PyPI - Status" src="https://img.shields.io/pypi/status/mappingtools">
56
+ <img alt="PyPI - Python Version" src="https://img.shields.io/pypi/pyversions/mappingtools">
57
+ <img alt="PyPI - Downloads" src="https://img.shields.io/pypi/dd/mappingtools">
58
+ <br>
59
+ <img alt="GitHub" src="https://img.shields.io/github/license/erivlis/mappingtools">
60
+ <img alt="GitHub repo size" src="https://img.shields.io/github/repo-size/erivlis/mappingtools">
61
+ <img alt="GitHub last commit (by committer)" src="https://img.shields.io/github/last-commit/erivlis/mappingtools">
62
+ <a href="https://github.com/erivlis/mappingtools/graphs/contributors"><img alt="Contributors" src="https://img.shields.io/github/contributors/erivlis/mappingtools.svg"></a>
63
+ </td>
64
+ </tr>
65
+ <tr>
66
+ <td>Tools</td>
67
+ <td>
68
+ <a href="https://www.jetbrains.com/pycharm/"><img alt="PyCharm" src="https://img.shields.io/badge/PyCharm-FCF84A.svg?logo=PyCharm&logoColor=black&labelColor=21D789&color=FCF84A"></a>
69
+ <a href="https://github.com/astral-sh/ruff"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json" alt="Ruff" style="max-width:100%;"></a>
70
+ <a href="https://github.com/astral-sh/uv"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json" alt="uv" style="max-width:100%;"></a>
71
+ <a href="https://squidfunk.github.io/mkdocs-material/"><img src="https://img.shields.io/badge/Material_for_MkDocs-526CFE?&logo=MaterialForMkDocs&logoColor=white&labelColor=grey"></a>
72
+ </td>
73
+ </tr>
74
+ <tr>
75
+ <td>CI/CD</td>
76
+ <td>
77
+ <a href="https://github.com/erivlis/mappingtools/actions/workflows/test.yml"><img alt="Tests" src="https://github.com/erivlis/mappingtools/actions/workflows/test.yml/badge.svg?branch=main"></a>
78
+ <a href="https://github.com/erivlis/mappingtools/actions/workflows/publish.yml"><img alt="Publish" src="https://github.com/erivlis/mappingtools/actions/workflows/publish.yml/badge.svg"></a>
79
+ <a href="https://github.com/erivlis/mappingtools/actions/workflows/publish-docs.yaml"><img alt="Publish Docs" src="https://github.com/erivlis/mappingtools/actions/workflows/publish-docs.yaml/badge.svg"></a>
80
+ </td>
81
+ </tr>
82
+ <tr>
83
+ <td>Scans</td>
84
+ <td>
85
+ <a href="https://codecov.io/gh/erivlis/mappingtools"><img alt="Coverage" src="https://codecov.io/gh/erivlis/mappingtools/graph/badge.svg?token=POODT8M9NV"/></a>
86
+ <br>
87
+ <a href="https://sonarcloud.io/summary/new_code?id=erivlis_mappingtools"><img alt="Quality Gate Status" src="https://sonarcloud.io/api/project_badges/measure?project=erivlis_mappingtools&metric=alert_status"></a>
88
+ <a href="https://sonarcloud.io/summary/new_code?id=erivlis_mappingtools"><img alt="Security Rating" src="https://sonarcloud.io/api/project_badges/measure?project=erivlis_mappingtools&metric=security_rating"></a>
89
+ <a href="https://sonarcloud.io/summary/new_code?id=erivlis_mappingtools"><img alt="Maintainability Rating" src="https://sonarcloud.io/api/project_badges/measure?project=erivlis_mappingtools&metric=sqale_rating"></a>
90
+ <a href="https://sonarcloud.io/summary/new_code?id=erivlis_mappingtools"><img alt="Reliability Rating" src="https://sonarcloud.io/api/project_badges/measure?project=erivlis_mappingtools&metric=reliability_rating"></a>
91
+ <br>
92
+ <a href="https://sonarcloud.io/summary/new_code?id=erivlis_mappingtools"><img alt="Lines of Code" src="https://sonarcloud.io/api/project_badges/measure?project=erivlis_mappingtools&metric=ncloc"></a>
93
+ <a href="https://sonarcloud.io/summary/new_code?id=erivlis_mappingtools"><img alt="Vulnerabilities" src="https://sonarcloud.io/api/project_badges/measure?project=erivlis_mappingtools&metric=vulnerabilities"></a>
94
+ <a href="https://sonarcloud.io/summary/new_code?id=erivlis_mappingtools"><img alt="Bugs" src="https://sonarcloud.io/api/project_badges/measure?project=erivlis_mappingtools&metric=bugs"></a>
95
+ <br>
96
+ <a href="https://app.codacy.com/gh/erivlis/mappingtools/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade"><img alt="Codacy Badge" src="https://app.codacy.com/project/badge/Grade/8b83a99f939b4883ae2f37d7ec3419d1"></a>
97
+ </td>
98
+ </tr>
99
+ </table>
100
+
101
+ ## Usage
102
+
103
+ ### `dictify`
104
+
105
+ Converts objects to dictionaries using handlers for mappings, iterables, and classes.
106
+
107
+ ```python
108
+ from mappingtools import dictify
109
+
110
+ data = {'key1': 'value1', 'key2': ['item1', 'item2']}
111
+ dictified_data = dictify(data)
112
+ print(dictified_data)
113
+ # Output: {'key1': 'value1', 'key2': ['item1', 'item2']}
114
+ ```
115
+
116
+ ### `distinct`
117
+
118
+ Yields distinct values for a specified key across multiple mappings.
119
+
120
+ ```python
121
+ from mappingtools import distinct
122
+
123
+ mappings = [
124
+ {'a': 1, 'b': 2},
125
+ {'a': 2, 'b': 3},
126
+ {'a': 1, 'b': 4}
127
+ ]
128
+ distinct_values = list(distinct('a', *mappings))
129
+ print(distinct_values)
130
+ # Output: [1, 2]
131
+ ```
132
+
133
+ ### `keep`
134
+
135
+ Yields subsets of mappings by retaining only the specified keys.
136
+
137
+ ```python
138
+ from mappingtools import keep
139
+
140
+ mappings = [
141
+ {'a': 1, 'b': 2, 'c': 3},
142
+ {'a': 4, 'b': 5, 'd': 6}
143
+ ]
144
+ keys_to_keep = ['a', 'b']
145
+ result = list(keep(keys_to_keep, *mappings))
146
+ # result: [{'a': 1, 'b': 2}, {'a': 4, 'b': 5}]
147
+ ```
148
+
149
+ ### `inverse`
150
+
151
+ Swaps keys and values in a dictionary.
152
+
153
+ ```python
154
+ from mappingtools import inverse
155
+
156
+ original_mapping = {'a': {1, 2}, 'b': {3}}
157
+ inverted_mapping = inverse(original_mapping)
158
+ print(inverted_mapping)
159
+ # Output: {1: 'a', 2: 'a', 3: 'b'}
160
+ ```
161
+
162
+ ### `nested_defaultdict`
163
+
164
+ Creates a nested defaultdict with specified depth and factory.
165
+
166
+ ```python
167
+ from mappingtools import nested_defaultdict
168
+
169
+ nested_dd = nested_defaultdict(2, list)
170
+ nested_dd[0][1].append('value')
171
+ print(nested_dd)
172
+ # Output: defaultdict(<function nested_defaultdict.<locals>.factory at ...>, {0: defaultdict(<function nested_defaultdict.<locals>.factory at ...>, {1: ['value']})})
173
+ ```
174
+
175
+ ### `remove`
176
+
177
+ Yields mappings with specified keys removed. It takes an iterable of keys and multiple mappings, and returns a generator
178
+ of mappings with those keys excluded.
179
+
180
+ ```python
181
+ from mappingtools import remove
182
+
183
+ mappings = [
184
+ {'a': 1, 'b': 2, 'c': 3},
185
+ {'a': 4, 'b': 5, 'd': 6}
186
+ ]
187
+ keys_to_remove = ['a', 'b']
188
+ result = list(remove(keys_to_remove, *mappings))
189
+ # result: [{'c': 3}, {'d': 6}]
190
+
191
+ ```
192
+
193
+ ### `unwrap`
194
+
195
+ Transforms complex objects into a list of dictionaries with key and value pairs.
196
+
197
+ ```python
198
+ from mappingtools import unwrap
199
+
200
+ wrapped_data = {'key1': {'subkey': 'value'}, 'key2': ['item1', 'item2']}
201
+ unwrapped_data = unwrap(wrapped_data)
202
+ print(unwrapped_data)
203
+ # Output: [{'key': 'key1', 'value': [{'key': 'subkey', 'value': 'value'}]}, {'key': 'key2', 'value': ['item1', 'item2']}]
204
+ ```
205
+
206
+ ## Development
207
+
208
+ ### Ruff
209
+
210
+ ```shell
211
+ ruff check src
212
+ ```
213
+
214
+ ### Test
215
+
216
+ #### Standard (cobertura) XML Coverage Report
217
+
218
+ ```shell
219
+ python -m pytest tests -n auto --cov=src --cov-branch --doctest-modules --cov-report=xml --junitxml=test_results.xml
220
+ ```
221
+
222
+ #### HTML Coverage Report
223
+
224
+ ```shell
225
+ python -m pytest tests -n auto --cov=src --cov-branch --doctest-modules --cov-report=html --junitxml=test_results.xml
226
+ ```
@@ -0,0 +1,5 @@
1
+ mappingtools.py,sha256=I7O2lKPXVW3D7spmfPJRH5rw0Bh21Al4HScaeX0TlCE,6973
2
+ mappingtools-0.0.1.dist-info/METADATA,sha256=mNuu4Re_ncNPgnnwt5Nw52R0lGhpce5IPqTxirbHDkM,9418
3
+ mappingtools-0.0.1.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
4
+ mappingtools-0.0.1.dist-info/licenses/LICENSE,sha256=fiCxD4qmBY6VdONaK43ANsvpmf9oxSpFLHaFaij0Jx4,1068
5
+ mappingtools-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.25.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Eran Rivlis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
mappingtools.py ADDED
@@ -0,0 +1,197 @@
1
+ import dataclasses
2
+ import inspect
3
+ from collections import defaultdict
4
+ from collections.abc import Callable, Generator, Iterable, Mapping
5
+ from itertools import chain
6
+ from typing import Any, TypeVar
7
+
8
+ K = TypeVar("K")
9
+
10
+
11
+ def _take(keys: Iterable[K], mapping: Mapping[K, Any], exclude: bool = False) -> dict[K, Any]:
12
+ if not isinstance(mapping, Mapping):
13
+ raise TypeError(f"Parameter 'mapping' should be of type 'Mapping', but instead is type '{type(mapping)}'")
14
+
15
+ mapping_keys = set(mapping.keys())
16
+ keys = set(keys) & mapping_keys # intersection with keys to get actual existing keys
17
+ if exclude:
18
+ keys = mapping_keys - keys
19
+
20
+ return {k: mapping.get(k) for k in keys}
21
+
22
+
23
+ def distinct(key: K, *mappings: Mapping[K, Any]) -> Generator[Any, Any, None]:
24
+ """
25
+ Yield distinct values for the specified key across multiple mappings.
26
+
27
+ Args:
28
+ key (K): The key to extract distinct values from the mappings.
29
+ *mappings (Mapping[K, Any]): Variable number of mappings to search for distinct values.
30
+
31
+ Yields:
32
+ Generator[K, Any, None]: A generator of distinct values extracted from the mappings.
33
+ """
34
+ distinct_value_type_pairs = set()
35
+ for mapping in mappings:
36
+ value = mapping.get(key, )
37
+ value_type_pair = (value, type(value))
38
+ if key in mapping and value_type_pair not in distinct_value_type_pairs:
39
+ distinct_value_type_pairs.add(value_type_pair)
40
+ yield value
41
+
42
+
43
+ def keep(keys: Iterable[K], *mappings: Mapping[K, Any]) -> Generator[Mapping[K, Any], Any, None]:
44
+ """
45
+ Yield a subset of mappings by keeping only the specified keys.
46
+
47
+ Args:
48
+ keys (Iterable[K]): The keys to keep in the mappings.
49
+ *mappings (Mapping[K, Any]): Variable number of mappings to filter.
50
+
51
+ Yields:
52
+ Generator[Mapping[K, Any], Any, None]: A generator of mappings with only the specified keys.
53
+ """
54
+ yield from (_take(keys, mapping) for mapping in mappings)
55
+
56
+
57
+ def remove(keys: Iterable[K], *mappings: Mapping[K, Any]) -> Generator[Mapping[K, Any], Any, None]:
58
+ """
59
+ Yield a subset of mappings by removing the specified keys.
60
+
61
+ Args:
62
+ keys (Iterable[K]): The keys to remove from the mappings.
63
+ *mappings (Mapping[K, Any]): Variable number of mappings to filter.
64
+
65
+ Yields:
66
+ Generator[Mapping[K, Any], Any, None]: A generator of mappings with specified keys removed.
67
+ """
68
+ yield from (_take(keys, mapping, exclude=True) for mapping in mappings)
69
+
70
+
71
+ def inverse(mapping: Mapping[Any, set]) -> Mapping[Any, set]:
72
+ """Return a new dictionary with keys and values swapped from the input mapping.
73
+
74
+ Args:
75
+ mapping (Mapping[Any, set]): The input mapping to invert.
76
+
77
+ Returns:
78
+ Mapping: A new Mapping with values as keys and keys as values.
79
+ """
80
+ items = chain.from_iterable(((vi, k) for vi in v) for k, v in mapping.items())
81
+ dd = defaultdict(set)
82
+ for k, v in items:
83
+ dd[k].add(v)
84
+
85
+ return dd
86
+
87
+
88
+ def _is_strict_iterable(obj: Iterable) -> bool:
89
+ return isinstance(obj, Iterable) and not isinstance(obj, str | bytes | bytearray)
90
+
91
+
92
+ def _is_class_instance(obj) -> bool:
93
+ return (dataclasses.is_dataclass(obj) and not isinstance(obj, type)) or hasattr(obj, '__dict__')
94
+
95
+
96
+ def _process_obj(obj: Any,
97
+ mapping_handler: Callable | None = None,
98
+ iterable_handler: Callable | None = None,
99
+ class_handler: Callable | None = None,
100
+ *args,
101
+ **kwargs):
102
+ if callable(mapping_handler) and isinstance(obj, Mapping):
103
+ return mapping_handler(obj, *args, **kwargs)
104
+ elif callable(iterable_handler) and _is_strict_iterable(obj):
105
+ return iterable_handler(obj, *args, **kwargs)
106
+ elif callable(class_handler) and _is_class_instance(obj):
107
+ return class_handler(obj, *args, **kwargs)
108
+ else:
109
+ return obj
110
+
111
+
112
+ def dictify(obj, key_converter: Callable[[Any], str] | None = None):
113
+ """Dictify an object using a specified key converter.
114
+
115
+ Args:
116
+ obj (Any): The object to be dictified.
117
+ key_converter (Optional[Callable[[Any], str]], optional): A function to convert keys. Defaults to None.
118
+
119
+ Returns:
120
+ The dictified object.
121
+ """
122
+ return _process_obj(obj, _dictify_mapping, _dictify_iterable, _dictify_class, key_converter=key_converter)
123
+
124
+
125
+ def _dictify_mapping(obj, key_converter: Callable[[Any], str] | None = None) -> dict:
126
+ return {(key_converter(k) if key_converter else k): dictify(v, key_converter) for k, v in obj.items()}
127
+
128
+
129
+ def _dictify_iterable(obj, key_converter: Callable[[Any], str] | None = None) -> list:
130
+ return [dictify(v, key_converter) for v in obj]
131
+
132
+
133
+ def _dictify_class(obj, key_converter: Callable[[Any], str] | None = None) -> dict | str:
134
+ return {
135
+ (key_converter(k) if key_converter else k): dictify(v, key_converter)
136
+ for k, v in inspect.getmembers(obj)
137
+ if not k.startswith('_')
138
+ }
139
+
140
+
141
+ def nested_defaultdict(nesting_depth: int = 0, default_factory: Callable | None = None, **kwargs) -> defaultdict:
142
+ """Return a nested defaultdict with the specified nesting depth and default factory.
143
+ A nested_defaultdict with nesting_depth=0 is equivalent to builtin 'collections.defaultdict'.
144
+ Each nesting_depth increment effectively adds an additional item accessor.
145
+
146
+ Args:
147
+ nesting_depth (int): The depth of nesting for the defaultdict (default is 0);
148
+ default_factory (Callable): The default factory function for the defaultdict (default is None).
149
+ **kwargs: Additional keyword arguments to initialize the most nested defaultdict.
150
+
151
+ Returns:
152
+ defaultdict: A nested defaultdict based on the specified parameters.
153
+ """
154
+
155
+ if nesting_depth < 0:
156
+ raise ValueError("'nesting_depth' must be zero or more.")
157
+
158
+ if default_factory is not None and not callable(default_factory):
159
+ raise TypeError("default_factory argument must be Callable or None")
160
+
161
+ def factory():
162
+ if nesting_depth > 0:
163
+ print(1)
164
+ return nested_defaultdict(nesting_depth=nesting_depth - 1, default_factory=default_factory, **kwargs)
165
+ else:
166
+ print(2)
167
+ return default_factory() if default_factory else None
168
+
169
+ return defaultdict(factory, **kwargs)
170
+
171
+
172
+ def unwrap(obj: Any):
173
+ """
174
+ Unwraps the given object.
175
+
176
+ Args:
177
+ obj (Any): The object to unwrap.
178
+
179
+ Returns:
180
+ Any: The unwrapped object.
181
+ """
182
+ return _process_obj(obj, _unwrap_mapping, _unwrap_iterable, _unwrap_class)
183
+
184
+
185
+ def _unwrap_mapping(obj: Mapping) -> list[dict]:
186
+ return [{'key': k, 'value': unwrap(v)} for k, v in obj.items()]
187
+
188
+
189
+ def _unwrap_iterable(obj: Iterable) -> list:
190
+ return [unwrap(v) for v in obj]
191
+
192
+
193
+ def _unwrap_class(obj):
194
+ return [{'key': k, 'value': unwrap(v)} for k, v in inspect.getmembers(obj) if not k.startswith('_')]
195
+
196
+
197
+ __all__ = ('dictify', 'distinct', 'keep', 'inverse', 'nested_defaultdict', 'remove', 'unwrap')