annet 0.10__py3-none-any.whl → 0.12__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.

@@ -0,0 +1,136 @@
1
+ from __future__ import annotations
2
+
3
+ import abc
4
+ import contextlib
5
+ import textwrap
6
+ from typing import Union, List
7
+
8
+ from annet import tabparser, tracing
9
+ from annet.tracing import tracing_connector
10
+ from .exceptions import InvalidValueFromGenerator
11
+
12
+
13
+ class DefaultBlockIfCondition:
14
+ pass
15
+
16
+
17
+ ParamsList = tabparser.JuniperList
18
+
19
+
20
+ class GenStringable(abc.ABC):
21
+ @abc.abstractmethod
22
+ def gen_str(self) -> str:
23
+ pass
24
+
25
+
26
+ def _filter_str(value: Union[
27
+ str, int, float, tabparser.JuniperList, ParamsList, GenStringable]):
28
+ if isinstance(value, (
29
+ str,
30
+ int,
31
+ float,
32
+ tabparser.JuniperList,
33
+ ParamsList,
34
+ )):
35
+ return str(value)
36
+
37
+ if hasattr(value, "gen_str") and callable(value.gen_str):
38
+ return value.gen_str()
39
+
40
+ raise InvalidValueFromGenerator(
41
+ "Invalid yield type: %s(%s)" % (type(value).__name__, value))
42
+
43
+
44
+ def _split_and_strip(text):
45
+ if "\n" in text:
46
+ rows = textwrap.dedent(text).strip().split("\n")
47
+ else:
48
+ rows = [text]
49
+ return rows
50
+
51
+
52
+ # =====
53
+ class BaseGenerator:
54
+ TYPE: str
55
+ TAGS: List[str]
56
+
57
+ def supports_device(self, device) -> bool: # pylint: disable=unused-argument
58
+ return True
59
+
60
+
61
+ class TreeGenerator(BaseGenerator):
62
+ def __init__(self, indent=" "):
63
+ self._indents = []
64
+ self._rows = []
65
+ self._block_path = []
66
+ self._indent = indent
67
+
68
+ @tracing.contextmanager(min_duration="0.1")
69
+ @contextlib.contextmanager
70
+ def block(self, *tokens, indent=None):
71
+ span = tracing_connector.get().get_current_span()
72
+ if span:
73
+ span.set_attribute("tokens", " ".join(map(str, tokens)))
74
+
75
+ indent = self._indent if indent is None else indent
76
+ block = " ".join(map(_filter_str, tokens))
77
+ self._block_path.append(block)
78
+ self._append_text(block)
79
+ self._indents.append(indent)
80
+ yield
81
+ self._indents.pop(-1)
82
+ self._block_path.pop(-1)
83
+
84
+ @contextlib.contextmanager
85
+ def block_if(self, *tokens, condition=DefaultBlockIfCondition):
86
+ if condition is DefaultBlockIfCondition:
87
+ condition = (None not in tokens and "" not in tokens)
88
+ if condition:
89
+ with self.block(*tokens):
90
+ yield
91
+ return
92
+ yield
93
+
94
+ @contextlib.contextmanager
95
+ def multiblock(self, *blocks):
96
+ if blocks:
97
+ blk = blocks[0]
98
+ tokens = blk if isinstance(blk, (list, tuple)) else [blk]
99
+ with self.block(*tokens):
100
+ with self.multiblock(*blocks[1:]):
101
+ yield
102
+ return
103
+ yield
104
+
105
+ @contextlib.contextmanager
106
+ def multiblock_if(self, *blocks, condition=DefaultBlockIfCondition):
107
+ if condition is DefaultBlockIfCondition:
108
+ condition = (None not in blocks)
109
+ if condition:
110
+ if blocks:
111
+ blk = blocks[0]
112
+ tokens = blk if isinstance(blk, (list, tuple)) else [blk]
113
+ with self.block(*tokens):
114
+ with self.multiblock(*blocks[1:]):
115
+ yield
116
+ return
117
+ yield
118
+
119
+ # ===
120
+ def _append_text(self, text):
121
+ self._append_text_cb(text)
122
+
123
+ def _append_text_cb(self, text, row_cb=None):
124
+ for row in _split_and_strip(text):
125
+ if row_cb:
126
+ row = row_cb(row)
127
+ self._rows.append("".join(self._indents) + row)
128
+
129
+
130
+ class TextGenerator(TreeGenerator):
131
+ def __add__(self, line):
132
+ self._append_text(line)
133
+ return self
134
+
135
+ def __iter__(self):
136
+ yield from self._rows
@@ -14,7 +14,7 @@ class InitialConfig(PartialGenerator):
14
14
  """
15
15
  def __init__(self, storage=None):
16
16
  self._do_run = not storage
17
- super().__init__(storage=None)
17
+ super().__init__(storage=storage)
18
18
 
19
19
  def run_huawei(self, device):
20
20
  if not self._do_run:
@@ -0,0 +1,97 @@
1
+ from __future__ import annotations
2
+
3
+ import pkgutil
4
+ import re
5
+ import types
6
+ from typing import (
7
+ FrozenSet,
8
+ Iterable,
9
+ List,
10
+ Optional,
11
+ Set,
12
+ Union,
13
+ )
14
+
15
+ from annet.lib import (
16
+ flatten,
17
+ jinja_render,
18
+ mako_render,
19
+ )
20
+ from .base import BaseGenerator, _filter_str
21
+ from .exceptions import NotSupportedDevice
22
+
23
+
24
+ class Entire(BaseGenerator):
25
+ TYPE = "ENTIRE"
26
+ TAGS: List[str] = []
27
+ REQUIRED_PACKAGES: FrozenSet[str] = frozenset()
28
+
29
+ def __init__(self, storage):
30
+ self.storage = storage
31
+ # между генераторами для одного и того же path - выбирается тот что больше
32
+ if not hasattr(self, "prio"):
33
+ self.prio = 100
34
+ self.__device = None
35
+
36
+ def supports_device(self, device):
37
+ return bool(self.path(device))
38
+
39
+ def run(self, device) -> Union[None, str, Iterable[Union[str, tuple]]]:
40
+ raise NotImplementedError
41
+
42
+ def reload(self, device) -> Optional[
43
+ str]: # pylint: disable=unused-argument
44
+ return
45
+
46
+ def get_reload_cmds(self, device) -> str:
47
+ ret = self.reload(device) or ""
48
+ path = self.path(device)
49
+ if path and device.hw.PC and device.hw.soft.startswith(
50
+ ("Cumulus", "SwitchDev", "SONiC"),
51
+ ):
52
+ parts = []
53
+ if ret:
54
+ parts.append(ret)
55
+ parts.append("/usr/bin/etckeeper commitreload %s" % path)
56
+ return "\n".join(parts)
57
+ return ret
58
+
59
+ def path(self, device) -> Optional[str]:
60
+ raise NotImplementedError("Required PATH for ENTIRE generator")
61
+
62
+ # pylint: disable=unused-argument
63
+ def is_safe(self, device) -> bool:
64
+ """Output gen results when --acl-safe flag is used"""
65
+ return False
66
+
67
+ def read(self, path) -> str:
68
+ return pkgutil.get_data(__name__, path).decode()
69
+
70
+ def mako(self, text, **kwargs) -> str:
71
+ return mako_render(text, dedent=True, device=self.__device, **kwargs)
72
+
73
+ def jinja(self, text, **kwargs) -> str:
74
+ return jinja_render(text, dedent=True, device=self.__device, **kwargs)
75
+
76
+ # =====
77
+
78
+ @classmethod
79
+ def get_aliases(cls) -> Set[str]:
80
+ return {cls.__name__, *cls.TAGS}
81
+
82
+ def __call__(self, device):
83
+ self.__device = device
84
+ parts = []
85
+ run_res = self.run(device)
86
+ if isinstance(run_res, str):
87
+ run_res = (run_res,)
88
+ if run_res is None or not isinstance(run_res, (tuple, types.GeneratorType)):
89
+ raise Exception("generator %s returns %s" % (
90
+ self.__class__.__name__, type(run_res)))
91
+ for text in run_res:
92
+ if isinstance(text, tuple):
93
+ text = " ".join(map(_filter_str, flatten(text)))
94
+ assert re.search(r"\bNone\b", text) is None, \
95
+ "Found 'None' in yield result: %s" % text
96
+ parts.append(text)
97
+ return "\n".join(parts)
@@ -0,0 +1,10 @@
1
+ class GeneratorError(Exception):
2
+ pass
3
+
4
+
5
+ class InvalidValueFromGenerator(ValueError):
6
+ pass
7
+
8
+
9
+ class NotSupportedDevice(GeneratorError):
10
+ pass
@@ -0,0 +1,125 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ from typing import (
5
+ Any,
6
+ Dict,
7
+ List,
8
+ Optional,
9
+ Set,
10
+ Union,
11
+ )
12
+
13
+ from annet.storage import Device, Storage
14
+ from .base import TreeGenerator, _filter_str
15
+ from .exceptions import NotSupportedDevice
16
+
17
+
18
+ class JSONFragment(TreeGenerator):
19
+ """Generates parts of JSON config file."""
20
+
21
+ TYPE = "JSON_FRAGMENT"
22
+ TAGS: List[str] = []
23
+
24
+ def __init__(self, storage: Storage):
25
+ super().__init__()
26
+ self.storage = storage
27
+ self._json_config: Dict[str, Any] = {}
28
+ self._config_pointer: List[str] = []
29
+
30
+ # if two generators edit same file, commands from generator with greater `reload_prio` will be used
31
+ if not hasattr(self, "reload_prio"):
32
+ self.reload_prio = 100
33
+
34
+ def supports_device(self, device: Device):
35
+ return bool(self.path(device))
36
+
37
+ def path(self, device: Device) -> Optional[str]:
38
+ raise NotImplementedError("Required PATH for JSON_FRAGMENT generator")
39
+
40
+ @classmethod
41
+ def get_aliases(cls) -> Set[str]:
42
+ return {cls.__name__, *cls.TAGS}
43
+
44
+ def acl(self, device: Device) -> Union[str, List[str]]:
45
+ """
46
+ Restrict the generator to a specified ACL using JSON Pointer syntax.
47
+
48
+ Expected ACL to be a list of strings, but a single string is also allowed.
49
+ """
50
+ raise NotImplementedError("Required ACL for JSON_FRAGMENT generator")
51
+
52
+ def acl_safe(self, device: Device) -> Union[str, List[str]]:
53
+ """
54
+ Restrict the generator to a specified safe ACL using JSON Pointer syntax.
55
+
56
+ Expected ACL to be a list of strings, but a single string is also allowed.
57
+ """
58
+ raise NotImplementedError("Required ACL for JSON_FRAGMENT generator")
59
+
60
+ def run(self, device: Device):
61
+ raise NotImplementedError
62
+
63
+ def get_reload_cmds(self, device: Device) -> str:
64
+ ret = self.reload(device) or ""
65
+ return ret
66
+
67
+ def reload(self, device) -> Optional[str]:
68
+ raise NotImplementedError
69
+
70
+ @contextlib.contextmanager
71
+ def block(self, *tokens, indent=None): # pylint: disable=unused-argument
72
+ block_str = "".join(map(_filter_str, tokens))
73
+ self._config_pointer.append(block_str)
74
+ try:
75
+ yield
76
+ finally:
77
+ self._config_pointer.pop()
78
+
79
+ @contextlib.contextmanager
80
+ def block_piped(self, *tokens, indent=None): # pylint: disable=unused-argument
81
+ block_str = "|".join(map(_filter_str, tokens))
82
+ self._config_pointer.append(block_str)
83
+ try:
84
+ yield
85
+ finally:
86
+ self._config_pointer.pop()
87
+
88
+ def __call__(self, device: Device, annotate: bool = False):
89
+ for cfg_fragment in self.run(device):
90
+ self._set_or_replace_dict(self._config_pointer, cfg_fragment)
91
+ return self._json_config
92
+
93
+ def _set_or_replace_dict(self, pointer, value):
94
+ if not pointer:
95
+ if self._json_config == {}:
96
+ self._json_config = value
97
+ else:
98
+ self._json_config = [self._json_config, value]
99
+ else:
100
+ self._set_dict(self._json_config, pointer, value)
101
+
102
+ @classmethod
103
+ def _to_str(cls, value: Any) -> str:
104
+ if isinstance(value, str):
105
+ return value
106
+ elif isinstance(value, list):
107
+ return [cls._to_str(x) for x in value]
108
+ elif isinstance(value, dict):
109
+ for k, v in value.items():
110
+ value[k] = cls._to_str(v)
111
+ return value
112
+ return str(value)
113
+
114
+ @classmethod
115
+ def _set_dict(cls, cfg, pointer, value):
116
+ # pointer has at least one key
117
+ if len(pointer) == 1:
118
+ if pointer[0] in cfg:
119
+ cfg[pointer[0]] = [cfg[pointer[0]], cls._to_str(value)]
120
+ else:
121
+ cfg[pointer[0]] = cls._to_str(value)
122
+ else:
123
+ if pointer[0] not in cfg:
124
+ cfg[pointer[0]] = {}
125
+ cls._set_dict(cfg[pointer[0]], pointer[1:], cls._to_str(value))
@@ -0,0 +1,119 @@
1
+ from __future__ import annotations
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
17
+
18
+
19
+ class PartialGenerator(TreeGenerator):
20
+ TYPE = "PARTIAL"
21
+ TAGS: List[str] = []
22
+
23
+ def __init__(self, storage):
24
+ super().__init__()
25
+ self.storage = storage
26
+ self._annotate = False
27
+ self._running_gen = None
28
+ self._annotations = []
29
+ self._annotation_module = self.__class__.__module__ or ""
30
+
31
+ def supports_device(self, device) -> bool:
32
+ if self.__class__.run is PartialGenerator.run:
33
+ return bool(self._get_vendor_func(device.hw.vendor, "run"))
34
+ else:
35
+ return True
36
+
37
+ def acl(self, device):
38
+ acl_func = self._get_vendor_func(device.hw.vendor, "acl")
39
+ if acl_func:
40
+ return acl_func(device)
41
+
42
+ def acl_safe(self, device):
43
+ acl_func = self._get_vendor_func(device.hw.vendor, "acl_safe")
44
+ if acl_func:
45
+ return acl_func(device)
46
+
47
+ def run(self, device) -> Iterable[Union[str, tuple]]:
48
+ run_func = self._get_vendor_func(device.hw.vendor, "run")
49
+ if run_func:
50
+ return run_func(device)
51
+
52
+ def get_user_runner(self, device):
53
+ if self.__class__.run is not PartialGenerator.run:
54
+ return self.run
55
+ return self._get_vendor_func(device.hw.vendor, "run")
56
+
57
+ def _get_vendor_func(self, vendor: str, func_name: str):
58
+ attr_name = f"{func_name}_{vendor}"
59
+ if hasattr(self, attr_name):
60
+ return getattr(self, attr_name)
61
+ return None
62
+
63
+ # =====
64
+
65
+ @classmethod
66
+ def get_aliases(cls) -> Set[str]:
67
+ return {cls.__name__, *cls.TAGS}
68
+
69
+ def __call__(self, device, annotate=False):
70
+ self._indents = []
71
+ 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
+
79
+ for text in self._running_gen:
80
+ if isinstance(text, tuple):
81
+ text = " ".join(map(_filter_str, flatten(text)))
82
+ else:
83
+ text = _filter_str(text)
84
+ self._append_text(text)
85
+
86
+ for row in self._rows:
87
+ assert re.search(r"\bNone\b", row) is None, \
88
+ "Found 'None' in yield result: %s" % (row)
89
+ if annotate:
90
+ generated_rows = (
91
+ add_annotation(x, y)
92
+ for (x, y) in zip(self._rows, self._annotations)
93
+ )
94
+ else:
95
+ generated_rows = self._rows
96
+ return "\n".join(generated_rows) + "\n"
97
+
98
+ def _append_text(self, text):
99
+ def annotation_cb(row):
100
+ annotation = "%s:%d" % self.get_running_line()
101
+ self._annotations.append(annotation)
102
+ return row
103
+
104
+ self._append_text_cb(
105
+ text,
106
+ annotation_cb if self._annotate else None
107
+ )
108
+
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
113
+
114
+ @classmethod
115
+ def literal(cls, item):
116
+ return '"{}"'.format(item)
117
+
118
+ def __repr__(self):
119
+ return "<%s>" % self.__class__.__name__
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from typing import (
5
+ Optional,
6
+ Union,
7
+ )
8
+
9
+ from annet import tracing
10
+ from annet.tracing import tracing_connector
11
+ from annet.types import (
12
+ GeneratorPartialRunArgs,
13
+ GeneratorPerf,
14
+ )
15
+ from .entire import Entire
16
+ from .partial import PartialGenerator
17
+
18
+
19
+ class GeneratorPerfMesurer:
20
+ def __init__(
21
+ self,
22
+ gen: Union[PartialGenerator, Entire],
23
+ run_args: Optional[GeneratorPartialRunArgs] = None,
24
+ trace_min_duration: tracing.MinDurationT = None
25
+ ):
26
+ self._gen = gen
27
+ self._run_args = run_args
28
+
29
+ self._start_time: float = 0.0
30
+ self._span_ctx = None
31
+ self._span = None
32
+ self._trace_min_duration = trace_min_duration
33
+
34
+ self.last_result: Optional[GeneratorPerf] = None
35
+
36
+ def start(self) -> None:
37
+ self.last_result = None
38
+
39
+ self._span_ctx = tracing_connector.get().start_as_current_span(
40
+ "gen:call",
41
+ tracer_name=self._gen.__class__.__module__,
42
+ min_duration=self._trace_min_duration,
43
+ )
44
+ self._span = self._span_ctx.__enter__() # pylint: disable=unnecessary-dunder-call
45
+
46
+ if self._span:
47
+ self._span.set_attributes(
48
+ {"generator.class": self._gen.__class__.__name__})
49
+ if self._run_args:
50
+ tracing_connector.get().set_device_attributes(
51
+ self._span, self._run_args.device,
52
+ )
53
+
54
+ self._start_time = time.monotonic()
55
+
56
+ def finish(self, exc_type=None, exc_val=None,
57
+ exc_tb=None) -> GeneratorPerf:
58
+ total = time.monotonic() - self._start_time
59
+ self._span_ctx.__exit__(exc_type, exc_val, exc_tb)
60
+
61
+ meta = {}
62
+ if tracing_connector.get().enabled:
63
+ span_context = self._span.get_span_context()
64
+ meta = {
65
+ "span": {
66
+ "trace_id": str(span_context.trace_id),
67
+ "span_id": str(span_context.span_id),
68
+ },
69
+ }
70
+
71
+ self.last_result = GeneratorPerf(total=total, rt=total, meta=meta)
72
+ return self.last_result
73
+
74
+ def __enter__(self):
75
+ self.start()
76
+ return self
77
+
78
+ def __exit__(self, exc_type, exc_val, exc_tb):
79
+ self.finish(exc_type, exc_val, exc_tb)
@@ -0,0 +1,15 @@
1
+ from .partial import PartialGenerator
2
+
3
+
4
+ class RefGenerator(PartialGenerator):
5
+ def __init__(self, storage, groups=None):
6
+ super().__init__(storage)
7
+ self.groups = groups
8
+
9
+ def ref(self, device):
10
+ if hasattr(self, "ref_" + device.hw.vendor):
11
+ return getattr(self, "ref_" + device.hw.vendor)(device)
12
+ return ""
13
+
14
+ def with_groups(self, groups):
15
+ return type(self)(self.storage, groups)