Sardou 0.1.0__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.
sardou-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: Sardou
3
+ Version: 0.1.0
4
+ Summary: Sardou TOSCA Library
5
+ Author-email: Jay DesLauriers <j.deslauriers@westminster.ac.uk>
6
+ Requires-Dist: ruamel.yaml
sardou-0.1.0/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # TOSCA in Swarmchestrate
2
+
3
+ This repository is home to TOSCA in the [Swarmchestrate](https://www.swarmchestrate.eu/) project, which will use TOSCA v2.0 to describe applications and capacities managed in a Swarmchestrate Universe.
4
+
5
+
6
+ ## Sardou TOSCA Library
7
+
8
+ Sardou validates and extracts info from a Swarmchestrate TOSCA template.
9
+
10
+ ### Prerequisites
11
+ - Python 3.10+
12
+ - [Puccini](https://github.com/tliron/puccini) 0.22.x
13
+ - Minimum GLIBC 2.34 (Ubuntu 22.04 or higher)
14
+
15
+ Install Puccini on Linux by:
16
+ ```sh
17
+ wget https://github.com/tliron/puccini/releases/download/v0.22.7/puccini_0.22.7_linux_amd64.deb
18
+ sudo dpkg -i puccini_0.22.7_linux_amd64.deb || sudo apt --fix-broken install -y
19
+ rm -f puccini_0.22.7_linux_amd64.deb
20
+ ```
21
+
22
+ ### Installation
23
+
24
+ Install using `pip` pointed at this GitHub repo. PyPI package coming soon.
25
+
26
+ ```bash
27
+ pip install git+https://github.com/Swarmchestrate/tosca.git
28
+ ```
29
+
30
+ ### Usage
31
+
32
+ Import the Sardou TOSCA Library
33
+
34
+ ```python
35
+ from sardou import Sardou # note the uppercase F
36
+ ```
37
+
38
+ Create a new `Sardou` object, passing it the path to your Swarmchestrate TOSCA template.
39
+ This will validate the template. If there are errors or warnings, they will be presented here.
40
+
41
+ ```python
42
+ >>> tosca = Sardou("my_app.yaml")
43
+ Processed successfully: my_app.yaml
44
+ ```
45
+
46
+ Grab the QoS requirements as a Python object.
47
+ You could wrap this as a dictionary and dump to JSON or YAML.
48
+
49
+ ```python
50
+ >>> tosca.get_qos()
51
+ [{'energy': {'type': 'swch:QoS.Energy.Budget', 'properties': {'priority': 0.3, 'target': 10}}}...
52
+ ```
53
+
54
+ Grab the Resource requirements as a Python object.
55
+ You could dump this to JSON or YAML.
56
+
57
+ ```python
58
+ >>> tosca.get_requirements()
59
+ {'worker-node': {'metadata': {'created_by': 'floria-tosca-lib', 'created_at': '2025-09-16T14:51:24Z', 'description': 'Generated from node worker-node', 'version': '1.0'}, 'capabilities': {'host': {'properties': {'num-cpus': {'$greater_than': 4}, 'mem-size': {'$greater_than': '8 GB'}}}, ...
60
+ ```
61
+
62
+ You can traverse YAML maps using dot notation if needed.
63
+
64
+ ```python
65
+ >>> tosca.service_template.node_templates
66
+ {'swarm': {'type': 'swch:Swarm', 'directives': ['substitute']}, ...
67
+ ```
68
+
69
+ ## Devs
70
+
71
+ It is recommended that developers open a GitHub Codespace on this repository, which includes dependencies and a Makefile for running Puccini manually.
72
+
73
+ ## TOSCA Template Validation with Puccini
74
+
75
+ This is an added feature that provides a Python validation library and script to check whether TOSCA service templates are valid using the [Puccini](https://github.com/tliron/puccini) parser.
76
+
77
+ ##### Validation Library (`lib/validation.py `)
78
+ - A library that defines the `validate_template()` function to validate a single TOSCA YAML file.
79
+ - Returns `True` if the template is valid, `False` if not.
80
+
81
+ ##### Validation Script (`run_validation.py`)
82
+ - A script that searches the `templates/` folder and validates all `.yaml` files in one run.
83
+ - Prints total successes/failures and exits with code `1` if any file fails.
84
+
85
+ Run:
86
+ - `python3 run_validation.py`
87
+
88
+
89
+ ## Contact
90
+
91
+ Contact Jay at Westminster for support with TOSCA and/or this repository.
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: Sardou
3
+ Version: 0.1.0
4
+ Summary: Sardou TOSCA Library
5
+ Author-email: Jay DesLauriers <j.deslauriers@westminster.ac.uk>
6
+ Requires-Dist: ruamel.yaml
@@ -0,0 +1,11 @@
1
+ README.md
2
+ pyproject.toml
3
+ Sardou.egg-info/PKG-INFO
4
+ Sardou.egg-info/SOURCES.txt
5
+ Sardou.egg-info/dependency_links.txt
6
+ Sardou.egg-info/requires.txt
7
+ Sardou.egg-info/top_level.txt
8
+ sardou/__init__.py
9
+ sardou/requirements.py
10
+ sardou/sardou.py
11
+ sardou/validation.py
@@ -0,0 +1 @@
1
+ ruamel.yaml
@@ -0,0 +1 @@
1
+ sardou
@@ -0,0 +1,17 @@
1
+ [project]
2
+ name = "Sardou"
3
+ version = "0.1.0"
4
+ description = "Sardou TOSCA Library"
5
+ authors = [
6
+ { name="Jay DesLauriers", email="j.deslauriers@westminster.ac.uk" }
7
+ ]
8
+ dependencies = [
9
+ "ruamel.yaml"
10
+ ]
11
+
12
+ [build-system]
13
+ requires = ["setuptools>=61.0"]
14
+ build-backend = "setuptools.build_meta"
15
+
16
+ [tool.setuptools]
17
+ packages = ["sardou"]
@@ -0,0 +1,2 @@
1
+ from .sardou import Sardou
2
+
@@ -0,0 +1,75 @@
1
+ """
2
+ TOSCA Resource Requirements Extractor
3
+ Converts TOSCA node_filter data to ask.yaml format
4
+ """
5
+
6
+ from datetime import datetime
7
+
8
+ def convert_node_filter_to_capabilities(node_filter):
9
+ """Convert TOSCA node_filter to capabilities format"""
10
+ capabilities = {}
11
+
12
+ # Extract conditions from $and
13
+ conditions = node_filter.get('$and', [])
14
+
15
+ for condition in conditions:
16
+ for constraint_func, args in condition.items():
17
+ if len(args) >= 2:
18
+ # Parse the property path: [SELF, TARGET, CAPABILITY, capability_type, property_name]
19
+ property_path = args[0].get('$get_property', [])
20
+ if len(property_path) >= 5:
21
+ capability_type = property_path[3]
22
+ property_name = property_path[4]
23
+ constraint_value = args[1]
24
+
25
+ # Initialize capability structure
26
+ if capability_type not in capabilities:
27
+ capabilities[capability_type] = {'properties': {}}
28
+
29
+ # Set the constraint
30
+ if constraint_func == '$equal':
31
+ capabilities[capability_type]['properties'][property_name] = constraint_value
32
+ else:
33
+ capabilities[capability_type]['properties'][property_name] = {constraint_func: constraint_value}
34
+
35
+ return capabilities
36
+
37
+ def extract_nodes_with_filter(tosca_dict):
38
+ """Extract nodes that have node_filter from node_templates"""
39
+ nodes_with_filter = {}
40
+
41
+ node_templates = tosca_dict.get('service_template', {}).get('node_templates', {})
42
+
43
+ for node_name, node_data in node_templates.items():
44
+ if 'node_filter' in node_data:
45
+ nodes_with_filter[node_name] = node_data['node_filter']
46
+
47
+ return nodes_with_filter
48
+
49
+ def tosca_to_ask_dict(tosca_dict):
50
+ """
51
+ Convert TOSCA dict to ask format dict
52
+
53
+ Args:
54
+ tosca_dict (dict): Parsed TOSCA YAML data
55
+
56
+ Returns:
57
+ dict: Ask format data
58
+ """
59
+ nodes_with_filter = extract_nodes_with_filter(tosca_dict)
60
+ ask_data = {}
61
+
62
+ for node_name, node_filter in nodes_with_filter.items():
63
+ ask_entry = {
64
+ 'metadata': {
65
+ 'created_by': 'sardou-tosca-lib',
66
+ 'created_at': datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ'),
67
+ 'description': f'Generated from node {node_name}',
68
+ 'version': '1.0'
69
+ },
70
+ 'capabilities': convert_node_filter_to_capabilities(node_filter)
71
+ }
72
+
73
+ ask_data[node_name] = ask_entry
74
+
75
+ return ask_data
@@ -0,0 +1,60 @@
1
+ from pathlib import Path
2
+ import json
3
+ from ruamel.yaml import YAML
4
+
5
+ from .validation import validate_template
6
+ from .requirements import tosca_to_ask_dict
7
+
8
+ class DotDict:
9
+ def __init__(self, **entries):
10
+ for k, v in entries.items():
11
+ if isinstance(v, dict):
12
+ v = DotDict(**v)
13
+ elif isinstance(v, list):
14
+ v = [DotDict(**i) if isinstance(i, dict) else i for i in v]
15
+ setattr(self, k, v)
16
+ def __getitem__(self, key):
17
+ return getattr(self, key)
18
+ def __setitem__(self, key, value):
19
+ setattr(self, key, value)
20
+ def __delitem__(self, key):
21
+ delattr(self, key)
22
+ def __contains__(self, key):
23
+ return hasattr(self, key)
24
+ def __repr__(self):
25
+ return repr(self._to_dict())
26
+ def _to_dict(self):
27
+ result = {}
28
+ for key, value in self.__dict__.items():
29
+ if isinstance(value, DotDict):
30
+ result[key] = value._to_dict()
31
+ elif isinstance(value, list):
32
+ result[key] = [
33
+ v._to_dict() if isinstance(v, DotDict) else v for v in value
34
+ ]
35
+ else:
36
+ result[key] = value
37
+ return result
38
+
39
+ def _to_json(self, indent=None, **kwargs):
40
+ return json.dumps(self._to_dict(), indent=indent, **kwargs)
41
+
42
+ class Sardou(DotDict):
43
+ def __init__(self, path):
44
+ path = Path(path)
45
+ if not path.exists():
46
+ raise FileNotFoundError(f"File does not exist: {path}")
47
+ if not validate_template(path):
48
+ raise ValueError(f"Validation failed for: {path}")
49
+ yaml = YAML(typ='safe')
50
+ with path.open('r') as f:
51
+ data = yaml.load(f)
52
+ super().__init__(**data)
53
+
54
+ def get_requirements(self):
55
+ return tosca_to_ask_dict(self._to_dict())
56
+
57
+ def get_qos(self, indent=None, **kwargs):
58
+ policies = self.service_template.policies
59
+ return [p._to_dict() if isinstance(p, DotDict) else p for p in policies]
60
+
@@ -0,0 +1,68 @@
1
+ import subprocess
2
+ import sys
3
+ from pathlib import Path
4
+ from tempfile import NamedTemporaryFile
5
+ from ruamel.yaml import YAML
6
+
7
+ PUCCINI_CMD = "/usr/bin/puccini-tosca"
8
+ PUCCINI_FLAGS = ["-x", "data_types.string.permissive"]
9
+
10
+ # Read and update YAML using ruamel.yaml
11
+ yaml = YAML()
12
+
13
+ def prevalidate(file_path: Path) -> bool:
14
+ # Check if file exists
15
+ if not file_path.exists():
16
+ print(f"File does not exist: {file_path}")
17
+ return False
18
+
19
+ try:
20
+ with file_path.open('r') as f:
21
+ data = yaml.load(f)
22
+ except Exception as e:
23
+ print(f"Error reading YAML file {file_path}: {e}")
24
+ return False
25
+ if not data:
26
+ print(f"No YAML content found")
27
+ return False
28
+ imports = data.get('imports', [])
29
+ nodes = data["service_template"].get('node_templates', {})
30
+
31
+ for imp in imports:
32
+ if isinstance(imp, dict) and 'profile' in imp:
33
+ imp['url'] = imp.pop('profile')
34
+
35
+ for _, node in nodes.items():
36
+ node.pop('node_filter', None)
37
+
38
+
39
+ return data
40
+
41
+
42
+ def validate_template(file_path: Path) -> bool:
43
+ # will run the puccini-tosca parse <with flag>
44
+ yaml_data = prevalidate(file_path)
45
+
46
+ # open a temp file
47
+ with NamedTemporaryFile() as temp_file:
48
+ yaml.dump(yaml_data, temp_file)
49
+
50
+ try:
51
+ result = subprocess.run(
52
+ [PUCCINI_CMD, "parse", str(temp_file.name)] + PUCCINI_FLAGS,
53
+ capture_output=True,
54
+ text=True
55
+ )
56
+ if result.returncode == 0:
57
+ print(f"Processed successfully: {file_path} \n")
58
+ return True
59
+ else:
60
+ print(f"Failed to process: {file_path} \n")
61
+ print("==== Error Output ====")
62
+ print(result.stderr.strip() or result.stdout.strip())
63
+ print("======================")
64
+ return False
65
+
66
+ except FileNotFoundError:
67
+ print(f"Puccini not found at {PUCCINI_CMD}. Please install it first.")
68
+ sys.exit(1)
sardou-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+