catalystwan 2.0.0a1__tar.gz → 2.0.0a2__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 (41) hide show
  1. catalystwan-2.0.0a2/PKG-INFO +147 -0
  2. catalystwan-2.0.0a2/README.md +109 -0
  3. {catalystwan-2.0.0a1 → catalystwan-2.0.0a2}/pyproject.toml +4 -3
  4. catalystwan-2.0.0a2/src/catalystwan/core/__init__.py +2 -0
  5. catalystwan-2.0.0a2/src/catalystwan/core/client.py +159 -0
  6. {catalystwan-2.0.0a1 → catalystwan-2.0.0a2}/src/catalystwan/core/models/deserialize.py +72 -39
  7. catalystwan-2.0.0a2/src/catalystwan/core/models/utils.py +27 -0
  8. {catalystwan-2.0.0a1 → catalystwan-2.0.0a2}/src/catalystwan/core/request_adapter.py +33 -22
  9. catalystwan-2.0.0a2/src/catalystwan.egg-info/PKG-INFO +147 -0
  10. {catalystwan-2.0.0a1 → catalystwan-2.0.0a2}/src/catalystwan.egg-info/SOURCES.txt +2 -1
  11. {catalystwan-2.0.0a1 → catalystwan-2.0.0a2}/tests/test_ft_serialize.py +5 -13
  12. {catalystwan-2.0.0a1 → catalystwan-2.0.0a2}/tests/test_model_deserialize.py +102 -0
  13. {catalystwan-2.0.0a1 → catalystwan-2.0.0a2}/tests/test_parcel_serialize.py +4 -12
  14. catalystwan-2.0.0a1/PKG-INFO +0 -238
  15. catalystwan-2.0.0a1/README.rst +0 -38
  16. catalystwan-2.0.0a1/src/catalystwan/core/__init__.py +0 -2
  17. catalystwan-2.0.0a1/src/catalystwan/core/client.py +0 -71
  18. catalystwan-2.0.0a1/src/catalystwan.egg-info/PKG-INFO +0 -238
  19. {catalystwan-2.0.0a1 → catalystwan-2.0.0a2}/LICENSE +0 -0
  20. {catalystwan-2.0.0a1 → catalystwan-2.0.0a2}/setup.cfg +0 -0
  21. {catalystwan-2.0.0a1 → catalystwan-2.0.0a2}/src/catalystwan/core/abstractions.py +0 -0
  22. {catalystwan-2.0.0a1 → catalystwan-2.0.0a2}/src/catalystwan/core/apigw_auth.py +0 -0
  23. {catalystwan-2.0.0a1 → catalystwan-2.0.0a2}/src/catalystwan/core/encoder.py +0 -0
  24. {catalystwan-2.0.0a1 → catalystwan-2.0.0a2}/src/catalystwan/core/exceptions.py +0 -0
  25. {catalystwan-2.0.0a1 → catalystwan-2.0.0a2}/src/catalystwan/core/loader.py +0 -0
  26. {catalystwan-2.0.0a1 → catalystwan-2.0.0a2}/src/catalystwan/core/metadata.py +0 -0
  27. {catalystwan-2.0.0a1 → catalystwan-2.0.0a2}/src/catalystwan/core/models/__init__.py +0 -0
  28. {catalystwan-2.0.0a1 → catalystwan-2.0.0a2}/src/catalystwan/core/models/serialize.py +0 -0
  29. {catalystwan-2.0.0a1 → catalystwan-2.0.0a2}/src/catalystwan/core/py.typed +0 -0
  30. {catalystwan-2.0.0a1 → catalystwan-2.0.0a2}/src/catalystwan/core/request_limiter.py +0 -0
  31. {catalystwan-2.0.0a1 → catalystwan-2.0.0a2}/src/catalystwan/core/response.py +0 -0
  32. {catalystwan-2.0.0a1 → catalystwan-2.0.0a2}/src/catalystwan/core/session.py +0 -0
  33. {catalystwan-2.0.0a1 → catalystwan-2.0.0a2}/src/catalystwan/core/types.py +0 -0
  34. {catalystwan-2.0.0a1 → catalystwan-2.0.0a2}/src/catalystwan/core/version.py +0 -0
  35. {catalystwan-2.0.0a1 → catalystwan-2.0.0a2}/src/catalystwan/core/vmanage_auth.py +0 -0
  36. {catalystwan-2.0.0a1 → catalystwan-2.0.0a2}/src/catalystwan.egg-info/dependency_links.txt +0 -0
  37. {catalystwan-2.0.0a1 → catalystwan-2.0.0a2}/src/catalystwan.egg-info/requires.txt +0 -0
  38. {catalystwan-2.0.0a1 → catalystwan-2.0.0a2}/src/catalystwan.egg-info/top_level.txt +0 -0
  39. {catalystwan-2.0.0a1 → catalystwan-2.0.0a2}/tests/test_ft_deserialize.py +0 -0
  40. {catalystwan-2.0.0a1 → catalystwan-2.0.0a2}/tests/test_model_serialize.py +0 -0
  41. {catalystwan-2.0.0a1 → catalystwan-2.0.0a2}/tests/test_parcel_deserialize.py +0 -0
