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.
- {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/PKG-INFO +107 -2
- catalystwan-2.0.0a3/README.md +105 -0
- {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/pyproject.toml +6 -3
- catalystwan-2.0.0a3/src/catalystwan/core/__init__.py +2 -0
- catalystwan-2.0.0a3/src/catalystwan/core/client.py +159 -0
- catalystwan-2.0.0a3/src/catalystwan/core/loader.py +20 -0
- {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/src/catalystwan/core/models/deserialize.py +72 -39
- catalystwan-2.0.0a3/src/catalystwan/core/models/utils.py +27 -0
- {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/src/catalystwan/core/request_adapter.py +33 -22
- catalystwan-2.0.0a3/src/catalystwan/core/tests.py +15 -0
- {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/src/catalystwan.egg-info/PKG-INFO +107 -2
- {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/src/catalystwan.egg-info/SOURCES.txt +4 -1
- catalystwan-2.0.0a3/src/catalystwan.egg-info/entry_points.txt +2 -0
- {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/tests/test_ft_serialize.py +5 -13
- {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/tests/test_model_deserialize.py +102 -0
- {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/tests/test_parcel_serialize.py +4 -12
- catalystwan-2.0.0a1/README.rst +0 -38
- catalystwan-2.0.0a1/src/catalystwan/core/__init__.py +0 -2
- catalystwan-2.0.0a1/src/catalystwan/core/client.py +0 -71
- catalystwan-2.0.0a1/src/catalystwan/core/loader.py +0 -37
- {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/LICENSE +0 -0
- {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/setup.cfg +0 -0
- {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/src/catalystwan/core/abstractions.py +0 -0
- {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/src/catalystwan/core/apigw_auth.py +0 -0
- {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/src/catalystwan/core/encoder.py +0 -0
- {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/src/catalystwan/core/exceptions.py +0 -0
- {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/src/catalystwan/core/metadata.py +0 -0
- {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/src/catalystwan/core/models/__init__.py +0 -0
- {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/src/catalystwan/core/models/serialize.py +0 -0
- {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/src/catalystwan/core/py.typed +0 -0
- {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/src/catalystwan/core/request_limiter.py +0 -0
- {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/src/catalystwan/core/response.py +0 -0
- {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/src/catalystwan/core/session.py +0 -0
- {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/src/catalystwan/core/types.py +0 -0
- {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/src/catalystwan/core/version.py +0 -0
- {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/src/catalystwan/core/vmanage_auth.py +0 -0
- {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/src/catalystwan.egg-info/dependency_links.txt +0 -0
- {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/src/catalystwan.egg-info/requires.txt +0 -0
- {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/src/catalystwan.egg-info/top_level.txt +0 -0
- {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/tests/test_ft_deserialize.py +0 -0
- {catalystwan-2.0.0a1 → catalystwan-2.0.0a3}/tests/test_model_serialize.py +0 -0
- {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.
|
|
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.
|
|
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,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
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
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
|
-
#
|
|
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
|
-
|
|
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(
|
|
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
|