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 +18 -0
- shellsmith/__main__.py +4 -0
- shellsmith/cli/__init__.py +0 -0
- shellsmith/cli/commands/__init__.py +5 -0
- shellsmith/cli/commands/info.py +47 -0
- shellsmith/cli/commands/nuke.py +9 -0
- shellsmith/cli/commands/shell.py +17 -0
- shellsmith/cli/commands/submodel.py +16 -0
- shellsmith/cli/commands/upload.py +14 -0
- shellsmith/cli/main.py +44 -0
- shellsmith/cli/parser.py +63 -0
- shellsmith/config.py +15 -0
- shellsmith/crud/__init__.py +0 -0
- shellsmith/crud/shells.py +61 -0
- shellsmith/crud/submodels.py +126 -0
- shellsmith/services.py +131 -0
- shellsmith/upload.py +40 -0
- shellsmith/utils.py +35 -0
- shellsmith-0.1.0.dist-info/METADATA +215 -0
- shellsmith-0.1.0.dist-info/RECORD +24 -0
- shellsmith-0.1.0.dist-info/WHEEL +5 -0
- shellsmith-0.1.0.dist-info/entry_points.txt +2 -0
- shellsmith-0.1.0.dist-info/licenses/LICENSE +21 -0
- shellsmith-0.1.0.dist-info/top_level.txt +1 -0
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
|
File without changes
|
|
@@ -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,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}")
|
shellsmith/cli/parser.py
ADDED
|
@@ -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
|
+
[](https://github.com/ptrstn/shellsmith/actions/workflows/python-package.yaml)
|
|
47
|
+
[](https://codecov.io/gh/ptrstn/shellsmith)
|
|
48
|
+
[](https://badge.fury.io/py/shellsmith)
|
|
49
|
+
[](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,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
|