shellsmith 0.1.0__tar.gz → 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {shellsmith-0.1.0/src/shellsmith.egg-info → shellsmith-0.2.1}/PKG-INFO +7 -1
- {shellsmith-0.1.0 → shellsmith-0.2.1}/README.md +4 -0
- {shellsmith-0.1.0 → shellsmith-0.2.1}/pyproject.toml +5 -1
- {shellsmith-0.1.0 → shellsmith-0.2.1}/src/shellsmith/__init__.py +1 -1
- {shellsmith-0.1.0 → shellsmith-0.2.1}/src/shellsmith/cli/commands/__init__.py +1 -0
- {shellsmith-0.1.0 → shellsmith-0.2.1}/src/shellsmith/cli/commands/info.py +10 -5
- shellsmith-0.2.1/src/shellsmith/cli/commands/sme.py +13 -0
- {shellsmith-0.1.0 → shellsmith-0.2.1}/src/shellsmith/cli/main.py +22 -4
- {shellsmith-0.1.0 → shellsmith-0.2.1}/src/shellsmith/cli/parser.py +35 -1
- {shellsmith-0.1.0 → shellsmith-0.2.1}/src/shellsmith/config.py +5 -1
- shellsmith-0.2.1/src/shellsmith/neo4j.py +115 -0
- {shellsmith-0.1.0 → shellsmith-0.2.1/src/shellsmith.egg-info}/PKG-INFO +7 -1
- {shellsmith-0.1.0 → shellsmith-0.2.1}/src/shellsmith.egg-info/SOURCES.txt +3 -0
- {shellsmith-0.1.0 → shellsmith-0.2.1}/src/shellsmith.egg-info/requires.txt +3 -0
- shellsmith-0.2.1/tests/test_neo4j.py +138 -0
- {shellsmith-0.1.0 → shellsmith-0.2.1}/LICENSE +0 -0
- {shellsmith-0.1.0 → shellsmith-0.2.1}/setup.cfg +0 -0
- {shellsmith-0.1.0 → shellsmith-0.2.1}/src/shellsmith/__main__.py +0 -0
- {shellsmith-0.1.0 → shellsmith-0.2.1}/src/shellsmith/cli/__init__.py +0 -0
- {shellsmith-0.1.0 → shellsmith-0.2.1}/src/shellsmith/cli/commands/nuke.py +0 -0
- {shellsmith-0.1.0 → shellsmith-0.2.1}/src/shellsmith/cli/commands/shell.py +0 -0
- {shellsmith-0.1.0 → shellsmith-0.2.1}/src/shellsmith/cli/commands/submodel.py +0 -0
- {shellsmith-0.1.0 → shellsmith-0.2.1}/src/shellsmith/cli/commands/upload.py +0 -0
- {shellsmith-0.1.0 → shellsmith-0.2.1}/src/shellsmith/crud/__init__.py +0 -0
- {shellsmith-0.1.0 → shellsmith-0.2.1}/src/shellsmith/crud/shells.py +0 -0
- {shellsmith-0.1.0 → shellsmith-0.2.1}/src/shellsmith/crud/submodels.py +0 -0
- {shellsmith-0.1.0 → shellsmith-0.2.1}/src/shellsmith/services.py +0 -0
- {shellsmith-0.1.0 → shellsmith-0.2.1}/src/shellsmith/upload.py +0 -0
- {shellsmith-0.1.0 → shellsmith-0.2.1}/src/shellsmith/utils.py +0 -0
- {shellsmith-0.1.0 → shellsmith-0.2.1}/src/shellsmith.egg-info/dependency_links.txt +0 -0
- {shellsmith-0.1.0 → shellsmith-0.2.1}/src/shellsmith.egg-info/entry_points.txt +0 -0
- {shellsmith-0.1.0 → shellsmith-0.2.1}/src/shellsmith.egg-info/top_level.txt +0 -0
- {shellsmith-0.1.0 → shellsmith-0.2.1}/tests/test_cli.py +0 -0
- {shellsmith-0.1.0 → shellsmith-0.2.1}/tests/test_cli_integration.py +0 -0
- {shellsmith-0.1.0 → shellsmith-0.2.1}/tests/test_crud.py +0 -0
- {shellsmith-0.1.0 → shellsmith-0.2.1}/tests/test_upload.py +0 -0
- {shellsmith-0.1.0 → shellsmith-0.2.1}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shellsmith
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: A Python toolkit and CLI for managing Asset Administration Shells
|
|
5
5
|
Author-email: Peter Stein <peterstein@dfki.de>
|
|
6
6
|
License: MIT License
|
|
@@ -41,6 +41,8 @@ Requires-Dist: isort; extra == "test"
|
|
|
41
41
|
Requires-Dist: pytest; extra == "test"
|
|
42
42
|
Requires-Dist: pytest-cov; extra == "test"
|
|
43
43
|
Requires-Dist: pytest-dotenv; extra == "test"
|
|
44
|
+
Provides-Extra: neo4j
|
|
45
|
+
Requires-Dist: neo4j; extra == "neo4j"
|
|
44
46
|
Dynamic: license-file
|
|
45
47
|
|
|
46
48
|
[](https://github.com/ptrstn/shellsmith/actions/workflows/python-package.yaml)
|
|
@@ -92,8 +94,12 @@ Common commands:
|
|
|
92
94
|
```bash
|
|
93
95
|
aas info # Show all shells and submodels
|
|
94
96
|
aas upload <file|folder> # Upload AAS file or folder
|
|
97
|
+
|
|
95
98
|
aas shell delete <id> # Delete a shell
|
|
96
99
|
aas submodel delete <id> # Delete a submodel
|
|
100
|
+
|
|
101
|
+
aas sme get <id> <path> # Get Submodel element value
|
|
102
|
+
aas sme patch <id> <path> <new_value> # Set Submodel element value
|
|
97
103
|
```
|
|
98
104
|
|
|
99
105
|
Use `--cascade` or `--unlink` to control deletion behavior:
|
|
@@ -47,8 +47,12 @@ Common commands:
|
|
|
47
47
|
```bash
|
|
48
48
|
aas info # Show all shells and submodels
|
|
49
49
|
aas upload <file|folder> # Upload AAS file or folder
|
|
50
|
+
|
|
50
51
|
aas shell delete <id> # Delete a shell
|
|
51
52
|
aas submodel delete <id> # Delete a submodel
|
|
53
|
+
|
|
54
|
+
aas sme get <id> <path> # Get Submodel element value
|
|
55
|
+
aas sme patch <id> <path> <new_value> # Set Submodel element value
|
|
52
56
|
```
|
|
53
57
|
|
|
54
58
|
Use `--cascade` or `--unlink` to control deletion behavior:
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "shellsmith"
|
|
7
|
-
version = "0.1
|
|
7
|
+
version = "0.2.1"
|
|
8
8
|
description = "A Python toolkit and CLI for managing Asset Administration Shells"
|
|
9
9
|
authors = [{ name = "Peter Stein", email = "peterstein@dfki.de" }]
|
|
10
10
|
license = { file = "LICENSE" }
|
|
@@ -29,6 +29,10 @@ test = [
|
|
|
29
29
|
"pytest-dotenv",
|
|
30
30
|
]
|
|
31
31
|
|
|
32
|
+
neo4j = [
|
|
33
|
+
"neo4j",
|
|
34
|
+
]
|
|
35
|
+
|
|
32
36
|
[project.scripts]
|
|
33
37
|
aas = "shellsmith.cli.main:main"
|
|
34
38
|
|
|
@@ -2,4 +2,5 @@ from .info import info # noqa: F401
|
|
|
2
2
|
from .nuke import nuke # noqa: F401
|
|
3
3
|
from .shell import shell_delete # noqa: F401
|
|
4
4
|
from .submodel import submodel_delete # noqa: F401
|
|
5
|
+
from .sme import submodel_element_get, submodel_element_patch # noqa: F401
|
|
5
6
|
from .upload import upload # noqa: F401
|
|
@@ -17,21 +17,25 @@ def print_unreferenced_submodels():
|
|
|
17
17
|
print("⚠️ Unreferenced Submodels:")
|
|
18
18
|
for submodel_id in submodel_ids:
|
|
19
19
|
submodel = shellsmith.get_submodel(submodel_id)
|
|
20
|
-
id_short = submodel
|
|
20
|
+
id_short = submodel.get("idShort", "<no idShort>")
|
|
21
21
|
print(f"- {id_short} ({submodel_id})")
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
def print_shells_tree():
|
|
25
25
|
shells = shellsmith.get_shells()
|
|
26
26
|
for shell in shells:
|
|
27
|
-
|
|
27
|
+
shell_id = shell['id']
|
|
28
|
+
shell_id_short = shell.get('idShort', "<no idShort>")
|
|
29
|
+
print(f"{shell_id_short}: {shell_id}")
|
|
28
30
|
|
|
29
|
-
submodels = services.get_shell_submodels(
|
|
31
|
+
submodels = services.get_shell_submodels(shell_id)
|
|
30
32
|
|
|
31
33
|
for i, submodel in enumerate(submodels):
|
|
32
34
|
is_last = i == len(submodels) - 1
|
|
33
35
|
prefix = "└──" if is_last else "├──"
|
|
34
|
-
|
|
36
|
+
submodel_id = submodel['id']
|
|
37
|
+
submodel_id_short = submodel.get('idShort', "<no idShort>")
|
|
38
|
+
print(f"{prefix} {submodel_id}: {submodel_id_short}")
|
|
35
39
|
|
|
36
40
|
|
|
37
41
|
def print_dangling_submodel_refs():
|
|
@@ -42,6 +46,7 @@ def print_dangling_submodel_refs():
|
|
|
42
46
|
print("⚠️ Dangling Submodel References:")
|
|
43
47
|
for shell_id, submodel_ids in dangling.items():
|
|
44
48
|
shell = shellsmith.get_shell(shell_id)
|
|
45
|
-
|
|
49
|
+
shell_id_short = shell.get('idShort', "<no idShort>")
|
|
50
|
+
print(f"- {shell_id_short}: {shell_id}")
|
|
46
51
|
for submodel_id in submodel_ids:
|
|
47
52
|
print(f" └── {submodel_id}")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import shellsmith
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def submodel_element_get(submodel_id: str, id_short_path: str):
|
|
5
|
+
print(f"{submodel_id} -> {id_short_path}:")
|
|
6
|
+
element = shellsmith.get_submodel_element(submodel_id, id_short_path)
|
|
7
|
+
value = element.get("value")
|
|
8
|
+
print(value)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def submodel_element_patch(submodel_id: str, id_short_path: str, value: str):
|
|
12
|
+
shellsmith.patch_submodel_element_value(submodel_id, id_short_path, value)
|
|
13
|
+
print(f"✅ Updated '{id_short_path}' in Submodel '{submodel_id}'")
|
|
@@ -3,20 +3,28 @@ import requests
|
|
|
3
3
|
from shellsmith import __version__, services
|
|
4
4
|
from shellsmith.config import config
|
|
5
5
|
|
|
6
|
-
from .commands import
|
|
6
|
+
from .commands import (
|
|
7
|
+
info,
|
|
8
|
+
nuke,
|
|
9
|
+
shell_delete,
|
|
10
|
+
submodel_delete,
|
|
11
|
+
submodel_element_get,
|
|
12
|
+
submodel_element_patch,
|
|
13
|
+
upload,
|
|
14
|
+
)
|
|
7
15
|
from .parser import build_parser
|
|
16
|
+
from ..utils import base64_decode, base64_encode
|
|
8
17
|
|
|
9
18
|
|
|
10
19
|
def print_header():
|
|
11
20
|
print("===============================================")
|
|
12
|
-
print(f"Shellsmith - AAS Toolkit v{__version__}")
|
|
13
|
-
print(f"Host: {config.host} ({services.health()})")
|
|
21
|
+
print(f" Shellsmith - AAS Toolkit v{__version__}")
|
|
22
|
+
print(f" Host: {config.host} ({services.health()})")
|
|
14
23
|
print("===============================================")
|
|
15
24
|
print()
|
|
16
25
|
|
|
17
26
|
|
|
18
27
|
def main():
|
|
19
|
-
print_header()
|
|
20
28
|
parser = build_parser()
|
|
21
29
|
args = parser.parse_args()
|
|
22
30
|
|
|
@@ -26,6 +34,12 @@ def main():
|
|
|
26
34
|
"nuke": lambda _args: nuke(),
|
|
27
35
|
"shell.delete": lambda _args: shell_delete(args.id, cascade=args.cascade),
|
|
28
36
|
"submodel.delete": lambda _args: submodel_delete(args.id, unlink=args.unlink),
|
|
37
|
+
"sme.get": lambda _args: submodel_element_get(args.id, args.path),
|
|
38
|
+
"sme.patch": lambda _args: submodel_element_patch(
|
|
39
|
+
args.id, args.path, args.value
|
|
40
|
+
),
|
|
41
|
+
"encode": lambda _args: print(base64_encode(args.id)),
|
|
42
|
+
"decode": lambda _args: print(base64_decode(args.value)),
|
|
29
43
|
}
|
|
30
44
|
|
|
31
45
|
try:
|
|
@@ -34,9 +48,13 @@ def main():
|
|
|
34
48
|
key += f".{args.shell_command}"
|
|
35
49
|
elif key == "submodel" and args.submodel_command:
|
|
36
50
|
key += f".{args.submodel_command}"
|
|
51
|
+
elif key == "sme" and args.submodel_element_command:
|
|
52
|
+
key += f".{args.submodel_element_command}"
|
|
37
53
|
|
|
38
54
|
handler = commands.get(key)
|
|
39
55
|
if handler:
|
|
56
|
+
if key not in ("encode", "decode"):
|
|
57
|
+
print_header()
|
|
40
58
|
handler(args)
|
|
41
59
|
else:
|
|
42
60
|
parser.print_help()
|
|
@@ -26,6 +26,7 @@ def build_parser():
|
|
|
26
26
|
# ──────────────────────────── shell ─────────────────────────────
|
|
27
27
|
shell_parser = subparsers.add_parser(
|
|
28
28
|
"shell",
|
|
29
|
+
aliases=["sh"],
|
|
29
30
|
help="Manage Asset Administration Shells",
|
|
30
31
|
)
|
|
31
32
|
shell_subparsers = shell_parser.add_subparsers(dest="shell_command")
|
|
@@ -42,7 +43,9 @@ def build_parser():
|
|
|
42
43
|
)
|
|
43
44
|
|
|
44
45
|
# ───────────────────────────── submodel ─────────────────────────────
|
|
45
|
-
submodel_parser = subparsers.add_parser(
|
|
46
|
+
submodel_parser = subparsers.add_parser(
|
|
47
|
+
"submodel", aliases=["sm"], help="Manage Submodels"
|
|
48
|
+
)
|
|
46
49
|
submodel_subparsers = submodel_parser.add_subparsers(dest="submodel_command")
|
|
47
50
|
submodel_delete_parser = submodel_subparsers.add_parser(
|
|
48
51
|
"delete",
|
|
@@ -60,4 +63,35 @@ def build_parser():
|
|
|
60
63
|
help="Remove all Shell references to this Submodel",
|
|
61
64
|
)
|
|
62
65
|
|
|
66
|
+
# ───────────────────── submodel-element ─────────────────────
|
|
67
|
+
sme_parser = subparsers.add_parser(
|
|
68
|
+
"submodel-element",
|
|
69
|
+
aliases=["sme"],
|
|
70
|
+
help="Manage Submodel Elements",
|
|
71
|
+
)
|
|
72
|
+
sme_subparsers = sme_parser.add_subparsers(dest="submodel_element_command")
|
|
73
|
+
|
|
74
|
+
sme_get_parser = sme_subparsers.add_parser(
|
|
75
|
+
"get",
|
|
76
|
+
help="Get value of a Submodel Element",
|
|
77
|
+
)
|
|
78
|
+
sme_get_parser.add_argument("id", type=str, help="Submodel ID")
|
|
79
|
+
sme_get_parser.add_argument("path", type=str, help="idShort path")
|
|
80
|
+
|
|
81
|
+
sme_patch_parser = sme_subparsers.add_parser(
|
|
82
|
+
"patch",
|
|
83
|
+
help="Patch value of a Submodel Element",
|
|
84
|
+
)
|
|
85
|
+
sme_patch_parser.add_argument("id", type=str, help="Submodel ID")
|
|
86
|
+
sme_patch_parser.add_argument("path", type=str, help="idShort path")
|
|
87
|
+
sme_patch_parser.add_argument("value", type=str, help="New value")
|
|
88
|
+
|
|
89
|
+
# ───────────────────────────── encode ─────────────────────────────
|
|
90
|
+
encode_parser = subparsers.add_parser("encode", help="Encode a string to base64")
|
|
91
|
+
encode_parser.add_argument("id", type=str, help="Text to encode")
|
|
92
|
+
|
|
93
|
+
# ───────────────────────────── decode ─────────────────────────────
|
|
94
|
+
decode_parser = subparsers.add_parser("decode", help="Decode a base64 string")
|
|
95
|
+
decode_parser.add_argument("value", type=str, help="Base64 string to decode")
|
|
96
|
+
|
|
63
97
|
return parser
|
|
@@ -9,7 +9,11 @@ class Settings(BaseSettings):
|
|
|
9
9
|
def host(self):
|
|
10
10
|
return self.basyx_env_host
|
|
11
11
|
|
|
12
|
-
model_config = SettingsConfigDict(
|
|
12
|
+
model_config = SettingsConfigDict(
|
|
13
|
+
env_file=".env",
|
|
14
|
+
env_prefix="SHELLSMITH_",
|
|
15
|
+
extra="ignore",
|
|
16
|
+
)
|
|
13
17
|
|
|
14
18
|
|
|
15
19
|
config = Settings()
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from typing import Any, Dict, List, Optional
|
|
2
|
+
|
|
3
|
+
from neo4j import Driver, GraphDatabase
|
|
4
|
+
|
|
5
|
+
from shellsmith.config import config
|
|
6
|
+
|
|
7
|
+
_driver: Optional[Driver] = None
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_driver() -> Driver:
|
|
11
|
+
global _driver
|
|
12
|
+
if _driver is None:
|
|
13
|
+
_driver = GraphDatabase.driver(config.neo4j_uri, auth=None)
|
|
14
|
+
return _driver
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def close_driver():
|
|
18
|
+
global _driver
|
|
19
|
+
if _driver is not None:
|
|
20
|
+
_driver.close()
|
|
21
|
+
_driver = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
##################
|
|
25
|
+
# Shells
|
|
26
|
+
##################
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_shells() -> List[Dict[str, Any]]:
|
|
30
|
+
query = """
|
|
31
|
+
MATCH (shell:AssetAdministrationShell)
|
|
32
|
+
RETURN shell;
|
|
33
|
+
"""
|
|
34
|
+
with get_driver().session() as session:
|
|
35
|
+
result = session.run(query)
|
|
36
|
+
shells = [dict(record["shell"]) for record in result]
|
|
37
|
+
return shells
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_shell(shell_id: str) -> Optional[Dict[str, Any]]:
|
|
41
|
+
query = """
|
|
42
|
+
MATCH (shell:AssetAdministrationShell {id: $shell_id})
|
|
43
|
+
RETURN shell;
|
|
44
|
+
"""
|
|
45
|
+
with get_driver().session() as session:
|
|
46
|
+
result = session.run(query, shell_id=shell_id)
|
|
47
|
+
record = result.single()
|
|
48
|
+
return dict(record["shell"]) if record else None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
##################
|
|
52
|
+
# Submodels
|
|
53
|
+
##################
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def get_submodels() -> List[Dict[str, Any]]:
|
|
57
|
+
query = """
|
|
58
|
+
MATCH (submodel:Submodel)
|
|
59
|
+
RETURN submodel
|
|
60
|
+
"""
|
|
61
|
+
with get_driver().session() as session:
|
|
62
|
+
result = session.run(query)
|
|
63
|
+
submodels = [dict(record["submodel"]) for record in result]
|
|
64
|
+
return submodels
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_submodel(submodel_id: str) -> Dict[str, Any]:
|
|
68
|
+
query = """
|
|
69
|
+
MATCH (submodel:Submodel {id: $submodel_id})
|
|
70
|
+
RETURN submodel
|
|
71
|
+
"""
|
|
72
|
+
with get_driver().session() as session:
|
|
73
|
+
result = session.run(query, submodel_id=submodel_id)
|
|
74
|
+
record = result.single()
|
|
75
|
+
return dict(record["submodel"]) if record else None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def get_submodel_elements(submodel_id: str) -> List[Dict[str, Any]]:
|
|
79
|
+
query = """
|
|
80
|
+
MATCH (sme:SubmodelElement {smId: $submodel_id})
|
|
81
|
+
RETURN sme;
|
|
82
|
+
"""
|
|
83
|
+
with get_driver().session() as session:
|
|
84
|
+
result = session.run(query, submodel_id=submodel_id)
|
|
85
|
+
return [dict(record["sme"]) for record in result]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def get_submodel_element(
|
|
89
|
+
submodel_id: str, id_short_path: str
|
|
90
|
+
) -> Optional[Dict[str, Any]]:
|
|
91
|
+
query = """
|
|
92
|
+
MATCH (sme:SubmodelElement {smId: $submodel_id, idShortPath: $id_short_path})
|
|
93
|
+
RETURN sme;
|
|
94
|
+
"""
|
|
95
|
+
with get_driver().session() as session:
|
|
96
|
+
result = session.run(
|
|
97
|
+
query,
|
|
98
|
+
submodel_id=submodel_id,
|
|
99
|
+
id_short_path=id_short_path,
|
|
100
|
+
)
|
|
101
|
+
record = result.single()
|
|
102
|
+
return dict(record["sme"]) if record else None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def detach_delete_all():
|
|
106
|
+
"""
|
|
107
|
+
Dangerous!
|
|
108
|
+
Deletes all nodes and relationships in the graph.
|
|
109
|
+
"""
|
|
110
|
+
query = """
|
|
111
|
+
MATCH (n)
|
|
112
|
+
DETACH DELETE n;
|
|
113
|
+
"""
|
|
114
|
+
with get_driver().session() as session:
|
|
115
|
+
session.run(query)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shellsmith
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: A Python toolkit and CLI for managing Asset Administration Shells
|
|
5
5
|
Author-email: Peter Stein <peterstein@dfki.de>
|
|
6
6
|
License: MIT License
|
|
@@ -41,6 +41,8 @@ Requires-Dist: isort; extra == "test"
|
|
|
41
41
|
Requires-Dist: pytest; extra == "test"
|
|
42
42
|
Requires-Dist: pytest-cov; extra == "test"
|
|
43
43
|
Requires-Dist: pytest-dotenv; extra == "test"
|
|
44
|
+
Provides-Extra: neo4j
|
|
45
|
+
Requires-Dist: neo4j; extra == "neo4j"
|
|
44
46
|
Dynamic: license-file
|
|
45
47
|
|
|
46
48
|
[](https://github.com/ptrstn/shellsmith/actions/workflows/python-package.yaml)
|
|
@@ -92,8 +94,12 @@ Common commands:
|
|
|
92
94
|
```bash
|
|
93
95
|
aas info # Show all shells and submodels
|
|
94
96
|
aas upload <file|folder> # Upload AAS file or folder
|
|
97
|
+
|
|
95
98
|
aas shell delete <id> # Delete a shell
|
|
96
99
|
aas submodel delete <id> # Delete a submodel
|
|
100
|
+
|
|
101
|
+
aas sme get <id> <path> # Get Submodel element value
|
|
102
|
+
aas sme patch <id> <path> <new_value> # Set Submodel element value
|
|
97
103
|
```
|
|
98
104
|
|
|
99
105
|
Use `--cascade` or `--unlink` to control deletion behavior:
|
|
@@ -4,6 +4,7 @@ pyproject.toml
|
|
|
4
4
|
src/shellsmith/__init__.py
|
|
5
5
|
src/shellsmith/__main__.py
|
|
6
6
|
src/shellsmith/config.py
|
|
7
|
+
src/shellsmith/neo4j.py
|
|
7
8
|
src/shellsmith/services.py
|
|
8
9
|
src/shellsmith/upload.py
|
|
9
10
|
src/shellsmith/utils.py
|
|
@@ -20,6 +21,7 @@ src/shellsmith/cli/commands/__init__.py
|
|
|
20
21
|
src/shellsmith/cli/commands/info.py
|
|
21
22
|
src/shellsmith/cli/commands/nuke.py
|
|
22
23
|
src/shellsmith/cli/commands/shell.py
|
|
24
|
+
src/shellsmith/cli/commands/sme.py
|
|
23
25
|
src/shellsmith/cli/commands/submodel.py
|
|
24
26
|
src/shellsmith/cli/commands/upload.py
|
|
25
27
|
src/shellsmith/crud/__init__.py
|
|
@@ -28,5 +30,6 @@ src/shellsmith/crud/submodels.py
|
|
|
28
30
|
tests/test_cli.py
|
|
29
31
|
tests/test_cli_integration.py
|
|
30
32
|
tests/test_crud.py
|
|
33
|
+
tests/test_neo4j.py
|
|
31
34
|
tests/test_upload.py
|
|
32
35
|
tests/test_utils.py
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
if not os.getenv("SHELLSMITH_NEO4J_ENABLED"):
|
|
7
|
+
pytest.skip(
|
|
8
|
+
reason="Skipping Neo4j tests. Set SHELLSMITH_NEO4J_ENABLED=1 to enable.",
|
|
9
|
+
allow_module_level=True,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from shellsmith import neo4j, services
|
|
13
|
+
from shellsmith.upload import upload_aas_folder
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
GRAPH_READY_TIMEOUT = 45 # seconds
|
|
17
|
+
EXPECTED_NODE_COUNT = 39
|
|
18
|
+
EXPECTED_SUBMODEL_ELEMENT_COUNT = 17 # or whatever your total is
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def wait_for_graph_node_count(
|
|
22
|
+
expected_count: int = EXPECTED_NODE_COUNT,
|
|
23
|
+
timeout: int = GRAPH_READY_TIMEOUT,
|
|
24
|
+
) -> (bool, int):
|
|
25
|
+
query = "MATCH (n) RETURN count(n) AS count"
|
|
26
|
+
start = time.time()
|
|
27
|
+
|
|
28
|
+
last_count = 0
|
|
29
|
+
|
|
30
|
+
while True:
|
|
31
|
+
try:
|
|
32
|
+
with neo4j.get_driver().session() as session:
|
|
33
|
+
result = session.run(query)
|
|
34
|
+
record = result.single()
|
|
35
|
+
count = record["count"]
|
|
36
|
+
last_count = count
|
|
37
|
+
if record and count == expected_count:
|
|
38
|
+
return True, last_count
|
|
39
|
+
except Exception: # noqa
|
|
40
|
+
pass # wait and retry
|
|
41
|
+
|
|
42
|
+
if time.time() - start > timeout:
|
|
43
|
+
return False, last_count
|
|
44
|
+
|
|
45
|
+
time.sleep(0.5)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_wait_for_graph():
|
|
49
|
+
# neo4j.detach_delete_all()
|
|
50
|
+
services.delete_all_shells()
|
|
51
|
+
services.delete_all_submodels()
|
|
52
|
+
success, count = wait_for_graph_node_count(0)
|
|
53
|
+
assert success, f"Graph was not fully destructed within timeout: {count}"
|
|
54
|
+
|
|
55
|
+
upload_aas_folder("aas")
|
|
56
|
+
success, count = wait_for_graph_node_count(39)
|
|
57
|
+
assert success, f"Graph was not fully built within timeout: {count}"
|
|
58
|
+
time.sleep(1)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_get_shells(semitrailer, workpiece_carrier_a1):
|
|
62
|
+
shells = neo4j.get_shells()
|
|
63
|
+
assert len(shells) == 2
|
|
64
|
+
|
|
65
|
+
shell_id_shorts = {shell["idShort"] for shell in shells}
|
|
66
|
+
shell_ids = {shell["id"] for shell in shells}
|
|
67
|
+
assert semitrailer.id in shell_ids
|
|
68
|
+
assert workpiece_carrier_a1.id in shell_ids
|
|
69
|
+
assert semitrailer.id_short in shell_id_shorts
|
|
70
|
+
assert workpiece_carrier_a1.id_short in shell_id_shorts
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_get_shell(
|
|
74
|
+
semitrailer,
|
|
75
|
+
workpiece_carrier_a1,
|
|
76
|
+
):
|
|
77
|
+
shell = neo4j.get_shell(semitrailer.id)
|
|
78
|
+
assert shell["id"] == semitrailer.id
|
|
79
|
+
assert shell["idShort"] == semitrailer.id_short
|
|
80
|
+
|
|
81
|
+
shell = neo4j.get_shell(workpiece_carrier_a1.id)
|
|
82
|
+
assert shell["id"] == workpiece_carrier_a1.id
|
|
83
|
+
assert shell["idShort"] == workpiece_carrier_a1.id_short
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_get_submodels(semitrailer, workpiece_carrier_a1):
|
|
87
|
+
submodels = neo4j.get_submodels()
|
|
88
|
+
assert len(submodels) == 4
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_get_submodel(
|
|
92
|
+
semitrailer,
|
|
93
|
+
workpiece_carrier_a1,
|
|
94
|
+
):
|
|
95
|
+
submodel = neo4j.get_submodel(semitrailer.product_identification.id)
|
|
96
|
+
assert submodel["idShort"] == semitrailer.product_identification.id_short
|
|
97
|
+
|
|
98
|
+
submodel = neo4j.get_submodel(semitrailer.production_plan.id)
|
|
99
|
+
assert submodel["idShort"] == semitrailer.production_plan.id_short
|
|
100
|
+
|
|
101
|
+
submodel = neo4j.get_submodel(workpiece_carrier_a1.good_information.id)
|
|
102
|
+
assert submodel["idShort"] == workpiece_carrier_a1.good_information.id_short
|
|
103
|
+
|
|
104
|
+
submodel = neo4j.get_submodel(workpiece_carrier_a1.asset_location.id)
|
|
105
|
+
assert submodel["idShort"] == workpiece_carrier_a1.asset_location.id_short
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_get_submodel_elements(semitrailer, workpiece_carrier_a1):
|
|
109
|
+
elements = neo4j.get_submodel_elements(semitrailer.product_identification.id)
|
|
110
|
+
assert len(elements) == 2
|
|
111
|
+
id_shorts = {el["idShort"] for el in elements}
|
|
112
|
+
assert "Identifier" in id_shorts
|
|
113
|
+
assert "ProductName" in id_shorts
|
|
114
|
+
|
|
115
|
+
elements = neo4j.get_submodel_elements(workpiece_carrier_a1.good_information.id)
|
|
116
|
+
assert len(elements) == 7
|
|
117
|
+
id_shorts = {el["idShort"] for el in elements}
|
|
118
|
+
assert "CurrentProduct" in id_shorts
|
|
119
|
+
assert "ListTransportableProducts" in id_shorts
|
|
120
|
+
assert "ProductName" in id_shorts
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_get_submodel_element(semitrailer, workpiece_carrier_a1):
|
|
124
|
+
element = neo4j.get_submodel_element(
|
|
125
|
+
semitrailer.product_identification.id, "ProductName"
|
|
126
|
+
)
|
|
127
|
+
assert element["idShort"] == "ProductName"
|
|
128
|
+
assert element["value"] == "Semitrailer"
|
|
129
|
+
|
|
130
|
+
element = neo4j.get_submodel_element(
|
|
131
|
+
workpiece_carrier_a1.asset_location.id, "CurrentFences[0].FenceName"
|
|
132
|
+
)
|
|
133
|
+
assert element["idShort"] == "FenceName"
|
|
134
|
+
assert element["value"] == "TSN-Module"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_close_driver():
|
|
138
|
+
neo4j.close_driver()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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
|