python-nso-client 0.1.1__tar.gz → 0.1.2__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.
- {python_nso_client-0.1.1 → python_nso_client-0.1.2}/Makefile +1 -1
- {python_nso_client-0.1.1 → python_nso_client-0.1.2}/PKG-INFO +1 -1
- {python_nso_client-0.1.1 → python_nso_client-0.1.2}/pyproject.toml +1 -1
- {python_nso_client-0.1.1 → python_nso_client-0.1.2}/src/nso_client/__init__.py +56 -59
- {python_nso_client-0.1.1 → python_nso_client-0.1.2}/src/nso_client/types.py +12 -0
- {python_nso_client-0.1.1 → python_nso_client-0.1.2}/tests/test_nso_client.py +2 -1
- {python_nso_client-0.1.1 → python_nso_client-0.1.2}/tests/test_patch.py +2 -2
- {python_nso_client-0.1.1 → python_nso_client-0.1.2}/uv.lock +1 -1
- {python_nso_client-0.1.1 → python_nso_client-0.1.2}/.allowed_signers +0 -0
- {python_nso_client-0.1.1 → python_nso_client-0.1.2}/.gitignore +0 -0
- {python_nso_client-0.1.1 → python_nso_client-0.1.2}/.gitlab-ci.yml +0 -0
- {python_nso_client-0.1.1 → python_nso_client-0.1.2}/.python-version +0 -0
- {python_nso_client-0.1.1 → python_nso_client-0.1.2}/LICENSE.txt +0 -0
- {python_nso_client-0.1.1 → python_nso_client-0.1.2}/README.md +0 -0
- {python_nso_client-0.1.1 → python_nso_client-0.1.2}/src/nso_client/exceptions.py +0 -0
- {python_nso_client-0.1.1 → python_nso_client-0.1.2}/src/nso_client/py.typed +0 -0
- {python_nso_client-0.1.1 → python_nso_client-0.1.2}/tests/fixtures.py +0 -0
- {python_nso_client-0.1.1 → python_nso_client-0.1.2}/util/pypi-token.py +0 -0
|
@@ -21,7 +21,7 @@ test: ## Run tests once, specify $TESTS for specific file/method
|
|
|
21
21
|
uv run pytest $(PYTEST_ARGS) $(TESTS)
|
|
22
22
|
|
|
23
23
|
watch-test: ## Run tests every time code changes
|
|
24
|
-
watchexec $(WATCHEXEC_ARGS)
|
|
24
|
+
watchexec $(WATCHEXEC_ARGS) uv run pytest $(PYTEST_ARGS) $(TESTS)
|
|
25
25
|
|
|
26
26
|
publish: ## Publish a relase to PyPI
|
|
27
27
|
rm -fr dist/
|
|
@@ -25,7 +25,7 @@ from nso_client.exceptions import (
|
|
|
25
25
|
RestConfError,
|
|
26
26
|
YangPatchError,
|
|
27
27
|
)
|
|
28
|
-
from nso_client.types import ContentType, InsertWhere, PatchType
|
|
28
|
+
from nso_client.types import ContentType, DryRunType, InsertWhere, PatchType, YangData
|
|
29
29
|
|
|
30
30
|
__all__ = [
|
|
31
31
|
"NSOClient",
|
|
@@ -44,7 +44,7 @@ class NSOCommitMode(Enum):
|
|
|
44
44
|
NO_DEPLOY = "no-deploy"
|
|
45
45
|
|
|
46
46
|
@classmethod
|
|
47
|
-
def _missing_(cls, value:
|
|
47
|
+
def _missing_(cls, value: object):
|
|
48
48
|
raise Exception(
|
|
49
49
|
f"{value} is not a valid {cls.__name__}"
|
|
50
50
|
f"valid options are {cls._member_names_}"
|
|
@@ -68,7 +68,7 @@ class NSOClient:
|
|
|
68
68
|
auth: BasicAuth | None = None,
|
|
69
69
|
verify: bool | ssl.SSLContext = True,
|
|
70
70
|
restconf_path: str = "/restconf/data",
|
|
71
|
-
logger: BoundLogger = None,
|
|
71
|
+
logger: BoundLogger | None = None,
|
|
72
72
|
**commit_kwargs,
|
|
73
73
|
):
|
|
74
74
|
"""
|
|
@@ -90,7 +90,7 @@ class NSOClient:
|
|
|
90
90
|
},
|
|
91
91
|
)
|
|
92
92
|
self.nso_url = nso_url + restconf_path
|
|
93
|
-
if
|
|
93
|
+
if logger is None:
|
|
94
94
|
logger = structlog.get_logger()
|
|
95
95
|
self.log = logger.bind(lib="NSOClient")
|
|
96
96
|
self.commit_kwargs = commit_kwargs
|
|
@@ -98,9 +98,9 @@ class NSOClient:
|
|
|
98
98
|
def get(
|
|
99
99
|
self,
|
|
100
100
|
path: str,
|
|
101
|
-
*path_params:
|
|
101
|
+
*path_params: str,
|
|
102
102
|
**kwargs,
|
|
103
|
-
) ->
|
|
103
|
+
) -> YangData:
|
|
104
104
|
"""
|
|
105
105
|
Retrieve RestConf data at path.
|
|
106
106
|
|
|
@@ -113,7 +113,7 @@ class NSOClient:
|
|
|
113
113
|
Raises: RestConfError or a sub-class
|
|
114
114
|
"""
|
|
115
115
|
url = self._make_url(path, path_params)
|
|
116
|
-
query_params = self.
|
|
116
|
+
query_params = self._query_params(**kwargs)
|
|
117
117
|
response = self.session.get(url, params=query_params)
|
|
118
118
|
self.log.debug(
|
|
119
119
|
"NSO Query",
|
|
@@ -135,8 +135,8 @@ class NSOClient:
|
|
|
135
135
|
def put(
|
|
136
136
|
self,
|
|
137
137
|
path: str,
|
|
138
|
-
*path_params:
|
|
139
|
-
payload:
|
|
138
|
+
*path_params: str,
|
|
139
|
+
payload: YangData,
|
|
140
140
|
**kwargs,
|
|
141
141
|
) -> None | DryRunResult:
|
|
142
142
|
"""
|
|
@@ -152,7 +152,7 @@ class NSOClient:
|
|
|
152
152
|
Raises: RestConfError or a sub-class
|
|
153
153
|
"""
|
|
154
154
|
url = self._make_url(path, path_params)
|
|
155
|
-
query_params = self.
|
|
155
|
+
query_params = self._query_params(**kwargs)
|
|
156
156
|
response = self.session.put(url, params=query_params, json=payload)
|
|
157
157
|
self.log.info(
|
|
158
158
|
"NSO Commit",
|
|
@@ -171,7 +171,7 @@ class NSOClient:
|
|
|
171
171
|
def delete(
|
|
172
172
|
self,
|
|
173
173
|
path: str,
|
|
174
|
-
*path_params:
|
|
174
|
+
*path_params: str,
|
|
175
175
|
**kwargs,
|
|
176
176
|
) -> None | DryRunResult:
|
|
177
177
|
"""
|
|
@@ -186,7 +186,7 @@ class NSOClient:
|
|
|
186
186
|
Raises: RestConfError or a sub-class
|
|
187
187
|
"""
|
|
188
188
|
url = self._make_url(path, path_params)
|
|
189
|
-
query_params = self.
|
|
189
|
+
query_params = self._query_params(**kwargs)
|
|
190
190
|
response = self.session.delete(url, params=query_params)
|
|
191
191
|
self.log.info(
|
|
192
192
|
"NSO Commit",
|
|
@@ -204,8 +204,8 @@ class NSOClient:
|
|
|
204
204
|
def post(
|
|
205
205
|
self,
|
|
206
206
|
path: str,
|
|
207
|
-
*path_params:
|
|
208
|
-
payload:
|
|
207
|
+
*path_params: str,
|
|
208
|
+
payload: YangData,
|
|
209
209
|
**kwargs,
|
|
210
210
|
):
|
|
211
211
|
"""
|
|
@@ -219,7 +219,7 @@ class NSOClient:
|
|
|
219
219
|
Raises: RestConfError or a sub-class
|
|
220
220
|
"""
|
|
221
221
|
url = self._make_url(path, path_params)
|
|
222
|
-
query_params = self.
|
|
222
|
+
query_params = self._query_params(**kwargs)
|
|
223
223
|
response = self.session.post(url, json=payload, params=query_params)
|
|
224
224
|
self.log.info(
|
|
225
225
|
"NSO Commit/Call",
|
|
@@ -237,12 +237,12 @@ class NSOClient:
|
|
|
237
237
|
def patch(
|
|
238
238
|
self,
|
|
239
239
|
path: str,
|
|
240
|
-
*path_params:
|
|
241
|
-
payload:
|
|
240
|
+
*path_params: str,
|
|
241
|
+
payload: YangData,
|
|
242
242
|
style: PatchType,
|
|
243
243
|
ignore_codes=(),
|
|
244
244
|
**kwargs,
|
|
245
|
-
) ->
|
|
245
|
+
) -> YangData | DryRunResult:
|
|
246
246
|
"""
|
|
247
247
|
PATCH data to a given path
|
|
248
248
|
|
|
@@ -273,7 +273,7 @@ class NSOClient:
|
|
|
273
273
|
- https://datatracker.ietf.org/doc/html/draft-ietf-netconf-yang-patch-14#appendix-D.1.5
|
|
274
274
|
"""
|
|
275
275
|
url = self._make_url(path, path_params)
|
|
276
|
-
query_params = self.
|
|
276
|
+
query_params = self._query_params(**kwargs)
|
|
277
277
|
|
|
278
278
|
# If using YANG-Patch, change content-type
|
|
279
279
|
headers = {}
|
|
@@ -301,7 +301,11 @@ class NSOClient:
|
|
|
301
301
|
)
|
|
302
302
|
return self._parse_response(response, ignore_codes)
|
|
303
303
|
|
|
304
|
-
def yang_patch(
|
|
304
|
+
def yang_patch(
|
|
305
|
+
self,
|
|
306
|
+
path: str,
|
|
307
|
+
*path_params: str,
|
|
308
|
+
) -> Patch:
|
|
305
309
|
"""Prepare a request using the YANG-Patch.
|
|
306
310
|
|
|
307
311
|
Example usage:
|
|
@@ -321,7 +325,7 @@ class NSOClient:
|
|
|
321
325
|
self,
|
|
322
326
|
response: httpx.Response,
|
|
323
327
|
ignore_codes: list[int] = [],
|
|
324
|
-
) ->
|
|
328
|
+
) -> YangData | DryRunResult:
|
|
325
329
|
self._raise_if_error(response, ignore_codes)
|
|
326
330
|
if len(response.content) == 0:
|
|
327
331
|
return None
|
|
@@ -330,7 +334,7 @@ class NSOClient:
|
|
|
330
334
|
return DryRunResult(rv)
|
|
331
335
|
return rv
|
|
332
336
|
|
|
333
|
-
def
|
|
337
|
+
def _query_params(
|
|
334
338
|
self,
|
|
335
339
|
**kwargs,
|
|
336
340
|
) -> dict[str, str]:
|
|
@@ -353,7 +357,7 @@ class NSOClient:
|
|
|
353
357
|
if "unhide" in kwargs:
|
|
354
358
|
unhide = kwargs.pop("unhide")
|
|
355
359
|
if unhide:
|
|
356
|
-
query_params["unhide"] = ",".join()
|
|
360
|
+
query_params["unhide"] = ",".join(unhide)
|
|
357
361
|
if "dry_run" in kwargs:
|
|
358
362
|
query_params["dry-run"] = kwargs.pop("dry_run")
|
|
359
363
|
if "content" in kwargs:
|
|
@@ -415,7 +419,7 @@ class Patch:
|
|
|
415
419
|
path: str
|
|
416
420
|
path_params: list[str]
|
|
417
421
|
unhide: list[str]
|
|
418
|
-
edits
|
|
422
|
+
edits: list[dict[str, YangData]]
|
|
419
423
|
|
|
420
424
|
def __init__(
|
|
421
425
|
self,
|
|
@@ -441,9 +445,9 @@ class Patch:
|
|
|
441
445
|
def create(
|
|
442
446
|
self,
|
|
443
447
|
path: str,
|
|
444
|
-
*path_params:
|
|
445
|
-
value:
|
|
446
|
-
edit_id: str = None,
|
|
448
|
+
*path_params: str,
|
|
449
|
+
value: YangData,
|
|
450
|
+
edit_id: str | None = None,
|
|
447
451
|
) -> str:
|
|
448
452
|
"""Create an element at a path, fail if it this path already exists
|
|
449
453
|
path: Target path to modify, may include {}
|
|
@@ -469,8 +473,8 @@ class Patch:
|
|
|
469
473
|
def delete(
|
|
470
474
|
self,
|
|
471
475
|
path: str,
|
|
472
|
-
*path_params:
|
|
473
|
-
edit_id: str = None,
|
|
476
|
+
*path_params: str,
|
|
477
|
+
edit_id: str | None = None,
|
|
474
478
|
):
|
|
475
479
|
"""Delete YANG data, fail if it does not exist
|
|
476
480
|
path: Target path to modify, may include {}
|
|
@@ -493,11 +497,11 @@ class Patch:
|
|
|
493
497
|
def insert(
|
|
494
498
|
self,
|
|
495
499
|
path: str,
|
|
496
|
-
*path_params:
|
|
500
|
+
*path_params: str,
|
|
497
501
|
where: InsertWhere,
|
|
498
|
-
point: str = None,
|
|
499
|
-
value:
|
|
500
|
-
edit_id: str = None,
|
|
502
|
+
point: str | None = None,
|
|
503
|
+
value: YangData,
|
|
504
|
+
edit_id: str | None = None,
|
|
501
505
|
):
|
|
502
506
|
"""Create an element at a path, inserting before/after an existing element. Only valid for user-ordered items
|
|
503
507
|
|
|
@@ -529,9 +533,9 @@ class Patch:
|
|
|
529
533
|
def merge(
|
|
530
534
|
self,
|
|
531
535
|
path: str,
|
|
532
|
-
*path_params:
|
|
533
|
-
value:
|
|
534
|
-
edit_id: str = None,
|
|
536
|
+
*path_params: str,
|
|
537
|
+
value: YangData,
|
|
538
|
+
edit_id: str | None = None,
|
|
535
539
|
):
|
|
536
540
|
"""Merge data into an existing path
|
|
537
541
|
|
|
@@ -558,10 +562,10 @@ class Patch:
|
|
|
558
562
|
def move(
|
|
559
563
|
self,
|
|
560
564
|
path: str,
|
|
561
|
-
*path_params:
|
|
565
|
+
*path_params: str,
|
|
562
566
|
where: InsertWhere,
|
|
563
|
-
point: str = None,
|
|
564
|
-
edit_id: str = None,
|
|
567
|
+
point: str | None = None,
|
|
568
|
+
edit_id: str | None = None,
|
|
565
569
|
):
|
|
566
570
|
"""Move an existing element, only valid for user-ordered lists
|
|
567
571
|
|
|
@@ -591,12 +595,10 @@ class Patch:
|
|
|
591
595
|
def replace(
|
|
592
596
|
self,
|
|
593
597
|
path: str,
|
|
594
|
-
*path_params:
|
|
595
|
-
value:
|
|
596
|
-
edit_id: str = None,
|
|
598
|
+
*path_params: str,
|
|
599
|
+
value: YangData,
|
|
600
|
+
edit_id: str | None = None,
|
|
597
601
|
):
|
|
598
|
-
pass
|
|
599
|
-
|
|
600
602
|
"""Delete YANG data, succeed if it's already gone
|
|
601
603
|
path: Target path to modify, may include {}
|
|
602
604
|
path_params: Values to fill into {} part of the format
|
|
@@ -618,8 +620,8 @@ class Patch:
|
|
|
618
620
|
|
|
619
621
|
def commit(
|
|
620
622
|
self,
|
|
621
|
-
patch_id: str = None,
|
|
622
|
-
comment: str = None,
|
|
623
|
+
patch_id: str | None = None,
|
|
624
|
+
comment: str | None = None,
|
|
623
625
|
**kwargs,
|
|
624
626
|
) -> PatchResult | DryRunResult:
|
|
625
627
|
"""Execute the prepared patch. Patch is executed atomically
|
|
@@ -654,7 +656,7 @@ class Patch:
|
|
|
654
656
|
edits = resp.get("edit-status", {}).get("edits", [])
|
|
655
657
|
return PatchResult(
|
|
656
658
|
patch_id=resp["patch-id"],
|
|
657
|
-
edits={e["edit-id"] for e in edits},
|
|
659
|
+
edits={e["edit-id"]: e for e in edits},
|
|
658
660
|
ok=("ok" in resp),
|
|
659
661
|
)
|
|
660
662
|
else:
|
|
@@ -668,7 +670,7 @@ class PatchResult:
|
|
|
668
670
|
"""Result of a Patch operation"""
|
|
669
671
|
|
|
670
672
|
patch_id: str
|
|
671
|
-
edits: dict[str,
|
|
673
|
+
edits: dict[str, YangData]
|
|
672
674
|
ok: bool
|
|
673
675
|
|
|
674
676
|
def raise_if_error(self):
|
|
@@ -682,26 +684,21 @@ class DryRunResult:
|
|
|
682
684
|
result.changes # => {"local-node": "..."}
|
|
683
685
|
"""
|
|
684
686
|
|
|
685
|
-
class DryRunType(Enum):
|
|
686
|
-
CLI = "cli"
|
|
687
|
-
XML = "xml"
|
|
688
|
-
NATIVE = "NATIVE"
|
|
689
|
-
|
|
690
687
|
dry_run: DryRunType
|
|
691
|
-
changes: dict[str,
|
|
688
|
+
changes: dict[str, YangData] # "local-node" -> "data"
|
|
692
689
|
|
|
693
|
-
def __init__(self, response: dict[str,
|
|
690
|
+
def __init__(self, response: dict[str, YangData]):
|
|
694
691
|
"""Construct a response from a RestConf response"""
|
|
695
692
|
response = response["dry-run-result"]
|
|
696
|
-
changes = {}
|
|
693
|
+
changes: dict[str, YangData] = {}
|
|
697
694
|
if "cli" in response:
|
|
698
|
-
self.dry_run =
|
|
695
|
+
self.dry_run = DryRunType.CLI
|
|
699
696
|
changes = response["cli"]
|
|
700
697
|
elif "result-xml" in response:
|
|
701
|
-
self.dry_run =
|
|
698
|
+
self.dry_run = DryRunType.XML
|
|
702
699
|
changes = response["result-xml"]
|
|
703
700
|
elif "native" in response:
|
|
704
|
-
self.dry_run =
|
|
701
|
+
self.dry_run = DryRunType.CLI
|
|
705
702
|
changes = response["native"]
|
|
706
703
|
else:
|
|
707
704
|
raise NotImplementedError(
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
|
+
from typing import TypeAlias
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
class ContentType(str, Enum):
|
|
@@ -16,3 +17,14 @@ class InsertWhere(str, Enum):
|
|
|
16
17
|
LAST = "last"
|
|
17
18
|
BEFORE = "before"
|
|
18
19
|
AFTER = "after"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
YangData: TypeAlias = (
|
|
23
|
+
None | bool | int | float | str | list["YangData"] | dict[str, "YangData"]
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DryRunType(str, Enum):
|
|
28
|
+
CLI = "cli"
|
|
29
|
+
XML = "xml"
|
|
30
|
+
NATIVE = "NATIVE"
|
|
@@ -19,6 +19,7 @@ from nso_client import (
|
|
|
19
19
|
NotFoundError,
|
|
20
20
|
NSOClient,
|
|
21
21
|
)
|
|
22
|
+
from nso_client.types import DryRunType
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
def test_get_200(httpx_mock: HTTPXMock):
|
|
@@ -166,7 +167,7 @@ def test_put_200_dry_run(httpx_mock: HTTPXMock):
|
|
|
166
167
|
|
|
167
168
|
# Check
|
|
168
169
|
assert isinstance(resp, DryRunResult)
|
|
169
|
-
assert resp.dry_run is
|
|
170
|
+
assert resp.dry_run is DryRunType.CLI
|
|
170
171
|
assert resp.changes["local-node"]
|
|
171
172
|
assert len(httpx_mock.get_requests()) == 1
|
|
172
173
|
|
|
@@ -11,7 +11,7 @@ from fixtures import (
|
|
|
11
11
|
)
|
|
12
12
|
from nso_client import DryRunResult, NSOClient
|
|
13
13
|
from nso_client.exceptions import PatchError, YangPatchError
|
|
14
|
-
from nso_client.types import InsertWhere
|
|
14
|
+
from nso_client.types import DryRunType, InsertWhere
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def test_patch_merge_200(httpx_mock: HTTPXMock):
|
|
@@ -153,7 +153,7 @@ def test_patch_create_dry_run_200(httpx_mock: HTTPXMock):
|
|
|
153
153
|
|
|
154
154
|
# Check that the response read correctly
|
|
155
155
|
assert type(resp) is DryRunResult
|
|
156
|
-
assert resp.dry_run ==
|
|
156
|
+
assert resp.dry_run == DryRunType.CLI
|
|
157
157
|
assert resp.changes["local-node"] == "Some changes"
|
|
158
158
|
|
|
159
159
|
# Check that the call was made correctly
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|