devaci 0.1.0__tar.gz → 1.0.1__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.
- {devaci-0.1.0 → devaci-1.0.1}/PKG-INFO +1 -1
- {devaci-0.1.0 → devaci-1.0.1}/pyproject.toml +8 -1
- {devaci-0.1.0 → devaci-1.0.1}/src/devaci/__init__.py +6 -1
- {devaci-0.1.0 → devaci-1.0.1}/src/devaci/cobra/__init__.py +6 -2
- {devaci-0.1.0 → devaci-1.0.1}/src/devaci/deploy.py +1 -1
- {devaci-0.1.0 → devaci-1.0.1}/src/devaci/jinja.py +2 -4
- {devaci-0.1.0 → devaci-1.0.1}/src/devaci/results.py +7 -3
- devaci-1.0.1/tests/test_cobra.py +59 -0
- devaci-1.0.1/tests/test_data.py +56 -0
- devaci-1.0.1/tests/test_deploy.py +98 -0
- devaci-1.0.1/tests/test_devaci.py +7 -0
- devaci-1.0.1/tests/test_filters.py +65 -0
- devaci-1.0.1/tests/test_jinja.py +34 -0
- devaci-1.0.1/tests/test_results.py +38 -0
- devaci-0.1.0/tests/test.py +0 -22
- devaci-0.1.0/tests/test_cobra.py +0 -10
- devaci-0.1.0/tests/test_deploy.py +0 -30
- devaci-0.1.0/tests/test_devaci.py +0 -5
- devaci-0.1.0/tests/test_outputs.py +0 -48
- {devaci-0.1.0 → devaci-1.0.1}/.gitignore +0 -0
- {devaci-0.1.0 → devaci-1.0.1}/LICENSE +0 -0
- {devaci-0.1.0 → devaci-1.0.1}/README.md +0 -0
- {devaci-0.1.0 → devaci-1.0.1}/src/devaci/cobra/base.py +0 -0
- {devaci-0.1.0 → devaci-1.0.1}/src/devaci/cobra/builders.py +0 -0
- {devaci-0.1.0 → devaci-1.0.1}/src/devaci/cobra/registry.py +0 -0
- {devaci-0.1.0 → devaci-1.0.1}/src/devaci/console.py +0 -0
- {devaci-0.1.0 → devaci-1.0.1}/src/devaci/data.py +0 -0
- {devaci-0.1.0 → devaci-1.0.1}/src/devaci/exceptions.py +0 -0
- {devaci-0.1.0 → devaci-1.0.1}/src/devaci/filters.py +0 -0
- {devaci-0.1.0 → devaci-1.0.1}/src/devaci/py.typed +0 -0
- {devaci-0.1.0 → devaci-1.0.1}/tests/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: devaci
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: Python library that generates Cisco ACI configuration and optionally pushes it to an APIC controller via Cisco's official Cobra SDK.
|
|
5
5
|
Project-URL: Homepage, https://github.com/cocuni80/devaci
|
|
6
6
|
Project-URL: Repository, https://github.com/cocuni80/devaci
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "devaci"
|
|
7
|
-
version = "0.1
|
|
7
|
+
version = "1.0.1"
|
|
8
8
|
description = "Python library that generates Cisco ACI configuration and optionally pushes it to an APIC controller via Cisco's official Cobra SDK."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -62,6 +62,13 @@ exclude = ["src/devaci/_legacy"]
|
|
|
62
62
|
testpaths = ["tests"]
|
|
63
63
|
addopts = "-ra"
|
|
64
64
|
|
|
65
|
+
[tool.coverage.run]
|
|
66
|
+
source = ["devaci"]
|
|
67
|
+
omit = ["*/_legacy/*"]
|
|
68
|
+
|
|
69
|
+
[tool.coverage.report]
|
|
70
|
+
show_missing = true
|
|
71
|
+
|
|
65
72
|
[tool.ruff]
|
|
66
73
|
target-version = "py310"
|
|
67
74
|
line-length = 100
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
"""devaci - Generate and push Cisco ACI configuration via the Cobra SDK."""
|
|
2
2
|
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
3
5
|
from devaci.cobra import CobraBuilder
|
|
4
6
|
from devaci.deploy import DeployClass
|
|
5
7
|
from devaci.exceptions import CobraError, DataError, DeployError, DevaciError, JinjaError
|
|
6
8
|
from devaci.jinja import JinjaRenderer
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
try:
|
|
11
|
+
__version__ = version("devaci")
|
|
12
|
+
except PackageNotFoundError:
|
|
13
|
+
__version__ = "0.0.0"
|
|
9
14
|
|
|
10
15
|
__all__ = [
|
|
11
16
|
"CobraBuilder",
|
|
@@ -40,12 +40,16 @@ class CobraBuilder:
|
|
|
40
40
|
|
|
41
41
|
@property
|
|
42
42
|
def xml(self) -> str | None:
|
|
43
|
-
"""Return the rendered XML payload."""
|
|
43
|
+
"""Return the rendered XML payload, or None when the config is empty."""
|
|
44
|
+
if not self.config.configMos:
|
|
45
|
+
return None
|
|
44
46
|
return cast(str | None, self.config.xmldata)
|
|
45
47
|
|
|
46
48
|
@property
|
|
47
49
|
def json(self) -> Any:
|
|
48
|
-
"""Return the rendered JSON payload as a dict."""
|
|
50
|
+
"""Return the rendered JSON payload as a dict, or None when the config is empty."""
|
|
51
|
+
if not self.config.configMos:
|
|
52
|
+
return None
|
|
49
53
|
return json.loads(self.config.data)
|
|
50
54
|
|
|
51
55
|
def render(self, output: dict[str, Any]) -> CobraResult:
|
|
@@ -120,7 +120,7 @@ class DeployClass:
|
|
|
120
120
|
|
|
121
121
|
try:
|
|
122
122
|
renderer = JinjaRenderer()
|
|
123
|
-
output = renderer.render(content,
|
|
123
|
+
output = renderer.render(content, **self._variables)
|
|
124
124
|
logs.append(f"[Jinja]: Template {name} was rendered successfully.")
|
|
125
125
|
logger.info(logs[-1])
|
|
126
126
|
|
|
@@ -26,7 +26,7 @@ class JinjaRenderer:
|
|
|
26
26
|
self._env.filters["range"] = range_filter
|
|
27
27
|
self._env.filters["nan"] = nan_filter
|
|
28
28
|
|
|
29
|
-
def render(self, template: str,
|
|
29
|
+
def render(self, template: str, **variables: Any) -> dict[str, Any]:
|
|
30
30
|
"""Render ``template`` with ``variables`` and parse the YAML output."""
|
|
31
31
|
try:
|
|
32
32
|
rendered = self._env.from_string(template).render(**variables)
|
|
@@ -36,7 +36,5 @@ class JinjaRenderer:
|
|
|
36
36
|
|
|
37
37
|
output = load_yaml(rendered)
|
|
38
38
|
if not isinstance(output, dict):
|
|
39
|
-
raise JinjaError(
|
|
40
|
-
f"[Jinja] -> [ConfigError]: Template {name} did not produce a mapping."
|
|
41
|
-
)
|
|
39
|
+
raise JinjaError("[Jinja] -> [ConfigError]: Template did not produce a mapping.")
|
|
42
40
|
return output
|
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import json
|
|
6
6
|
from dataclasses import dataclass, field
|
|
7
7
|
from datetime import datetime
|
|
8
|
-
from typing import Any
|
|
8
|
+
from typing import Any, cast
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def _timestamp() -> str:
|
|
@@ -44,11 +44,15 @@ class CobraResult(Result):
|
|
|
44
44
|
|
|
45
45
|
@property
|
|
46
46
|
def xml(self) -> str | None:
|
|
47
|
-
|
|
47
|
+
if self.config is None or not self.config.configMos:
|
|
48
|
+
return None
|
|
49
|
+
return cast(str | None, self.config.xmldata)
|
|
48
50
|
|
|
49
51
|
@property
|
|
50
52
|
def json(self) -> Any:
|
|
51
|
-
|
|
53
|
+
if self.config is None or not self.config.configMos:
|
|
54
|
+
return None
|
|
55
|
+
return json.loads(self.config.data)
|
|
52
56
|
|
|
53
57
|
def to_dict(self) -> dict[str, Any]:
|
|
54
58
|
data = super().to_dict()
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import cobra.mit.request
|
|
2
|
+
import cobra.model.coop
|
|
3
|
+
|
|
4
|
+
from devaci.cobra import CobraBuilder
|
|
5
|
+
from devaci.cobra.base import not_nan_str
|
|
6
|
+
from devaci.cobra.registry import REGISTRY
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_cobra_model_coop_available():
|
|
10
|
+
assert hasattr(cobra.model.coop, "Pol")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_cobra_mit_request_available():
|
|
14
|
+
assert hasattr(cobra.mit.request, "ConfigRequest")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_not_nan_str():
|
|
18
|
+
assert not_nan_str({"name": "ok"}, ["name"]) is True
|
|
19
|
+
assert not_nan_str({"name": None}, ["name"]) is False
|
|
20
|
+
assert not_nan_str({"name": ""}, ["name"]) is False
|
|
21
|
+
assert not_nan_str({"name": " "}, ["name"]) is False
|
|
22
|
+
assert not_nan_str({"name": "nan"}, ["name"]) is False
|
|
23
|
+
assert not_nan_str({"name": " NaN "}, ["name"]) is False
|
|
24
|
+
assert not_nan_str({"name": float("nan")}, ["name"]) is False
|
|
25
|
+
assert not_nan_str({}, ["name"]) is True
|
|
26
|
+
assert not_nan_str({"other": "x"}, ["name"]) is True
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_cobra_builder_renders_tenant():
|
|
30
|
+
builder = CobraBuilder()
|
|
31
|
+
result = builder.render({"fvTenant": [{"name": "acme"}]})
|
|
32
|
+
assert result.success is True
|
|
33
|
+
assert builder.xml is not None
|
|
34
|
+
assert "acme" in builder.xml
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_cobra_builder_unknown_key():
|
|
38
|
+
builder = CobraBuilder()
|
|
39
|
+
result = builder.render({"unknownKey": [{"x": 1}]})
|
|
40
|
+
assert result.success is False
|
|
41
|
+
assert any("does not exist" in msg for msg in result.log)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_cobra_builder_empty_output():
|
|
45
|
+
builder = CobraBuilder()
|
|
46
|
+
result = builder.render({})
|
|
47
|
+
assert result.success is False
|
|
48
|
+
assert any("No object was found" in msg for msg in result.log)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_cobra_builder_skips_empty_values():
|
|
52
|
+
builder = CobraBuilder()
|
|
53
|
+
result = builder.render({"fvTenant": []})
|
|
54
|
+
assert result.success is False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_registry_handlers_are_callable():
|
|
58
|
+
assert "fvTenant" in REGISTRY
|
|
59
|
+
assert all(callable(handler) for handler in REGISTRY.values())
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
from devaci.data import apply_filter, load_csv, load_xlsx
|
|
5
|
+
from devaci.exceptions import DataError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _df() -> pd.DataFrame:
|
|
9
|
+
return pd.DataFrame({"tag": ["a", "b", None, ""], "name": ["x", "y", "z", "w"]})
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_apply_filter_no_filters():
|
|
13
|
+
assert len(apply_filter(_df(), "tag")) == 4
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_apply_filter_with_filters():
|
|
17
|
+
assert apply_filter(_df(), "tag", ["a"]) == [{"tag": "a", "name": "x"}]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_apply_filter_missing_column():
|
|
21
|
+
assert apply_filter(_df(), "missing", ["a"]) == []
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_apply_filter_empty_filters():
|
|
25
|
+
assert len(apply_filter(_df(), "tag", [])) == 4
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_load_csv(tmp_path):
|
|
29
|
+
path = tmp_path / "tenants.csv"
|
|
30
|
+
pd.DataFrame({"tag": ["a", "b"], "name": ["x", "y"]}).to_csv(path, index=False)
|
|
31
|
+
|
|
32
|
+
assert load_csv(path, filters=["a"], by="tag") == {
|
|
33
|
+
"tenants": [{"tag": "a", "name": "x"}]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_load_xlsx_with_filters_sheet(tmp_path):
|
|
38
|
+
path = tmp_path / "data.xlsx"
|
|
39
|
+
with pd.ExcelWriter(path) as writer:
|
|
40
|
+
pd.DataFrame({"tag": ["a", "b"], "name": ["x", "y"]}).to_excel(
|
|
41
|
+
writer, sheet_name="tenants", index=False
|
|
42
|
+
)
|
|
43
|
+
pd.DataFrame({"name": ["a"], "enabled": [True]}).to_excel(
|
|
44
|
+
writer, sheet_name="filters", index=False
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
result = load_xlsx(path, by="tag", filters_source_sheet="filters")
|
|
48
|
+
assert result["tenants"] == [{"tag": "a", "name": "x"}]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_load_xlsx_missing_filters_sheet(tmp_path):
|
|
52
|
+
path = tmp_path / "data.xlsx"
|
|
53
|
+
pd.DataFrame({"tag": ["a"], "name": ["x"]}).to_excel(path, sheet_name="tenants", index=False)
|
|
54
|
+
|
|
55
|
+
with pytest.raises(DataError):
|
|
56
|
+
load_xlsx(path, by="tag", filters_source_sheet="nope")
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
from devaci import DeployClass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_deploy_in_memory_template(tmp_path):
|
|
7
|
+
aci = DeployClass(testing=True, working_folder=tmp_path, logging=False)
|
|
8
|
+
aci.template = ("fvTenant:\n - name: acme\n", "tenant.j2")
|
|
9
|
+
aci.deploy()
|
|
10
|
+
|
|
11
|
+
assert "acme" in aci.config
|
|
12
|
+
assert aci.results[0]["success"] is True
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_deploy_from_xlsx(tmp_path):
|
|
16
|
+
pd.DataFrame([{"name": "acme"}, {"name": "beta"}]).to_excel(
|
|
17
|
+
tmp_path / "data.xlsx", sheet_name="tenants", index=False
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
aci = DeployClass(testing=True, working_folder=tmp_path, logging=False)
|
|
21
|
+
aci.xlsx = "data.xlsx"
|
|
22
|
+
aci.template = (
|
|
23
|
+
"fvTenant:\n{% for row in tenants %}\n - name: {{ row.name }}\n{% endfor %}\n",
|
|
24
|
+
"tenant.j2",
|
|
25
|
+
)
|
|
26
|
+
aci.deploy()
|
|
27
|
+
|
|
28
|
+
config = aci.config
|
|
29
|
+
assert "acme" in config
|
|
30
|
+
assert "beta" in config
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_deploy_multiple_templates(tmp_path):
|
|
34
|
+
aci = DeployClass(testing=True, working_folder=tmp_path, logging=False)
|
|
35
|
+
aci.template = [
|
|
36
|
+
("fvTenant:\n - name: one\n", "one.j2"),
|
|
37
|
+
("fvTenant:\n - name: two\n", "two.j2"),
|
|
38
|
+
]
|
|
39
|
+
aci.deploy()
|
|
40
|
+
|
|
41
|
+
assert len(aci.results) == 2
|
|
42
|
+
assert all(result["success"] for result in aci.results)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_deploy_template_from_file(tmp_path):
|
|
46
|
+
(tmp_path / "tenant.j2").write_text("fvTenant:\n - name: acme\n", encoding="utf-8")
|
|
47
|
+
|
|
48
|
+
aci = DeployClass(testing=True, working_folder=tmp_path, logging=False)
|
|
49
|
+
aci.template = "tenant.j2"
|
|
50
|
+
aci.deploy()
|
|
51
|
+
|
|
52
|
+
assert "acme" in aci.config
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_deploy_from_csv(tmp_path):
|
|
56
|
+
pd.DataFrame({"tag": ["a", "b"], "name": ["acme", "beta"]}).to_csv(
|
|
57
|
+
tmp_path / "tenants.csv", index=False
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
aci = DeployClass(testing=True, working_folder=tmp_path, logging=False)
|
|
61
|
+
aci.csv = "tenants.csv"
|
|
62
|
+
aci.template = (
|
|
63
|
+
"fvTenant:\n{% for row in tenants %}\n - name: {{ row.name }}\n{% endfor %}\n",
|
|
64
|
+
"tenant.j2",
|
|
65
|
+
)
|
|
66
|
+
aci.deploy()
|
|
67
|
+
|
|
68
|
+
config = aci.config
|
|
69
|
+
assert "acme" in config
|
|
70
|
+
assert "beta" in config
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_deploy_save_output(tmp_path):
|
|
74
|
+
aci = DeployClass(
|
|
75
|
+
testing=True,
|
|
76
|
+
working_folder=tmp_path,
|
|
77
|
+
logging=False,
|
|
78
|
+
file_output="scripts/config",
|
|
79
|
+
)
|
|
80
|
+
aci.template = ("fvTenant:\n - name: acme\n", "tenant.j2")
|
|
81
|
+
aci.deploy()
|
|
82
|
+
|
|
83
|
+
output = tmp_path / "scripts" / "config.xml"
|
|
84
|
+
assert output.exists()
|
|
85
|
+
assert "acme" in output.read_text(encoding="utf-8")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_deploy_variable_named_name(tmp_path):
|
|
89
|
+
aci = DeployClass(testing=True, working_folder=tmp_path, logging=False)
|
|
90
|
+
aci.variables = {"name": "test", "descr": "Test tenant"}
|
|
91
|
+
aci.template = (
|
|
92
|
+
"fvTenant:\n - name: {{ name }}\n descr: {{ descr }}\n",
|
|
93
|
+
"tenant.j2",
|
|
94
|
+
)
|
|
95
|
+
aci.deploy()
|
|
96
|
+
|
|
97
|
+
assert aci.results[0]["success"] is True
|
|
98
|
+
assert "test" in aci.config
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import math
|
|
2
|
+
|
|
3
|
+
from devaci.filters import (
|
|
4
|
+
load_yaml,
|
|
5
|
+
nan_filter,
|
|
6
|
+
range_filter,
|
|
7
|
+
replace_str_nan_with_empty,
|
|
8
|
+
split_filter,
|
|
9
|
+
str_to_bool,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_split_filter():
|
|
14
|
+
assert split_filter("a,b,c") == ["a", "b", "c"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_split_filter_custom_delimiter():
|
|
18
|
+
assert split_filter("a;b", delimiter=";") == ["a", "b"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_range_filter():
|
|
22
|
+
assert range_filter("1-3,5") == [1, 2, 3, 5]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_range_filter_single():
|
|
26
|
+
assert range_filter("7") == [7]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_nan_filter():
|
|
30
|
+
assert nan_filter("nan") is False
|
|
31
|
+
assert nan_filter("value") is True
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_str_to_bool():
|
|
35
|
+
assert str_to_bool(True) is True
|
|
36
|
+
assert str_to_bool("yes") is True
|
|
37
|
+
assert str_to_bool("1") is True
|
|
38
|
+
assert str_to_bool("false") is False
|
|
39
|
+
assert str_to_bool("0") is False
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_replace_str_nan_with_empty():
|
|
43
|
+
assert replace_str_nan_with_empty("nan") == ""
|
|
44
|
+
assert replace_str_nan_with_empty(" NaN ") == ""
|
|
45
|
+
assert replace_str_nan_with_empty("value") == "value"
|
|
46
|
+
assert replace_str_nan_with_empty(42) == 42
|
|
47
|
+
assert replace_str_nan_with_empty({"a": "nan", "b": {"c": "NaN"}}) == {
|
|
48
|
+
"a": "",
|
|
49
|
+
"b": {"c": ""},
|
|
50
|
+
}
|
|
51
|
+
assert replace_str_nan_with_empty(["nan", 1, None]) == ["", 1, None]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_replace_str_nan_keeps_real_nan():
|
|
55
|
+
assert math.isnan(replace_str_nan_with_empty(float("nan")))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_load_yaml_no_coercion():
|
|
59
|
+
result = load_yaml("key: 123\nflag: true\nratio: 1.5\nmissing: nan\n")
|
|
60
|
+
assert result == {"key": "123", "flag": "true", "ratio": "1.5", "missing": ""}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_load_yaml_nested():
|
|
64
|
+
result = load_yaml("nested:\n - 1\n - false\n")
|
|
65
|
+
assert result == {"nested": ["1", "false"]}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from devaci.exceptions import JinjaError
|
|
4
|
+
from devaci.jinja import JinjaRenderer
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_render_mapping():
|
|
8
|
+
renderer = JinjaRenderer()
|
|
9
|
+
out = renderer.render("fvTenant:\n - name: acme\n")
|
|
10
|
+
assert out == {"fvTenant": [{"name": "acme"}]}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_render_with_variables():
|
|
14
|
+
renderer = JinjaRenderer()
|
|
15
|
+
out = renderer.render("fvTenant:\n - name: {{ tenant }}\n", tenant="acme")
|
|
16
|
+
assert out == {"fvTenant": [{"name": "acme"}]}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_render_variable_named_name():
|
|
20
|
+
renderer = JinjaRenderer()
|
|
21
|
+
out = renderer.render("fvTenant:\n - name: {{ name }}\n", name="acme")
|
|
22
|
+
assert out == {"fvTenant": [{"name": "acme"}]}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_render_syntax_error():
|
|
26
|
+
renderer = JinjaRenderer()
|
|
27
|
+
with pytest.raises(JinjaError):
|
|
28
|
+
renderer.render("{% if %}")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_render_non_mapping():
|
|
32
|
+
renderer = JinjaRenderer()
|
|
33
|
+
with pytest.raises(JinjaError):
|
|
34
|
+
renderer.render("- a\n- b\n")
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from dataclasses import FrozenInstanceError
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from devaci.results import CobraResult, DeployResult, JinjaResult, Result
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_result_to_dict():
|
|
9
|
+
result = Result(success=True, log=["a"], date="d")
|
|
10
|
+
assert result.to_dict() == {"date": "d", "success": True, "log": ["a"]}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_deploy_result_to_dict():
|
|
14
|
+
result = DeployResult(success=False, log=[], date="d", path="/p", name="n")
|
|
15
|
+
assert result.to_dict() == {
|
|
16
|
+
"date": "d",
|
|
17
|
+
"success": False,
|
|
18
|
+
"log": [],
|
|
19
|
+
"path": "/p",
|
|
20
|
+
"name": "n",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_jinja_result_to_dict():
|
|
25
|
+
result = JinjaResult(success=True, log=[], date="d", output={"a": 1})
|
|
26
|
+
assert result.to_dict() == {"date": "d", "success": True, "log": [], "output": {"a": 1}}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_cobra_result_none_config():
|
|
30
|
+
result = CobraResult(config=None)
|
|
31
|
+
assert result.xml is None
|
|
32
|
+
assert result.json is None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_results_are_frozen():
|
|
36
|
+
result = Result()
|
|
37
|
+
with pytest.raises(FrozenInstanceError):
|
|
38
|
+
result.success = True
|
devaci-0.1.0/tests/test.py
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
|
|
3
|
-
from devaci import DeployClass
|
|
4
|
-
|
|
5
|
-
ROOT = Path(__file__).parent / "testing/working"
|
|
6
|
-
|
|
7
|
-
OC = "OC164431"
|
|
8
|
-
|
|
9
|
-
SETTINGS = {
|
|
10
|
-
"testing": True,
|
|
11
|
-
"filters_source_sheet": "OCs",
|
|
12
|
-
"file_output": f"scripts/{OC}_config",
|
|
13
|
-
"logging_output": f"logs/{OC}_logging",
|
|
14
|
-
"working_folder": ROOT,
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if __name__ == "__main__":
|
|
19
|
-
aci = DeployClass(**SETTINGS)
|
|
20
|
-
aci.xlsx = "IBKACI6_v2.2_jriveros.xlsx"
|
|
21
|
-
aci.template = "template_v2.3.j2"
|
|
22
|
-
aci.deploy()
|
devaci-0.1.0/tests/test_cobra.py
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import pandas as pd
|
|
2
|
-
|
|
3
|
-
from devaci import DeployClass
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def test_deploy_in_memory_template(tmp_path):
|
|
7
|
-
aci = DeployClass(testing=True, working_folder=tmp_path, logging=False)
|
|
8
|
-
aci.template = ("fvTenant:\n - name: acme\n", "tenant.j2")
|
|
9
|
-
aci.deploy()
|
|
10
|
-
|
|
11
|
-
assert "acme" in aci.config
|
|
12
|
-
assert aci.results[0]["success"] is True
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def test_deploy_from_xlsx(tmp_path):
|
|
16
|
-
pd.DataFrame([{"name": "acme"}, {"name": "beta"}]).to_excel(
|
|
17
|
-
tmp_path / "data.xlsx", sheet_name="tenants", index=False
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
aci = DeployClass(testing=True, working_folder=tmp_path, logging=False)
|
|
21
|
-
aci.xlsx = "data.xlsx"
|
|
22
|
-
aci.template = (
|
|
23
|
-
"fvTenant:\n{% for row in tenants %}\n - name: {{ row.name }}\n{% endfor %}\n",
|
|
24
|
-
"tenant.j2",
|
|
25
|
-
)
|
|
26
|
-
aci.deploy()
|
|
27
|
-
|
|
28
|
-
config = aci.config
|
|
29
|
-
assert "acme" in config
|
|
30
|
-
assert "beta" in config
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import xml.dom.minidom
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
|
|
5
|
-
import pytest
|
|
6
|
-
|
|
7
|
-
from devaci import DeployClass
|
|
8
|
-
|
|
9
|
-
REPO = Path(__file__).resolve().parent.parent
|
|
10
|
-
XLSX = REPO / "data" / "IBKACI6_v2.2_jriveros.xlsx"
|
|
11
|
-
TEMPLATE = REPO / "data" / "templates" / "generic_v2.3.j2"
|
|
12
|
-
|
|
13
|
-
OC = "OC180460"
|
|
14
|
-
|
|
15
|
-
SETTINGS = {
|
|
16
|
-
"testing": True,
|
|
17
|
-
"filters_source_sheet": "OCs",
|
|
18
|
-
"file_output": f"outputs/scripts/{OC}_config",
|
|
19
|
-
"logging_output": f"outputs/logs/{OC}_logging",
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
@pytest.mark.skipif(not XLSX.exists() or not TEMPLATE.exists(), reason="private data not available")
|
|
24
|
-
def test_deploy_outputs():
|
|
25
|
-
aci = DeployClass(working_folder=REPO, **SETTINGS)
|
|
26
|
-
aci.xlsx = "data/IBKACI6_v2.2_jriveros.xlsx"
|
|
27
|
-
aci.template = "data/templates/generic_v2.3.j2"
|
|
28
|
-
aci.deploy()
|
|
29
|
-
|
|
30
|
-
# 1. Resultado exitoso
|
|
31
|
-
assert aci.results[0]["success"] is True
|
|
32
|
-
|
|
33
|
-
# 2. XML de salida: existe, no vacio, valido y filtrado
|
|
34
|
-
xml_path = REPO / f"outputs/scripts/{OC}_config.xml"
|
|
35
|
-
assert xml_path.exists()
|
|
36
|
-
xml_text = xml_path.read_text(encoding="utf-8")
|
|
37
|
-
assert xml_text.strip()
|
|
38
|
-
xml.dom.minidom.parseString(xml_text)
|
|
39
|
-
assert "Backend_AP" in xml_text
|
|
40
|
-
assert 'name="Interbank"' in xml_text
|
|
41
|
-
assert "OC152069" not in xml_text
|
|
42
|
-
|
|
43
|
-
# 3. Logging JSON: lista valida con entradas
|
|
44
|
-
log_path = REPO / f"outputs/logs/{OC}_logging.json"
|
|
45
|
-
assert log_path.exists()
|
|
46
|
-
history = json.loads(log_path.read_text(encoding="utf-8"))
|
|
47
|
-
assert isinstance(history, list) and history
|
|
48
|
-
assert all(isinstance(e, dict) and "success" in e and "log" in e for e in history)
|
|
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
|
|
File without changes
|
|
File without changes
|