Skip to content

Commit 4213343

Browse files
committed
update with uv
1 parent 1ca9b0b commit 4213343

23 files changed

Lines changed: 2313 additions & 3 deletions

.github/workflows/generate-stubs.yml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,12 @@ jobs:
4040
run: |
4141
uv run python scripts/generate_stubs.py
4242
43-
- name: Commit updated stubs
43+
- name: Generate offline API manifest
44+
run: |
45+
uv run python scripts/generate_api_manifest.py
46+
47+
- name: Commit updated stubs and manifest
4448
uses: stefanzweifel/git-auto-commit-action@v7
4549
with:
46-
commit_message: "chore: regenerate Java stubs"
47-
file_pattern: "src/jneqsim/**/*.pyi"
50+
commit_message: "chore: regenerate Java stubs and API manifest"
51+
file_pattern: "src/jneqsim-stubs/**/*.pyi src/neqsim/data/neqsim_api.json.gz"

README.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,84 @@ print(f"Compressor power: {comp.getPower()/1e6:.2f} MW")
217217
| Configuration-driven design | ProcessBuilder |
218218
| Advanced Java features | Direct Java access |
219219

220+
> **The `jneqsim` gateway is the first-class path for the long tail.** Only a
221+
> curated subset of NeqSim's ~2500 Java classes has hand-written Python
222+
> wrappers. Mechanical design, safety, field development, automation, and most
223+
> specialized equipment are used directly through `jneqsim` — no wrapper needed.
224+
225+
---
226+
227+
## 🔎 Discovering the Full API
228+
229+
Direct `jneqsim` access is powerful but hard to explore (a `JPackage` has no
230+
autocomplete). The `neqsim.discovery` module scans the API at runtime so you can
231+
list, search, and inspect every class from Python:
232+
233+
```python
234+
from neqsim import discovery
235+
236+
discovery.list_equipment() # every process-equipment class
237+
discovery.list_packages('process') # sub-packages of neqsim.process
238+
discovery.find_classes('scrubber') # search the whole API by keyword
239+
print(discovery.describe('Compressor')) # constructors + methods via reflection
240+
241+
Compressor = discovery.get_class('Compressor') # JClass by simple or full name
242+
```
243+
244+
For IDE autocomplete and type checking across the entire Java API, generate type
245+
stubs (already packaged as `jneqsim-stubs`, regenerate with
246+
`python scripts/generate_stubs.py`) and point your editor at `src`. An offline
247+
API manifest (`python scripts/generate_api_manifest.py`) lets `discovery` list,
248+
search, and describe classes instantly and JVM-free.
249+
250+
### Typed, validated flowsheets (optional)
251+
252+
With `pip install "neqsim[schema]"` you can build flowsheets from typed
253+
[pydantic](https://docs.pydantic.dev) models — autocomplete and validation
254+
*before* the JVM runs:
255+
256+
```python
257+
from neqsim.process.schema import ProcessModel, Fluid, Unit
258+
259+
model = ProcessModel(
260+
fluid=Fluid(eos="srk", components={"methane": 0.9, "ethane": 0.1}),
261+
process=[
262+
Unit(type="Stream", name="feed",
263+
properties={"flowRate": [50000.0, "kg/hr"], "pressure": [50.0, "bara"]}),
264+
Unit(type="Separator", name="HP Sep", inlet="feed"),
265+
],
266+
)
267+
result = model.run() # validates, builds, and runs
268+
```
269+
270+
### Component-name helpers
271+
272+
```python
273+
from neqsim.thermo.components import find_components, suggest_component
274+
find_components("glycol") # search the component database
275+
suggest_component("methan") # ['methane', 'methanol', ...] — catch typos
276+
```
277+
278+
### Rich Jupyter display
279+
280+
Streams and processes render as HTML tables in notebooks automatically (just
281+
display the object) — no extra call needed.
282+
283+
### Results to pandas
284+
285+
One helper turns any process into a tidy stream table (works for every
286+
equipment type, because it walks the flowsheet's streams):
287+
288+
```python
289+
from neqsim.process import stream_table, equipment_table, runProcess
290+
291+
runProcess()
292+
stream_table() # one row per stream: flow, T, P, phases, density, molar mass
293+
equipment_table() # one row per unit: name, type, inlet/outlet counts
294+
295+
stream_table(my_process) # or pass an explicit ProcessSystem / ProcessContext
296+
```
297+
220298
---
221299

222300
## 🧪 PVT Simulation

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ interactive = [
2828
"jupyter>=1.0.0,<2",
2929
"tabulate>=0.9.0,<0.10",
3030
]
31+
schema = [
32+
"pydantic>=2.0,<3",
33+
]
3134

3235
[project.urls]
3336
Homepage = "https://github.com/Equinor/neqsim-python"
@@ -54,6 +57,7 @@ source-include = [
5457
"src/jpype-stubs/**",
5558
"src/neqsim/**/*.pyi",
5659
"src/neqsim/py.typed",
60+
"src/neqsim/data/neqsim_api.json.gz",
5761
]
5862

5963
[build-system]

scripts/generate_api_manifest.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
"""Generate the offline NeqSim API manifest consumed by ``neqsim.discovery``.
2+
3+
Runs once at release time. It reflects the whole Java API (via ``neqsim.discovery``
4+
live reflection) and writes ``src/neqsim/data/neqsim_api.json`` containing every
5+
class with its constructors and public method names. Shipping that file makes
6+
``discovery.list_classes`` / ``find_classes`` / ``describe`` work instantly and
7+
JVM-free for end users, and it can drive a docs site.
8+
9+
Usage
10+
-----
11+
python scripts/generate_api_manifest.py [--out PATH] [--packages neqsim ...]
12+
"""
13+
14+
from __future__ import annotations
15+
16+
import argparse
17+
import gzip
18+
import json
19+
import sys
20+
from datetime import datetime, timezone
21+
from pathlib import Path
22+
from typing import Dict, List
23+
24+
_REPO_ROOT = Path(__file__).resolve().parent.parent
25+
_SRC = _REPO_ROOT / "src"
26+
if _SRC.is_dir():
27+
sys.path.insert(0, str(_SRC))
28+
29+
from neqsim import discovery # noqa: E402
30+
31+
32+
def _class_entry(fqcn: str) -> Dict[str, List[str]]:
33+
"""Build the manifest entry (constructors + methods) for one class.
34+
35+
Args:
36+
fqcn: Fully-qualified Java class name.
37+
38+
Returns:
39+
A dict with ``constructors`` and ``methods`` lists (possibly empty).
40+
"""
41+
constructors: List[str] = []
42+
methods: List[str] = []
43+
try:
44+
java_class = discovery.get_class(fqcn).class_
45+
constructors = sorted(
46+
discovery._simplify_signature(str(c)) for c in java_class.getConstructors()
47+
)
48+
method_names = set()
49+
for m in java_class.getMethods():
50+
name = str(m.getName())
51+
if name.isidentifier():
52+
method_names.add(name)
53+
methods = sorted(method_names)
54+
except Exception:
55+
pass
56+
return {"constructors": constructors, "methods": methods}
57+
58+
59+
def _detect_version() -> str:
60+
"""Return the NeqSim jar version if derivable, else ``"unknown"``."""
61+
try:
62+
for entry in discovery._classpath_entries():
63+
name = Path(entry).name
64+
if name.startswith("neqsim-") and name.endswith(".jar"):
65+
return name[len("neqsim-") : -len(".jar")]
66+
except Exception:
67+
pass
68+
return "unknown"
69+
70+
71+
def generate(out_path: Path, packages: List[str]) -> int:
72+
"""Generate the manifest for the requested packages.
73+
74+
Args:
75+
out_path: Destination JSON file.
76+
packages: Package prefixes to include.
77+
78+
Returns:
79+
The number of classes written.
80+
"""
81+
class_names: List[str] = []
82+
for prefix in packages:
83+
class_names.extend(discovery.list_classes(prefix, recursive=True))
84+
class_names = sorted(set(class_names))
85+
86+
classes: Dict[str, Dict[str, List[str]]] = {}
87+
for fqcn in class_names:
88+
classes[fqcn] = _class_entry(fqcn)
89+
90+
manifest = {
91+
"schema": "neqsim-api-manifest/1.0",
92+
"version": _detect_version(),
93+
"generated": datetime.now(timezone.utc).isoformat(),
94+
"class_count": len(classes),
95+
"classes": classes,
96+
}
97+
98+
out_path.parent.mkdir(parents=True, exist_ok=True)
99+
payload = json.dumps(manifest, indent=1, sort_keys=True)
100+
if out_path.suffix == ".gz":
101+
with gzip.open(out_path, "wt", encoding="utf-8") as handle:
102+
handle.write(payload)
103+
else:
104+
with open(out_path, "w", encoding="utf-8") as handle:
105+
handle.write(payload)
106+
return len(classes)
107+
108+
109+
def main(argv: List[str] | None = None) -> int:
110+
"""CLI entry point.
111+
112+
Args:
113+
argv: Optional argument list (defaults to ``sys.argv``).
114+
115+
Returns:
116+
Process exit code.
117+
"""
118+
parser = argparse.ArgumentParser(description=__doc__)
119+
parser.add_argument(
120+
"--out",
121+
default=str(_SRC / "neqsim" / "data" / "neqsim_api.json.gz"),
122+
help="Output path for the manifest JSON (use .gz to gzip).",
123+
)
124+
parser.add_argument(
125+
"--packages",
126+
nargs="*",
127+
default=["neqsim"],
128+
help="Package prefixes to include (default: the whole neqsim tree).",
129+
)
130+
args = parser.parse_args(argv)
131+
132+
count = generate(Path(args.out), args.packages)
133+
print(f"Wrote manifest with {count} classes to {args.out}")
134+
return 0
135+
136+
137+
if __name__ == "__main__":
138+
raise SystemExit(main())
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
"""Generate a ``Literal`` type of NeqSim component names for editor autocomplete.
2+
3+
Produces two files under ``src/neqsim/thermo``:
4+
5+
* ``component_names.py`` - runtime module exposing ``COMPONENTS`` (a tuple) and
6+
``ComponentName`` (``str`` at runtime).
7+
* ``component_names.pyi`` - type stub declaring ``ComponentName`` as a
8+
``Literal[...]`` of every known component, so editors autocomplete and type
9+
checkers flag typos when a function is annotated ``name: ComponentName``.
10+
11+
Run at release time (component database rarely changes)::
12+
13+
python scripts/generate_component_literals.py
14+
"""
15+
16+
from __future__ import annotations
17+
18+
import argparse
19+
import sys
20+
from pathlib import Path
21+
from typing import List
22+
23+
_REPO_ROOT = Path(__file__).resolve().parent.parent
24+
_SRC = _REPO_ROOT / "src"
25+
if _SRC.is_dir():
26+
sys.path.insert(0, str(_SRC))
27+
28+
29+
def _component_names() -> List[str]:
30+
"""Return all NeqSim component names via the runtime helper.
31+
32+
Returns:
33+
A sorted list of component names.
34+
"""
35+
from neqsim.thermo.components import list_components
36+
37+
return list_components()
38+
39+
40+
def _render_pyi(names: List[str]) -> str:
41+
"""Render the ``.pyi`` stub text.
42+
43+
Args:
44+
names: Component names.
45+
46+
Returns:
47+
The stub file contents.
48+
"""
49+
literals = ",\n ".join(repr(n) for n in names)
50+
return (
51+
"# Auto-generated by scripts/generate_component_literals.py - do not edit.\n"
52+
"from typing import Literal, Tuple\n\n"
53+
"COMPONENTS: Tuple[str, ...]\n\n"
54+
"ComponentName = Literal[\n "
55+
f"{literals},\n]\n"
56+
)
57+
58+
59+
def _render_py(names: List[str]) -> str:
60+
"""Render the runtime ``.py`` module text.
61+
62+
Args:
63+
names: Component names.
64+
65+
Returns:
66+
The module file contents.
67+
"""
68+
joined = ",\n ".join(repr(n) for n in names)
69+
header = (
70+
'"""Auto-generated NeqSim component names.\n\n'
71+
"See scripts/generate_component_literals.py. ComponentName is a plain\n"
72+
"``str`` at runtime; the companion .pyi makes it a ``Literal`` for type\n"
73+
'checkers and editors.\n"""\n\n'
74+
)
75+
return header + "ComponentName = str\n\n" + f"COMPONENTS = (\n {joined},\n)\n"
76+
77+
78+
def main(argv: List[str] | None = None) -> int:
79+
"""CLI entry point.
80+
81+
Args:
82+
argv: Optional argument list.
83+
84+
Returns:
85+
Process exit code.
86+
"""
87+
parser = argparse.ArgumentParser(description=__doc__)
88+
parser.add_argument(
89+
"--out-dir",
90+
default=str(_SRC / "neqsim" / "thermo"),
91+
help="Directory to write component_names.py/.pyi into.",
92+
)
93+
args = parser.parse_args(argv)
94+
95+
names = _component_names()
96+
if not names:
97+
print("No component names found (is the NeqSim jar available?)", file=sys.stderr)
98+
return 1
99+
100+
out_dir = Path(args.out_dir)
101+
(out_dir / "component_names.py").write_text(_render_py(names), encoding="utf-8")
102+
(out_dir / "component_names.pyi").write_text(_render_pyi(names), encoding="utf-8")
103+
print(f"Wrote {len(names)} component names to {out_dir}/component_names.py[i]")
104+
return 0
105+
106+
107+
if __name__ == "__main__":
108+
raise SystemExit(main())

0 commit comments

Comments
 (0)