@@ -0,0 +1,147 @@
1
+ Metadata-Version: 2.4
2
+ Name: catalystwan
3
+ Version: 2.0.0a2
4
+ Summary: Cisco Catalyst WAN SDK for Python
5
+ License-Expression: Apache-2.0
6
+ Project-URL: Homepage, https://github.com/cisco-en-programmability/catalystwan-sdk-next-python
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Programming Language :: Python
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.8
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Topic :: Internet
18
+ Requires-Python: >=3.8
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: packaging>=23.0
22
+ Requires-Dist: requests>=2.32.3
23
+ Requires-Dist: typing-extensions>=4.12.2
24
+ Requires-Dist: urllib3>=2.2.2
25
+ Requires-Dist: catalystwan-types==2.0.0a0
26
+ Requires-Dist: catalystwan-v20-15==2.0.0a1
27
+ Requires-Dist: catalystwan-v20-16==2.0.0a1
28
+ Provides-Extra: test
29
+ Requires-Dist: pytest>=8.2.2; extra == "test"
30
+ Provides-Extra: ver-all
31
+ Requires-Dist: catalystwan-v20-15==2.0.0a1; extra == "ver-all"
32
+ Requires-Dist: catalystwan-v20-16==2.0.0a1; extra == "ver-all"
33
+ Provides-Extra: ver-2015
34
+ Requires-Dist: catalystwan-v20-15==2.0.0a1; extra == "ver-2015"
35
+ Provides-Extra: ver-2016
36
+ Requires-Dist: catalystwan-v20-16==2.0.0a1; extra == "ver-2016"
37
+ Dynamic: license-file
38
+
39
+ Cisco Catalyst WAN SDK 2.0
40
+ ==========================
41
+
42
+ Welcome to the official documentation for the Cisco Catalyst WAN SDK, a package designed for creating simple and parallel automatic requests via the official SD-WAN Manager API.
43
+
44
+ Overview
45
+ --------
46
+
47
+ Cisco Catalyst WAN SDK serves as a multiple session handler (provider, provider as a tenant, tenant) and is environment-independent. You just need a connection to any SD-WAN Manager.
48
+
49
+ Supported Catalystwan WAN Server Versions
50
+ -----------------------------------------
51
+
52
+ - 20.15
53
+ - 20.16
54
+
55
+
56
+ Important Notice: Version Incompatibility
57
+ -----------------------------------------
58
+
59
+ We are excited to announce the release of Cisco Catalyst WAN SDK version 2.0.
60
+ This new version introduces a range of enhancements and features designed
61
+ to improve performance and usability. However, it is important to note that version 2.0
62
+ is not compatible with any previous legacy versions of the SDK.
63
+
64
+
65
+ Actions Recommended:
66
+ Backup: Ensure you have backups of your current projects before attempting to upgrade.
67
+ Review Documentation: Carefully review the updated documentation and release notes for guidance on migration and new features.
68
+ Test Thoroughly: Test your projects thoroughly in a development environment before deploying version 2.0 in production.
69
+
70
+ We appreciate your understanding and cooperation as we continue to enhance the Cisco Catalyst WAN SDK. Should you have any questions or require assistance, please reach out through our support channels.
71
+
72
+ Thank you for your continued support and feedback!
73
+
74
+
75
+ Not recommend to use in production environments.
76
+ ------------------------------------------------
77
+ Cisco Catalyst WAN SDK in its `pre-alpha` release phase. This marks a significant milestone
78
+ in empowering developers to unlock the full capabilities of Cisco's networking solutions.
79
+ Please note that, as a pre-alpha release, this version of the SDK is still in active development
80
+ and testing. It is provided "as is," with limited support offered on a best-effort basis.
81
+
82
+
83
+ Supported Python Versions
84
+ -------------------------
85
+
86
+ Python >= 3.8
87
+
88
+ > If you don't have a specific version, you can just use [Pyenv](https://github.com/pyenv/pyenv) to manage Python versions.
89
+
90
+
91
+ Installation
92
+ ------------
93
+
94
+ To install the SDK, run the following command:
95
+
96
+ ```bash
97
+ pip install catalystwan==2.0.0a0
98
+ ```
99
+
100
+ To manually install the necessary Python packages in editable mode, you can use the `pip install -e` command.
101
+
102
+ ```bash
103
+ pip install -e ./packages/catalystwan-types \
104
+ -e ./packages/catalystwan-core \
105
+ -e ./versions/catalystwan-v20_15 \
106
+ -e ./versions/catalystwan-v20_16
107
+ ```
108
+
109
+
110
+ Getting Started
111
+ ---------------
112
+
113
+ To execute SDK APIs, you need to create a `ApiClient`. Use the `create_client()` method to configure a session, perform authentication, and obtain a `ApiClient` instance in an operational state.
114
+
115
+ ### Example Usage
116
+
117
+ Here's a quick example of how to use the SDK:
118
+
119
+ ```python
120
+ from catalystwan.core import create_client
121
+
122
+ url = "example.com"
123
+ username = "admin"
124
+ password = "password123"
125
+
126
+ with create_client(url=url, username=username, password=password) as client:
127
+ result = client.health.devices.get_devices_health()
128
+ print(result)
129
+ ```
130
+
131
+ If you need to preform more complex operations that require models, they can utilize an alias: `m`.
132
+ ```python
133
+
134
+ with create_client(...) as client:
135
+ result = client.admin.aaa.update_aaa_config(
136
+ client.admin.aaa.m.Aaa(
137
+ accounting: True,
138
+ admin_auth_order: False,
139
+ audit_disable: False,
140
+ auth_fallback: False,
141
+ auth_order: ["local"]
142
+ )
143
+ )
144
+ print(result)
145
+ ```
146
+
147
+ Using an alias allows for easier access and management of models, simplifying workflows and improving efficiency. This approach helps streamline operations without requiring direct integration with underlying models, making them more user-friendly and scalable.
@@ -0,0 +1,109 @@
1
+ Cisco Catalyst WAN SDK 2.0
2
+ ==========================
3
+
4
+ Welcome to the official documentation for the Cisco Catalyst WAN SDK, a package designed for creating simple and parallel automatic requests via the official SD-WAN Manager API.
5
+
6
+ Overview
7
+ --------
8
+
9
+ Cisco Catalyst WAN SDK serves as a multiple session handler (provider, provider as a tenant, tenant) and is environment-independent. You just need a connection to any SD-WAN Manager.
10
+
11
+ Supported Catalystwan WAN Server Versions
12
+ -----------------------------------------
13
+
14
+ - 20.15
15
+ - 20.16
16
+
17
+
18
+ Important Notice: Version Incompatibility
19
+ -----------------------------------------
20
+
21
+ We are excited to announce the release of Cisco Catalyst WAN SDK version 2.0.
22
+ This new version introduces a range of enhancements and features designed
23
+ to improve performance and usability. However, it is important to note that version 2.0
24
+ is not compatible with any previous legacy versions of the SDK.
25
+
26
+
27
+ Actions Recommended:
28
+ Backup: Ensure you have backups of your current projects before attempting to upgrade.
29
+ Review Documentation: Carefully review the updated documentation and release notes for guidance on migration and new features.
30
+ Test Thoroughly: Test your projects thoroughly in a development environment before deploying version 2.0 in production.
31
+
32
+ We appreciate your understanding and cooperation as we continue to enhance the Cisco Catalyst WAN SDK. Should you have any questions or require assistance, please reach out through our support channels.
33
+
34
+ Thank you for your continued support and feedback!
35
+
36
+
37
+ Not recommend to use in production environments.
38
+ ------------------------------------------------
39
+ Cisco Catalyst WAN SDK in its `pre-alpha` release phase. This marks a significant milestone
40
+ in empowering developers to unlock the full capabilities of Cisco's networking solutions.
41
+ Please note that, as a pre-alpha release, this version of the SDK is still in active development
42
+ and testing. It is provided "as is," with limited support offered on a best-effort basis.
43
+
44
+
45
+ Supported Python Versions
46
+ -------------------------
47
+
48
+ Python >= 3.8
49
+
50
+ > If you don't have a specific version, you can just use [Pyenv](https://github.com/pyenv/pyenv) to manage Python versions.
51
+
52
+
53
+ Installation
54
+ ------------
55
+
56
+ To install the SDK, run the following command:
57
+
58
+ ```bash
59
+ pip install catalystwan==2.0.0a0
60
+ ```
61
+
62
+ To manually install the necessary Python packages in editable mode, you can use the `pip install -e` command.
63
+
64
+ ```bash
65
+ pip install -e ./packages/catalystwan-types \
66
+ -e ./packages/catalystwan-core \
67
+ -e ./versions/catalystwan-v20_15 \
68
+ -e ./versions/catalystwan-v20_16
69
+ ```
70
+
71
+
72
+ Getting Started
73
+ ---------------
74
+
75
+ To execute SDK APIs, you need to create a `ApiClient`. Use the `create_client()` method to configure a session, perform authentication, and obtain a `ApiClient` instance in an operational state.
76
+
77
+ ### Example Usage
78
+
79
+ Here's a quick example of how to use the SDK:
80
+
81
+ ```python
82
+ from catalystwan.core import create_client
83
+
84
+ url = "example.com"
85
+ username = "admin"
86
+ password = "password123"
87
+
88
+ with create_client(url=url, username=username, password=password) as client:
89
+ result = client.health.devices.get_devices_health()
90
+ print(result)
91
+ ```
92
+
93
+ If you need to preform more complex operations that require models, they can utilize an alias: `m`.
94
+ ```python
95
+
96
+ with create_client(...) as client:
97
+ result = client.admin.aaa.update_aaa_config(
98
+ client.admin.aaa.m.Aaa(
99
+ accounting: True,
100
+ admin_auth_order: False,
101
+ audit_disable: False,
102
+ auth_fallback: False,
103
+ auth_order: ["local"]
104
+ )
105
+ )
106
+ print(result)
107
+ ```
108
+
109
+ Using an alias allows for easier access and management of models, simplifying workflows and improving efficiency. This approach helps streamline operations without requiring direct integration with underlying models, making them more user-friendly and scalable.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "catalystwan"
3
- version = "2.0.0a1"
3
+ version = "2.0.0a2"
4
4
  description = "Cisco Catalyst WAN SDK for Python"
