apytizer 0.0.1a0__py3-none-any.whl → 0.0.1b1__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.
- apytizer/__init__.py +2 -12
- apytizer/adapters/__init__.py +2 -3
- apytizer/adapters/transport_adapter.py +91 -0
- apytizer/apis/__init__.py +6 -0
- apytizer/apis/abstract_api.py +36 -0
- apytizer/apis/web_api.py +461 -0
- apytizer/connections/__init__.py +6 -0
- apytizer/connections/abstract_connection.py +28 -0
- apytizer/connections/http_connection.py +431 -0
- apytizer/decorators/__init__.py +5 -5
- apytizer/decorators/caching.py +60 -9
- apytizer/decorators/chunking.py +105 -0
- apytizer/decorators/connection.py +55 -20
- apytizer/decorators/json_response.py +93 -0
- apytizer/decorators/pagination.py +50 -32
- apytizer/endpoints/__init__.py +6 -0
- apytizer/endpoints/abstract_endpoint.py +38 -0
- apytizer/endpoints/web_endpoint.py +519 -0
- apytizer/engines/__init__.py +6 -0
- apytizer/engines/abstract_engine.py +45 -0
- apytizer/engines/http_engine.py +129 -0
- apytizer/errors.py +34 -0
- apytizer/factories/__init__.py +5 -0
- apytizer/factories/abstract_factory.py +17 -0
- apytizer/http_methods.py +34 -0
- apytizer/managers/__init__.py +12 -0
- apytizer/managers/abstract_manager.py +80 -0
- apytizer/managers/base_manager.py +116 -0
- apytizer/mappers/__init__.py +6 -0
- apytizer/mappers/abstract_mapper.py +48 -0
- apytizer/mappers/base_mapper.py +78 -0
- apytizer/media_types.py +118 -0
- apytizer/models/__init__.py +6 -0
- apytizer/models/abstract_model.py +119 -0
- apytizer/models/base_model.py +85 -0
- apytizer/protocols.py +38 -0
- apytizer/repositories/__init__.py +6 -0
- apytizer/repositories/abstract_repository.py +81 -0
- apytizer/repositories/managed_repository.py +92 -0
- apytizer/routes/__init__.py +6 -0
- apytizer/routes/abstract_route.py +32 -0
- apytizer/routes/base_route.py +138 -0
- apytizer/sessions/__init__.py +33 -0
- apytizer/sessions/abstract_session.py +63 -0
- apytizer/sessions/requests_session.py +125 -0
- apytizer/states/__init__.py +6 -0
- apytizer/states/abstract_state.py +71 -0
- apytizer/states/local_state.py +99 -0
- apytizer/utils/__init__.py +9 -4
- apytizer/utils/caching.py +39 -0
- apytizer/utils/dictionaries.py +375 -0
- apytizer/utils/errors.py +104 -0
- apytizer/utils/iterables.py +91 -0
- apytizer/utils/objects.py +145 -0
- apytizer/utils/strings.py +69 -0
- apytizer/utils/typing.py +29 -0
- apytizer-0.0.1b1.dist-info/METADATA +41 -0
- apytizer-0.0.1b1.dist-info/RECORD +60 -0
- {apytizer-0.0.1a0.dist-info → apytizer-0.0.1b1.dist-info}/WHEEL +1 -2
- apytizer/abstracts/__init__.py +0 -8
- apytizer/abstracts/api.py +0 -147
- apytizer/abstracts/endpoint.py +0 -177
- apytizer/abstracts/model.py +0 -50
- apytizer/abstracts/session.py +0 -39
- apytizer/adapters/transport.py +0 -40
- apytizer/base/__init__.py +0 -8
- apytizer/base/api.py +0 -510
- apytizer/base/endpoint.py +0 -443
- apytizer/base/model.py +0 -119
- apytizer/decorators/json.py +0 -35
- apytizer/utils/generate_key.py +0 -18
- apytizer/utils/merge.py +0 -19
- apytizer-0.0.1a0.dist-info/METADATA +0 -27
- apytizer-0.0.1a0.dist-info/RECORD +0 -25
- apytizer-0.0.1a0.dist-info/top_level.txt +0 -1
- {apytizer-0.0.1a0.dist-info → apytizer-0.0.1b1.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# src/apytizer/states/abstract_state.py
|
|
3
|
+
"""Abstract State Class Interface.
|
|
4
|
+
|
|
5
|
+
This module defines an abstract state class which provides an interface
|
|
6
|
+
for subclasses to implement.
|
|
7
|
+
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
# Standard Library Imports
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
import abc
|
|
13
|
+
from typing import Any
|
|
14
|
+
from typing import Generator
|
|
15
|
+
from typing import Mapping
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
__all__ = ["AbstractState"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AbstractState(abc.ABC):
|
|
22
|
+
"""Represents an abstract state."""
|
|
23
|
+
|
|
24
|
+
@abc.abstractmethod
|
|
25
|
+
def __contains__(self, key: str) -> bool:
|
|
26
|
+
raise NotImplementedError
|
|
27
|
+
|
|
28
|
+
@abc.abstractmethod
|
|
29
|
+
def __eq__(self, other: object) -> bool:
|
|
30
|
+
raise NotImplementedError
|
|
31
|
+
|
|
32
|
+
@abc.abstractmethod
|
|
33
|
+
def __getitem__(self, key: str) -> Any:
|
|
34
|
+
raise NotImplementedError
|
|
35
|
+
|
|
36
|
+
@abc.abstractmethod
|
|
37
|
+
def __setitem__(self, key: str, value: Any) -> None:
|
|
38
|
+
raise NotImplementedError
|
|
39
|
+
|
|
40
|
+
@abc.abstractmethod
|
|
41
|
+
def __iter__(self) -> Generator[Any, None, None]:
|
|
42
|
+
raise NotImplementedError
|
|
43
|
+
|
|
44
|
+
@abc.abstractmethod
|
|
45
|
+
def get(self, key: str) -> Any:
|
|
46
|
+
"""Abstract method for getting an item from state."""
|
|
47
|
+
raise NotImplementedError
|
|
48
|
+
|
|
49
|
+
@abc.abstractmethod
|
|
50
|
+
def items(self) -> Any:
|
|
51
|
+
"""Abstract method for getting items from state."""
|
|
52
|
+
raise NotImplementedError
|
|
53
|
+
|
|
54
|
+
@abc.abstractmethod
|
|
55
|
+
def update(
|
|
56
|
+
self,
|
|
57
|
+
__m: Optional[Mapping[str, Any]] = None,
|
|
58
|
+
**kwargs: Any,
|
|
59
|
+
) -> None:
|
|
60
|
+
"""Abstract method for updating state."""
|
|
61
|
+
raise NotImplementedError
|
|
62
|
+
|
|
63
|
+
@abc.abstractmethod
|
|
64
|
+
def rollback(self) -> None:
|
|
65
|
+
"""Abstract method for rolling back changes to state."""
|
|
66
|
+
raise NotImplementedError
|
|
67
|
+
|
|
68
|
+
@abc.abstractmethod
|
|
69
|
+
def save(self) -> None:
|
|
70
|
+
"""Abstract method for saving changes to state."""
|
|
71
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# src/apytizer/states/base_state.py
|
|
3
|
+
"""Base State Class.
|
|
4
|
+
|
|
5
|
+
This module defines the implementation of a base state class.
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# Standard Library Imports
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
import collections
|
|
12
|
+
from typing import Any
|
|
13
|
+
from typing import Dict
|
|
14
|
+
from typing import Generator
|
|
15
|
+
from typing import Mapping
|
|
16
|
+
from typing import Optional
|
|
17
|
+
from typing import Tuple
|
|
18
|
+
|
|
19
|
+
# Local Imports
|
|
20
|
+
from . import AbstractState
|
|
21
|
+
from .. import utils
|
|
22
|
+
|
|
23
|
+
__all__ = ["LocalState"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class LocalState(AbstractState):
|
|
27
|
+
"""Class implements a local state."""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
base: Optional[Dict[str, Any]] = None,
|
|
32
|
+
default: Optional[Dict[str, Any]] = None,
|
|
33
|
+
) -> None:
|
|
34
|
+
self._state = collections.ChainMap(base or {}, default or {})
|
|
35
|
+
|
|
36
|
+
def __contains__(self, key: str) -> bool:
|
|
37
|
+
return key in self._state
|
|
38
|
+
|
|
39
|
+
def __eq__(self, other: object) -> bool:
|
|
40
|
+
return (
|
|
41
|
+
dict(other) == dict(self)
|
|
42
|
+
if isinstance(other, AbstractState)
|
|
43
|
+
else False
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def __getitem__(self, key: str) -> Any:
|
|
47
|
+
result = self.get(key)
|
|
48
|
+
return result
|
|
49
|
+
|
|
50
|
+
def __setitem__(self, key: str, value: Any) -> None:
|
|
51
|
+
self._state = utils.deep_set(self._state, key, value)
|
|
52
|
+
|
|
53
|
+
def __iter__(self) -> Generator[Tuple[str, Any], None, None]:
|
|
54
|
+
yield from self._state.items()
|
|
55
|
+
|
|
56
|
+
def get(self, key: str) -> Any:
|
|
57
|
+
"""Get an item from state.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
key: Key.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Value of key in state.
|
|
64
|
+
|
|
65
|
+
"""
|
|
66
|
+
if not isinstance(key, str): # type: ignore
|
|
67
|
+
message = f"expected type 'str', got {type(key)} instead"
|
|
68
|
+
raise TypeError(message)
|
|
69
|
+
|
|
70
|
+
result = utils.deep_get(self._state, key)
|
|
71
|
+
return result
|
|
72
|
+
|
|
73
|
+
def items(self) -> Any:
|
|
74
|
+
"""Get items from state."""
|
|
75
|
+
results = self._state.items()
|
|
76
|
+
return results
|
|
77
|
+
|
|
78
|
+
def update(
|
|
79
|
+
self,
|
|
80
|
+
__m: Optional[Mapping[str, Any]] = None,
|
|
81
|
+
**kwargs: Any,
|
|
82
|
+
) -> None:
|
|
83
|
+
"""Update state.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
__m (optional): Mapping. Default ``None``.
|
|
87
|
+
**kwargs: Keyword arguments.
|
|
88
|
+
|
|
89
|
+
"""
|
|
90
|
+
self._state.update(__m or {}, **kwargs)
|
|
91
|
+
|
|
92
|
+
def rollback(self) -> None:
|
|
93
|
+
"""Roll back changes to state."""
|
|
94
|
+
self._state.clear()
|
|
95
|
+
|
|
96
|
+
def save(self) -> None:
|
|
97
|
+
"""Save changes to state."""
|
|
98
|
+
if self._state.maps[0]: # type: ignore
|
|
99
|
+
self._state = self._state.new_child() # type: ignore
|
apytizer/utils/__init__.py
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
|
+
# src/apytizer/utils/__init__.py
|
|
2
3
|
|
|
3
|
-
#
|
|
4
|
-
|
|
5
|
-
from .
|
|
6
|
-
from .
|
|
4
|
+
# Local Imports
|
|
5
|
+
from .caching import *
|
|
6
|
+
from .dictionaries import *
|
|
7
|
+
from .errors import *
|
|
8
|
+
from .iterables import *
|
|
9
|
+
from .objects import *
|
|
10
|
+
from .strings import *
|
|
11
|
+
from .typing import *
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
# Standard Library Import
|
|
4
|
+
from typing import Any
|
|
5
|
+
from typing import Callable
|
|
6
|
+
from typing import Hashable
|
|
7
|
+
from typing import Tuple
|
|
8
|
+
|
|
9
|
+
# Third-Party Imports
|
|
10
|
+
from cachetools.keys import hashkey
|
|
11
|
+
|
|
12
|
+
__all__ = ["generate_key"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def generate_key(*tags: str) -> Callable[..., Tuple[Hashable, ...]]:
|
|
16
|
+
"""Generates a hashable key for caching values.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
*tags: Tags.
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def hash_parameters(*args: Any, **kwargs: Any) -> Tuple[Hashable, ...]:
|
|
24
|
+
"""Hashes function parameters.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
*args: Positional arguments.
|
|
28
|
+
**kwargs: Keyword arguments.
|
|
29
|
+
|
|
30
|
+
Return:
|
|
31
|
+
Cache Key.
|
|
32
|
+
|
|
33
|
+
"""
|
|
34
|
+
result = hashkey(
|
|
35
|
+
*tags, *args, *[f"{k!s}={v!s}" for k, v in sorted(kwargs.items())]
|
|
36
|
+
)
|
|
37
|
+
return result
|
|
38
|
+
|
|
39
|
+
return hash_parameters
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# src/apytizer/utils/dictionaries.py
|
|
3
|
+
|
|
4
|
+
# Standard Library Imports
|
|
5
|
+
from collections import ChainMap
|
|
6
|
+
import functools
|
|
7
|
+
from typing import Any
|
|
8
|
+
from typing import Collection
|
|
9
|
+
from typing import Dict
|
|
10
|
+
from typing import Hashable
|
|
11
|
+
from typing import List
|
|
12
|
+
from typing import Optional
|
|
13
|
+
from typing import Set
|
|
14
|
+
from typing import TypeVar
|
|
15
|
+
from typing import Union
|
|
16
|
+
|
|
17
|
+
# Local Imports
|
|
18
|
+
from .errors import raise_for_instance
|
|
19
|
+
from .typing import allinstance
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"deep_get",
|
|
23
|
+
"deep_set",
|
|
24
|
+
"iter_get",
|
|
25
|
+
"iter_set",
|
|
26
|
+
"omit",
|
|
27
|
+
"pick",
|
|
28
|
+
"merge",
|
|
29
|
+
"remap_keys",
|
|
30
|
+
"remove_nulls",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
# Custom types:
|
|
34
|
+
T = TypeVar("T")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def deep_get(
|
|
38
|
+
__d: Dict[Hashable, Any],
|
|
39
|
+
/,
|
|
40
|
+
keys: str,
|
|
41
|
+
default: Optional[object] = None,
|
|
42
|
+
) -> Any:
|
|
43
|
+
"""Get value from nested dictionary object.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
__d: Dictionary object.
|
|
47
|
+
keys: String of keys seperated by periods.
|
|
48
|
+
default (optional): Default if value not found. Default ``None``.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Value of key in nested dictionary object.
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
TypeError: when argument is not an instance of 'dict'.
|
|
55
|
+
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def _get(data: Dict[str, Any], key: str) -> Any:
|
|
59
|
+
"""Get value of key from dictionary.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
data: Dictionary object.
|
|
63
|
+
key: Key for which to get value.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Value.
|
|
67
|
+
|
|
68
|
+
"""
|
|
69
|
+
try:
|
|
70
|
+
result = data.get(key, default)
|
|
71
|
+
except AttributeError: # if data is `None`
|
|
72
|
+
return default
|
|
73
|
+
else:
|
|
74
|
+
return result
|
|
75
|
+
|
|
76
|
+
raise_for_instance(__d, (dict, ChainMap))
|
|
77
|
+
raise_for_instance(keys, str)
|
|
78
|
+
|
|
79
|
+
value = functools.reduce(_get, keys.split("."), __d)
|
|
80
|
+
return value
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def deep_set(
|
|
84
|
+
__d: Dict[Hashable, Any],
|
|
85
|
+
/,
|
|
86
|
+
keys: Union[List[str], str],
|
|
87
|
+
value: Any,
|
|
88
|
+
) -> Dict[Hashable, Any]:
|
|
89
|
+
"""Sets key to value in nested dictionary object.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
__d: Dictionary object.
|
|
93
|
+
keys: Either list of keys, or string of keys seperated by periods.
|
|
94
|
+
value: Value to set for key.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Updated dictionary object.
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
TypeError: when argument is not an instance of 'dict'.
|
|
101
|
+
|
|
102
|
+
"""
|
|
103
|
+
raise_for_instance(__d, (dict, ChainMap))
|
|
104
|
+
|
|
105
|
+
if isinstance(keys, str):
|
|
106
|
+
keys = keys.split(".")
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
key, remaining = keys[0], keys[1:]
|
|
110
|
+
__d[key] = (
|
|
111
|
+
deep_set(__d.get(key) or {}, remaining, value)
|
|
112
|
+
if len(remaining) >= 1
|
|
113
|
+
else value
|
|
114
|
+
)
|
|
115
|
+
except KeyError as error:
|
|
116
|
+
raise KeyError(f"{key}.{error.args[0]}") from error # type: ignore
|
|
117
|
+
|
|
118
|
+
except (IndexError, TypeError) as error:
|
|
119
|
+
raise KeyError(keys[0]) from error
|
|
120
|
+
|
|
121
|
+
return __d
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def iter_get(
|
|
125
|
+
__iter: List[Dict[Hashable, Any]],
|
|
126
|
+
/,
|
|
127
|
+
key: str,
|
|
128
|
+
) -> List[object]:
|
|
129
|
+
"""Get value for key from each dictionary in an iterable object.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
__iter: Iterable object containing dictionaries.
|
|
133
|
+
key: Key for which to retrieve value.
|
|
134
|
+
|
|
135
|
+
Raises:
|
|
136
|
+
TypeError: when argument is not an iterable object.
|
|
137
|
+
ValueError: when not all items are dictionaries.
|
|
138
|
+
|
|
139
|
+
"""
|
|
140
|
+
raise_for_instance(__iter, list)
|
|
141
|
+
|
|
142
|
+
if not allinstance(__iter, dict):
|
|
143
|
+
raise ValueError("iterable object must contain dictionaries")
|
|
144
|
+
|
|
145
|
+
results = [deep_get(item, key) for item in __iter]
|
|
146
|
+
return results
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def iter_set(
|
|
150
|
+
__iter: List[Dict[Hashable, Any]],
|
|
151
|
+
/,
|
|
152
|
+
key: str,
|
|
153
|
+
value: Any,
|
|
154
|
+
) -> List[Dict[Hashable, Any]]:
|
|
155
|
+
"""Set value of key on each dictionary in an iterable object.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
__iter: Iterable object containing dictionaries.
|
|
159
|
+
key: Key for which to set value.
|
|
160
|
+
value: Value to set.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
List of updated dictionary objects.
|
|
164
|
+
|
|
165
|
+
Raises:
|
|
166
|
+
TypeError: when argument is not an iterable object.
|
|
167
|
+
ValueError: when not all items are dictionaries.
|
|
168
|
+
|
|
169
|
+
"""
|
|
170
|
+
raise_for_instance(__iter, list)
|
|
171
|
+
|
|
172
|
+
if not allinstance(__iter, dict):
|
|
173
|
+
raise ValueError("iterable object must contain dictionaries")
|
|
174
|
+
|
|
175
|
+
results = [deep_set(item, key, value) for item in __iter]
|
|
176
|
+
return results
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def merge(
|
|
180
|
+
*args: Optional[Dict[Hashable, T]],
|
|
181
|
+
overwrite: bool = True,
|
|
182
|
+
) -> Optional[Dict[Hashable, T]]:
|
|
183
|
+
"""Combines dictionary objects into a single dictionary.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
*args: Dictionary objects to merge.
|
|
187
|
+
overwrite (optional): Overwrite existing keys. Default ``True``.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Merged dictionary.
|
|
191
|
+
|
|
192
|
+
Raises:
|
|
193
|
+
TypeError: when arguments are not all dictionaries.
|
|
194
|
+
ValueError: when keys conflict and overwrite is ``False``.
|
|
195
|
+
|
|
196
|
+
"""
|
|
197
|
+
if not allinstance(args, (dict, type(None))):
|
|
198
|
+
raise TypeError("all arguments must be instances of 'dict'")
|
|
199
|
+
|
|
200
|
+
def _merge_dictionaries(
|
|
201
|
+
first: Dict[Hashable, Any],
|
|
202
|
+
second: Dict[Hashable, Any],
|
|
203
|
+
/,
|
|
204
|
+
path: Optional[List[str]] = None,
|
|
205
|
+
overwrite: bool = False,
|
|
206
|
+
) -> Dict[Hashable, Any]:
|
|
207
|
+
"""Merge two dictionaries.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
first: First dictionary.
|
|
211
|
+
second: Second dictionary.
|
|
212
|
+
path: Path of keys in nested dictionary.
|
|
213
|
+
overwrite (optional): Overwrite existing keys. Default ``False``.
|
|
214
|
+
|
|
215
|
+
Raises:
|
|
216
|
+
ValueError: when keys conflict and overwrite is ``False``.
|
|
217
|
+
|
|
218
|
+
.. _Based On:
|
|
219
|
+
https://stackoverflow.com/questions/7204805/how-to-merge-dictionaries-of-dictionaries.
|
|
220
|
+
|
|
221
|
+
"""
|
|
222
|
+
__path: List[str] = [] if path is None else path
|
|
223
|
+
|
|
224
|
+
for key in second:
|
|
225
|
+
if key in first:
|
|
226
|
+
if allinstance((first[key], second[key]), dict):
|
|
227
|
+
first[key] = _merge_dictionaries(
|
|
228
|
+
first[key],
|
|
229
|
+
second[key],
|
|
230
|
+
path=[*__path, str(key)],
|
|
231
|
+
overwrite=overwrite,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
elif allinstance((first[key], second[key]), list):
|
|
235
|
+
first[key] = _merge_lists(first[key], second[key])
|
|
236
|
+
|
|
237
|
+
elif allinstance((first[key], second[key]), set):
|
|
238
|
+
first[key] = _merge_sets(first[key], second[key])
|
|
239
|
+
|
|
240
|
+
elif overwrite is True:
|
|
241
|
+
first[key] = second[key]
|
|
242
|
+
|
|
243
|
+
else:
|
|
244
|
+
location = ".".join(k for k in [*__path, str(key)] if k)
|
|
245
|
+
message = f"Conflict at {location!s}"
|
|
246
|
+
raise ValueError(message)
|
|
247
|
+
|
|
248
|
+
else:
|
|
249
|
+
first[key] = second[key]
|
|
250
|
+
|
|
251
|
+
return first
|
|
252
|
+
|
|
253
|
+
def _merge_lists(first: List[Any], second: List[Any]) -> List[Any]:
|
|
254
|
+
return [*first, *second]
|
|
255
|
+
|
|
256
|
+
def _merge_sets(first: Set[Any], second: Set[Any]) -> Set[Any]:
|
|
257
|
+
return first.union(second)
|
|
258
|
+
|
|
259
|
+
func = functools.partial(_merge_dictionaries, overwrite=overwrite)
|
|
260
|
+
result: Dict[Hashable, Any] = functools.reduce(
|
|
261
|
+
lambda acc, cur: func(acc, cur) if cur else acc, args, {}
|
|
262
|
+
)
|
|
263
|
+
return result if result else None
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def omit(
|
|
267
|
+
__d: Dict[Hashable, Any],
|
|
268
|
+
/,
|
|
269
|
+
keys: Collection[str],
|
|
270
|
+
) -> Dict[Hashable, Any]:
|
|
271
|
+
"""Omit multiple key-value pairs from dictionary.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
__d: Dictionary object.
|
|
275
|
+
keys: Collection of keys.
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
Dictionary without the selected key-value pairs.
|
|
279
|
+
|
|
280
|
+
Raises:
|
|
281
|
+
TypeError: when argument is not an instance of 'dict'.
|
|
282
|
+
|
|
283
|
+
"""
|
|
284
|
+
raise_for_instance(__d, dict)
|
|
285
|
+
|
|
286
|
+
# TODO: Add support for omitting key-value pairs from nested dictionaries.
|
|
287
|
+
results: Dict[Hashable, Any] = {k: __d[k] for k in __d if k not in keys}
|
|
288
|
+
return results
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def pick(
|
|
292
|
+
__d: Dict[Hashable, Any],
|
|
293
|
+
/,
|
|
294
|
+
keys: Collection[str],
|
|
295
|
+
) -> Dict[Hashable, Any]:
|
|
296
|
+
"""Pick multiple values from a dictionary.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
__d: Dictionary object.
|
|
300
|
+
keys: Collection of keys.
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
Dictionary containing the selected key-value pairs.
|
|
304
|
+
|
|
305
|
+
Raises:
|
|
306
|
+
TypeError: when argument is not an instance of 'dict'.
|
|
307
|
+
|
|
308
|
+
"""
|
|
309
|
+
raise_for_instance(__d, (dict, ChainMap))
|
|
310
|
+
|
|
311
|
+
def _last(key: str) -> Hashable:
|
|
312
|
+
return key.split(".")[-1]
|
|
313
|
+
|
|
314
|
+
results = {_last(key): deep_get(__d, key) for key in keys}
|
|
315
|
+
return results
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def remap_keys(
|
|
319
|
+
__d: Dict[Hashable, Any],
|
|
320
|
+
/,
|
|
321
|
+
key_map: Dict[Hashable, str],
|
|
322
|
+
remove: bool = False,
|
|
323
|
+
) -> Dict[Hashable, Any]:
|
|
324
|
+
"""Remap dictionary object to new keys.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
__d: Dictionary object for which keys will be remapped.
|
|
328
|
+
key_map: Dictionary mapping old keys to new ones.
|
|
329
|
+
remove (optional): Whether to drop key-value pairs if key is not found
|
|
330
|
+
in key map. Default ``False``.
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
Remapped dictionary.
|
|
334
|
+
|
|
335
|
+
Raises:
|
|
336
|
+
TypeError: when argument is not an instance of 'dict'.
|
|
337
|
+
|
|
338
|
+
"""
|
|
339
|
+
raise_for_instance(__d, dict)
|
|
340
|
+
|
|
341
|
+
result: Dict[Hashable, Any] = {
|
|
342
|
+
key_map.get(key, key): value
|
|
343
|
+
for key, value in __d.items()
|
|
344
|
+
if key in key_map or remove is False
|
|
345
|
+
}
|
|
346
|
+
return result
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def remove_nulls(
|
|
350
|
+
__d: Dict[Hashable, Any],
|
|
351
|
+
/,
|
|
352
|
+
null_values: Optional[Collection[Any]] = None,
|
|
353
|
+
) -> Dict[Hashable, Any]:
|
|
354
|
+
"""Remove all null values from dictionary.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
__d: Dictionary from which to remove null values.
|
|
358
|
+
null_values (optional): Additional values to recognize as null.
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
Dictionary without null values.
|
|
362
|
+
|
|
363
|
+
Raises:
|
|
364
|
+
TypeError: when argument is not an instance of 'dict'.
|
|
365
|
+
|
|
366
|
+
"""
|
|
367
|
+
raise_for_instance(__d, dict)
|
|
368
|
+
|
|
369
|
+
nulls = null_values or []
|
|
370
|
+
result: Dict[Hashable, Any] = {
|
|
371
|
+
key: value
|
|
372
|
+
for key, value in __d.items()
|
|
373
|
+
if value is not None and value not in nulls
|
|
374
|
+
}
|
|
375
|
+
return result
|
apytizer/utils/errors.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# src/apytizer/utils/errors.py
|
|
3
|
+
|
|
4
|
+
# Standard Library Imports
|
|
5
|
+
from typing import Any
|
|
6
|
+
from typing import Tuple
|
|
7
|
+
from typing import Union
|
|
8
|
+
|
|
9
|
+
# Local Imports
|
|
10
|
+
from .. import utils
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"raise_for_attribute",
|
|
14
|
+
"raise_for_instance",
|
|
15
|
+
"raise_for_none",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def raise_for_attribute(__obj: object, __attr: str, /) -> None:
|
|
20
|
+
"""Raise error if object does not contain expected attribute.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
__obj: Object to check for attribute.
|
|
24
|
+
__attr: Attribute for which to check.
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
if not hasattr(__obj, __attr):
|
|
28
|
+
cls = __obj.__class__.__name__
|
|
29
|
+
message = f"type object '{cls!s}' has no attribute '{__attr!s}'"
|
|
30
|
+
raise AttributeError(message)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def raise_for_instance(
|
|
34
|
+
__value: object,
|
|
35
|
+
__expected: Union[type, Tuple[Union[type, Tuple[Any, ...]], ...]],
|
|
36
|
+
/,
|
|
37
|
+
) -> None:
|
|
38
|
+
"""Raise error if value is not an instance of expected type.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
__value: Object to check for type.
|
|
42
|
+
__expected: Expected type(s).
|
|
43
|
+
|
|
44
|
+
"""
|
|
45
|
+
correct_type = isinstance(__value, __expected)
|
|
46
|
+
|
|
47
|
+
if not correct_type and isinstance(__expected, tuple):
|
|
48
|
+
_raise_for_multiple_types(__value, __expected)
|
|
49
|
+
|
|
50
|
+
if not correct_type and not isinstance(__expected, tuple):
|
|
51
|
+
_raise_for_single_type(__value, __expected)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _raise_for_multiple_types(
|
|
55
|
+
__value: object, __types: Tuple[Union[type, Tuple[Any, ...]], ...], /
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Raise error if value is not among expected types.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
__value: Object to check for type.
|
|
61
|
+
__types: Expected types.
|
|
62
|
+
|
|
63
|
+
"""
|
|
64
|
+
type_names = utils.iter_getattr(__types, "__name__")
|
|
65
|
+
formatted_names = utils.iter_format(type_names, "'{}'")
|
|
66
|
+
expected = utils.syntactic_list(formatted_names, "or")
|
|
67
|
+
actual = type(__value).__name__
|
|
68
|
+
|
|
69
|
+
message = f"expected types {expected!s}, got {actual!s} instead"
|
|
70
|
+
raise TypeError(message)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _raise_for_single_type(__value: object, __type: type, /) -> None:
|
|
74
|
+
"""Raise error if value is not an instance of expected type.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
__value: Object to check for type.
|
|
78
|
+
__type: Expected type.
|
|
79
|
+
|
|
80
|
+
"""
|
|
81
|
+
expected, actual = f"'{__type.__name__}'", type(__value).__name__
|
|
82
|
+
message = f"expected type {expected!s}, got {actual!s} instead"
|
|
83
|
+
raise TypeError(message)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def raise_for_none(*args: Any, **kwargs: Any) -> None:
|
|
87
|
+
"""Raise error if value is None.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
*args: Positional arguments.
|
|
91
|
+
**kwargs: Keyword arguments.
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
ValueError: when any argument is ``None``.
|
|
95
|
+
|
|
96
|
+
"""
|
|
97
|
+
if any(arg is None for arg in args):
|
|
98
|
+
message = "argument cannot be 'None'"
|
|
99
|
+
raise ValueError(message)
|
|
100
|
+
|
|
101
|
+
for name, value in kwargs.items():
|
|
102
|
+
if value is None:
|
|
103
|
+
message = f"{name} cannot be 'None'"
|
|
104
|
+
raise ValueError(message)
|