sysml-windtrader 0.2.1__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.
- sysml_windtrader-0.2.1.dist-info/METADATA +238 -0
- sysml_windtrader-0.2.1.dist-info/RECORD +9 -0
- sysml_windtrader-0.2.1.dist-info/WHEEL +4 -0
- sysml_windtrader-0.2.1.dist-info/entry_points.txt +2 -0
- sysml_windtrader-0.2.1.dist-info/licenses/LICENSE +21 -0
- windtrader/__init__.py +19 -0
- windtrader/_jars.py +166 -0
- windtrader/cli.py +148 -0
- windtrader/client.py +271 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: sysml-windtrader
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Python wrapper around windtrader-java for SysML v2 parse-only validation.
|
|
5
|
+
Author: Westfall-io
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 chriscox-westfall
|
|
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
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Python: >=3.10
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: coverage[toml]>=7.6; extra == 'dev'
|
|
31
|
+
Requires-Dist: interrogate>=1.7; extra == 'dev'
|
|
32
|
+
Requires-Dist: pre-commit>=3.7; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
35
|
+
Requires-Dist: python-semantic-release>=10.0.0; extra == 'dev'
|
|
36
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
|
|
39
|
+
# windtrader
|
|
40
|
+
|
|
41
|
+
**windtrader** is a lightweight Python wrapper around the `windtrader-java` SysML v2 validator.
|
|
42
|
+
It provides a simple, scriptable way to validate SysML v2 textual syntax from Python while
|
|
43
|
+
delegating all grammar authority to the official Java-based validator.
|
|
44
|
+
|
|
45
|
+
This project is intentionally *parse-only*: it validates syntax, reports diagnostics, and
|
|
46
|
+
(optionally) echoes parsed output, but does **not** perform semantic resolution or model linking.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Status
|
|
51
|
+
|
|
52
|
+

|
|
53
|
+

|
|
54
|
+

|
|
55
|
+

|
|
56
|
+

