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.
@@ -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) poetry run pytest $(PYTEST_ARGS) $(TESTS)
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/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-nso-client
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Add your description here
5
5
  Author-email: James Harr <jharr@internet2.edu>
6
6
  License-Expression: Apache-2.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "python-nso-client"
3
- version = "0.1.1"
3
+ version = "0.1.2"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -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: str):
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 not logger:
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: list[str],
101
+ *path_params: str,
102
102
  **kwargs,
103
- ) -> list | dict:
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.query_params(**kwargs)
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: list[str],
139
- payload: dict[str, str],
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.query_params(**kwargs)
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: list[str],
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.query_params(**kwargs)
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: list[str],
208
- payload: dict[str, str],
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.query_params(**kwargs)
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: list[str],
241
- payload: dict[str, str],
240
+ *path_params: str,
241
+ payload: YangData,
242
242
  style: PatchType,
243
243
  ignore_codes=(),
244
244
  **kwargs,
245
- ) -> PatchResult | DryRunResult:
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.query_params(**kwargs)
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(self, path: str, *path_params: list[str]) -> 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
- ) -> dict | DryRunResult:
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 query_params(
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 = list[dict]
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: list[str],
445
- value: dict[str, str],
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: list[str],
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: list[str],
500
+ *path_params: str,
497
501
  where: InsertWhere,
498
- point: str = None,
499
- value: dict[str, str],
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: list[str],
533
- value: dict[str, str],
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: list[str],
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: list[str],
595
- value: dict[str, str],
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, dict]
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, str] | None # "local-node" -> "data"
688
+ changes: dict[str, YangData] # "local-node" -> "data"
692
689
 
693
- def __init__(self, response: dict[str, any]):
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 = self.DryRunType.CLI
695
+ self.dry_run = DryRunType.CLI
699
696
  changes = response["cli"]
700
697
  elif "result-xml" in response:
701
- self.dry_run = self.DryRunType.XML
698
+ self.dry_run = DryRunType.XML
702
699
  changes = response["result-xml"]
703
700
  elif "native" in response:
704
- self.dry_run = self.DryRunType.CLI
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 DryRunResult.DryRunType.CLI
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 == DryRunResult.DryRunType.CLI
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
@@ -205,7 +205,7 @@ wheels = [
205
205
 
206
206
  [[package]]
207
207
  name = "python-nso-client"
208
- version = "0.1.1"
208
+ version = "0.1.2"
209
209
  source = { editable = "." }
210
210
  dependencies = [
211
211
  { name = "httpx" },