Skip to content

Commit 53489e9

Browse files
committed
Replace argparse with typer
1 parent 19dc399 commit 53489e9

52 files changed

Lines changed: 1893 additions & 615 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 98 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,16 @@
99
<a href="https://github.com/astral-sh/ruff"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json" alt="Ruff"></a>
1010
</p>
1111

12-
Shellsmith is a Python toolkit and CLI for managing Asset Administration Shells (AAS), Submodels, and related resources.
13-
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).
12+
**Shellsmith** is a Python SDK and CLI for managing [Asset Administration Shells (AAS)](https://industrialdigitaltwin.org/en/content-hub/aasspecifications), Submodels, and Submodel Elements via the [Eclipse BaSyx](https://www.eclipse.org/basyx/) REST API.
13+
14+
It provides full client-side access to AAS resources with a clean Python interface and a powerful `typer`-based CLI — ideal for scripting, automation, and digital twin integration workflows.
1415

1516
### Features
1617

17-
- Python API for CRUD operations on shells, submodels, and submodel elements
18-
- CLI interface for quick scripting
19-
- `.env`-based configuration
18+
- 🔧 **Python SDK** for full CRUD access to Shells, Submodels, and Submodel Elements
19+
-**CLI tool** powered by [Typer](https://typer.tiangolo.com/) for fast scripting and automation
20+
- ⚙️ Simple `.env`-based configuration for flexible environment switching
21+
- 🔁 Seamless integration with the [Eclipse BaSyx](https://www.eclipse.org/basyx/) AAS REST API
2022

2123
## 🚀 Installation
2224

@@ -34,56 +36,114 @@ The default AAS environment host is:
3436
http://localhost:8081
3537
```
3638

37-
You can override it by setting the `SHELLSMITH_BASYX_ENV_HOST` environment variable, or by creating a `.env` file in your project root with:
39+
You can override it by setting the `SHELLSMITH_BASYX_ENV_HOST` environment variable, or by creating a `.env` file in the current working directory or your project root.
3840

3941
```bash
4042
SHELLSMITH_BASYX_ENV_HOST=http://your-host:1234
4143
```
4244

43-
## 🛠️ Usage
45+
## 🧠 CLI Usage
4446

45-
```bash
46-
aas --help
47-
```
48-
49-
Common commands:
47+
Shellsmith provides a powerful command-line interface:
5048

5149
```bash
52-
aas info # Show all shells and submodels
53-
aas upload <file|folder> # Upload AAS file or folder
54-
55-
aas shell delete <id> # Delete a shell
56-
aas submodel delete <id> # Delete a submodel
57-
58-
aas sme get <id> <path> # Get Submodel element value
59-
aas sme patch <id> <path> <new_value> # Set Submodel element value
60-
```
61-
62-
Use `--cascade` or `--unlink` to control deletion behavior:
63-
64-
```bash
65-
aas shell delete <id> --cascade # Also delete referenced submodels
66-
aas submodel delete <id> --unlink # Remove references from shells
50+
aas --help
6751
```
6852

69-
## 📡 API Usage
70-
71-
You can also use `shellsmith` as a Python package:
53+
| Command | Description |
54+
|----------|----------------------------------------------------------|
55+
| `upload` | Upload a single AAS file or all AAS files from a folder. |
56+
| `info` | Display the current Shell tree and identify issues. |
57+
| `nuke` | ☢️ Delete all Shells and Submodels (irrevocable). |
58+
| `encode` | Encode a value (e.g. Shell ID) to Base64. |
59+
| `decode` | Decode a Base64-encoded value. |
60+
| `get` | Get Shells, Submodels, and Submodel Elements. |
61+
| `delete` | Delete Shells, Submodels, or Submodel Elements. |
62+
| `update` | Update Shells, Submodels, or Submodel Elements. |
63+
| `create` | Create new Shells, Submodels, or Submodel Elements. |
64+
65+
> ℹ️ Run `aas <command> --help` to view subcommands and options.
66+
67+
### 🔎 Get Commands
68+
69+
| Command | Description |
70+
|-------------------------------------------------|---------------------------------------------|
71+
| `aas get shells` | 🔹 Get all available Shells. |
72+
| `aas get shell <id>` | 🔹 Get a specific Shell by ID. |
73+
| `aas get submodel-refs <shell-id>` | 🔹 Get all Submodel References of a Shell. |
74+
| `aas get submodels` | 🔸 Get all Submodels. |
75+
| `aas get submodel <id>` | 🔸 Get a specific Submodel by ID. |
76+
| `aas get submodel-value <id>` | 🔸 Get the `$value` of a Submodel. |
77+
| `aas get submodel-meta <id>` | 🔸 Get the `$metadata` of a Submodel. |
78+
| `aas get elements <submodel-id>` | 🔻 Get all Submodel Elements of a Submodel. |
79+
| `aas get element <submodel-id> <idShort>` | 🔻 Get a specific Submodel Element. |
80+
| `aas get element-value <submodel-id> <idShort>` | 🔻 Get the `$value` of a Submodel Element. |
81+
82+
### 🛠️ Create Commands
83+
84+
| Command | Description |
85+
|------------------------------------------------------------------------------|-----------------------------------------|
86+
| `aas create shell [--data <json>] [--file <path>]` | 🔹 Create a new Shell. |
87+
| `aas create submodel-ref <shell-id> [--data <json>] [--file <path>]` | 🔹 Add a Submodel Reference to a Shell. |
88+
| `aas create submodel [--data <json>] [--file <path>]` | 🔸 Create a new Submodel. |
89+
| `aas create element <submodel-id> [--data <json>] [--file <path>]` | 🔻 Create a new Submodel Element. |
90+
| `aas create element <submodel-id> <idShort> [--data <json>] [--file <path>]` | 🔻 Create an Element at a nested path. |
91+
92+
> ℹ️ Input can be passed via `--data "<json>"` or `--file <*.json|*.yaml>`, but **not both**
93+
94+
### 🧬 Update Commands
95+
96+
| Command | Description |
97+
|------------------------------------------------------------------------------|----------------------------------------------------------------|
98+
| `aas update shell <id> [--data <json>] [--file <path>]` | 🔹 Update a Shell (full replacement). |
99+
| `aas update submodel <id> [--data <json>] [--file <path>]` | 🔸 Update a Submodel (full replacement). |
100+
| `aas update submodel-value <id> [--data <json>] [--file <path>]` | 🔸 Update the `$value` of a Submodel (partial update). |
101+
| `aas update element <submodel-id> <idShort> [--data <json>] [--file <path>]` | 🔻 Update a Submodel Element (full replacement). |
102+
| `aas update element-value <submodel-id> <idShort> <value>` | 🔻 Update the `$value` of a Submodel Element (partial update). |
103+
104+
> ℹ️ All updates are either full replacements (`PUT`) or partial updates (`PATCH`)
105+
106+
### 🧹 Delete Commands
107+
108+
| Command | Description |
109+
|----------------------------------------------------|----------------------------------------------------------------|
110+
| `aas delete shell <id> [--cascade]` | 🔹 Delete a Shell and optionally all referenced Submodels. |
111+
| `aas delete submodel-ref <shell-id> <submodel-id>` | 🔹 Remove a Submodel reference from a Shell. |
112+
| `aas delete submodel <id> [--remove-refs]` | 🔸 Delete a Submodel and optionally unlink it from all Shells. |
113+
| `aas delete element <submodel-id> <idShort>` | 🔻 Delete a Submodel Element. |
114+
115+
116+
## 🐍 Python API Usage
117+
118+
You can also use `shellsmith` as a Python client library to interact with the BaSyx Environment REST API.
72119

73120
```python
74121
import shellsmith
75122

76-
# Get all available shells
123+
# List all AAS Shells
77124
shells = shellsmith.get_shells()
78125

79-
# Get a specific shell by ID (automatically base64-encoded)
80-
shell = shellsmith.get_shell("example_aas_id")
126+
# Fetch a specific Shell by ID
127+
shell = shellsmith.get_shell("https://example.com/aas/my-asset")
128+
129+
# List Submodels or Submodel References of a Shell
130+
submodels = shellsmith.get_submodels()
131+
refs = shellsmith.get_submodel_refs("https://example.com/aas/my-asset")
132+
133+
# Fetch a specific Submodel
134+
submodel = shellsmith.get_submodel("https://example.com/submodels/my-submodel")
135+
136+
# Read and update a Submodel Element's value
137+
value = shellsmith.get_submodel_element_value(submodel["id"], "temperature")
138+
shellsmith.patch_submodel_element_value(submodel["id"], "temperature", "42.0")
81139

82-
# Disable base64 encoding if your ID is already encoded
83-
submodel = shellsmith.get_submodel("ZXhhbXBsZV9hYXNfaWQ=", encode=False)
140+
# Upload a single AAS file or an entire folder (.aasx / .json / .xml)
141+
shellsmith.upload_aas("MyAsset.aasx")
142+
shellsmith.upload_aas_folder("aas_folder/")
84143

85-
# Use a custom AAS environment host
86-
submodel_refs = shellsmith.get_submodel_refs("example_aas_id", host="http://localhost:8081")
144+
# Delete a Shell or Submodel by ID
145+
shellsmith.delete_shell("https://example.com/aas/my-asset")
146+
shellsmith.delete_submodel("https://example.com/submodels/my-submodel")
87147
```
88148

89149
> ℹ️ `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.
@@ -94,7 +154,7 @@ The tables below show the mapping between BaSyx AAS REST API endpoints and the i
94154
95155
### Shells
96156

97-
| Method | BaSyx Endpoint | Shellsmith Function |
157+
| Method | BaSyx Endpoint | `shellsmith` Function |
98158
|--------|--------------------------------------------------------------|-----------------------|
99159
| GET | `/shells` | `get_shells` |
100160
| POST | `/shells` | `post_shell` |

pyproject.toml

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,40 @@ build-backend = "setuptools.build_meta"
55
[project]
66
name = "shellsmith"
77
version = "0.2.1"
8-
description = "A Python toolkit and CLI for managing Asset Administration Shells"
98
authors = [{ name = "Peter Stein", email = "peterstein@dfki.de" }]
9+
description = "Python client and CLI for Eclipse BaSyx to manage Asset Administration Shells (AAS)"
1010
license = { file = "LICENSE" }
1111
readme = { file = "README.md", content-type = "text/markdown" }
1212
requires-python = ">=3.10"
1313
classifiers = [
1414
"Programming Language :: Python :: 3",
15+
"Programming Language :: Python :: 3.10",
16+
"Programming Language :: Python :: 3.11",
17+
"Programming Language :: Python :: 3.12",
18+
"Programming Language :: Python :: 3.13",
1519
"Operating System :: OS Independent",
20+
"Intended Audience :: Developers",
21+
"Topic :: Software Development :: Libraries",
22+
"Topic :: Software Development :: Libraries :: Python Modules",
23+
]
24+
keywords = [
25+
"aas",
26+
"asset-administration-shell",
27+
"basyx",
28+
"eclipse-basyx",
29+
"industry40",
30+
"i40",
31+
"digital-twin",
32+
"cli",
33+
"typer",
34+
"rest-client",
35+
"python",
1636
]
1737
dependencies = [
1838
"pydantic-settings",
39+
"pyyaml",
1940
"requests",
41+
"typer",
2042
]
2143

2244
[project.optional-dependencies]
@@ -32,7 +54,7 @@ neo4j = [
3254
]
3355

3456
[project.scripts]
35-
aas = "shellsmith.cli.main:main"
57+
aas = "shellsmith.cli.app:main"
3658

3759
[project.urls]
3860
Homepage = "https://github.com/ptrstn/shellsmith"

pytest.ini

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
11
[pytest]
22
pythonpath = .
3-

scripts/section.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""Utility to generate formatted section headers for the CLI."""
2+
3+
import sys
4+
5+
TOTAL_WIDTH = 88
6+
CHAR = "─"
7+
8+
9+
def section(title: str) -> str:
10+
"""Creates a formatted section header for organizing code in a Python file."""
11+
title = title.strip()
12+
prefix = "# "
13+
padding = TOTAL_WIDTH - len(prefix) - len(title) - 2 # 2 spaces around title
14+
if padding < 0:
15+
return f"{prefix}{title}"
16+
pad_left = padding // 2 - len(prefix)
17+
pad_right = padding - pad_left
18+
return f"{prefix}{CHAR * pad_left} {title} {CHAR * pad_right}"
19+
20+
21+
if __name__ == "__main__":
22+
NUM_ARGS = 2
23+
if len(sys.argv) != NUM_ARGS:
24+
print('Usage: python section.py "Your Section Title"')
25+
sys.exit(1)
26+
27+
print(section(sys.argv[1]))

src/shellsmith/__init__.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,28 @@
11
__version__ = "0.2.1"
22

3-
from .crud.shells import (
3+
from .crud import (
44
delete_shell,
5+
delete_submodel,
6+
delete_submodel_element,
57
delete_submodel_ref,
68
get_shell,
79
get_shells,
8-
get_submodel_refs,
9-
post_shell,
10-
post_submodel_ref,
11-
put_shell,
12-
)
13-
from .crud.submodels import (
14-
delete_submodel,
15-
delete_submodel_element,
1610
get_submodel,
1711
get_submodel_element,
1812
get_submodel_element_value,
1913
get_submodel_elements,
2014
get_submodel_metadata,
15+
get_submodel_refs,
2116
get_submodel_value,
2217
get_submodels,
2318
patch_submodel_element_value,
2419
patch_submodel_value,
20+
post_shell,
2521
post_submodel,
2622
post_submodel_element,
23+
post_submodel_ref,
24+
put_shell,
2725
put_submodel,
2826
put_submodel_element,
2927
)
28+
from .upload import upload_aas, upload_aas_folder

src/shellsmith/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Entry point for running shellsmith as a CLI tool."""
22

3-
from shellsmith.cli.main import main
3+
from shellsmith.cli.app import main
44

55
if __name__ == "__main__":
66
main()

src/shellsmith/cli/app.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"""Main CLI entry point for shellsmith with Typer."""
2+
3+
import requests
4+
import typer
5+
6+
from .commands.decode import app as decode_app
7+
from .commands.encode import app as encode_app
8+
from .commands.groups.create import app as create_app
9+
from .commands.groups.delete import app as delete_app
10+
from .commands.groups.get import app as get_app
11+
from .commands.groups.update import app as update_app
12+
from .commands.info import app as info_app
13+
from .commands.nuke import app as nuke_app
14+
from .commands.upload import app as upload_app
15+
16+
app = typer.Typer(
17+
help="shellsmith - AAS Toolkit command-line interface.",
18+
no_args_is_help=True,
19+
)
20+
21+
app.add_typer(upload_app)
22+
app.add_typer(info_app)
23+
app.add_typer(nuke_app)
24+
app.add_typer(encode_app)
25+
app.add_typer(decode_app)
26+
app.add_typer(get_app)
27+
app.add_typer(delete_app)
28+
app.add_typer(update_app)
29+
app.add_typer(create_app)
30+
31+
32+
def main() -> None:
33+
"""Main entry point for the CLI."""
34+
try:
35+
app()
36+
except requests.exceptions.ConnectionError as e:
37+
typer.secho(f"😩 {e}", fg=typer.colors.RED)
38+
except Exception as e:
39+
typer.secho(f"💥 Unexpected error: {e}", fg=typer.colors.RED)
40+
raise
41+
42+
43+
if __name__ == "__main__":
44+
main()

src/shellsmith/cli/args.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""Reusable CLI arguments in commands."""
2+
3+
import typer
4+
5+
SHELL_ID: typer.Argument = typer.Argument(
6+
...,
7+
help="The unique identifier of the Shell.",
8+
)
9+
10+
SUBMODEL_ID: typer.Argument = typer.Argument(
11+
...,
12+
help="The unique identifier of the Submodel.",
13+
)
14+
15+
ID_SHORT_PATH: typer.Argument = typer.Argument(
16+
...,
17+
help="The idShort path for the Submodel Element.",
18+
)
19+
20+
OPTIONAL_ID_SHORT_PATH: typer.Argument = typer.Argument(
21+
None,
22+
help="Optional idShort path for a nested Submodel Element.",
23+
)
24+
25+
AAS_PATH: typer.Argument = typer.Argument(
26+
...,
27+
help="The path to the AAS file or folder to upload. Accepts: .json, .xml, .aasx",
28+
)
29+
30+
VALUE: typer.Argument = typer.Argument(..., help="The new value as string.")
Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +0,0 @@
1-
from .info import info
2-
from .nuke import nuke
3-
from .shell import shell_delete
4-
from .sme import submodel_element_get, submodel_element_patch
5-
from .submodel import submodel_delete
6-
from .upload import upload

0 commit comments

Comments
 (0)