shellsmith 0.1.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.
shellsmith/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+ __version__ = "0.1.0"
2
+
3
+ from .crud.shells import ( # noqa: F401
4
+ delete_shell,
5
+ delete_submodel_ref,
6
+ get_shell,
7
+ get_shells,
8
+ get_submodel_refs,
9
+ )
10
+ from .crud.submodels import ( # noqa: F401
11
+ delete_submodel,
12
+ delete_submodel_element,
13
+ get_submodel,
14
+ get_submodel_element,
15
+ get_submodel_elements,
16
+ get_submodels,
17
+ patch_submodel_element_value,
18
+ )
shellsmith/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from shellsmith.cli.main import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
File without changes
@@ -0,0 +1,5 @@
1
+ from .info import info # noqa: F401
2
+ from .nuke import nuke # noqa: F401
3
+ from .shell import shell_delete # noqa: F401
4
+ from .submodel import submodel_delete # noqa: F401
5
+ from .upload import upload # noqa: F401
@@ -0,0 +1,47 @@
1
+ import shellsmith
2
+ from shellsmith import services
3
+
4
+
5
+ def info():
6
+ print("ℹ️ Showing all shells")
7
+ print_shells_tree()
8
+ print_unreferenced_submodels()
9
+ print_dangling_submodel_refs()
10
+
11
+
12
+ def print_unreferenced_submodels():
13
+ submodel_ids = services.find_unreferenced_submodels()
14
+
15
+ if submodel_ids:
16
+ print()
17
+ print("⚠️ Unreferenced Submodels:")
18
+ for submodel_id in submodel_ids:
19
+ submodel = shellsmith.get_submodel(submodel_id)
20
+ id_short = submodel["idShort"]
21
+ print(f"- {id_short} ({submodel_id})")
22
+
23
+
24
+ def print_shells_tree():
25
+ shells = shellsmith.get_shells()
26
+ for shell in shells:
27
+ print(f"{shell['idShort']}: {shell['id']}")
28
+
29
+ submodels = services.get_shell_submodels(shell["id"])
30
+
31
+ for i, submodel in enumerate(submodels):
32
+ is_last = i == len(submodels) - 1
33
+ prefix = "└──" if is_last else "├──"
34
+ print(f"{prefix} {submodel['idShort']}: {submodel['id']}")
35
+
36
+
37
+ def print_dangling_submodel_refs():
38
+ dangling = services.find_dangling_submodel_refs()
39
+
40
+ if dangling:
41
+ print()
42
+ print("⚠️ Dangling Submodel References:")
43
+ for shell_id, submodel_ids in dangling.items():
44
+ shell = shellsmith.get_shell(shell_id)
45
+ print(f"- {shell['idShort']}: {shell_id}")
46
+ for submodel_id in submodel_ids:
47
+ print(f" └── {submodel_id}")
@@ -0,0 +1,9 @@
1
+ from shellsmith import services
2
+
3
+
4
+ def nuke():
5
+ print("☣️ Deleting all Shells and Submodels!")
6
+ print("☢️ Deleting all Shells and Submodels!")
7
+ print("⚠️ Deleting all Shells and Submodels!")
8
+ services.delete_all_shells()
9
+ services.delete_all_submodels()
@@ -0,0 +1,17 @@
1
+ import requests.exceptions
2
+
3
+ import shellsmith
4
+ from shellsmith import services
5
+
6
+
7
+ def shell_delete(shell_id: str, cascade: bool = False):
8
+ print(f"🗑️ Deleting Shell: {shell_id}")
9
+ try:
10
+ if cascade:
11
+ services.delete_shell_cascading(shell_id)
12
+ print(f"✅ Shell '{shell_id}' and its submodels deleted.")
13
+ else:
14
+ shellsmith.delete_shell(shell_id)
15
+ print(f"✅ Shell '{shell_id}' deleted.")
16
+ except requests.exceptions.HTTPError as e:
17
+ print(f"❌ Failed to delete Shell '{shell_id}': {e}")
@@ -0,0 +1,16 @@
1
+ import requests
2
+
3
+ import shellsmith
4
+ from shellsmith import services
5
+
6
+
7
+ def submodel_delete(submodel_id: str, unlink: bool = False):
8
+ print(f"🗑️ Deleting Submodel: {submodel_id}")
9
+ try:
10
+ shellsmith.delete_submodel(submodel_id)
11
+ print(f"✅ Submodel '{submodel_id}' deleted.")
12
+ except requests.exceptions.HTTPError:
13
+ print(f"❌ Submodel '{submodel_id}' doesn't exist.")
14
+ if unlink:
15
+ services.remove_submodel_references(submodel_id)
16
+ print(f"✅ Removed Shell references to Submodel '{submodel_id}'.")
@@ -0,0 +1,14 @@
1
+ from pathlib import Path
2
+
3
+ from shellsmith.upload import upload_aas, upload_aas_folder
4
+
5
+
6
+ def upload(path: Path):
7
+ if path.is_file():
8
+ print(f"ℹ️ Uploading file: {path}")
9
+ upload_aas(path)
10
+ elif path.is_dir():
11
+ print(f"ℹ️ Uploading all AAS files in folder: {path}")
12
+ upload_aas_folder(path)
13
+ else:
14
+ print(f"❌ Path '{path}' does not exist or is invalid.")
shellsmith/cli/main.py ADDED
@@ -0,0 +1,44 @@
1
+ import requests
2
+
3
+ from shellsmith import __version__, services
4
+ from shellsmith.config import config
5
+
6
+ from .commands import info, nuke, shell_delete, submodel_delete, upload
7
+ from .parser import build_parser
8
+
9
+
10
+ def print_header():
11
+ print("===============================================")
12
+ print(f"Shellsmith - AAS Toolkit v{__version__}")
13
+ print(f"Host: {config.host} ({services.health()})")
14
+ print("===============================================")
15
+ print()
16
+
17
+
18
+ def main():
19
+ print_header()
20
+ parser = build_parser()
21
+ args = parser.parse_args()
22
+
23
+ commands = {
24
+ "upload": lambda _args: upload(args.path),
25
+ "info": lambda _args: info(),
26
+ "nuke": lambda _args: nuke(),
27
+ "shell.delete": lambda _args: shell_delete(args.id, cascade=args.cascade),
28
+ "submodel.delete": lambda _args: submodel_delete(args.id, unlink=args.unlink),
29
+ }
30
+
31
+ try:
32
+ key = args.command
33
+ if key == "shell" and args.shell_command:
34
+ key += f".{args.shell_command}"
35
+ elif key == "submodel" and args.submodel_command:
36
+ key += f".{args.submodel_command}"
37
+
38
+ handler = commands.get(key)
39
+ if handler:
40
+ handler(args)
41
+ else:
42
+ parser.print_help()
43
+ except requests.exceptions.ConnectionError as e:
44
+ print(f"😩 Cannot reach {config.host}: {e}")
@@ -0,0 +1,63 @@
1
+ import argparse
2
+ from pathlib import Path
3
+
4
+ from shellsmith import __version__
5
+
6
+
7
+ def build_parser():
8
+ parser = argparse.ArgumentParser(description="AAS Tools CLI")
9
+ parser.add_argument(
10
+ "--version",
11
+ action="version",
12
+ version=f"%(prog)s v{__version__}",
13
+ )
14
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
15
+
16
+ # ──────────────────────────── upload ────────────────────────────
17
+ upload_parser = subparsers.add_parser("upload", help="Upload AAS file or folder")
18
+ upload_parser.add_argument("path", type=Path, help="Path to the AAS file or folder")
19
+
20
+ # ───────────────────────────── info ─────────────────────────────
21
+ subparsers.add_parser("info", help="Display all AAS shells and their submodels")
22
+
23
+ # ───────────────────────────── nuke ─────────────────────────────
24
+ subparsers.add_parser("nuke", help="Delete all AAS shells and submodels")
25
+
26
+ # ──────────────────────────── shell ─────────────────────────────
27
+ shell_parser = subparsers.add_parser(
28
+ "shell",
29
+ help="Manage Asset Administration Shells",
30
+ )
31
+ shell_subparsers = shell_parser.add_subparsers(dest="shell_command")
32
+ shell_delete_parser = shell_subparsers.add_parser(
33
+ "delete",
34
+ help="Delete an AAS Shell by ID",
35
+ )
36
+ shell_delete_parser.add_argument("id", type=str, help="ID of the shell to delete")
37
+ shell_delete_parser.add_argument(
38
+ "--cascade",
39
+ "-c",
40
+ action="store_true",
41
+ help="Also delete all referenced submodels",
42
+ )
43
+
44
+ # ───────────────────────────── submodel ─────────────────────────────
45
+ submodel_parser = subparsers.add_parser("submodel", help="Manage Submodels")
46
+ submodel_subparsers = submodel_parser.add_subparsers(dest="submodel_command")
47
+ submodel_delete_parser = submodel_subparsers.add_parser(
48
+ "delete",
49
+ help="Delete a Submodel by ID",
50
+ )
51
+ submodel_delete_parser.add_argument(
52
+ "id",
53
+ type=str,
54
+ help="ID of the submodel to delete",
55
+ )
56
+ submodel_delete_parser.add_argument(
57
+ "--unlink",
58
+ "-u",
59
+ action="store_true",
60
+ help="Remove all Shell references to this Submodel",
61
+ )
62
+
63
+ return parser
shellsmith/config.py ADDED
@@ -0,0 +1,15 @@
1
+ from pydantic_settings import BaseSettings, SettingsConfigDict
2
+
3
+
4
+ class Settings(BaseSettings):
5
+ basyx_env_host: str = "http://localhost:8081"
6
+ neo4j_uri: str = "neo4j://localhost:7687"
7
+
8
+ @property
9
+ def host(self):
10
+ return self.basyx_env_host
11
+
12
+ model_config = SettingsConfigDict(env_file=".env", env_prefix="SHELLSMITH_")
13
+
14
+
15
+ config = Settings()
File without changes
@@ -0,0 +1,61 @@
1
+ from typing import Dict, List
2
+
3
+ import requests
4
+
5
+ from shellsmith.config import config
6
+ from shellsmith.utils import base64_encoded
7
+
8
+
9
+ def get_shells(host: str = config.host) -> List[Dict]:
10
+ url = f"{host}/shells"
11
+ response = requests.get(url)
12
+ response.raise_for_status()
13
+ json_response = response.json()
14
+ shells = json_response["result"]
15
+ return shells
16
+
17
+
18
+ def get_shell(shell_id, encode=True, host: str = config.host) -> Dict:
19
+ shell_id = base64_encoded(shell_id, encode)
20
+ url = f"{host}/shells/{shell_id}"
21
+
22
+ response = requests.get(url)
23
+ response.raise_for_status()
24
+ shell = response.json()
25
+ return shell
26
+
27
+
28
+ def delete_shell(shell_id: str, encode=True, host: str = config.host):
29
+ shell_id = base64_encoded(shell_id, encode)
30
+
31
+ url = f"{host}/shells/{shell_id}"
32
+ response = requests.delete(url)
33
+ response.raise_for_status()
34
+
35
+
36
+ def get_submodel_refs(
37
+ shell_id: str,
38
+ encode=True,
39
+ host: str = config.host,
40
+ ):
41
+ shell_id = base64_encoded(shell_id, encode)
42
+
43
+ url = f"{host}/shells/{shell_id}/submodel-refs"
44
+ response = requests.get(url)
45
+ response.raise_for_status()
46
+ submodel_refs = response.json()["result"]
47
+ return submodel_refs
48
+
49
+
50
+ def delete_submodel_ref(
51
+ shell_id: str,
52
+ submodel_id,
53
+ encode=True,
54
+ host: str = config.host,
55
+ ):
56
+ shell_id = base64_encoded(shell_id, encode)
57
+ submodel_id = base64_encoded(submodel_id, encode)
58
+
59
+ url = f"{host}/shells/{shell_id}/submodel-refs/{submodel_id}"
60
+ response = requests.delete(url)
61
+ response.raise_for_status()
@@ -0,0 +1,126 @@
1
+ from typing import Dict, List
2
+ from urllib.parse import quote
3
+
4
+ import requests
5
+
6
+ from shellsmith.config import config
7
+ from shellsmith.utils import base64_encoded
8
+
9
+
10
+ def get_submodels(host: str = config.host) -> List[Dict]:
11
+ """
12
+ Returns all Submodels
13
+
14
+ GET /submodels
15
+ """
16
+ url = f"{host}/submodels"
17
+
18
+ response = requests.get(url)
19
+ response.raise_for_status()
20
+ json_response = response.json()
21
+ submodels = json_response["result"]
22
+ return submodels
23
+
24
+
25
+ def get_submodel(submodel_id: str, encode=True, host: str = config.host) -> Dict:
26
+ """
27
+ Returns a specific Submodel
28
+
29
+ GET /submodels/{submodel_id}
30
+ """
31
+ submodel_id = base64_encoded(submodel_id, encode)
32
+ url = f"{host}/submodels/{submodel_id}"
33
+
34
+ response = requests.get(url)
35
+ response.raise_for_status()
36
+ submodel = response.json()
37
+ return submodel
38
+
39
+
40
+ def delete_submodel(submodel_id: str, encode=True, host: str = config.host):
41
+ """
42
+ Deletes a specific Submodel
43
+
44
+ DELETE /submodels/{submodel_id}
45
+ """
46
+ submodel_id = base64_encoded(submodel_id, encode)
47
+ url = f"{host}/submodels/{submodel_id}"
48
+
49
+ response = requests.delete(url)
50
+ response.raise_for_status()
51
+
52
+
53
+ # ─────────────────────────── Submodel elements ───────────────────────────
54
+
55
+
56
+ def get_submodel_elements(submodel_id: str, encode=True, host: str = config.host):
57
+ """
58
+ Returns all submodel elements of a specific Submodel
59
+
60
+ GET /submodels/{submodel_id}/submodel-elements
61
+ """
62
+ submodel_id = base64_encoded(submodel_id, encode)
63
+ url = f"{host}/submodels/{submodel_id}/submodel-elements"
64
+
65
+ response = requests.get(url)
66
+ response.raise_for_status()
67
+ json_response = response.json()
68
+ elements = json_response["result"]
69
+ return elements
70
+
71
+
72
+ def get_submodel_element(
73
+ submodel_id: str, id_short_path: str, encode=True, host: str = config.host
74
+ ):
75
+ """
76
+ Returns all submodel elements including their hierarchy
77
+
78
+ GET /submodels/{submodel_id}/submodel-elements/{id_short_path}
79
+ """
80
+ submodel_id = base64_encoded(submodel_id, encode)
81
+ url = f"{host}/submodels/{submodel_id}/submodel-elements/{id_short_path}"
82
+
83
+ response = requests.get(url)
84
+ response.raise_for_status()
85
+ element = response.json()
86
+ return element
87
+
88
+
89
+ def delete_submodel_element(
90
+ submodel_id: str,
91
+ id_short_path: str,
92
+ value: str,
93
+ encode=True,
94
+ host: str = config.host,
95
+ ):
96
+ """
97
+ Deletes a submodel element at a specified path
98
+
99
+ DELETE /submodels/{submodel_id}/submodel-elements/{idShortPath}
100
+ """
101
+ submodel_id = base64_encoded(submodel_id, encode)
102
+ id_short_path = quote(id_short_path)
103
+
104
+ url = f"{host}/submodels/{submodel_id}/submodel-elements/{id_short_path}"
105
+ response = requests.delete(url, json=value)
106
+ response.raise_for_status()
107
+
108
+
109
+ def patch_submodel_element_value(
110
+ submodel_id: str,
111
+ id_short_path: str,
112
+ value: str,
113
+ encode=True,
114
+ host: str = config.host,
115
+ ):
116
+ """
117
+ Updates the value of an existing Submodel Element
118
+
119
+ PATCH /submodels/{submodel_id}/submodel-elements/{id_short_path}/$value
120
+ """
121
+ submodel_id = base64_encoded(submodel_id, encode)
122
+ id_short_path = quote(id_short_path)
123
+ url = f"{host}/submodels/{submodel_id}" f"/submodel-elements/{id_short_path}/$value"
124
+
125
+ response = requests.patch(url, json=value)
126
+ response.raise_for_status()
shellsmith/services.py ADDED
@@ -0,0 +1,131 @@
1
+ from typing import Dict, List
2
+
3
+ import requests
4
+
5
+ import shellsmith
6
+ from shellsmith.config import config
7
+
8
+
9
+ def get_shell_submodels(shell_id: str) -> List[Dict]:
10
+ shell = shellsmith.get_shell(shell_id)
11
+ if "submodels" not in shell:
12
+ return []
13
+
14
+ submodel_ids = extract_shell_submodel_refs(shell)
15
+ submodels: List[Dict] = []
16
+
17
+ for submodel_id in submodel_ids:
18
+ try:
19
+ submodel = shellsmith.get_submodel(submodel_id)
20
+ submodels.append(submodel)
21
+ except requests.exceptions.HTTPError:
22
+ print(f"⚠️ Submodel '{submodel_id}' not found")
23
+
24
+ return submodels
25
+
26
+
27
+ def delete_shell_cascading(
28
+ shell_id: str,
29
+ host: str = config.host,
30
+ ):
31
+ delete_submodels_of_shell(shell_id, host=host)
32
+ shellsmith.delete_shell(shell_id, host=host)
33
+
34
+
35
+ def delete_submodels_of_shell(
36
+ shell_id: str,
37
+ host: str = config.host,
38
+ ):
39
+ shell = shellsmith.get_shell(shell_id, host=host)
40
+
41
+ if "submodels" in shell:
42
+ for submodel in shell["submodels"]:
43
+ submodel_id = submodel["keys"][0]["value"]
44
+ try:
45
+ shellsmith.delete_submodel(submodel_id, host=host)
46
+ except requests.exceptions.HTTPError:
47
+ print(f"Warning: Submodel {submodel_id} doesn't exist")
48
+
49
+
50
+ def remove_submodel_references(submodel_id: str):
51
+ shells = shellsmith.get_shells()
52
+ for shell in shells:
53
+ if submodel_id in extract_shell_submodel_refs(shell):
54
+ shellsmith.delete_submodel_ref(shell["id"], submodel_id)
55
+
56
+
57
+ def remove_dangling_submodel_refs():
58
+ shells = shellsmith.get_shells()
59
+ submodels = shellsmith.get_submodels()
60
+ submodel_ids = {submodel["id"] for submodel in submodels}
61
+
62
+ for shell in shells:
63
+ for submodel_id in extract_shell_submodel_refs(shell):
64
+ if submodel_id not in submodel_ids:
65
+ shellsmith.delete_submodel_ref(shell["id"], submodel_id)
66
+
67
+
68
+ def delete_all_submodels(host: str = config.host):
69
+ submodels = shellsmith.get_submodels(host=host)
70
+ for submodel in submodels:
71
+ shellsmith.delete_submodel(submodel["id"])
72
+
73
+
74
+ def delete_all_shells(host: str = config.host):
75
+ shells = shellsmith.get_shells()
76
+ for shell in shells:
77
+ shellsmith.delete_shell(shell["id"], host=host)
78
+
79
+
80
+ def health(timeout: float = 0.1) -> str:
81
+ url = f"{config.host}/actuator/health"
82
+
83
+ try:
84
+ response = requests.get(url, timeout=timeout)
85
+ response.raise_for_status()
86
+ data = response.json()
87
+ return data["status"]
88
+ except requests.exceptions.ConnectionError:
89
+ return "DOWN"
90
+
91
+
92
+ def extract_shell_submodel_refs(shell: Dict) -> List[str]:
93
+ return [
94
+ submodel["keys"][0]["value"]
95
+ for submodel in shell["submodels"]
96
+ if "submodels" in shell
97
+ ]
98
+
99
+
100
+ def find_unreferenced_submodels() -> list[str]:
101
+ shells = shellsmith.get_shells()
102
+ submodels = shellsmith.get_submodels()
103
+
104
+ submodel_ref_ids = {
105
+ submodel_id
106
+ for shell in shells
107
+ for submodel_id in extract_shell_submodel_refs(shell)
108
+ }
109
+
110
+ submodel_ids = {submodel["id"] for submodel in submodels}
111
+ return list(submodel_ids - submodel_ref_ids)
112
+
113
+
114
+ def find_dangling_submodel_refs() -> dict[str, list[str]]:
115
+ """
116
+ Returns a mapping of shell_id -> list of submodel IDs
117
+ that are referenced in the shell but do not exist anymore.
118
+ """
119
+ shells = shellsmith.get_shells()
120
+ submodels = shellsmith.get_submodels()
121
+ existing_submodel_ids = {submodel["id"] for submodel in submodels}
122
+
123
+ dangling_refs: dict[str, list[str]] = {}
124
+
125
+ for shell in shells:
126
+ shell_id = shell["id"]
127
+ for submodel_id in extract_shell_submodel_refs(shell):
128
+ if submodel_id not in existing_submodel_ids:
129
+ dangling_refs.setdefault(shell_id, []).append(submodel_id)
130
+
131
+ return dangling_refs
shellsmith/upload.py ADDED
@@ -0,0 +1,40 @@
1
+ import mimetypes
2
+ from pathlib import Path
3
+
4
+ import requests
5
+
6
+ from shellsmith.config import config
7
+
8
+
9
+ def upload_aas_folder(path: Path | str):
10
+ folder_path = Path(path)
11
+
12
+ if not folder_path.is_dir():
13
+ raise ValueError(f"{folder_path} is not a valid directory.")
14
+
15
+ for aas_file in folder_path.iterdir():
16
+ if aas_file.is_file() and aas_file.suffix in {".json", ".xml", ".aasx"}:
17
+ print(f"Uploading: '{aas_file.name}'")
18
+ upload_aas(aas_file)
19
+
20
+
21
+ def upload_aas(path: Path | str):
22
+ path = Path(path)
23
+ url = f"{config.host}/upload"
24
+
25
+ mime_type, _ = mimetypes.guess_type(path)
26
+ if mime_type is None:
27
+ # .aasx
28
+ mime_type = "application/octet-stream"
29
+
30
+ with open(path, "rb") as file:
31
+ files = [("file", (path.name, file, mime_type))]
32
+ try:
33
+ response = requests.post(url, files=files)
34
+ response.raise_for_status()
35
+ success = response.json()
36
+ print(f"✅ Successfully uploaded '{path.name}': {success}")
37
+ return success
38
+ except requests.exceptions.HTTPError as e:
39
+ print(f"❌ Failed to upload '{path.name}': {e}")
40
+ return False
shellsmith/utils.py ADDED
@@ -0,0 +1,35 @@
1
+ import base64
2
+ from typing import Optional
3
+
4
+
5
+ def base64_encode(text: Optional[str]) -> Optional[str]:
6
+ try:
7
+ return (
8
+ base64.urlsafe_b64encode(text.encode("utf-8"))
9
+ .decode("utf-8")
10
+ .rstrip("=") # padding character if input long multiple of 3
11
+ )
12
+ except (TypeError, AttributeError) as e:
13
+ if text is None:
14
+ return None
15
+ raise e
16
+
17
+
18
+ def base64_decode(encoded_text: Optional[str]) -> Optional[str]:
19
+ try:
20
+ missing_padding = 4 - (len(encoded_text) % 4)
21
+ if missing_padding > 0:
22
+ encoded_text += "=" * missing_padding
23
+ return base64.urlsafe_b64decode(encoded_text).decode("utf-8")
24
+ except TypeError as e:
25
+ if encoded_text is None:
26
+ return None
27
+ raise e
28
+
29
+
30
+ def base64_encoded(identifier: str, encode: bool) -> str:
31
+ """
32
+ Return the base64-encoded identifier if encode is True;
33
+ otherwise, return it unchanged.
34
+ """
35
+ return base64_encode(identifier) if encode else identifier
@@ -0,0 +1,215 @@
1
+ Metadata-Version: 2.4
2
+ Name: shellsmith
3
+ Version: 0.1.0
4
+ Summary: A Python toolkit and CLI for managing Asset Administration Shells
5
+ Author-email: Peter Stein <peterstein@dfki.de>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2025 Peter Stein
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/ptrstn/shellsmith
29
+ Project-URL: Issues, https://github.com/ptrstn/shellsmith/issues
30
+ Classifier: Programming Language :: Python :: 3
31
+ Classifier: Operating System :: OS Independent
32
+ Requires-Python: >=3.10
33
+ Description-Content-Type: text/markdown
34
+ License-File: LICENSE
35
+ Requires-Dist: pydantic-settings
36
+ Requires-Dist: requests
37
+ Provides-Extra: test
38
+ Requires-Dist: black; extra == "test"
39
+ Requires-Dist: flake8; extra == "test"
40
+ Requires-Dist: isort; extra == "test"
41
+ Requires-Dist: pytest; extra == "test"
42
+ Requires-Dist: pytest-cov; extra == "test"
43
+ Requires-Dist: pytest-dotenv; extra == "test"
44
+ Dynamic: license-file
45
+
46
+ [![Python Package](https://github.com/ptrstn/shellsmith/actions/workflows/python-package.yaml/badge.svg)](https://github.com/ptrstn/shellsmith/actions/workflows/python-package.yaml)
47
+ [![codecov](https://codecov.io/gh/ptrstn/shellsmith/branch/main/graph/badge.svg)](https://codecov.io/gh/ptrstn/shellsmith)
48
+ [![PyPI version](https://badge.fury.io/py/shellsmith.svg)](https://badge.fury.io/py/shellsmith)
49
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
50
+
51
+ # Shellsmith
52
+
53
+ Shellsmith is a Python toolkit and CLI for managing Asset Administration Shells (AAS), Submodels, and related resources.
54
+ It is designed to interact with [Eclipse BaSyx](https://www.eclipse.org/basyx/), a middleware platform for AAS that follows the [Industry 4.0 standard](https://industrialdigitaltwin.org/en/content-hub/aasspecifications).
55
+
56
+ ### Features
57
+
58
+ - Python API for CRUD operations on shells, submodels, and submodel elements
59
+ - CLI interface for quick scripting
60
+ - `.env`-based configuration
61
+
62
+ ## 🚀 Installation
63
+
64
+ ```bash
65
+ pip install shellsmith
66
+ ```
67
+
68
+ **Requires**: Python 3.10+
69
+
70
+ ## 🔧 Configuration
71
+
72
+ The default AAS environment host is:
73
+
74
+ ```
75
+ http://localhost:8081
76
+ ```
77
+
78
+ You can override it by setting the `SHELLSMITH_BASYX_ENV_HOST` environment variable, or by creating a `.env` file in your project root with:
79
+
80
+ ```bash
81
+ SHELLSMITH_BASYX_ENV_HOST=http://your-host:1234
82
+ ```
83
+
84
+ ## 🛠️ Usage
85
+
86
+ ```bash
87
+ aas --help
88
+ ```
89
+
90
+ Common commands:
91
+
92
+ ```bash
93
+ aas info # Show all shells and submodels
94
+ aas upload <file|folder> # Upload AAS file or folder
95
+ aas shell delete <id> # Delete a shell
96
+ aas submodel delete <id> # Delete a submodel
97
+ ```
98
+
99
+ Use `--cascade` or `--unlink` to control deletion behavior:
100
+
101
+ ```bash
102
+ aas shell delete <id> --cascade # Also delete referenced submodels
103
+ aas submodel delete <id> --unlink # Remove references from shells
104
+ ```
105
+
106
+ ## 📡 API Usage
107
+
108
+ You can also use `shellsmith` as a Python package:
109
+
110
+ ```python
111
+ import shellsmith
112
+
113
+ # Get all available shells
114
+ shells = shellsmith.get_shells()
115
+
116
+ # Get a specific shell by ID (automatically base64-encoded)
117
+ shell = shellsmith.get_shell("example_aas_id")
118
+
119
+ # Disable base64 encoding if your ID is already encoded
120
+ submodel = shellsmith.get_submodel("ZXhhbXBsZV9hYXNfaWQ=", encode=False)
121
+
122
+ # Use a custom AAS environment host
123
+ submodel_refs = shellsmith.get_submodel_refs("example_aas_id", host="http://localhost:8081")
124
+ ```
125
+
126
+ > ℹ️ `shell_id` and `submodel_id` are automatically base64-encoded unless you set `encode=False`. This is required by the BaSyx API for identifier-based URLs.
127
+
128
+ The tables below show the mapping between BaSyx AAS REST API endpoints and the implemented client functions.
129
+
130
+ > 📚 See [Plattform_i40 API reference](https://app.swaggerhub.com/apis/Plattform_i40/Entire-API-Collection) for endpoint details.
131
+
132
+ ### Shells
133
+
134
+ | Method | BaSyx Endpoint | Shellsmith Function |
135
+ |--------|--------------------------------------------------------------|-----------------------|
136
+ | GET | `/shells` | `get_shells` |
137
+ | POST | `/shells` | ❌ |
138
+ | GET | `/shells/{aasIdentifier}` | `get_shell` |
139
+ | PUT | `/shells/{aasIdentifier}` | ❌ |
140
+ | DELETE | `/shells/{aasIdentifier}` | `delete_shell` |
141
+ | GET | `/shells/{aasIdentifier}/submodel-refs` | `get_submodel_refs` |
142
+ | POST | `/shells/{aasIdentifier}/submodel-refs` | ❌ |
143
+ | DELETE | `/shells/{aasIdentifier}/submodel-refs/{submodelIdentifier}` | `delete_submodel_ref` |
144
+
145
+ ### Submodels
146
+
147
+ | Method | BaSyx Endpoint | Shellsmith Function |
148
+ |--------|---------------------------------------------|---------------------|
149
+ | GET | `/submodels` | `get_submodels` |
150
+ | POST | `/submodels` | ❌ |
151
+ | GET | `/submodels/{submodelIdentifier}` | `get_submodel` |
152
+ | PUT | `/submodels/{submodelIdentifier}` | ❌ |
153
+ | DELETE | `/submodels/{submodelIdentifier}` | `delete_submodel` |
154
+ | GET | `/submodels/{submodelIdentifier}/$value` | ❌ |
155
+ | PATCH | `/submodels/{submodelIdentifier}/$value` | ❌ |
156
+ | GET | `/submodels/{submodelIdentifier}/$metadata` | ❌ |
157
+
158
+ ### Submodel Elements
159
+
160
+ | Method | BaSyx Endpoint | Shellsmith Function |
161
+ |--------|--------------------------------------------------------------------------|--------------------------------|
162
+ | GET | `/submodels/{submodelIdentifier}/submodel-elements` | `get_submodel_elements` |
163
+ | POST | `/submodels/{submodelIdentifier}/submodel-elements` | ❌ |
164
+ | GET | `/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}` | `get_submodel_element` |
165
+ | PUT | `/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}` | ❌ |
166
+ | POST | `/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}` | ❌ |
167
+ | DELETE | `/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}` | `delete_submodel_element` |
168
+ | GET | `/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/$value` | ❌ |
169
+ | PATCH | `/submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/$value` | `patch_submodel_element_value` |
170
+
171
+ ### Upload
172
+
173
+ | Method | BaSyx Endpoint | Shellsmith Function |
174
+ |--------|----------------|-----------------------------------------------------|
175
+ | POST | `/upload` | `upload.upload_aas` <br> `upload.upload_aas_folder` |
176
+
177
+ > ℹ️ Upload functions are available under the `shellsmith.upload` submodule.
178
+
179
+ ## ⚙️ Development
180
+
181
+ ```bash
182
+ git clone https://github.com/ptrstn/shellsmith
183
+ cd shellsmith
184
+ python -m venv .venv
185
+ source .venv/bin/activate # or .venv\Scripts\activate on Windows
186
+ pip install -e .[test]
187
+ ```
188
+
189
+ ### ✅ Testing
190
+
191
+ Before running the tests, make sure the BaSyx stack is up and running:
192
+
193
+ ```bash
194
+ docker compose up -d
195
+ ```
196
+
197
+ Then run the test suite with coverage:
198
+
199
+ ```bash
200
+ pytest --cov
201
+ ```
202
+
203
+ To view a detailed, visual coverage report:
204
+
205
+ ```bash
206
+ pytest --cov --cov-report=html
207
+ ```
208
+
209
+ Then open `htmlcov/index.html` in your web browser to explore which lines are covered and which are missing.
210
+
211
+ ## Resources
212
+
213
+ - https://github.com/eclipse-basyx/basyx-java-server-sdk
214
+ - https://github.com/admin-shell-io/aas-specs-api
215
+ - https://app.swaggerhub.com/apis/Plattform_i40/Entire-API-Collection
@@ -0,0 +1,24 @@
1
+ shellsmith/__init__.py,sha256=cgAbE5dd7c9JHg5tQzh2nk8aPGLkYxrKv9FgpYhjHdU,383
2
+ shellsmith/__main__.py,sha256=9-8Zh6cInl-4TDL8Dc-pyS9mM2japglycAa2lqq7qy4,76
3
+ shellsmith/config.py,sha256=EzDrxpCUVs7MPHIhE68OL9iiCjfSlA-m1sDGKp5gyl8,365
4
+ shellsmith/services.py,sha256=D0Pz_17pd5f018djXKpVwBcVH6zyYNRKjzD4Eps00ak,3833
5
+ shellsmith/upload.py,sha256=w79XYBFU1BZ1rhkFjfAYi1RwLkEs8Y6UhVG6tWnSRqc,1186
6
+ shellsmith/utils.py,sha256=yP1qzk0_k8TdB6y7QrVRj43rKuMInwKp8TR7c0U3qz8,1026
7
+ shellsmith/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ shellsmith/cli/main.py,sha256=P-jfSmSEMdPlK7ib6iYxnAZxGPoprct7Qo2LOP8ReGI,1357
9
+ shellsmith/cli/parser.py,sha256=CSrzvEnoKNmqf6011VcTQnhzjlahk4DRjk56T3umjFw,2805
10
+ shellsmith/cli/commands/__init__.py,sha256=Fjs4QXdP5SlgZeuI1S9w_L3fu2bKGpXG7z-NntS4Zeo,213
11
+ shellsmith/cli/commands/info.py,sha256=9O783zMvZcDccSDi6S3z04CboVhc-P_3f-Mzf5PgL2A,1445
12
+ shellsmith/cli/commands/nuke.py,sha256=H9WnUN6X3a9_Guo22vEkdtbvQceh6u2fhRuNPyqBkaE,280
13
+ shellsmith/cli/commands/shell.py,sha256=GACLGrWyLzEq0WO_jG1VvUu9cKG6FLLBrwYTLJS0-wU,564
14
+ shellsmith/cli/commands/submodel.py,sha256=GQq8WC2q2hy8OUk22szRfvrXbnUpNFxAJwYCXDVJkgY,550
15
+ shellsmith/cli/commands/upload.py,sha256=DcooOCfX9RnWCfR_f3iLaQ6i1o7CMWv2PmiRBG8j6CQ,407
16
+ shellsmith/crud/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ shellsmith/crud/shells.py,sha256=cHuKhHf1Eo5jmApHbrwt4QahgIxAvjlvBJsRfDf1IIs,1546
18
+ shellsmith/crud/submodels.py,sha256=IJY8PJdPKjsIruv-luxLz-OWgKQO0b3dPHYvdwSqFyI,3451
19
+ shellsmith-0.1.0.dist-info/licenses/LICENSE,sha256=_FYnm0XtBFVmy-ujEvLeCQyhtX9TwxwqsRpSqtayqwE,1068
20
+ shellsmith-0.1.0.dist-info/METADATA,sha256=7ED7fzluSNxMwoOc-6CSTv8B2t8Pe348ZxjkywMigjY,9293
21
+ shellsmith-0.1.0.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
22
+ shellsmith-0.1.0.dist-info/entry_points.txt,sha256=ezYPS3a9PsteA-0j5M0uybQvekGpoCqvuKZ8BPYK0sY,49
23
+ shellsmith-0.1.0.dist-info/top_level.txt,sha256=DSeEc6iCej548xSglvE-AA5TP1AzaXQy9z3awRnCJHc,11
24
+ shellsmith-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (77.0.3)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ aas = shellsmith.cli.main:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Peter Stein
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ shellsmith