devaci 0.1.0__tar.gz → 2.0.0a1__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.
Files changed (31) hide show
  1. {devaci-0.1.0 → devaci-2.0.0a1}/PKG-INFO +1 -1
  2. {devaci-0.1.0 → devaci-2.0.0a1}/pyproject.toml +8 -1
  3. {devaci-0.1.0 → devaci-2.0.0a1}/src/devaci/__init__.py +6 -1
  4. devaci-2.0.0a1/tests/test_cobra.py +59 -0
  5. devaci-2.0.0a1/tests/test_data.py +56 -0
  6. devaci-2.0.0a1/tests/test_deploy.py +85 -0
  7. devaci-2.0.0a1/tests/test_devaci.py +7 -0
  8. devaci-2.0.0a1/tests/test_filters.py +65 -0
  9. devaci-2.0.0a1/tests/test_jinja.py +28 -0
  10. devaci-2.0.0a1/tests/test_results.py +38 -0
  11. devaci-0.1.0/tests/test.py +0 -22
  12. devaci-0.1.0/tests/test_cobra.py +0 -10
  13. devaci-0.1.0/tests/test_deploy.py +0 -30
  14. devaci-0.1.0/tests/test_devaci.py +0 -5
  15. devaci-0.1.0/tests/test_outputs.py +0 -48
  16. {devaci-0.1.0 → devaci-2.0.0a1}/.gitignore +0 -0
  17. {devaci-0.1.0 → devaci-2.0.0a1}/LICENSE +0 -0
  18. {devaci-0.1.0 → devaci-2.0.0a1}/README.md +0 -0
  19. {devaci-0.1.0 → devaci-2.0.0a1}/src/devaci/cobra/__init__.py +0 -0
  20. {devaci-0.1.0 → devaci-2.0.0a1}/src/devaci/cobra/base.py +0 -0
  21. {devaci-0.1.0 → devaci-2.0.0a1}/src/devaci/cobra/builders.py +0 -0
  22. {devaci-0.1.0 → devaci-2.0.0a1}/src/devaci/cobra/registry.py +0 -0
  23. {devaci-0.1.0 → devaci-2.0.0a1}/src/devaci/console.py +0 -0
  24. {devaci-0.1.0 → devaci-2.0.0a1}/src/devaci/data.py +0 -0
  25. {devaci-0.1.0 → devaci-2.0.0a1}/src/devaci/deploy.py +0 -0
  26. {devaci-0.1.0 → devaci-2.0.0a1}/src/devaci/exceptions.py +0 -0
  27. {devaci-0.1.0 → devaci-2.0.0a1}/src/devaci/filters.py +0 -0
  28. {devaci-0.1.0 → devaci-2.0.0a1}/src/devaci/jinja.py +0 -0
  29. {devaci-0.1.0 → devaci-2.0.0a1}/src/devaci/py.typed +0 -0
  30. {devaci-0.1.0 → devaci-2.0.0a1}/src/devaci/results.py +0 -0
  31. {devaci-0.1.0 → devaci-2.0.0a1}/tests/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: devaci
3
- Version: 0.1.0
3
+ Version: 2.0.0a1
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.0"
7
+ version = "2.0.0a1"
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
- __version__ = "0.1.0"
10
+ try:
11
+ __version__ = version("devaci")
12
+ except PackageNotFoundError:
13
+ __version__ = "0.0.0"
9
14
 
10
15
  __all__ = [
11
16
  "CobraBuilder",
@@ -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,85 @@
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")
@@ -0,0 +1,7 @@
1
+ from importlib.metadata import version
2
+
3
+ import devaci
4
+
5
+
6
+ def test_version():
7
+ assert devaci.__version__ == version("devaci")
@@ -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,28 @@
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", name="t.j2")
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", name="t.j2", tenant="acme")
16
+ assert out == {"fvTenant": [{"name": "acme"}]}
17
+
18
+
19
+ def test_render_syntax_error():
20
+ renderer = JinjaRenderer()
21
+ with pytest.raises(JinjaError):
22
+ renderer.render("{% if %}", name="t.j2")
23
+
24
+
25
+ def test_render_non_mapping():
26
+ renderer = JinjaRenderer()
27
+ with pytest.raises(JinjaError):
28
+ renderer.render("- a\n- b\n", name="t.j2")
@@ -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
@@ -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()
@@ -1,10 +0,0 @@
1
- import cobra.mit.request
2
- import cobra.model.coop
3
-
4
-
5
- def test_cobra_model_coop_available():
6
- assert hasattr(cobra.model.coop, "Pol")
7
-
8
-
9
- def test_cobra_mit_request_available():
10
- assert hasattr(cobra.mit.request, "ConfigRequest")
@@ -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,5 +0,0 @@
1
- import devaci
2
-
3
-
4
- def test_version():
5
- assert devaci.__version__ == "0.1.0"
@@ -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