|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Key Features
|
|
61
|
+
|
|
62
|
+
- โ
Validates SysML v2 textual syntax using a pinned Java validator
|
|
63
|
+
- ๐งฉ Thin, explicit Python API (no hidden model interpretation)
|
|
64
|
+
- ๐ฆ Automatic download and caching of the validator JAR
|
|
65
|
+
- ๐งช Designed for CI, generators, and downstream tooling
|
|
66
|
+
- ๐ Stable exit-code contract from Java โ Python
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Installation
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
pip install sysml-windtrader
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Or for development:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pip install -e ".[dev]"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Java **must** be available on your system (CI uses Temurin JDK).
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Command-Line Usage
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
echo "part { attribute mass; }" | windtrader # validate (default: check)
|
|
90
|
+
echo "part { attribute mass; }" | windtrader export # print element JSON graph
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Subcommands:
|
|
94
|
+
|
|
95
|
+
```text
|
|
96
|
+
check (default) parse-validate SysML text
|
|
97
|
+
echo print the normalized/echoed representation
|
|
98
|
+
export parse SysML and print the SysMLv2 element JSON graph (API shape)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Options:
|
|
102
|
+
|
|
103
|
+
```text
|
|
104
|
+
--version Show windtrader package version
|
|
105
|
+
--java-version windtrader-java version to use (default: 0.2.0)
|
|
106
|
+
--timeout Subprocess timeout (default: 10s for check/echo, 120s for export)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The CLI forwards stdout/stderr directly from the Java tool and exits with the same exit code.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Python API
|
|
114
|
+
|
|
115
|
+
> **0.2.0 rename:** `windtrader.validator` (module) / `WindtraderValidator` /
|
|
116
|
+
> `ValidationResult` are now `windtrader.client` / `WindtraderClient` /
|
|
117
|
+
> `CommandResult`. The module drives `check`, `echo`, and now `export`, so the
|
|
118
|
+
> old "validator" name was misleading. Update imports from `windtrader.validator`
|
|
119
|
+
> to `windtrader.client` and the class/type names accordingly.
|
|
120
|
+
>
|
|
121
|
+
> ```python
|
|
122
|
+
> # before (0.1.x)
|
|
123
|
+
> from windtrader.validator import WindtraderValidator, ValidationResult
|
|
124
|
+
> # after (0.2.0)
|
|
125
|
+
> from windtrader.client import WindtraderClient, CommandResult
|
|
126
|
+
> ```
|
|
127
|
+
|
|
128
|
+
### Basic validation
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from windtrader import validate
|
|
132
|
+
|
|
133
|
+
result = validate("part { attribute mass; }")
|
|
134
|
+
|
|
135
|
+
if result.ok:
|
|
136
|
+
print("Valid SysML")
|
|
137
|
+
else:
|
|
138
|
+
print("Invalid:", result.stderr)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Client object
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from windtrader import WindtraderClient
|
|
145
|
+
|
|
146
|
+
client = WindtraderClient(version="0.2.0")
|
|
147
|
+
res = client.validate("part { attrib mass; }")
|
|
148
|
+
|
|
149
|
+
print(res.exit_code) # 0 = valid, 2 = syntax error, 3 = runtime error
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Export
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from windtrader import export
|
|
156
|
+
|
|
157
|
+
res = export("part { attribute mass; }")
|
|
158
|
+
|
|
159
|
+
if res.ok:
|
|
160
|
+
print(res.stdout) # SysMLv2 element JSON graph (array of API-shaped elements)
|
|
161
|
+
else:
|
|
162
|
+
print("Failed:", res.stderr)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### CommandResult
|
|
166
|
+
|
|
167
|
+
Returned objects include:
|
|
168
|
+
|
|
169
|
+
- `ok`: boolean success flag
|
|
170
|
+
- `exit_code`: Java exit code
|
|
171
|
+
- `stdout` / `stderr`: raw tool output
|
|
172
|
+
- `jar_path`: cached JAR location
|
|
173
|
+
- `duration_s`: execution time
|
|
174
|
+
- `is_invalid_syntax`
|
|
175
|
+
- `is_runtime_error`
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Exit Code Contract
|
|
180
|
+
|
|
181
|
+
The Java validator defines:
|
|
182
|
+
|
|
183
|
+
| Exit Code | Meaning |
|
|
184
|
+
|----------:|--------------------|
|
|
185
|
+
| 0 | Valid syntax |
|
|
186
|
+
| 2 | Invalid SysML |
|
|
187
|
+
| 3 | Runtime error |
|
|
188
|
+
|
|
189
|
+
Python preserves these codes exactly.
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Configuration
|
|
194
|
+
|
|
195
|
+
Environment variables:
|
|
196
|
+
|
|
197
|
+
- `WINDTRADER_CACHE_DIR` โ override JAR cache directory
|
|
198
|
+
- `WINDTRADER_JAVA_REPO` โ override GitHub repo for JAR downloads
|
|
199
|
+
- `WINDTRADER_JAVA_STABLE_ASSET` โ explicit asset URL (optional)
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Design Philosophy
|
|
204
|
+
|
|
205
|
+
- **Grammar is authoritative**: validation truth lives in Java
|
|
206
|
+
- **Python is orchestration only**
|
|
207
|
+
- **No silent fallbacks**
|
|
208
|
+
- **Reproducible CI behavior**
|
|
209
|
+
|
|
210
|
+
This makes `windtrader` suitable as a foundation for:
|
|
211
|
+
|
|
212
|
+
- Code generators
|
|
213
|
+
- Corpus analysis
|
|
214
|
+
- Model-building libraries
|
|
215
|
+
- CI validation gates
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Relationship to Other Projects
|
|
220
|
+
|
|
221
|
+
`windtrader` is designed to act as the validation backbone for higher-level SysML tooling,
|
|
222
|
+
including programmatic builders and corpus-driven API generation pipelines.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## License
|
|
227
|
+
|
|
228
|
+
MIT License. See [LICENSE](LICENSE).
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## Contributing
|
|
233
|
+
|
|
234
|
+
Pull requests welcome. Please ensure:
|
|
235
|
+
|
|
236
|
+
- CI passes
|
|
237
|
+
- Docstring coverage remains high
|
|
238
|
+
- Backend version bumps are intentional
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
windtrader/__init__.py,sha256=eYOmz4YYtPqXmVALpCZlo1DT2mGJ1POdD3GBbpPC_kI,288
|
|
2
|
+
windtrader/_jars.py,sha256=abVotYnUIU-qh-QwUxonFK2vRJzwYLN1cKLGj7_5Kj0,5613
|
|
3
|
+
windtrader/cli.py,sha256=k6Wfk6NtoyvsnZQcXkFxqIx5CFxNF1EKTdO7ECxfIxc,4524
|
|
4
|
+
windtrader/client.py,sha256=JGSqbIrAyRP1Lk48oorKfnF2kbbL1G7ReBGZQzPuyds,8411
|
|
5
|
+
sysml_windtrader-0.2.1.dist-info/METADATA,sha256=lkBKn4T15O4SEwUluK0QkH-qYafVy1MWRYCSIyea-Ho,6717
|
|
6
|
+
sysml_windtrader-0.2.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
7
|
+
sysml_windtrader-0.2.1.dist-info/entry_points.txt,sha256=GKbsRIUmxzbnKnE6wQF4J3dEBuPfXX2x67gEEy2SFds,51
|
|
8
|
+
sysml_windtrader-0.2.1.dist-info/licenses/LICENSE,sha256=4VyG3BfBk1RobrJx73i9jLX9WX1TqNHcPdNbFgOZA6o,1074
|
|
9
|
+
sysml_windtrader-0.2.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 chriscox-westfall
|
|
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.
|
windtrader/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .client import (
|
|
2
|
+
CommandResult,
|
|
3
|
+
WindtraderClient,
|
|
4
|
+
echo,
|
|
5
|
+
export,
|
|
6
|
+
validate,
|
|
7
|
+
validate_across_versions,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"CommandResult",
|
|
12
|
+
"WindtraderClient",
|
|
13
|
+
"echo",
|
|
14
|
+
"export",
|
|
15
|
+
"validate",
|
|
16
|
+
"validate_across_versions",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
__version__ = "0.2.0"
|
windtrader/_jars.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""
|
|
2
|
+
windtrader._jars
|
|
3
|
+
|
|
4
|
+
Utilities for locating, downloading, and caching the `windtrader-java` validator JAR.
|
|
5
|
+
|
|
6
|
+
This module is intentionally dependency-free (stdlib only) so the Python wrapper can
|
|
7
|
+
remain lightweight while still fetching the pinned Java validator from GitHub Releases.
|
|
8
|
+
|
|
9
|
+
Key behavior:
|
|
10
|
+
- Determine a local cache directory (default: `~/.cache/windtrader`, overridable via env).
|
|
11
|
+
- Resolve the expected JAR cache path for a requested version.
|
|
12
|
+
- Attempt to download the JAR from a small set of candidate release URLs.
|
|
13
|
+
- Cache the JAR on disk so subsequent validations do not hit the network.
|
|
14
|
+
|
|
15
|
+
Environment variables:
|
|
16
|
+
- WINDTRADER_CACHE_DIR:
|
|
17
|
+
Override the root cache directory where downloaded JARs are stored.
|
|
18
|
+
- WINDTRADER_JAVA_REPO:
|
|
19
|
+
Override the GitHub repo used to fetch JARs (default: "Westfall-io/windtrader-java").
|
|
20
|
+
- WINDTRADER_JAVA_STABLE_ASSET:
|
|
21
|
+
Optional: a fully-qualified URL to a stable asset (first-choice download target).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import os
|
|
27
|
+
import urllib.request
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
DEFAULT_REPO = "Westfall-io/windtrader-java"
|
|
31
|
+
DEFAULT_VERSION = "0.2.0" # Default windtrader-java backend version when none is provided.
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _cache_dir() -> Path:
|
|
35
|
+
"""
|
|
36
|
+
Return the base cache directory for windtrader artifacts.
|
|
37
|
+
|
|
38
|
+
By default this is `~/.cache/windtrader` on Unix-like systems. Users can override
|
|
39
|
+
the location using the `WINDTRADER_CACHE_DIR` environment variable.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Path: Absolute path to the cache base directory.
|
|
43
|
+
"""
|
|
44
|
+
base = os.environ.get("WINDTRADER_CACHE_DIR")
|
|
45
|
+
if base:
|
|
46
|
+
return Path(base).expanduser().resolve()
|
|
47
|
+
return Path.home() / ".cache" / "windtrader"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _jar_cache_path(version: str) -> Path:
|
|
51
|
+
"""
|
|
52
|
+
Return the expected local cache path for the given windtrader-java version.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
version: windtrader-java version string (e.g. "0.1.1").
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Path: Path where the JAR should be stored in the cache.
|
|
59
|
+
"""
|
|
60
|
+
return _cache_dir() / "jars" / f"windtrader-java-{version}.jar"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _download(url: str, dest: Path) -> None:
|
|
64
|
+
"""
|
|
65
|
+
Download a URL to the destination path atomically.
|
|
66
|
+
|
|
67
|
+
The download is written to a temporary file first and then replaced into place.
|
|
68
|
+
This helps avoid leaving partial/corrupt JARs in the cache if the process is interrupted.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
url: The URL to download.
|
|
72
|
+
dest: Destination path for the downloaded content.
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
URLError / HTTPError: If the network request fails.
|
|
76
|
+
OSError: If writing to disk fails.
|
|
77
|
+
"""
|
|
78
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
79
|
+
tmp = dest.with_suffix(dest.suffix + ".tmp")
|
|
80
|
+
|
|
81
|
+
req = urllib.request.Request(
|
|
82
|
+
url,
|
|
83
|
+
headers={"User-Agent": "windtrader/0.1.0 (+https://github.com/Westfall-io/windtrader)"},
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
with urllib.request.urlopen(req) as r, open(tmp, "wb") as f:
|
|
87
|
+
f.write(r.read())
|
|
88
|
+
|
|
89
|
+
tmp.replace(dest)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _candidate_urls(version: str) -> list[str]:
|
|
93
|
+
"""
|
|
94
|
+
Build a prioritized list of candidate GitHub Release URLs for a given version.
|
|
95
|
+
|
|
96
|
+
This wrapper expects the Java validator JAR to be published as an asset on GitHub Releases.
|
|
97
|
+
Because tag/asset conventions can vary, we try a small fallback list.
|
|
98
|
+
|
|
99
|
+
Candidates include:
|
|
100
|
+
- Optional stable asset URL (env: WINDTRADER_JAVA_STABLE_ASSET)
|
|
101
|
+
- Tag formatted as `v{version}` with asset named `windtrader-java-{version}.jar`
|
|
102
|
+
- Tag formatted as `{version}` with the same asset name
|
|
103
|
+
- `releases/latest/download` as a fallback (still using the versioned filename)
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
version: windtrader-java version string (e.g. "0.1.1").
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
list[str]: Candidate URLs to try in order.
|
|
110
|
+
"""
|
|
111
|
+
repo = os.environ.get("WINDTRADER_JAVA_REPO", DEFAULT_REPO)
|
|
112
|
+
|
|
113
|
+
stable_asset = os.environ.get("WINDTRADER_JAVA_STABLE_ASSET", "").strip()
|
|
114
|
+
|
|
115
|
+
urls: list[str] = []
|
|
116
|
+
if stable_asset:
|
|
117
|
+
urls.append(stable_asset)
|
|
118
|
+
|
|
119
|
+
urls.append(
|
|
120
|
+
f"https://github.com/{repo}/releases/download/v{version}/windtrader-java-{version}.jar"
|
|
121
|
+
)
|
|
122
|
+
urls.append(
|
|
123
|
+
f"https://github.com/{repo}/releases/download/{version}/windtrader-java-{version}.jar"
|
|
124
|
+
)
|
|
125
|
+
urls.append(f"https://github.com/{repo}/releases/latest/download/windtrader-java-{version}.jar")
|
|
126
|
+
|
|
127
|
+
return urls
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def get_jar_path(version: str) -> Path:
|
|
131
|
+
"""
|
|
132
|
+
Ensure the requested windtrader-java JAR is present locally and return its path.
|
|
133
|
+
|
|
134
|
+
If the JAR is already present in the local cache and appears non-empty, it is reused.
|
|
135
|
+
Otherwise, the function attempts to download the JAR from GitHub Release URLs and
|
|
136
|
+
caches it.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
version: windtrader-java version to fetch. If blank/None-like, DEFAULT_VERSION is used.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Path: Local filesystem path to the cached JAR.
|
|
143
|
+
|
|
144
|
+
Raises:
|
|
145
|
+
RuntimeError: If the JAR could not be downloaded from any candidate URL.
|
|
146
|
+
"""
|
|
147
|
+
version = (version or "").strip() or DEFAULT_VERSION
|
|
148
|
+
dest = _jar_cache_path(version)
|
|
149
|
+
|
|
150
|
+
if dest.exists() and dest.stat().st_size > 0:
|
|
151
|
+
return dest
|
|
152
|
+
|
|
153
|
+
last_err: Exception | None = None
|
|
154
|
+
tried: list[str] = []
|
|
155
|
+
|
|
156
|
+
for url in _candidate_urls(version):
|
|
157
|
+
tried.append(url)
|
|
158
|
+
try:
|
|
159
|
+
_download(url, dest)
|
|
160
|
+
if dest.exists() and dest.stat().st_size > 0:
|
|
161
|
+
return dest
|
|
162
|
+
except Exception as e: # noqa: BLE001 - intentionally try any download failure against the next candidate URL
|
|
163
|
+
last_err = e
|
|
164
|
+
|
|
165
|
+
msg = "Failed to download windtrader-java jar.\nTried:\n" + "\n".join(f" - {u}" for u in tried)
|
|
166
|
+
raise RuntimeError(msg) from last_err
|
windtrader/cli.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""
|
|
2
|
+
windtrader CLI.
|
|
3
|
+
|
|
4
|
+
Provides the `windtrader` command-line interface for driving the `windtrader-java`
|
|
5
|
+
backend: `check` (parse-only validation), `echo`, and `export` (element JSON graph).
|
|
6
|
+
|
|
7
|
+
Usage model
|
|
8
|
+
-----------
|
|
9
|
+
- The CLI reads SysML text from stdin.
|
|
10
|
+
- It invokes the Java backend jar (`windtrader-java`) via the Python client.
|
|
11
|
+
- It forwards stdout/stderr from the Java tool verbatim.
|
|
12
|
+
- It exits with the same exit code as the Java tool.
|
|
13
|
+
|
|
14
|
+
Subcommands
|
|
15
|
+
-----------
|
|
16
|
+
- `check` (default): parse-validate SysML text.
|
|
17
|
+
- `echo`: print the normalized/echoed representation.
|
|
18
|
+
- `export`: parse SysML and print the SysMLv2 element JSON graph (API shape).
|
|
19
|
+
|
|
20
|
+
Exit codes (as defined by windtrader-java)
|
|
21
|
+
------------------------------------------
|
|
22
|
+
- 0: Valid SysML (parse succeeded)
|
|
23
|
+
- 2: Invalid SysML (syntax/parse error)
|
|
24
|
+
- 3: Runtime error (tool failure)
|
|
25
|
+
- Any other non-zero: Unexpected failure
|
|
26
|
+
|
|
27
|
+
Notes
|
|
28
|
+
-----
|
|
29
|
+
- `--version` prints the Python package version (not the Java backend version).
|
|
30
|
+
- Use `--java-version` to select which `windtrader-java` release asset to download/use.
|
|
31
|
+
- `--timeout` defaults per subcommand: 10s for `check`/`echo`, 120s for `export`
|
|
32
|
+
(export loads the standard library and runs resolve/transform).
|
|
33
|
+
- An unknown subcommand is rejected by argparse with exit 2, the same code used
|
|
34
|
+
for invalid SysML. Prefer `check`/`echo`/`export` explicitly in scripts.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import argparse
|
|
40
|
+
import subprocess
|
|
41
|
+
import sys
|
|
42
|
+
|
|
43
|
+
from . import __version__
|
|
44
|
+
from ._jars import DEFAULT_VERSION
|
|
45
|
+
from .client import echo as client_echo
|
|
46
|
+
from .client import export as client_export
|
|
47
|
+
from .client import validate
|
|
48
|
+
|
|
49
|
+
_SUBCOMMAND_DEFAULT_TIMEOUT = {
|
|
50
|
+
"check": 10.0,
|
|
51
|
+
"echo": 10.0,
|
|
52
|
+
"export": 120.0,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def main(argv: list[str] | None = None) -> int:
|
|
57
|
+
"""
|
|
58
|
+
Entry point for the `windtrader` CLI.
|
|
59
|
+
|
|
60
|
+
Parameters
|
|
61
|
+
----------
|
|
62
|
+
argv:
|
|
63
|
+
Optional argument list (excluding program name). If None, arguments are read
|
|
64
|
+
from `sys.argv` (the default argparse behavior).
|
|
65
|
+
|
|
66
|
+
Returns
|
|
67
|
+
-------
|
|
68
|
+
int
|
|
69
|
+
The exit code returned by the Java backend process.
|
|
70
|
+
|
|
71
|
+
Behavior
|
|
72
|
+
--------
|
|
73
|
+
- Reads all input from stdin (blocking until EOF).
|
|
74
|
+
- Runs the selected subcommand with the provided Java backend version and timeout.
|
|
75
|
+
- Writes any backend stdout to stdout and stderr to stderr (verbatim).
|
|
76
|
+
- Returns the backend exit code (0/2/3), or 3 on a subprocess timeout.
|
|
77
|
+
"""
|
|
78
|
+
p = argparse.ArgumentParser(
|
|
79
|
+
prog="windtrader",
|
|
80
|
+
description="SysML v2 wrapper around windtrader-java (check / echo / export)",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Standard CLI version flag (prints Python package version)
|
|
84
|
+
p.add_argument(
|
|
85
|
+
"--version",
|
|
86
|
+
action="version",
|
|
87
|
+
version=f"windtrader {__version__}",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
p.add_argument(
|
|
91
|
+
"subcommand",
|
|
92
|
+
nargs="?",
|
|
93
|
+
default="check",
|
|
94
|
+
choices=["check", "echo", "export"],
|
|
95
|
+
help="backend subcommand to run (default: check)",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Java backend configuration
|
|
99
|
+
p.add_argument(
|
|
100
|
+
"--java-version",
|
|
101
|
+
default=DEFAULT_VERSION,
|
|
102
|
+
help="windtrader-java version to use (GitHub release asset version)",
|
|
103
|
+
)
|
|
104
|
+
p.add_argument(
|
|
105
|
+
"--timeout",
|
|
106
|
+
type=float,
|
|
107
|
+
default=None,
|
|
108
|
+
help="timeout in seconds for the Java subprocess "
|
|
109
|
+
"(default: 10s for check/echo, 120s for export)",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
args = p.parse_args(argv)
|
|
113
|
+
|
|
114
|
+
timeout_s = (
|
|
115
|
+
args.timeout if args.timeout is not None else _SUBCOMMAND_DEFAULT_TIMEOUT[args.subcommand]
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
text = sys.stdin.read()
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
if args.subcommand == "export":
|
|
122
|
+
res = client_export(text, version=args.java_version, timeout_s=timeout_s)
|
|
123
|
+
elif args.subcommand == "echo":
|
|
124
|
+
res = client_echo(text, version=args.java_version, timeout_s=timeout_s)
|
|
125
|
+
else:
|
|
126
|
+
res = validate(text, version=args.java_version, timeout_s=timeout_s)
|
|
127
|
+
except subprocess.TimeoutExpired:
|
|
128
|
+
# A timeout is a runtime/tool failure (exit 3), not a traceback.
|
|
129
|
+
sys.stderr.write("error: windtrader-java exceeded its timeout\n")
|
|
130
|
+
return 3
|
|
131
|
+
|
|
132
|
+
# Forward tool output verbatim
|
|
133
|
+
if res.stdout:
|
|
134
|
+
sys.stdout.write(res.stdout)
|
|
135
|
+
if not res.stdout.endswith("\n"):
|
|
136
|
+
sys.stdout.write("\n")
|
|
137
|
+
|
|
138
|
+
if res.stderr:
|
|
139
|
+
sys.stderr.write(res.stderr)
|
|
140
|
+
if not res.stderr.endswith("\n"):
|
|
141
|
+
sys.stderr.write("\n")
|
|
142
|
+
|
|
143
|
+
# Match Java tool exit codes
|
|
144
|
+
return int(res.exit_code)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
if __name__ == "__main__":
|
|
148
|
+
raise SystemExit(main())
|
windtrader/client.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Python client for the `windtrader-java` CLI (check / echo / export).
|
|
3
|
+
|
|
4
|
+
This module provides a small, stable API for driving the published
|
|
5
|
+
`windtrader-java` shaded jar from Python. Each function runs the jar as an
|
|
6
|
+
external black-box subprocess (`java -jar <jar> <subcommand>`) and captures
|
|
7
|
+
its stdout/stderr/exit code; the jar is never imported or embedded.
|
|
8
|
+
|
|
9
|
+
Subcommands and exit codes (as defined by windtrader-java)
|
|
10
|
+
----------------------------------------------------------
|
|
11
|
+
- `check`: parse-only validation. 0 valid, 2 invalid, 3 runtime failure.
|
|
12
|
+
- `echo`: print a normalized/echoed representation when valid (implementation-defined).
|
|
13
|
+
- `export`: parse SysML and print the SysMLv2 element JSON graph (API shape).
|
|
14
|
+
|
|
15
|
+
The Python API mirrors these exactly: `validate`/`check`, `echo`, `export`.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import subprocess
|
|
21
|
+
import time
|
|
22
|
+
from collections.abc import Sequence
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
|
|
25
|
+
from ._jars import DEFAULT_VERSION, get_jar_path
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class CommandResult:
|
|
30
|
+
"""
|
|
31
|
+
Result of invoking `windtrader-java` on a text input.
|
|
32
|
+
|
|
33
|
+
Attributes
|
|
34
|
+
----------
|
|
35
|
+
ok:
|
|
36
|
+
True when the tool exited with code 0.
|
|
37
|
+
version:
|
|
38
|
+
windtrader-java version string used to resolve/download the jar.
|
|
39
|
+
exit_code:
|
|
40
|
+
Process exit code returned by the jar.
|
|
41
|
+
stdout:
|
|
42
|
+
Captured standard output from the jar.
|
|
43
|
+
stderr:
|
|
44
|
+
Captured standard error from the jar (often contains diagnostics).
|
|
45
|
+
jar_path:
|
|
46
|
+
Local filesystem path to the jar used during this invocation.
|
|
47
|
+
duration_s:
|
|
48
|
+
Wall-clock runtime in seconds for the subprocess call.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
ok: bool
|
|
52
|
+
version: str
|
|
53
|
+
exit_code: int
|
|
54
|
+
stdout: str
|
|
55
|
+
stderr: str
|
|
56
|
+
jar_path: str
|
|
57
|
+
duration_s: float
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def is_invalid_syntax(self) -> bool:
|
|
61
|
+
"""
|
|
62
|
+
True if the input is syntactically invalid per windtrader-java.
|
|
63
|
+
|
|
64
|
+
Convention: exit code 2 means "invalid SysML" (parse error).
|
|
65
|
+
"""
|
|
66
|
+
return self.exit_code == 2
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def is_runtime_error(self) -> bool:
|
|
70
|
+
"""
|
|
71
|
+
True if the tool failed for reasons other than a parse error.
|
|
72
|
+
|
|
73
|
+
Convention:
|
|
74
|
+
- 0 is success
|
|
75
|
+
- 2 is invalid syntax
|
|
76
|
+
Any other code indicates a tool/runtime failure (e.g., linkage errors).
|
|
77
|
+
"""
|
|
78
|
+
return self.exit_code not in (0, 2)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class WindtraderClient:
|
|
82
|
+
"""
|
|
83
|
+
Thin client around `windtrader-java`.
|
|
84
|
+
|
|
85
|
+
This class is intentionally small. It:
|
|
86
|
+
- resolves/downloads the correct jar for a given version
|
|
87
|
+
- runs the jar with a bounded timeout
|
|
88
|
+
- returns captured stdout/stderr + metadata as a CommandResult
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def __init__(self, version: str = DEFAULT_VERSION):
|
|
92
|
+
"""
|
|
93
|
+
Parameters
|
|
94
|
+
----------
|
|
95
|
+
version:
|
|
96
|
+
windtrader-java version string to download/use. Defaults to DEFAULT_VERSION.
|
|
97
|
+
"""
|
|
98
|
+
self.version = version
|
|
99
|
+
|
|
100
|
+
def _run(self, subcommand: str, text: str, timeout_s: float) -> CommandResult:
|
|
101
|
+
"""Run the jar with the given subcommand on `text`, returning a CommandResult."""
|
|
102
|
+
jar = get_jar_path(self.version)
|
|
103
|
+
|
|
104
|
+
t0 = time.time()
|
|
105
|
+
p = subprocess.run(
|
|
106
|
+
["java", "-jar", str(jar), subcommand],
|
|
107
|
+
input=text,
|
|
108
|
+
text=True,
|
|
109
|
+
capture_output=True,
|
|
110
|
+
timeout=timeout_s,
|
|
111
|
+
check=False,
|
|
112
|
+
)
|
|
113
|
+
t1 = time.time()
|
|
114
|
+
|
|
115
|
+
return CommandResult(
|
|
116
|
+
ok=(p.returncode == 0),
|
|
117
|
+
version=self.version,
|
|
118
|
+
exit_code=p.returncode,
|
|
119
|
+
stdout=p.stdout or "",
|
|
120
|
+
stderr=p.stderr or "",
|
|
121
|
+
jar_path=str(jar),
|
|
122
|
+
duration_s=(t1 - t0),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def validate_text(self, text: str, timeout_s: float = 10.0) -> CommandResult:
|
|
126
|
+
"""
|
|
127
|
+
Validate SysML v2 text using `windtrader-java check`.
|
|
128
|
+
|
|
129
|
+
Parameters
|
|
130
|
+
----------
|
|
131
|
+
text:
|
|
132
|
+
SysML v2 textual syntax to validate.
|
|
133
|
+
timeout_s:
|
|
134
|
+
Subprocess timeout in seconds.
|
|
135
|
+
|
|
136
|
+
Returns
|
|
137
|
+
-------
|
|
138
|
+
CommandResult
|
|
139
|
+
Captures process exit code, stdout, stderr, jar path, and duration.
|
|
140
|
+
"""
|
|
141
|
+
return self._run("check", text, timeout_s)
|
|
142
|
+
|
|
143
|
+
def echo(self, text: str, timeout_s: float = 10.0) -> CommandResult:
|
|
144
|
+
"""
|
|
145
|
+
Run `windtrader-java echo` on SysML v2 text.
|
|
146
|
+
|
|
147
|
+
This is useful for debugging/parsing investigations because it returns the tool's
|
|
148
|
+
"echoed" / normalized representation (implementation-defined by windtrader-java).
|
|
149
|
+
|
|
150
|
+
Parameters
|
|
151
|
+
----------
|
|
152
|
+
text:
|
|
153
|
+
SysML v2 textual syntax to parse/echo.
|
|
154
|
+
timeout_s:
|
|
155
|
+
Subprocess timeout in seconds.
|
|
156
|
+
|
|
157
|
+
Returns
|
|
158
|
+
-------
|
|
159
|
+
CommandResult
|
|
160
|
+
Captures process exit code, stdout, stderr, jar path, and duration.
|
|
161
|
+
"""
|
|
162
|
+
return self._run("echo", text, timeout_s)
|
|
163
|
+
|
|
164
|
+
def export(self, text: str, timeout_s: float = 120.0) -> CommandResult:
|
|
165
|
+
"""
|
|
166
|
+
Run `windtrader-java export` on SysML v2 text.
|
|
167
|
+
|
|
168
|
+
On success (exit 0) `stdout` holds the SysMLv2 element JSON graph (an
|
|
169
|
+
array of API-shaped elements). On invalid input (exit 2) no JSON is
|
|
170
|
+
emitted and `stderr` carries the parse diagnostics. On runtime failure
|
|
171
|
+
(exit 3, e.g. standard library unavailable) nothing valid is emitted.
|
|
172
|
+
|
|
173
|
+
Parameters
|
|
174
|
+
----------
|
|
175
|
+
text:
|
|
176
|
+
SysML v2 textual syntax to export to its element JSON graph.
|
|
177
|
+
timeout_s:
|
|
178
|
+
Subprocess timeout in seconds (default higher than check/echo because
|
|
179
|
+
export loads the standard library and runs resolve/transform, which
|
|
180
|
+
can take ~55s on a cold JVM even for small models; 120s leaves headroom
|
|
181
|
+
for a loaded machine or cold CI runner).
|
|
182
|
+
|
|
183
|
+
Returns
|
|
184
|
+
-------
|
|
185
|
+
CommandResult
|
|
186
|
+
Captures process exit code, stdout (JSON), stderr, jar path, and duration.
|
|
187
|
+
"""
|
|
188
|
+
return self._run("export", text, timeout_s)
|
|
189
|
+
|
|
190
|
+
def validate(self, text: str, timeout_s: float = 10.0) -> CommandResult:
|
|
191
|
+
"""
|
|
192
|
+
Convenience alias for validate_text().
|
|
193
|
+
|
|
194
|
+
Kept for readability in user code and tests.
|
|
195
|
+
"""
|
|
196
|
+
return self.validate_text(text, timeout_s=timeout_s)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def validate(text: str, version: str = DEFAULT_VERSION, timeout_s: float = 10.0) -> CommandResult:
|
|
200
|
+
"""
|
|
201
|
+
Validate SysML v2 text in one call without instantiating WindtraderClient.
|
|
202
|
+
|
|
203
|
+
Parameters
|
|
204
|
+
----------
|
|
205
|
+
text:
|
|
206
|
+
SysML v2 textual syntax to validate.
|
|
207
|
+
version:
|
|
208
|
+
windtrader-java version string to download/use.
|
|
209
|
+
timeout_s:
|
|
210
|
+
Subprocess timeout in seconds.
|
|
211
|
+
|
|
212
|
+
Returns
|
|
213
|
+
-------
|
|
214
|
+
CommandResult
|
|
215
|
+
Captures exit code, stdout, stderr, jar path, and duration.
|
|
216
|
+
"""
|
|
217
|
+
return WindtraderClient(version=version).validate_text(text, timeout_s=timeout_s)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def echo(text: str, version: str = DEFAULT_VERSION, timeout_s: float = 10.0) -> CommandResult:
|
|
221
|
+
"""
|
|
222
|
+
Echo SysML v2 text in one call without instantiating WindtraderClient.
|
|
223
|
+
|
|
224
|
+
Returns
|
|
225
|
+
-------
|
|
226
|
+
CommandResult
|
|
227
|
+
Captures exit code, stdout, stderr, jar path, and duration.
|
|
228
|
+
"""
|
|
229
|
+
return WindtraderClient(version=version).echo(text, timeout_s=timeout_s)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def export(text: str, version: str = DEFAULT_VERSION, timeout_s: float = 120.0) -> CommandResult:
|
|
233
|
+
"""
|
|
234
|
+
Export SysML v2 text to its element JSON graph in one call.
|
|
235
|
+
|
|
236
|
+
On success (exit 0) `stdout` holds the SysMLv2 element JSON graph.
|
|
237
|
+
|
|
238
|
+
Returns
|
|
239
|
+
-------
|
|
240
|
+
CommandResult
|
|
241
|
+
Captures exit code, stdout (JSON), stderr, jar path, and duration.
|
|
242
|
+
"""
|
|
243
|
+
return WindtraderClient(version=version).export(text, timeout_s=timeout_s)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def validate_across_versions(
|
|
247
|
+
text: str,
|
|
248
|
+
versions: Sequence[str],
|
|
249
|
+
timeout_s: float = 10.0,
|
|
250
|
+
) -> list[CommandResult]:
|
|
251
|
+
"""
|
|
252
|
+
Validate the same SysML v2 text against multiple windtrader-java versions.
|
|
253
|
+
|
|
254
|
+
Parameters
|
|
255
|
+
----------
|
|
256
|
+
text:
|
|
257
|
+
SysML v2 textual syntax to validate.
|
|
258
|
+
versions:
|
|
259
|
+
Iterable of windtrader-java version strings.
|
|
260
|
+
timeout_s:
|
|
261
|
+
Subprocess timeout for each version.
|
|
262
|
+
|
|
263
|
+
Returns
|
|
264
|
+
-------
|
|
265
|
+
list[CommandResult]
|
|
266
|
+
One result per version, in the same order as `versions`.
|
|
267
|
+
"""
|
|
268
|
+
results: list[CommandResult] = []
|
|
269
|
+
for v in versions:
|
|
270
|
+
results.append(validate(text, version=v, timeout_s=timeout_s))
|
|
271
|
+
return results
|