annet 3.18.0__py3-none-any.whl → 3.20.0__py3-none-any.whl

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.

Potentially problematic release.


This version of annet might be problematic. Click here for more details.

@@ -1,7 +1,11 @@
1
1
  import ssl
2
2
  from ipaddress import ip_interface
3
3
  from logging import getLogger
4
- from typing import Any, Optional, List, Union, Dict, cast, Generic, TypeVar
4
+ from typing import Any, Callable, Optional, List, Union, Dict, cast, Generic, TypeVar
5
+
6
+ from requests import Session
7
+
8
+ from requests_cache import CachedSession, SQLiteCache
5
9
 
6
10
  from annetbox.v37 import models as api_models
7
11
 
@@ -45,6 +49,7 @@ class BaseNetboxStorage(
45
49
  token = ""
46
50
  self.exact_host_filter = False
47
51
  threads = 1
52
+ session_factory = None
48
53
  if opts:
49
54
  if opts.insecure:
50
55
  ctx = ssl.create_default_context()
@@ -55,7 +60,17 @@ class BaseNetboxStorage(
55
60
  threads = opts.threads
56
61
  self.exact_host_filter = opts.exact_host_filter
57
62
  self.all_hosts_filter = opts.all_hosts_filter
58
- self.netbox = self._init_adapter(url=url, token=token, ssl_context=ctx, threads=threads)
63
+
64
+ if opts.cache_path:
65
+ session_factory = cached_requests_session(opts)
66
+
67
+ self.netbox = self._init_adapter(
68
+ url=url,
69
+ token=token,
70
+ ssl_context=ctx,
71
+ threads=threads,
72
+ session_factory=session_factory,
73
+ )
59
74
  self._all_fqdns: Optional[list[str]] = None
60
75
  self._id_devices: dict[int, NetboxDeviceT] = {}
61
76
  self._name_devices: dict[str, NetboxDeviceT] = {}
@@ -67,6 +82,7 @@ class BaseNetboxStorage(
67
82
  token: str,
68
83
  ssl_context: Optional[ssl.SSLContext],
69
84
  threads: int,
85
+ session_factory: Callable[[Session], Session] | None = None,
70
86
  ) -> NetboxAdapter[NetboxDeviceT, InterfaceT, IpAddressT, PrefixT, FHRPGroupT, FHRPGroupAssignmentT]:
71
87
  raise NotImplementedError()
72
88
 
@@ -259,3 +275,16 @@ def parse_glob(exact_host_filter: bool, query: NetboxQuery) -> dict[str, list[st
259
275
  else:
260
276
  query_groups["name__ic"] = [_hostname_dot_hack(name) for name in names]
261
277
  return query_groups
278
+
279
+
280
+ def cached_requests_session(opts: NetboxStorageOpts) -> Callable[[Session], Session]:
281
+ def session_factory(session: Session) -> Session:
282
+ backend = SQLiteCache(db_path=opts.cache_path)
283
+ if opts.recache:
284
+ backend.clear()
285
+ return CachedSession.wrap(
286
+ session,
287
+ backend=backend,
288
+ expire_after=opts.cache_ttl,
289
+ )
290
+ return session_factory
@@ -1,5 +1,6 @@
1
1
  import os
2
2
  from typing import Any, Optional
3
+ from datetime import timedelta
3
4
  from .query import parse_query
4
5
 
5
6
  DEFAULT_URL = "http://localhost"
@@ -14,6 +15,9 @@ class NetboxStorageOpts:
14
15
  exact_host_filter: bool = False,
15
16
  threads: int = 1,
16
17
  all_hosts_filter: dict[str, list[str]] | None = None,
18
+ cache_path: str = "",
19
+ cache_ttl: timedelta = timedelta(0),
20
+ recache: bool = False,
17
21
  ):
18
22
  self.url = url
19
23
  self.token = token
@@ -21,6 +25,9 @@ class NetboxStorageOpts:
21
25
  self.exact_host_filter = exact_host_filter
22
26
  self.threads = threads
23
27
  self.all_hosts_filter: dict[str, list[str]] = all_hosts_filter or {}
28
+ self.cache_path = cache_path
29
+ self.cache_ttl = cache_ttl
30
+ self.recache = recache
24
31
 
25
32
  @classmethod
26
33
  def parse_params(cls, conf_params: Optional[dict[str, str]], cli_opts: Any):
@@ -41,6 +48,17 @@ class NetboxStorageOpts:
41
48
  exact_host_filter = exact_host_filter_env in ("true", "1", "t")
42
49
  else:
43
50
  exact_host_filter = bool(conf_params.get("exact_host_filter") or False)
51
+
52
+ if cache_path := os.getenv("NETBOX_CACHE_PATH"):
53
+ cache_path = cache_path
54
+ else:
55
+ cache_path = str(conf_params.get("cache_path", ""))
56
+
57
+ if cache_ttl := os.getenv("NETBOX_CACHE_TTL"):
58
+ cache_ttl = timedelta(seconds=int(cache_ttl))
59
+ else:
60
+ cache_ttl = timedelta(seconds=conf_params.get("cache_ttl", 0))
61
+
44
62
  return cls(
45
63
  url=url,
46
64
  token=token,
@@ -48,4 +66,7 @@ class NetboxStorageOpts:
48
66
  exact_host_filter=exact_host_filter,
49
67
  threads=int(threads),
50
68
  all_hosts_filter=all_hosts_filter,
69
+ cache_path=cache_path,
70
+ cache_ttl=cache_ttl,
71
+ recache=cli_opts.recache,
51
72
  )
@@ -46,7 +46,8 @@ def storage_factory(opts: NetboxStorageOpts) -> Storage:
46
46
 
47
47
  class NetboxProvider(StorageProvider, AdapterWithName, AdapterWithConfig):
48
48
  def __init__(self, url: Optional[str] = None, token: Optional[str] = None, insecure: bool = False,
49
- exact_host_filter: bool = False, threads: int = 1, all_hosts_filter: dict[str, list[str]] | None = None):
49
+ exact_host_filter: bool = False, threads: int = 1, all_hosts_filter: dict[str, list[str]] | None = None,
50
+ cache_path: str = "", cache_ttl: int = 0):
50
51
  self.url = url
51
52
  self.token = token
52
53
  self.insecure = insecure
@@ -1,4 +1,7 @@
1
1
  import ssl
2
+ from typing import Callable
3
+
4
+ from requests import Session
2
5
 
3
6
  from adaptix import P
4
7
  from adaptix.conversion import get_converter, link_function, link_constant, link
@@ -23,8 +26,9 @@ class NetboxV37Adapter(NetboxAdapter[
23
26
  token: str,
24
27
  ssl_context: ssl.SSLContext | None,
25
28
  threads: int,
29
+ session_factory: Callable[[Session], Session] | None,
26
30
  ):
27
- self.netbox = client_sync.NetboxV37(url=url, token=token, ssl_context=ssl_context, threads=threads)
31
+ self.netbox = client_sync.NetboxV37(url=url, token=token, ssl_context=ssl_context, threads=threads, session_factory=session_factory)
28
32
  self.convert_device = get_converter(
29
33
  api_models.Device,
30
34
  NetboxDeviceV37,
@@ -120,5 +124,6 @@ class NetboxStorageV37(BaseNetboxStorage[
120
124
  token: str,
121
125
  ssl_context: ssl.SSLContext | None,
122
126
  threads: int,
127
+ session_factory: Callable[[Session], Session] | None = None,
123
128
  ) -> NetboxAdapter[NetboxDeviceV37, InterfaceV37, IpAddressV37, PrefixV37, FHRPGroupV37, FHRPGroupAssignmentV37]:
124
- return NetboxV37Adapter(self, url, token, ssl_context, threads)
129
+ return NetboxV37Adapter(self, url, token, ssl_context, threads, session_factory)
@@ -1,4 +1,8 @@
1
1
  import ssl
2
+ from typing import Callable
3
+
4
+ from requests import Session
5
+
2
6
  from adaptix import P
3
7
  from adaptix.conversion import get_converter, link, link_constant, link_function
4
8
  from annetbox.v41 import client_sync
@@ -24,8 +28,9 @@ class NetboxV41Adapter(NetboxAdapter[
24
28
  token: str,
25
29
  ssl_context: ssl.SSLContext | None,
26
30
  threads: int,
31
+ session_factory: Callable[[Session], Session] | None,
27
32
  ):
28
- self.netbox = client_sync.NetboxV41(url=url, token=token, ssl_context=ssl_context, threads=threads)
33
+ self.netbox = client_sync.NetboxV41(url=url, token=token, ssl_context=ssl_context, threads=threads, session_factory=session_factory)
29
34
  self.convert_device = get_converter(
30
35
  api_models.Device,
31
36
  NetboxDeviceV41,
@@ -122,5 +127,6 @@ class NetboxStorageV41(BaseNetboxStorage[
122
127
  token: str,
123
128
  ssl_context: ssl.SSLContext | None,
124
129
  threads: int,
130
+ session_factory: Callable[[Session], Session] | None = None,
125
131
  ) -> NetboxAdapter[NetboxDeviceV41, InterfaceV41, IpAddressV41, PrefixV41, FHRPGroupV41, FHRPGroupAssignmentV41]:
126
- return NetboxV41Adapter(self, url, token, ssl_context, threads)
132
+ return NetboxV41Adapter(self, url, token, ssl_context, threads, session_factory)
@@ -1,5 +1,7 @@
1
1
  import ssl
2
- from typing import Optional
2
+ from typing import Callable, Optional
3
+
4
+ from requests import Session
3
5
 
4
6
  from adaptix import P
5
7
  from adaptix.conversion import get_converter, link, link_constant, link_function
@@ -25,8 +27,9 @@ class NetboxV42Adapter(NetboxAdapter[
25
27
  token: str,
26
28
  ssl_context: ssl.SSLContext | None,
27
29
  threads: int,
30
+ session_factory: Callable[[Session], Session] | None,
28
31
  ):
29
- self.netbox = client_sync.NetboxV42(url=url, token=token, ssl_context=ssl_context, threads=threads)
32
+ self.netbox = client_sync.NetboxV42(url=url, token=token, ssl_context=ssl_context, threads=threads, session_factory=session_factory)
30
33
  self.convert_device = get_converter(
31
34
  api_models.Device,
32
35
  NetboxDeviceV42,
@@ -136,11 +139,12 @@ class NetboxStorageV42(BaseNetboxStorage[
136
139
  token: str,
137
140
  ssl_context: ssl.SSLContext | None,
138
141
  threads: int,
142
+ session_factory: Callable[[Session], Session] | None = None,
139
143
  ) -> NetboxAdapter[
140
144
  NetboxDeviceV42, InterfaceV42, IpAddressV42, PrefixV42,
141
145
  FHRPGroupV41, FHRPGroupAssignmentV41,
142
146
  ]:
143
- return NetboxV42Adapter(self, url, token, ssl_context, threads)
147
+ return NetboxV42Adapter(self, url, token, ssl_context, threads, session_factory)
144
148
 
145
149
  def resolve_all_vlans(self) -> list[Vlan]:
146
150
  if self._all_vlans is None:
annet/cli_args.py CHANGED
@@ -140,6 +140,12 @@ opt_tolerate_fails = Arg(
140
140
  help="Show errors without interrupting the generation"
141
141
  )
142
142
 
143
+ opt_recache = Arg(
144
+ "--recache", default=False,
145
+ help="Force expiration of storage's local cache if it is supported"
146
+ )
147
+
148
+
143
149
  # При параллельном запуске и включённом --tolerate-fails код возврата
144
150
  # всегда нулевой. Это не позволяет нам легко понять, прошла ли генерация
145
151
  # успешно для всех устройств. С этим флажком код будет ненулевой, если
@@ -353,6 +359,7 @@ opt_selected_context_name = Arg(
353
359
 
354
360
  # ====
355
361
  class CacheOptions(ArgGroup):
362
+ recache = opt_recache
356
363
  no_mesh = False
357
364
 
358
365
 
@@ -26,11 +26,12 @@ from annet.types import (
26
26
  )
27
27
  from annet.vendors import registry_connector, tabparser
28
28
 
29
+ from .annotate import AbstractAnnotateFormatter, annotate_formatter_connector
29
30
  from .base import BaseGenerator
30
31
  from .base import ParamsList as ParamsList
31
32
  from .base import TextGenerator as TextGenerator
32
33
  from .entire import Entire
33
- from .exceptions import GeneratorError, NotSupportedDevice
34
+ from .exceptions import GeneratorError, InvalidValueFromGenerator, NotSupportedDevice
34
35
  from .jsonfragment import JSONFragment
35
36
  from .partial import PartialGenerator
36
37
  from .perf import GeneratorPerfMesurer
@@ -165,11 +166,10 @@ def _run_partial_generator(gen: "PartialGenerator", run_args: GeneratorPartialRu
165
166
  safe_config = odict()
166
167
 
167
168
  if not gen.supports_device(device):
168
- logger.info("generator %s is not supported for device %s", gen, device.hostname)
169
+ logger.debug("generator %s is not supported for device %s", gen, device.hostname)
169
170
  return None
170
171
 
171
- span = tracing_connector.get().get_current_span()
172
- if span:
172
+ if span := tracing_connector.get().get_current_span():
173
173
  tracing_connector.get().set_device_attributes(span, run_args.device)
174
174
  tracing_connector.get().set_dimensions_attributes(span, gen, run_args.device)
175
175
  span.set_attributes({
@@ -184,8 +184,7 @@ def _run_partial_generator(gen: "PartialGenerator", run_args: GeneratorPartialRu
184
184
  logger.info("Generating PARTIAL ...")
185
185
  try:
186
186
  output = gen(device, run_args.annotate)
187
- except NotSupportedDevice:
188
- # это исключение нужно передать выше в оригинальном виде
187
+ except NotSupportedDevice: # это исключение нужно передать выше в оригинальном виде
189
188
  raise
190
189
  except Exception as err:
191
190
  filename, lineno = gen.get_running_line()
@@ -0,0 +1,37 @@
1
+ import abc
2
+ from types import GeneratorType
3
+
4
+ from annet.connectors import Connector
5
+
6
+ from .base import BaseGenerator
7
+
8
+
9
+ class AbstractAnnotateFormatter(abc.ABC):
10
+ def __init__(self, gen: BaseGenerator):
11
+ self.gen = gen
12
+ self._annotation_module = ".".join(gen.__class__.__module__.split(".")[-2:])
13
+
14
+ @abc.abstractmethod
15
+ def make_annotation(self, running_gen: GeneratorType) -> str:
16
+ raise NotImplementedError
17
+
18
+ def get_running_line(self, running_gen: GeneratorType) -> tuple[str, int]:
19
+ if not running_gen or not running_gen.gi_frame:
20
+ return repr(running_gen), -1
21
+ return self._annotation_module, running_gen.gi_frame.f_lineno
22
+
23
+
24
+ class DefaultAnnotateFormatter(AbstractAnnotateFormatter):
25
+ def make_annotation(self, running_gen: GeneratorType) -> str:
26
+ return "%s:%d" % self.get_running_line(running_gen)
27
+
28
+
29
+ class _AnnotateFormatterConnector(Connector[AbstractAnnotateFormatter]):
30
+ name = "AnnotateFormatterConnector"
31
+ ep_name = "annotate_formatter"
32
+
33
+ def _get_default(self) -> type[AbstractAnnotateFormatter]:
34
+ return DefaultAnnotateFormatter
35
+
36
+
37
+ annotate_formatter_connector = _AnnotateFormatterConnector()
annet/generators/base.py CHANGED
@@ -2,15 +2,20 @@ from __future__ import annotations
2
2
 
3
3
  import abc
4
4
  import contextlib
5
+ import re
5
6
  import textwrap
6
- from typing import Union, List
7
+ from typing import List, Union
7
8
 
8
9
  from annet import tracing
9
- from annet.vendors import tabparser
10
10
  from annet.tracing import tracing_connector
11
+ from annet.vendors import tabparser
12
+
11
13
  from .exceptions import InvalidValueFromGenerator
12
14
 
13
15
 
16
+ NONE_SEARCHER = re.compile(r"\bNone\b")
17
+
18
+
14
19
  class DefaultBlockIfCondition:
15
20
  pass
16
21
 
@@ -53,11 +58,15 @@ def _split_and_strip(text):
53
58
  # =====
54
59
  class BaseGenerator:
55
60
  TYPE: str
56
- TAGS: List[str]
61
+ TAGS: List[str] = []
57
62
 
58
63
  def supports_device(self, device) -> bool: # pylint: disable=unused-argument
59
64
  return True
60
65
 
66
+ @classmethod
67
+ def get_aliases(cls) -> set[str]:
68
+ return {cls.__name__, *cls.TAGS}
69
+
61
70
 
62
71
  class TreeGenerator(BaseGenerator):
63
72
  def __init__(self, indent=" "):
@@ -1,14 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import pkgutil
4
- import re
5
4
  import types
6
- from typing import FrozenSet, Iterable, List, Optional, Set, Union
5
+ from typing import FrozenSet, Iterable, List, Optional, Union
7
6
 
8
7
  from annet.lib import flatten, mako_render
9
8
 
10
- from .base import BaseGenerator, _filter_str
11
- from .exceptions import NotSupportedDevice
9
+ from .base import NONE_SEARCHER, BaseGenerator, _filter_str
10
+ from .exceptions import InvalidValueFromGenerator
12
11
 
13
12
 
14
13
  class Entire(BaseGenerator):
@@ -62,10 +61,6 @@ class Entire(BaseGenerator):
62
61
 
63
62
  # =====
64
63
 
65
- @classmethod
66
- def get_aliases(cls) -> Set[str]:
67
- return {cls.__name__, *cls.TAGS}
68
-
69
64
  def __call__(self, device):
70
65
  self.__device = device
71
66
  parts = []
@@ -73,15 +68,19 @@ class Entire(BaseGenerator):
73
68
  if isinstance(run_res, str):
74
69
  run_res = (run_res,)
75
70
  if run_res is None or not isinstance(run_res, (tuple, types.GeneratorType)):
76
- raise Exception("generator %s returns %s" % (
77
- self.__class__.__name__, type(run_res)))
71
+ raise Exception("generator %s returns %s" % (self.__class__.__name__, type(run_res)))
72
+
78
73
  for text in run_res:
79
74
  if isinstance(text, tuple):
80
75
  text = " ".join(map(_filter_str, flatten(text)))
81
- assert re.search(r"\bNone\b", text) is None, \
82
- "Found 'None' in yield result: %s" % text
76
+ if NONE_SEARCHER.search(text):
77
+ raise InvalidValueFromGenerator(
78
+ "Found 'None' in yield result: %s" % text
79
+ )
83
80
  parts.append(text)
81
+
84
82
  ret = "\n".join(parts)
85
83
  if self.ENSURE_END_NEWLINE and not ret.endswith("\n"):
86
84
  ret += "\n"
85
+
87
86
  return ret
@@ -1,16 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import contextlib
4
- from typing import (
5
- Any,
6
- Dict,
7
- List,
8
- Optional,
9
- Set,
10
- Union,
11
- )
4
+ from typing import Any, Optional, Union
12
5
 
13
6
  from annet.storage import Device, Storage
7
+
14
8
  from .base import TreeGenerator, _filter_str
15
9
 
16
10
 
@@ -18,13 +12,12 @@ class JSONFragment(TreeGenerator):
18
12
  """Generates parts of JSON config file."""
19
13
 
20
14
  TYPE = "JSON_FRAGMENT"
21
- TAGS: List[str] = []
22
15
 
23
16
  def __init__(self, storage: Storage):
24
17
  super().__init__()
25
18
  self.storage = storage
26
- self._json_config: Dict[str, Any] = {}
27
- self._config_pointer: List[str] = []
19
+ self._json_config: dict[str, Any] = {}
20
+ self._config_pointer: list[str] = []
28
21
 
29
22
  # if two generators edit same file, commands from generator with greater `reload_prio` will be used
30
23
  if not hasattr(self, "reload_prio"):
@@ -36,11 +29,7 @@ class JSONFragment(TreeGenerator):
36
29
  def path(self, device: Device) -> Optional[str]:
37
30
  raise NotImplementedError("Required PATH for JSON_FRAGMENT generator")
38
31
 
39
- @classmethod
40
- def get_aliases(cls) -> Set[str]:
41
- return {cls.__name__, *cls.TAGS}
42
-
43
- def acl(self, device: Device) -> Union[str, List[str]]:
32
+ def acl(self, device: Device) -> Union[str, list[str]]:
44
33
  """
45
34
  Restrict the generator to a specified ACL using JSON Pointer syntax.
46
35
 
@@ -48,7 +37,7 @@ class JSONFragment(TreeGenerator):
48
37
  """
49
38
  raise NotImplementedError("Required ACL for JSON_FRAGMENT generator")
50
39
 
51
- def acl_safe(self, device: Device) -> Union[str, List[str]]:
40
+ def acl_safe(self, device: Device) -> Union[str, list[str]]:
52
41
  """
53
42
  Restrict the generator to a specified safe ACL using JSON Pointer syntax.
54
43
 
@@ -1,32 +1,23 @@
1
1
  from __future__ import annotations
2
2
 
3
- import re
4
- from typing import (
5
- Iterable,
6
- List,
7
- Set,
8
- Union,
9
- )
10
-
11
- from annet.lib import (
12
- add_annotation,
13
- flatten,
14
- )
15
- from .base import TreeGenerator, _filter_str
16
- from .exceptions import NotSupportedDevice
3
+ from typing import Iterable, Union
4
+
5
+ from annet.lib import add_annotation, flatten
6
+
7
+ from .annotate import AbstractAnnotateFormatter, annotate_formatter_connector
8
+ from .base import NONE_SEARCHER, TreeGenerator, _filter_str
9
+ from .exceptions import InvalidValueFromGenerator
17
10
 
18
11
 
19
12
  class PartialGenerator(TreeGenerator):
20
13
  TYPE = "PARTIAL"
21
- TAGS: List[str] = []
22
14
 
23
15
  def __init__(self, storage):
24
16
  super().__init__()
25
17
  self.storage = storage
26
- self._annotate = False
27
18
  self._running_gen = None
19
+ self._annotate: AbstractAnnotateFormatter = annotate_formatter_connector.get(self)
28
20
  self._annotations = []
29
- self._annotation_module = self.__class__.__module__ or ""
30
21
 
31
22
  def supports_device(self, device) -> bool:
32
23
  if self.__class__.run is PartialGenerator.run:
@@ -35,19 +26,19 @@ class PartialGenerator(TreeGenerator):
35
26
  return True
36
27
 
37
28
  def acl(self, device):
38
- acl_func = self._get_vendor_func(device.hw.vendor, "acl")
39
- if acl_func:
29
+ if acl_func := self._get_vendor_func(device.hw.vendor, "acl"):
40
30
  return acl_func(device)
31
+ return None
41
32
 
42
33
  def acl_safe(self, device):
43
- acl_func = self._get_vendor_func(device.hw.vendor, "acl_safe")
44
- if acl_func:
34
+ if acl_func := self._get_vendor_func(device.hw.vendor, "acl_safe"):
45
35
  return acl_func(device)
36
+ return None
46
37
 
47
- def run(self, device) -> Iterable[Union[str, tuple]]:
48
- run_func = self._get_vendor_func(device.hw.vendor, "run")
49
- if run_func:
38
+ def run(self, device) -> Iterable[Union[str, tuple]] | None:
39
+ if run_func := self._get_vendor_func(device.hw.vendor, "run"):
50
40
  return run_func(device)
41
+ return None
51
42
 
52
43
  def get_user_runner(self, device):
53
44
  if self.__class__.run is not PartialGenerator.run:
@@ -56,26 +47,15 @@ class PartialGenerator(TreeGenerator):
56
47
 
57
48
  def _get_vendor_func(self, vendor: str, func_name: str):
58
49
  attr_name = f"{func_name}_{vendor}"
59
- if hasattr(self, attr_name):
60
- return getattr(self, attr_name)
61
- return None
50
+ return getattr(self, attr_name, None)
62
51
 
63
52
  # =====
64
53
 
65
- @classmethod
66
- def get_aliases(cls) -> Set[str]:
67
- return {cls.__name__, *cls.TAGS}
68
-
69
- def __call__(self, device, annotate=False):
54
+ def __call__(self, device, annotate: bool = False):
70
55
  self._indents = []
71
56
  self._rows = []
72
- self._running_gen = self.run(device)
73
- self._annotate = annotate
74
-
75
- if annotate and self.__class__.__module__:
76
- self._annotation_module = ".".join(
77
- self.__class__.__module__.split(".")[-2:])
78
57
 
58
+ self._running_gen = self.run(device)
79
59
  for text in self._running_gen:
80
60
  if isinstance(text, tuple):
81
61
  text = " ".join(map(_filter_str, flatten(text)))
@@ -83,9 +63,12 @@ class PartialGenerator(TreeGenerator):
83
63
  text = _filter_str(text)
84
64
  self._append_text(text)
85
65
 
86
- for row in self._rows:
87
- assert re.search(r"\bNone\b", row) is None, \
88
- "Found 'None' in yield result: %s" % (row)
66
+ for (row, annotation) in zip(self._rows, self._annotations):
67
+ if NONE_SEARCHER.search(row):
68
+ raise InvalidValueFromGenerator(
69
+ "Found 'None' in yield result: %s" % add_annotation(row, annotation)
70
+ )
71
+
89
72
  if annotate:
90
73
  generated_rows = (
91
74
  add_annotation(x, y)
@@ -93,23 +76,20 @@ class PartialGenerator(TreeGenerator):
93
76
  )
94
77
  else:
95
78
  generated_rows = self._rows
96
- return "\n".join(generated_rows) + "\n"
79
+
80
+ return "\n".join((*generated_rows, ""))
97
81
 
98
82
  def _append_text(self, text):
99
83
  def annotation_cb(row):
100
- annotation = "%s:%d" % self.get_running_line()
101
- self._annotations.append(annotation)
84
+ self._annotations.append(
85
+ self._annotate.make_annotation(self._running_gen)
86
+ )
102
87
  return row
103
88
 
104
- self._append_text_cb(
105
- text,
106
- annotation_cb if self._annotate else None
107
- )
89
+ self._append_text_cb(text, row_cb=annotation_cb)
108
90
 
109
- def get_running_line(self):
110
- if not self._running_gen or not self._running_gen.gi_frame:
111
- return (repr(self._running_gen), -1)
112
- return self._annotation_module, self._running_gen.gi_frame.f_lineno
91
+ def get_running_line(self) -> tuple[str, int]:
92
+ return self._annotate.get_running_line(self._running_gen)
113
93
 
114
94
  @classmethod
115
95
  def literal(cls, item):
@@ -2,19 +2,10 @@ from __future__ import annotations
2
2
 
3
3
  import textwrap
4
4
  from collections import OrderedDict as odict
5
- from typing import (
6
- Any,
7
- Callable,
8
- Dict,
9
- Optional,
10
- Sequence,
11
- Tuple,
12
- )
5
+ from typing import Any, Callable, Optional, Sequence
13
6
 
14
7
  from annet.annlib import jsontools
15
- from annet.lib import (
16
- merge_dicts,
17
- )
8
+ from annet.lib import merge_dicts
18
9
  from annet.reference import RefMatcher, RefTracker
19
10
  from annet.types import (
20
11
  GeneratorEntireResult,
@@ -24,7 +15,7 @@ from annet.types import (
24
15
 
25
16
 
26
17
  def _combine_acl_text(
27
- partial_results: Dict[str, GeneratorPartialResult],
18
+ partial_results: dict[str, GeneratorPartialResult],
28
19
  acl_getter: Callable[[GeneratorPartialResult], str]
29
20
  ) -> str:
30
21
  acl_text = ""
@@ -43,9 +34,9 @@ class RunGeneratorResult:
43
34
  """
44
35
 
45
36
  def __init__(self):
46
- self.partial_results: Dict[str, GeneratorPartialResult] = {}
47
- self.entire_results: Dict[str, GeneratorEntireResult] = {}
48
- self.json_fragment_results: Dict[str, GeneratorJSONFragmentResult] = {}
37
+ self.partial_results: dict[str, GeneratorPartialResult] = {}
38
+ self.entire_results: dict[str, GeneratorEntireResult] = {}
39
+ self.json_fragment_results: dict[str, GeneratorJSONFragmentResult] = {}
49
40
  self.ref_track: RefTracker = RefTracker()
50
41
  self.ref_matcher: RefMatcher = RefMatcher()
51
42
 
@@ -62,14 +53,14 @@ class RunGeneratorResult:
62
53
  def add_json_fragment(self, result: GeneratorJSONFragmentResult) -> None:
63
54
  self.json_fragment_results[result.name] = result
64
55
 
65
- def config_tree(self, safe: bool = False) -> Dict[str, Any]: # OrderedDict
56
+ def config_tree(self, safe: bool = False) -> dict[str, Any]: # OrderedDict
66
57
  tree = odict()
67
58
  for gr in self.partial_results.values():
68
59
  config = gr.safe_config if safe else gr.config
69
60
  tree = merge_dicts(tree, config)
70
61
  return tree
71
62
 
72
- def new_files(self, safe: bool = False) -> Dict[str, Tuple[str, str]]:
63
+ def new_files(self, safe: bool = False) -> dict[str, tuple[str, str]]:
73
64
  files = {}
74
65
  for gr in self.entire_results.values():
75
66
  if not safe or gr.is_safe:
@@ -84,14 +75,14 @@ class RunGeneratorResult:
84
75
 
85
76
  def new_json_fragment_files(
86
77
  self,
87
- old_files: Dict[str, Optional[str]],
78
+ old_files: dict[str, Optional[str]],
88
79
  use_acl: bool = True,
89
80
  safe: bool = False,
90
81
  filters: Sequence[str] | None = None,
91
- ) -> Dict[str, Tuple[Any, Optional[str]]]:
82
+ ) -> dict[str, tuple[Any, Optional[str]]]:
92
83
  # TODO: safe
93
- files: Dict[str, Tuple[Any, Optional[str]]] = {}
94
- reload_prios: Dict[str, int] = {}
84
+ files: dict[str, tuple[Any, Optional[str]]] = {}
85
+ reload_prios: dict[str, int] = {}
95
86
  for generator_result in self.json_fragment_results.values():
96
87
  filepath = generator_result.path
97
88
  if filepath not in files:
@@ -105,7 +96,7 @@ class RunGeneratorResult:
105
96
  result_acl = generator_result.acl_safe
106
97
  else:
107
98
  result_acl = None
108
- previous_config: Dict[str, Any] = files[filepath][0]
99
+ previous_config: dict[str, Any] = files[filepath][0]
109
100
  new_fragment = generator_result.config
110
101
  new_config = jsontools.apply_json_fragment(
111
102
  previous_config, new_fragment,
@@ -126,7 +117,7 @@ class RunGeneratorResult:
126
117
  files[filepath] = (new_config, reload_cmd)
127
118
  return files
128
119
 
129
- def perf_mesures(self) -> Dict[str, Dict[str, int]]:
120
+ def perf_mesures(self) -> dict[str, dict[str, int]]:
130
121
  mesures = {}
131
122
  for gr in self.partial_results.values():
132
123
  mesures[gr.name] = {"total": gr.perf.total,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: annet
3
- Version: 3.18.0
3
+ Version: 3.20.0
4
4
  Summary: annet
5
5
  Home-page: https://github.com/annetutil/annet
6
6
  License: MIT
@@ -22,7 +22,8 @@ Requires-Dist: adaptix==3.0.0b7
22
22
  Requires-Dist: dataclass-rest==0.4
23
23
  Requires-Dist: ordered-set>=4.1.0
24
24
  Provides-Extra: netbox
25
- Requires-Dist: annetbox[sync]>=0.8.0; extra == "netbox"
25
+ Requires-Dist: annetbox[sync]>=0.9.0; extra == "netbox"
26
+ Requires-Dist: requests-cache>=1.2.1; extra == "netbox"
26
27
  Dynamic: home-page
27
28
  Dynamic: license
28
29
  Dynamic: license-file
@@ -3,7 +3,7 @@ annet/annet.py,sha256=vyQ__n5hkjub3aWO8tksHPoUSbTeK97MyMcR_U8_BSU,1016
3
3
  annet/argparse.py,sha256=v1MfhjR0B8qahza0WinmXClpR8UiDFhmwDDWtNroJPA,12855
4
4
  annet/bgp_models.py,sha256=roSDKhxzcg9N8iEh9zI4Jww1vOObWcwRVzMl9FJnh1M,14046
5
5
  annet/cli.py,sha256=shq3hHzrTxFL3x1_zTOR43QHo0JYs8QSwyOvGtL86Co,12733
6
- annet/cli_args.py,sha256=d0WuGiZfe42-nMvurX1bOb3iUMg0TSK-hxk8J1THxSY,13728
6
+ annet/cli_args.py,sha256=GwFSEJDUZMzUtXo6NBs_1B7tlyFhaaKRfcgmRUpAkfI,13881
7
7
  annet/connectors.py,sha256=aoiDVLPizx8CW2p8SAwGCzyO_WW8H9xc2aujbGC4bDg,4882
8
8
  annet/deploy.py,sha256=toIaR2QLvLJxpdeXoJhZzKdXfhn784ciTNZlMGT9huo,7709
9
9
  annet/deploy_ui.py,sha256=XsN1i7E9cNp1SAf1YBwhEBuwN91MvlNMSrLhnQrumA8,28672
@@ -28,7 +28,7 @@ annet/adapters/fetchers/stub/fetcher.py,sha256=FzpkIzPJBQVuNNSdXKoGzkALmIGMgo2gm
28
28
  annet/adapters/file/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
29
29
  annet/adapters/file/provider.py,sha256=Raw5DzsGS8I9XcKNeMuk5O3dPxqeRSCNmuB5JwClRqY,7687
30
30
  annet/adapters/netbox/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
- annet/adapters/netbox/provider.py,sha256=xxPVCLOFkiw56zK4iDKLYLCHJ0-5y3k325nZtGA0NPM,2408
31
+ annet/adapters/netbox/provider.py,sha256=UY9_5cRWQaieNkzerCmm67a4Nnokg1kilAoP314GU14,2467
32
32
  annet/adapters/netbox/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
33
  annet/adapters/netbox/common/adapter.py,sha256=1rR9PZAU72hIreozWFVb9CKpFVjfUH_i9kQEbaNiEUI,2639
34
34
  annet/adapters/netbox/common/client.py,sha256=PaxHG4W9H8_uunIwMBNYkLq4eQJYoO6p6gY-ciQs7Nc,2563
@@ -36,20 +36,20 @@ annet/adapters/netbox/common/manufacturer.py,sha256=9jTfzwx5XmETrjSbIJu_FhNaByaU
36
36
  annet/adapters/netbox/common/models.py,sha256=nMruBugAKNY8D63rqUnPYyMYGMIU-haxX0vnfxLGNWc,8990
37
37
  annet/adapters/netbox/common/query.py,sha256=mz5oOn5vjU-lCV48TedvtXZ14GtBXU3jpcXa_cNawWg,1949
38
38
  annet/adapters/netbox/common/status_client.py,sha256=POaqiQJ0jPcqUQH-X_fWHVnKB7TBYveNriaT0eNTlfI,769
39
- annet/adapters/netbox/common/storage_base.py,sha256=lMRiDkzQylpA5Fxa4B6CUOWzq2d1cpjeSDbFGp8jH7o,9787
40
- annet/adapters/netbox/common/storage_opts.py,sha256=8LCxX0m-Wd-jNZ2iRAzkBJnbDg-MuE7RS17uPxjZo3o,2066
39
+ annet/adapters/netbox/common/storage_base.py,sha256=mdteEWxZAlw6re5FPciB5FoV2nAGnqozQdx-iNX-T80,10601
40
+ annet/adapters/netbox/common/storage_opts.py,sha256=pDtkFpfVIoRLBruMYNuIC34XpZe9aRrAygy2cOVpqyU,2800
41
41
  annet/adapters/netbox/v24/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
42
42
  annet/adapters/netbox/v24/models.py,sha256=RH2ooUPHOtHT0q1ZQE7v23FgmcsGenzzSgJyft13o1k,7605
43
43
  annet/adapters/netbox/v24/storage.py,sha256=zptzrW4aZUv_exuGw0DqQPm_kXZR3DwL4zRHYL6twTk,6219
44
44
  annet/adapters/netbox/v37/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
45
  annet/adapters/netbox/v37/models.py,sha256=LTjotRcTYWUR3KFvdg3LjBhek4OQblOvJr5yvP9AAPg,1940
46
- annet/adapters/netbox/v37/storage.py,sha256=1CO1s1lM01vjN5o6MOF-WK7nHsY8cSJg5KrAj4Z-N8Y,5115
46
+ annet/adapters/netbox/v37/storage.py,sha256=DKPCIucpND8M7ebhfhm6lWCcVYif0vtcaJ0mZu22YCU,5362
47
47
  annet/adapters/netbox/v41/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
48
48
  annet/adapters/netbox/v41/models.py,sha256=RI8QHpNIcrXNubis1rUFJ1j9bL881fz4meLXvAY8Yi0,2197
49
- annet/adapters/netbox/v41/storage.py,sha256=FDdaUugvFEMACSRLW8T8HRv_1V7VbTN95u5wvIgt4Mo,5169
49
+ annet/adapters/netbox/v41/storage.py,sha256=ZMvNJbXb1XN8WKcAY2cTL_Ybyjk_QOwbxzD93do9DXM,5417
50
50
  annet/adapters/netbox/v42/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
51
51
  annet/adapters/netbox/v42/models.py,sha256=JNrBMeZ1SznjRzaqKNBP5jtHEKHN40X1V-BlgrMpKwE,2054
52
- annet/adapters/netbox/v42/storage.py,sha256=mXswX_wqXIcRFKAyTbjYkPR2IIm9_SNeGJDBw9-ZIl8,6096
52
+ annet/adapters/netbox/v42/storage.py,sha256=Dg6kiSOVXO2ApXXgQGRdrE4GLd8EkeeRhE6aGxHGvMU,6325
53
53
  annet/annlib/__init__.py,sha256=fT1l4xV5fqqg8HPw9HqmZVN2qwS8i6X1aIm2zGDjxKY,252
54
54
  annet/annlib/command.py,sha256=GsgzQ8oFiHFan1Tfyo340rWgfhyU4_sCKJGhBm2jG6Y,1258
55
55
  annet/annlib/diff.py,sha256=MZ6eQAU3cadQp8KaSE6uAYFtcfMDCIe_eNuVROnYkCk,4496
@@ -78,15 +78,16 @@ annet/annlib/rulebook/common.py,sha256=6A3sOkOanDRTuR7F426zakShGaGKRBmBLDCnqBbBJ
78
78
  annet/api/__init__.py,sha256=5YzScmeCDsHiDhtT3RwY_lMuikRZ6o7CVrlnJCln2A8,34116
79
79
  annet/configs/context.yml,sha256=RVLrKLIHpCty7AGwOnmqf7Uu0iZQCn-AjYhophDJer8,259
80
80
  annet/configs/logging.yaml,sha256=EUagfir99QqA73Scc3k7sfQccbU3E1SvEQdyhLFtCl4,997
81
- annet/generators/__init__.py,sha256=LnW6G_7K5ScGYkFjFk7_QfuUaFAk5oTSHYaTaL6xUwA,16735
82
- annet/generators/base.py,sha256=iIDshBnA6Jb6gXz-qSm3L7gyK5_ZRCw05fVPeIjOEmY,3695
83
- annet/generators/entire.py,sha256=jFTbx9BKoiXMstq_QeesRH2yi01LHyYHxgCtvvwBmeM,2872
81
+ annet/generators/__init__.py,sha256=jxQJ5rLeXHiWGJgwhqygmBgCJsxc6oG-E9gDI9LzLog,16818
82
+ annet/generators/annotate.py,sha256=TG7ozOkQmjiz7BVAKRrXfvuekGBR5Wl1nKmA4PLkG_g,1188
83
+ annet/generators/base.py,sha256=qvvxKFzvgJWGBUcw8_bK1oFKTc4AQotN4CCuICWHXSk,3850
84
+ annet/generators/entire.py,sha256=H_xQ33GH8P696fnKBFFJs9NQcdX9h-gV3D-jJYtpX0U,2823
84
85
  annet/generators/exceptions.py,sha256=GPXTBgn2xZ3Ev_bdOPlfCLGRC1kQHe_dEq88S-uyi5s,151
85
- annet/generators/jsonfragment.py,sha256=Cl43t9_OtNWRxesk3B69h60KvD37tiK9W7sLLmppAT4,4379
86
- annet/generators/partial.py,sha256=XI01KDA--XwjSEU33SOQCCJZRXFq5boRz1uJA8lVA1g,3502
86
+ annet/generators/jsonfragment.py,sha256=H_hmZTHwnm_cToUbBNQRLf_1LlOZgLuXpSWpCVZ177o,4212
87
+ annet/generators/partial.py,sha256=lm6uCTUDgGq7MFPmnAOzGochNwu7nUVy7x2dvW9OkBI,3167
87
88
  annet/generators/perf.py,sha256=IaAcfEVtX7UNO11VOCXzp-FPj_tOx_CDQ34HGThCn4c,2225
88
89
  annet/generators/ref.py,sha256=QVdeL8po1D0kBsVLOpCjFR81D8yNTk-kaQj5WUM4hng,438
89
- annet/generators/result.py,sha256=YFXKWHzTyNweF2ioVpKiH3IVSw475qLsqJPsCToWFFE,5187
90
+ annet/generators/result.py,sha256=XnVaCsRVAKxlARM7jFERu5KzGmWFrJJJ_FcMuqp7mwQ,5136
90
91
  annet/generators/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
91
92
  annet/generators/common/initial.py,sha256=qYBxXFhyOPy34cxc6hsIXseod-lYCmmbuNHpM0uteY0,1244
92
93
  annet/mesh/__init__.py,sha256=lcgdnBIxc2MAN7Er1bcErEKPqrjWO4uIp_1FldMXTYg,557
@@ -208,8 +209,8 @@ annet/vendors/library/optixtrans.py,sha256=VdME69Ca4HAEgoaKN21fZxnmmsqqaxOe_HZja
208
209
  annet/vendors/library/pc.py,sha256=vfv31_NPi7M-4AUDL89UcpawK2E6xvCpELA209cd1ho,1086
209
210
  annet/vendors/library/ribbon.py,sha256=DDOBq-_-FL9dCxqXs2inEWZ-pvw-dJ-A-prA7cKMhec,1216
210
211
  annet/vendors/library/routeros.py,sha256=iQa7m_4wjuvcgBOI9gyZwlw1BvzJfOkvUbyoEk-NI9I,1254
211
- annet-3.18.0.dist-info/licenses/AUTHORS,sha256=rh3w5P6gEgqmuC-bw-HB68vBCr-yIBFhVL0PG4hguLs,878
212
- annet-3.18.0.dist-info/licenses/LICENSE,sha256=yPxl7dno02Pw7gAcFPIFONzx_gapwDoPXsIsh6Y7lC0,1079
212
+ annet-3.20.0.dist-info/licenses/AUTHORS,sha256=rh3w5P6gEgqmuC-bw-HB68vBCr-yIBFhVL0PG4hguLs,878
213
+ annet-3.20.0.dist-info/licenses/LICENSE,sha256=yPxl7dno02Pw7gAcFPIFONzx_gapwDoPXsIsh6Y7lC0,1079
213
214
  annet_generators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
214
215
  annet_generators/example/__init__.py,sha256=OJ77uj8axc-FIyIu_Xdcnzmde3oQW5mk5qbODkhuVc8,355
215
216
  annet_generators/example/hostname.py,sha256=RloLzNVetEoWPLITzfJ13Nk3CC0yi-cZB1RTd6dnuhI,2541
@@ -222,8 +223,8 @@ annet_generators/rpl_example/generator.py,sha256=EWah19gOH8G-QyNyWqxCqdRi0BK7GbM
222
223
  annet_generators/rpl_example/items.py,sha256=HPgxScDvSqJPdz0c2SppDrH82DZYC4zUaniQwcWmh4A,1176
223
224
  annet_generators/rpl_example/mesh.py,sha256=z_WgfDZZ4xnyh3cSf75igyH09hGvtexEVwy1gCD_DzA,288
224
225
  annet_generators/rpl_example/route_policy.py,sha256=z6nPb0VDeQtKD1NIg9sFvmUxBD5tVs2frfNIuKdM-5c,2318
225
- annet-3.18.0.dist-info/METADATA,sha256=BzSwXAELrXMy0FfrX33OkF9h-iiKfXkeIKAroB_Z9D4,850
226
- annet-3.18.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
227
- annet-3.18.0.dist-info/entry_points.txt,sha256=5lIaDGlGi3l6QQ2ry2jZaqViP5Lvt8AmsegdD0Uznck,192
228
- annet-3.18.0.dist-info/top_level.txt,sha256=QsoTZBsUtwp_FEcmRwuN8QITBmLOZFqjssRfKilGbP8,23
229
- annet-3.18.0.dist-info/RECORD,,
226
+ annet-3.20.0.dist-info/METADATA,sha256=HLKfcTZGcmRvrYEwMMntQjHeVCTXS9BqVp1kEtiNtqA,906
227
+ annet-3.20.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
228
+ annet-3.20.0.dist-info/entry_points.txt,sha256=5lIaDGlGi3l6QQ2ry2jZaqViP5Lvt8AmsegdD0Uznck,192
229
+ annet-3.20.0.dist-info/top_level.txt,sha256=QsoTZBsUtwp_FEcmRwuN8QITBmLOZFqjssRfKilGbP8,23
230
+ annet-3.20.0.dist-info/RECORD,,
File without changes