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.
@@ -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
+ ![CI](https://github.com/Westfall-io/windtrader/actions/workflows/ci.yml/badge.svg)
53
+ ![Coverage](https://codecov.io/gh/Westfall-io/windtrader/branch/main/graph/badge.svg)
54
+ ![Docstrings](https://img.shields.io/badge/docstrings-interrogate-success)
55
+ ![Python](https://img.shields.io/badge/python-3.10%2B-blue)
56
+ ![License](https://img.shields.io/github/license/Westfall-io/windtrader)
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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ windtrader = windtrader.cli:main
@@ -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