catalystwan 2.0.0a1__tar.gz → 2.0.0a3__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 (42) hide show
  1. {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/PKG-INFO +107 -2
  2. catalystwan-2.0.0a3/README.md +105 -0
  3. {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/pyproject.toml +6 -3
  4. catalystwan-2.0.0a3/src/catalystwan/core/__init__.py +2 -0
  5. catalystwan-2.0.0a3/src/catalystwan/core/client.py +159 -0
  6. catalystwan-2.0.0a3/src/catalystwan/core/loader.py +20 -0
  7. {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/src/catalystwan/core/models/deserialize.py +72 -39
  8. catalystwan-2.0.0a3/src/catalystwan/core/models/utils.py +27 -0
  9. {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/src/catalystwan/core/request_adapter.py +33 -22
  10. catalystwan-2.0.0a3/src/catalystwan/core/tests.py +15 -0
  11. {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/src/catalystwan.egg-info/PKG-INFO +107 -2
  12. {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/src/catalystwan.egg-info/SOURCES.txt +4 -1
  13. catalystwan-2.0.0a3/src/catalystwan.egg-info/entry_points.txt +2 -0
  14. {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/tests/test_ft_serialize.py +5 -13
  15. {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/tests/test_model_deserialize.py +102 -0
  16. {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/tests/test_parcel_serialize.py +4 -12
  17. catalystwan-2.0.0a1/README.rst +0 -38
  18. catalystwan-2.0.0a1/src/catalystwan/core/__init__.py +0 -2
  19. catalystwan-2.0.0a1/src/catalystwan/core/client.py +0 -71
  20. catalystwan-2.0.0a1/src/catalystwan/core/loader.py +0 -37
  21. {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/LICENSE +0 -0
  22. {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/setup.cfg +0 -0
  23. {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/src/catalystwan/core/abstractions.py +0 -0
  24. {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/src/catalystwan/core/apigw_auth.py +0 -0
  25. {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/src/catalystwan/core/encoder.py +0 -0
  26. {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/src/catalystwan/core/exceptions.py +0 -0
  27. {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/src/catalystwan/core/metadata.py +0 -0
  28. {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/src/catalystwan/core/models/__init__.py +0 -0
  29. {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/src/catalystwan/core/models/serialize.py +0 -0
  30. {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/src/catalystwan/core/py.typed +0 -0
  31. {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/src/catalystwan/core/request_limiter.py +0 -0
  32. {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/src/catalystwan/core/response.py +0 -0
  33. {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/src/catalystwan/core/session.py +0 -0
  34. {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/src/catalystwan/core/types.py +0 -0
  35. {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/src/catalystwan/core/version.py +0 -0
  36. {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/src/catalystwan/core/vmanage_auth.py +0 -0
  37. {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/src/catalystwan.egg-info/dependency_links.txt +0 -0
  38. {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/src/catalystwan.egg-info/requires.txt +0 -0
  39. {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/src/catalystwan.egg-info/top_level.txt +0 -0
  40. {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/tests/test_ft_deserialize.py +0 -0
  41. {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/tests/test_model_serialize.py +0 -0
  42. {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/tests/test_parcel_deserialize.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: catalystwan
3
- Version: 2.0.0a1
3
+ Version: 2.0.0a3
4
4
  Summary: Cisco Catalyst WAN SDK for Python
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
@@ -207,7 +207,6 @@ License: Apache License
207
207
  Project-URL: Homepage, https://github.com/cisco-en-programmability/catalystwan-sdk-next-python
208
208
  Classifier: Development Status :: 3 - Alpha
209
209
  Classifier: Intended Audience :: Developers
210
- Classifier: License :: OSI Approved :: Apache Software License
211
210
  Classifier: Programming Language :: Python
212
211
  Classifier: Programming Language :: Python :: 3
213
212
  Classifier: Programming Language :: Python :: 3.8
@@ -236,3 +235,109 @@ Provides-Extra: ver-2015
236
235
  Requires-Dist: catalystwan-v20-15==2.0.0a1; extra == "ver-2015"
237
236
  Provides-Extra: ver-2016
238
237
  Requires-Dist: catalystwan-v20-16==2.0.0a1; extra == "ver-2016"
238
+
239
+ Cisco Catalyst WAN SDK 2.0
240
+ ==========================
241
+
242
+ 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.
243
+
244
+ Overview
245
+ --------
246
+
247
+ 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.
248
+
249
+ Supported Catalystwan WAN Server Versions
250
+ -----------------------------------------
251
+
252
+ - 20.15
253
+ - 20.16
254
+
255
+ Cisco Catalyst WAN SDK – Early Access Release
256
+ ---------------------------------------------
257
+
258
+ We are excited to introduce the Cisco Catalyst WAN SDK in its early access release phase,
259
+ marking an important step in enabling developers to harness the full potential of Cisco's
260
+ networking solutions. This release provides a unique opportunity to explore and experiment
261
+ with the SDK's capabilities as we continue to refine and enhance its features.
262
+
263
+ As this version is part of an early development stage, it is provided "as is" and is still
264
+ undergoing active testing and iteration. While we are committed to supporting your experience
265
+ on a best-effort basis, we recommend exercising caution and conducting thorough testing before
266
+ deploying it in a production environment.
267
+
268
+ Your feedback during this phase is invaluable in shaping the SDK to meet the needs of our developer
269
+ community. Thank you for partnering with us on this journey to innovate and advance networking automation.
270
+
271
+ Not recommend to use in production environments.
272
+ ------------------------------------------------
273
+ Cisco Catalyst WAN SDK in its `pre-alpha` release phase. This marks a significant milestone
274
+ in empowering developers to unlock the full capabilities of Cisco's networking solutions.
275
+ Please note that, as a pre-alpha release, this version of the SDK is still in active development
276
+ and testing. It is provided "as is," with limited support offered on a best-effort basis.
277
+
278
+
279
+ Supported Python Versions
280
+ -------------------------
281
+
282
+ Python >= 3.8
283
+
284
+ > If you don't have a specific version, you can just use [Pyenv](https://github.com/pyenv/pyenv) to manage Python versions.
285
+
286
+
287
+ Installation
288
+ ------------
289
+
290
+ To install the SDK, run the following command:
291
+
292
+ ```bash
293
+ pip install catalystwan==2.0.0a0
294
+ ```
295
+
296
+ To manually install the necessary Python packages in editable mode, you can use the `pip install -e` command.
297
+
298
+ ```bash
299
+ pip install -e ./packages/catalystwan-types \
300
+ -e ./packages/catalystwan-core \
301
+ -e ./versions/catalystwan-v20_15 \
302
+ -e ./versions/catalystwan-v20_16
303
+ ```
304
+
305
+
306
+ Getting Started
307
+ ---------------
308
+
309
+ 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.
310
+
311
+ ### Example Usage
312
+
313
+ Here's a quick example of how to use the SDK:
314
+
315
+ ```python
316
+ from catalystwan.core import create_client
317
+
318
+ url = "example.com"
319
+ username = "admin"
320
+ password = "password123"
321
+
322
+ with create_client(url=url, username=username, password=password) as client:
323
+ result = client.health.devices.get_devices_health()
324
+ print(result)
325
+ ```
326
+
327
+ If you need to preform more complex operations that require models, they can utilize an alias: `m`.
328
+ ```python
329
+
330
+ with create_client(...) as client:
331
+ result = client.admin.aaa.update_aaa_config(
332
+ client.admin.aaa.m.Aaa(
333
+ accounting: True,
334
+ admin_auth_order: False,
335
+ audit_disable: False,
336
+ auth_fallback: False,
337
+ auth_order: ["local"]
338
+ )
339
+ )
340
+ print(result)
341
+ ```
342
+
343
+ 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,105 @@
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
+ Cisco Catalyst WAN SDK – Early Access Release
18
+ ---------------------------------------------
19
+
20
+ We are excited to introduce the Cisco Catalyst WAN SDK in its early access release phase,
21
+ marking an important step in enabling developers to harness the full potential of Cisco's
22
+ networking solutions. This release provides a unique opportunity to explore and experiment
23
+ with the SDK's capabilities as we continue to refine and enhance its features.
24
+
25
+ As this version is part of an early development stage, it is provided "as is" and is still
26
+ undergoing active testing and iteration. While we are committed to supporting your experience
27
+ on a best-effort basis, we recommend exercising caution and conducting thorough testing before
28
+ deploying it in a production environment.
29
+
30
+ Your feedback during this phase is invaluable in shaping the SDK to meet the needs of our developer
31
+ community. Thank you for partnering with us on this journey to innovate and advance networking automation.
32
+
33
+ Not recommend to use in production environments.
34
+ ------------------------------------------------
35
+ Cisco Catalyst WAN SDK in its `pre-alpha` release phase. This marks a significant milestone
36
+ in empowering developers to unlock the full capabilities of Cisco's networking solutions.
37
+ Please note that, as a pre-alpha release, this version of the SDK is still in active development
38
+ and testing. It is provided "as is," with limited support offered on a best-effort basis.
39
+
40
+
41
+ Supported Python Versions
42
+ -------------------------
43
+
44
+ Python >= 3.8
45
+
46
+ > If you don't have a specific version, you can just use [Pyenv](https://github.com/pyenv/pyenv) to manage Python versions.
47
+
48
+
49
+ Installation
50
+ ------------
51
+
52
+ To install the SDK, run the following command:
53
+
54
+ ```bash
55
+ pip install catalystwan==2.0.0a0
56
+ ```
57
+
58
+ To manually install the necessary Python packages in editable mode, you can use the `pip install -e` command.
59
+
60
+ ```bash
61
+ pip install -e ./packages/catalystwan-types \
62
+ -e ./packages/catalystwan-core \
63
+ -e ./versions/catalystwan-v20_15 \
64
+ -e ./versions/catalystwan-v20_16
65
+ ```
66
+
67
+
68
+ Getting Started
69
+ ---------------
70
+
71
+ 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.
72
+
73
+ ### Example Usage
74
+
75
+ Here's a quick example of how to use the SDK:
76
+
77
+ ```python
78
+ from catalystwan.core import create_client
79
+
80
+ url = "example.com"
81
+ username = "admin"
82
+ password = "password123"
83
+
84
+ with create_client(url=url, username=username, password=password) as client:
85
+ result = client.health.devices.get_devices_health()
86
+ print(result)
87
+ ```
88
+
89
+ If you need to preform more complex operations that require models, they can utilize an alias: `m`.
90
+ ```python
91
+
92
+ with create_client(...) as client:
93
+ result = client.admin.aaa.update_aaa_config(
94
+ client.admin.aaa.m.Aaa(
95
+ accounting: True,
96
+ admin_auth_order: False,
97
+ audit_disable: False,
98
+ auth_fallback: False,
99
+ auth_order: ["local"]
100
+ )
101
+ )
102
+ print(result)
103
+ ```
104
+
105
+ 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.0a3"
4
4
  description = "Cisco Catalyst WAN SDK for Python"
5
5
  dependencies = [
6
6
  "packaging >= 23.0",
@@ -12,7 +12,8 @@ dependencies = [
12
12
  "catalystwan-v20-16 == 2.0.0a1"
13
13
  ]
14
14
  readme = "README.md"
15
- license = {file = "LICENSE"}
15
+ license = { file = "LICENSE"}
16
+
16
17
  requires-python = ">= 3.8"
17
18
  classifiers = [
18
19
  # one of:
@@ -21,7 +22,6 @@ classifiers = [
21
22
  # "Development Status :: 5 - Production/Stable"
22
23
  "Development Status :: 3 - Alpha",
23
24
  "Intended Audience :: Developers",
24
- "License :: OSI Approved :: Apache Software License",
25
25
  "Programming Language :: Python",
26
26
  "Programming Language :: Python :: 3",
27
27
  "Programming Language :: Python :: 3.8",
@@ -80,3 +80,6 @@ exclude="loader\\.py$"
80
80
  mypy_path = "$MYPY_CONFIG_FILE_DIR/src/"
81
81
  namespace_packages = true
82
82
  explicit_package_bases = true
83
+
84
+ [project.entry-points.pytest11]
85
+ catalystwan = "catalystwan.core.tests"
@@ -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")
@@ -0,0 +1,20 @@
1
+ # This file is autogenerated and cannot be modified manually.
2
+
3
+ from __future__ import annotations
4
+
5
+ import typing as t
6
+
7
+ if t.TYPE_CHECKING:
8
+ from catalystwan.versions.v20_15.api_client import ApiClient as ApiClientV20_15
9
+
10
+ ApiClient = t.Union[ApiClientV20_15]
11
+
12
+ VERSIONS = ["20.15"]
13
+
14
+
15
+ def load_client(version: str) -> t.Type[ApiClient]:
16
+ if version == "20.15":
17
+ from catalystwan.versions.v20_15.api_client import ApiClient as ApiClientV20_15
18
+
19
+ return ApiClientV20_15
20
+ raise RuntimeError("Unsupported version: {}".format(version))
@@ -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