5
5
  dependencies = [
6
6
  "packaging >= 23.0",
@@ -12,7 +12,9 @@ dependencies = [
12
12
  "catalystwan-v20-16 == 2.0.0a1"
13
13
  ]
14
14
  readme = "README.md"
15
- license = {file = "LICENSE"}
15
+ license = "Apache-2.0"
16
+ license-files = ["LICENSE"]
17
+
16
18
  requires-python = ">= 3.8"
17
19
  classifiers = [
18
20
  # one of:
@@ -21,7 +23,6 @@ classifiers = [
21
23
  # "Development Status :: 5 - Production/Stable"
22
24
  "Development Status :: 3 - Alpha",
23
25
  "Intended Audience :: Developers",
24
- "License :: OSI Approved :: Apache Software License",
25
26
  "Programming Language :: Python",
26
27
  "Programming Language :: Python :: 3",
27
28
  "Programming Language :: Python :: 3.8",
@@ -0,0 +1,2 @@
1
+ from catalystwan.core.client import create_client as create_client
2
+ from catalystwan.core.client import create_client_from_auth as create_client_from_auth
@@ -0,0 +1,159 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from contextlib import contextmanager
5
+ from copy import copy
6
+ from inspect import isclass
7
+ from typing import TYPE_CHECKING, Generator, Optional, Type, TypeVar, Union, cast, overload
8
+
9
+ from catalystwan.core.apigw_auth import ApiGwAuth
10
+ from catalystwan.core.exceptions import CatalystwanException
11
+ from catalystwan.core.loader import load_client
12
+ from catalystwan.core.request_adapter import RequestAdapter
13
+ from catalystwan.core.request_limiter import RequestLimiter
14
+ from catalystwan.core.session import ManagerSession, create_base_url, create_manager_session
15
+ from catalystwan.core.vmanage_auth import vManageAuth
16
+ from typing_extensions import TypeGuard
17
+
18
+ if TYPE_CHECKING:
19
+ from catalystwan.core.loader import ApiClient
20
+
21
+
22
+ class CatalystwanNotAClientException(CatalystwanException): ...
23
+
24
+
25
+ Client = TypeVar("Client")
26
+
27
+
28
+ # TODO: Better TypeGuards - it may be hard since we want to avoid direct imports
29
+ # For now, it's more of a hack for typing purposes
30
+ def _is_client_instance(obj: object) -> TypeGuard[ApiClient]:
31
+ return not isclass(obj) and hasattr(obj, "api_version")
32
+
33
+
34
+ def _is_client_class(obj: object) -> TypeGuard[Type[ApiClient]]:
35
+ return isclass(obj) and hasattr(obj, "api_version")
36
+
37
+
38
+ @overload
39
+ @contextmanager
40
+ def create_client_from_auth(
41
+ url: str,
42
+ auth: Union[vManageAuth, ApiGwAuth],
43
+ port: Optional[int] = None,
44
+ subdomain: Optional[str] = None,
45
+ logger: Optional[logging.Logger] = None,
46
+ request_limiter: Optional[RequestLimiter] = None,
47
+ ) -> Generator[ApiClient, None, None]: ...
48
+
49
+
50
+ @overload
51
+ @contextmanager
52
+ def create_client_from_auth(
53
+ url: str,
54
+ auth: Union[vManageAuth, ApiGwAuth],
55
+ port: Optional[int] = None,
56
+ subdomain: Optional[str] = None,
57
+ logger: Optional[logging.Logger] = None,
58
+ request_limiter: Optional[RequestLimiter] = None,
59
+ *,
60
+ api_client_class: Type[Client],
61
+ ) -> Generator[Client, None, None]: ...
62
+
63
+
64
+ @contextmanager
65
+ def create_client_from_auth(
66
+ url: str,
67
+ auth: Union[vManageAuth, ApiGwAuth],
68
+ port: Optional[int] = None,
69
+ subdomain: Optional[str] = None,
70
+ logger: Optional[logging.Logger] = None,
71
+ request_limiter: Optional[RequestLimiter] = None,
72
+ api_client_class: Optional[Type[Client]] = None,
73
+ ) -> Generator[Union[ApiClient, Client], None, None]:
74
+ if logger is None:
75
+ logger = logging.getLogger(__name__)
76
+ session = ManagerSession(
77
+ base_url=create_base_url(url, port),
78
+ auth=auth,
79
+ subdomain=subdomain,
80
+ logger=logger,
81
+ request_limiter=request_limiter,
82
+ )
83
+ with session.login():
84
+ version = session.api_version.base_version
85
+ if api_client_class is None:
86
+ logger.debug(f"Choosing client for version {version}...")
87
+ client = load_client(session.api_version.base_version)
88
+ logger.debug(f"Client for version {version} loaded")
89
+ yield client(RequestAdapter(session=session, logger=logger))
90
+ elif _is_client_class(api_client_class):
91
+ logger.debug(f"Creating instance for client class {api_client_class}")
92
+ yield api_client_class(RequestAdapter(session=session, logger=logger))
93
+ else:
94
+ raise CatalystwanNotAClientException(f"{api_client_class} is not a client class")
95
+
96
+
97
+ @overload
98
+ @contextmanager
99
+ def create_client(
100
+ url: str,
101
+ username: str,
102
+ password: str,
103
+ port: Optional[int] = None,
104
+ subdomain: Optional[str] = None,
105
+ logger: Optional[logging.Logger] = None,
106
+ ) -> Generator[ApiClient, None, None]: ...
107
+
108
+
109
+ @overload
110
+ @contextmanager
111
+ def create_client(
112
+ url: str,
113
+ username: str,
114
+ password: str,
115
+ port: Optional[int] = None,
116
+ subdomain: Optional[str] = None,
117
+ logger: Optional[logging.Logger] = None,
118
+ *,
119
+ api_client_class: Type[Client],
120
+ ) -> Generator[Client, None, None]: ...
121
+
122
+
123
+ @contextmanager
124
+ def create_client(
125
+ url: str,
126
+ username: str,
127
+ password: str,
128
+ port: Optional[int] = None,
129
+ subdomain: Optional[str] = None,
130
+ logger: Optional[logging.Logger] = None,
131
+ api_client_class: Optional[Type[Client]] = None,
132
+ ) -> Generator[Union[ApiClient, Client], None, None]:
133
+ if logger is None:
134
+ logger = logging.getLogger(__name__)
135
+ with create_manager_session(url, username, password, port, subdomain, logger) as session:
136
+ if api_client_class is None:
137
+ version = session.api_version.base_version
138
+ logger.debug(f"Choosing client for version {version}...")
139
+ client = load_client(session.api_version.base_version)
140
+ logger.debug(f"Client for version {version} loaded")
141
+ yield client(RequestAdapter(session=session, logger=logger))
142
+ elif _is_client_class(api_client_class):
143
+ logger.debug(f"Creating instance for client class {api_client_class}")
144
+ yield api_client_class(RequestAdapter(session=session, logger=logger))
145
+ else:
146
+ raise CatalystwanNotAClientException(f"{api_client_class} is not a client class")
147
+
148
+
149
+ @contextmanager
150
+ def copy_client(client: Client) -> Generator[Client, None, None]:
151
+ if _is_client_instance(client):
152
+ request_adapter = copy(client._request_adapter)
153
+ session = request_adapter.session
154
+ with session.login():
155
+ new_client = load_client(session.api_version.base_version)(request_adapter)
156
+ assert new_client.api_version == client.api_version
157
+ yield cast(Client, new_client)
158
+ else:
159
+ raise CatalystwanNotAClientException(f"{client} is not a client instance")
@@ -1,14 +1,15 @@
1
1
  from collections import deque
2
2
  from copy import deepcopy
3
- from dataclasses import fields, is_dataclass
3
+ from dataclasses import dataclass, fields, is_dataclass
4
4
  from functools import reduce
5
5
  from inspect import isclass, unwrap
6
- from typing import Any, Dict, List, Literal, Protocol, Tuple, Type, TypeVar, Union
6
+ from typing import Any, Dict, List, Literal, Optional, Protocol, Tuple, Type, TypeVar, Union, cast
7
7
 
8
8
  from catalystwan.core.exceptions import (
9
9
  CatalystwanModelInputException,
10
10
  CatalystwanModelValidationError,
11
11
  )
12
+ from catalystwan.core.models.utils import count_matching_keys
12
13
  from catalystwan.core.types import MODEL_TYPES, AliasPath, DataclassInstance
13
14
  from typing_extensions import Annotated, get_args, get_origin, get_type_hints
14
15
 
@@ -19,6 +20,13 @@ class ValueExtractorCallable(Protocol):
19
20
  def __call__(self, field_value: Any) -> Any: ...
20
21
 
21
22
 
23
+ @dataclass
24
+ class ExtractedValue:
25
+ value: Any
26
+ exact_match: bool
27
+ matched_keys: Optional[int] = None
28
+
29
+
22
30
  class ModelDeserializer:
23
31
  def __init__(self, model: Type[T]) -> None:
24
32
  self.model = model
@@ -47,7 +55,6 @@ class ModelDeserializer:
47
55
 
48
56
  def __check_errors(self):
49
57
  if self._exceptions:
50
- print(self._exceptions)
51
58
  # Put exceptions from current model first
52
59
  self._exceptions.sort(key=lambda x: isinstance(x, CatalystwanModelValidationError))
53
60
  current_model_errors = sum(
@@ -58,67 +65,91 @@ class ModelDeserializer:
58
65
  message += f"{exc}\n"
59
66
  raise CatalystwanModelValidationError(message)
60
67
 
61
- def __is_optional(self, t: Any) -> bool:
62
- if get_origin(t) is Union and type(None) in get_args(t):
63
- return True
64
- return False
65
-
66
- def __extract_type(self, field_type: Any, field_value: Any, field_name: str) -> Any:
68
+ def __extract_type(self, field_type: Any, field_value: Any, field_name: str) -> ExtractedValue:
67
69
  origin = get_origin(field_type)
68
70
  # check for simple types and classes
69
71
  if origin is None:
70
- if field_type is Any:
71
- return field_value
72
- if isinstance(field_value, field_type):
73
- return field_value
72
+ if field_type is Any or isinstance(field_value, field_type):
73
+ return ExtractedValue(value=field_value, exact_match=True)
74
+ # Do not cast bool values
75
+ elif field_type is bool:
76
+ ...
77
+ # False/Empty values (like empty string or list) can match to None
78
+ elif field_type is type(None):
79
+ if not field_value:
80
+ return ExtractedValue(value=None, exact_match=False)
74
81
  elif is_dataclass(field_type):
75
- assert isinstance(field_type, type)
76
- return deserialize(field_type, **field_value)
82
+ model_instance = deserialize(
83
+ cast(Type[DataclassInstance], field_type), **field_value
84
+ )
85
+ return ExtractedValue(
86
+ value=model_instance,
87
+ exact_match=False,
88
+ matched_keys=count_matching_keys(model_instance, field_value),
89
+ )
77
90
  elif isclass(unwrap(field_type)):
78
91
  if isinstance(field_value, dict):
79
- return field_type(**field_value)
92
+ return ExtractedValue(value=field_type(**field_value), exact_match=False)
80
93
  else:
81
94
  try:
82
- return field_type(field_value)
95
+ return ExtractedValue(value=field_type(field_value), exact_match=False)
83
96
  except ValueError:
84
97
  raise CatalystwanModelInputException(
85
98
  f"Unable to match or cast input value for {field_name} [expected_type={unwrap(field_type)}, input={field_value}, input_type={type(field_value)}]"
86
99
  )
100
+ # List is an exact match only if all of its elements are
87
101
  elif origin is list:
88
102
  if isinstance(field_value, list):
89
- return [
90
- self.__extract_type(get_args(field_type)[0], value, field_name)
91
- for value in field_value
92
- ]
93
- elif self.__is_optional(field_type):
94
- if field_value is None:
95
- return None
96
- else:
97
- try:
98
- return self.__extract_type(get_args(field_type)[0], field_value, field_name)
99
- except CatalystwanModelInputException as e:
100
- if not field_value:
101
- return None
102
- raise e
103
+ values = []
104
+ exact_match = True
105
+ for value in field_value:
106
+ extracted_value = self.__extract_type(
107
+ get_args(field_type)[0], value, field_name
108
+ )
109
+ values.append(extracted_value.value)
110
+ if not extracted_value.exact_match:
111
+ exact_match = False
112
+ return ExtractedValue(value=values, exact_match=exact_match)
103
113
  elif origin is Literal:
104
114
  for arg in get_args(field_type):
105
115
  try:
106
116
  if type(arg)(field_value) == arg:
107
- return type(arg)(field_value)
117
+ return ExtractedValue(
118
+ value=type(arg)(field_value), exact_match=type(arg) is type(field_value)
119
+ )
108
120
  except Exception:
109
121
  continue
110
122
  elif origin is Annotated:
111
123
  validator, caster = field_type.__metadata__
112
124
  if validator(field_value):
113
- return field_value
114
- return caster(field_value)
115
- # TODO: Currently, casting is done left-to-right. Searching deeper for a better match may be the way to go.
125
+ return ExtractedValue(value=field_value, exact_match=True)
126
+ return ExtractedValue(value=caster(field_value), exact_match=False)
127
+ # When parsing Unions, try to find the best match. Currently, it involves:
128
+ # 1. Finding the exact match
129
+ # 2. If not found, favors dataclasses - sorted by number of matched keys, then None values
130
+ # 3. If no dataclasses are present, return the leftmost matched argument
116
131
  elif origin is Union:
132
+ matches: List[ExtractedValue] = []
117
133
  for arg in get_args(field_type):
118
134
  try:
119
- return self.__extract_type(arg, field_value, field_name)
135
+ extracted_value = self.__extract_type(arg, field_value, field_name)
136
+ # exact match, return
137
+ if extracted_value.exact_match:
138
+ return extracted_value
139
+ else:
140
+ matches.append(extracted_value)
120
141
  except Exception:
121
142
  continue
143
+ # Only one element matched, return
144
+ if len(matches) == 1:
145
+ return matches[0]
146
+ # Only non-exact matches left, sort and return first element
147
+ elif len(matches) > 1:
148
+ matches.sort(
149
+ key=lambda x: (x.matched_keys is not None, x.matched_keys, x.value is None),
150
+ reverse=True,
151
+ )
152
+ return matches[0]
122
153
  # Correct type not found, add exception
123
154
  raise CatalystwanModelInputException(
124
155
  f"Unable to match or cast input value for {field_name} [expected_type={unwrap(field_type)}, input={field_value}, input_type={type(field_value)}]"
@@ -131,7 +162,7 @@ class ModelDeserializer:
131
162
  kwargs_copy = deepcopy(kwargs)
132
163
  new_args = []
133
164
  new_kwargs = {}
134
- field_types = get_type_hints(cls)
165
+ field_types = get_type_hints(cls, include_extras=True)
135
166
  for field in fields(cls):
136
167
  if not field.init:
137
168
  continue
@@ -141,7 +172,9 @@ class ModelDeserializer:
141
172
  field_value = args_copy.popleft()
142
173
  try:
143
174
  new_args.append(
144
- self.__extract_type(field_type, value_extractor(field_value), field.name)
175
+ self.__extract_type(
176
+ field_type, value_extractor(field_value), field.name
177
+ ).value
145
178
  )
146
179
  except (
147
180
  CatalystwanModelInputException,
@@ -165,7 +198,7 @@ class ModelDeserializer:
165
198
  try:
166
199
  new_kwargs[field.name] = self.__extract_type(
167
200
  field_type, value_extractor(field_value), field.name
168
- )
201
+ ).value
169
202
  except (
170
203
  CatalystwanModelInputException,
171
204
  CatalystwanModelValidationError,
@@ -0,0 +1,27 @@
1
+ from dataclasses import is_dataclass
2
+ from typing import TypeVar, cast
3
+
4
+ from catalystwan.core.types import DataclassInstance
5
+
6
+ DataclassType = TypeVar("DataclassType", bound=DataclassInstance)
7
+
8
+
9
+ def count_matching_keys(model: DataclassType, model_payload: dict):
10
+ matched_keys = 0
11
+ for key, value in model_payload.items():
12
+ try:
13
+ model_value = getattr(model, key)
14
+ matched_keys += 1
15
+ if is_dataclass(model_value) and isinstance(value, dict):
16
+ matched_keys += count_matching_keys(cast(DataclassType, model_value), value)
17
+ elif (
18
+ isinstance(model_value, list)
19
+ and all([is_dataclass(element) for element in model_value])
20
+ and isinstance(value, list)
21
+ ):
22
+ for model_v, input_v in zip(model_value, value):
23
+ matched_keys += count_matching_keys(model_v, input_v)
24
+ except AttributeError:
25
+ continue
26
+
27
+ return matched_keys