nexusflow-sdk 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nexusflow_sdk-0.3.0/PKG-INFO +180 -0
- nexusflow_sdk-0.3.0/README.md +156 -0
- nexusflow_sdk-0.3.0/pyproject.toml +42 -0
- nexusflow_sdk-0.3.0/setup.cfg +4 -0
- nexusflow_sdk-0.3.0/src/nexusflow_sdk/__init__.py +36 -0
- nexusflow_sdk-0.3.0/src/nexusflow_sdk/_constraints.py +61 -0
- nexusflow_sdk-0.3.0/src/nexusflow_sdk/_expression.py +137 -0
- nexusflow_sdk-0.3.0/src/nexusflow_sdk/auth.py +113 -0
- nexusflow_sdk-0.3.0/src/nexusflow_sdk/client.py +81 -0
- nexusflow_sdk-0.3.0/src/nexusflow_sdk/component.py +261 -0
- nexusflow_sdk-0.3.0/src/nexusflow_sdk/connection.py +42 -0
- nexusflow_sdk-0.3.0/src/nexusflow_sdk/exceptions.py +28 -0
- nexusflow_sdk-0.3.0/src/nexusflow_sdk/http.py +97 -0
- nexusflow_sdk-0.3.0/src/nexusflow_sdk/job.py +306 -0
- nexusflow_sdk-0.3.0/src/nexusflow_sdk/meta.py +76 -0
- nexusflow_sdk-0.3.0/src/nexusflow_sdk/model.py +448 -0
- nexusflow_sdk-0.3.0/src/nexusflow_sdk/result.py +333 -0
- nexusflow_sdk-0.3.0/src/nexusflow_sdk/types.py +112 -0
- nexusflow_sdk-0.3.0/src/nexusflow_sdk.egg-info/PKG-INFO +180 -0
- nexusflow_sdk-0.3.0/src/nexusflow_sdk.egg-info/SOURCES.txt +25 -0
- nexusflow_sdk-0.3.0/src/nexusflow_sdk.egg-info/dependency_links.txt +1 -0
- nexusflow_sdk-0.3.0/src/nexusflow_sdk.egg-info/requires.txt +6 -0
- nexusflow_sdk-0.3.0/src/nexusflow_sdk.egg-info/top_level.txt +1 -0
- nexusflow_sdk-0.3.0/tests/test_job.py +174 -0
- nexusflow_sdk-0.3.0/tests/test_model.py +300 -0
- nexusflow_sdk-0.3.0/tests/test_public_api.py +57 -0
- nexusflow_sdk-0.3.0/tests/test_result.py +132 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nexusflow-sdk
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Python SDK for NexusFlow model editing and simulation workflows
|
|
5
|
+
Author: NexusFlow Team
|
|
6
|
+
License-Expression: LicenseRef-Proprietary
|
|
7
|
+
Keywords: nexusflow,pipeline-simulation,oil-gas,sdk,digital-twin
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: requests>=2.31.0
|
|
20
|
+
Requires-Dist: websocket-client>=1.8.0
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: build>=1.2.2; extra == "dev"
|
|
23
|
+
Requires-Dist: twine>=5.1.1; extra == "dev"
|
|
24
|
+
|
|
25
|
+
# nexusflow-sdk
|
|
26
|
+
|
|
27
|
+
nexusflow-sdk is a Python SDK for NexusFlow model editing, simulation execution, streaming result retrieval, and runtime command control.
|
|
28
|
+
|
|
29
|
+
## Features
|
|
30
|
+
|
|
31
|
+
- Token-based authentication against the NexusFlow backend
|
|
32
|
+
- Create models for supported workspace types and open existing models
|
|
33
|
+
- Add, remove, connect, and configure components in the model graph
|
|
34
|
+
- Save model snapshots and start simulation jobs
|
|
35
|
+
- Poll or stream logs, plots, tables, inspect data, and container messages
|
|
36
|
+
- Send runtime commands to components during online or transient execution
|
|
37
|
+
|
|
38
|
+
## Requirements
|
|
39
|
+
|
|
40
|
+
- Python 3.10 or later
|
|
41
|
+
- A reachable NexusFlow service endpoint
|
|
42
|
+
- A NexusFlow account that can obtain access tokens
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
Install from a built wheel or from PyPI after publication:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install nexusflow-sdk
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
For local development in this repository:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install -e .
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Quick Start
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from nexusflow_sdk import NexusFlowClient
|
|
62
|
+
|
|
63
|
+
client = NexusFlowClient(
|
|
64
|
+
base_url="http://localhost:8000",
|
|
65
|
+
username="admin",
|
|
66
|
+
password="your-password",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
model = client.create_model(
|
|
70
|
+
name="sdk-demo-model",
|
|
71
|
+
workspace_type="lps_online",
|
|
72
|
+
description="Created by nexusflow-sdk",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
source = model.add_component("Source", position=(0, 0))
|
|
76
|
+
pipe = model.add_component("Pipe", position=(240, 0))
|
|
77
|
+
load = model.add_component("Load", position=(480, 0))
|
|
78
|
+
|
|
79
|
+
model.connect(source, "0", pipe, "0")
|
|
80
|
+
model.connect(pipe, "1", load, "0")
|
|
81
|
+
|
|
82
|
+
model.save()
|
|
83
|
+
job = model.run(timeout=120, image="nexusflow")
|
|
84
|
+
|
|
85
|
+
for message in job.stream_results(timeout=2):
|
|
86
|
+
print(message.get("type"), message.get("key"))
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Public API
|
|
90
|
+
|
|
91
|
+
The package currently exports these main entry points:
|
|
92
|
+
|
|
93
|
+
- `NexusFlowClient`
|
|
94
|
+
- `Model`
|
|
95
|
+
- `Component`
|
|
96
|
+
- `Connection`
|
|
97
|
+
- `Job`
|
|
98
|
+
- `MetaService`
|
|
99
|
+
- `ResultStream`
|
|
100
|
+
- `LogMessage`
|
|
101
|
+
- `PlotMessage`
|
|
102
|
+
- `TableMessage`
|
|
103
|
+
- `InspectMessage`
|
|
104
|
+
- `ContainerMessage`
|
|
105
|
+
- `NexusFlowError`
|
|
106
|
+
- `AuthError`
|
|
107
|
+
- `APIError`
|
|
108
|
+
- `NotFoundError`
|
|
109
|
+
- `ValidationError`
|
|
110
|
+
|
|
111
|
+
## Examples
|
|
112
|
+
|
|
113
|
+
- `examples/quickstart.py`: create, save, run, and fetch results
|
|
114
|
+
- `examples/transient_streaming.py`: stream transient online results and send commands during execution
|
|
115
|
+
- `examples/run_existing_model.py`: open an existing model by ID and run it
|
|
116
|
+
|
|
117
|
+
## Release Validation
|
|
118
|
+
|
|
119
|
+
Build and metadata check:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
python -m build
|
|
123
|
+
python -m twine check dist/*
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Minimal install smoke test with the built wheel:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
python -m venv .smoke-venv
|
|
130
|
+
.smoke-venv\Scripts\python -m pip install dist\nexusflow_sdk-0.1.2-py3-none-any.whl
|
|
131
|
+
.smoke-venv\Scripts\python -c "from nexusflow_sdk import NexusFlowClient, Model, Job, ResultStream; print('smoke ok')"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Release Workflow
|
|
135
|
+
|
|
136
|
+
If you want to publish a new version yourself, the repository now includes a reusable PowerShell script:
|
|
137
|
+
|
|
138
|
+
```powershell
|
|
139
|
+
.\scripts\release.ps1 -Repository pypi -Token "pypi-..."
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Recommended process:
|
|
143
|
+
|
|
144
|
+
1. Update `version` in `pyproject.toml`.
|
|
145
|
+
2. Run the release script from the `nexusflow-sdk` directory.
|
|
146
|
+
3. Wait for PyPI index propagation if the published install smoke test fails on the first try.
|
|
147
|
+
|
|
148
|
+
The script performs these steps automatically:
|
|
149
|
+
|
|
150
|
+
- clean `dist/`
|
|
151
|
+
- `python -m build`
|
|
152
|
+
- `python -m twine check dist/*`
|
|
153
|
+
- `python -m unittest tests.test_public_api`
|
|
154
|
+
- install the built wheel into a temporary virtual environment for a local smoke test
|
|
155
|
+
- upload to PyPI or TestPyPI
|
|
156
|
+
- create a fresh temporary virtual environment and install the published package for a post-publish smoke test
|
|
157
|
+
|
|
158
|
+
Examples:
|
|
159
|
+
|
|
160
|
+
```powershell
|
|
161
|
+
# Full publish to PyPI
|
|
162
|
+
.\scripts\release.ps1 -Repository pypi -Token "pypi-..."
|
|
163
|
+
|
|
164
|
+
# Full publish to TestPyPI
|
|
165
|
+
.\scripts\release.ps1 -Repository testpypi -Token "pypi-..."
|
|
166
|
+
|
|
167
|
+
# Local validation only, without upload
|
|
168
|
+
.\scripts\release.ps1 -SkipUpload -SkipSmokeTest
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
You can also avoid passing tokens on the command line by using environment variables:
|
|
172
|
+
|
|
173
|
+
```powershell
|
|
174
|
+
$env:PYPI_TOKEN = "pypi-..."
|
|
175
|
+
.\scripts\release.ps1 -Repository pypi
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## License
|
|
179
|
+
|
|
180
|
+
This package is currently distributed as proprietary software.
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# nexusflow-sdk
|
|
2
|
+
|
|
3
|
+
nexusflow-sdk is a Python SDK for NexusFlow model editing, simulation execution, streaming result retrieval, and runtime command control.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Token-based authentication against the NexusFlow backend
|
|
8
|
+
- Create models for supported workspace types and open existing models
|
|
9
|
+
- Add, remove, connect, and configure components in the model graph
|
|
10
|
+
- Save model snapshots and start simulation jobs
|
|
11
|
+
- Poll or stream logs, plots, tables, inspect data, and container messages
|
|
12
|
+
- Send runtime commands to components during online or transient execution
|
|
13
|
+
|
|
14
|
+
## Requirements
|
|
15
|
+
|
|
16
|
+
- Python 3.10 or later
|
|
17
|
+
- A reachable NexusFlow service endpoint
|
|
18
|
+
- A NexusFlow account that can obtain access tokens
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
Install from a built wheel or from PyPI after publication:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install nexusflow-sdk
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
For local development in this repository:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install -e .
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from nexusflow_sdk import NexusFlowClient
|
|
38
|
+
|
|
39
|
+
client = NexusFlowClient(
|
|
40
|
+
base_url="http://localhost:8000",
|
|
41
|
+
username="admin",
|
|
42
|
+
password="your-password",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
model = client.create_model(
|
|
46
|
+
name="sdk-demo-model",
|
|
47
|
+
workspace_type="lps_online",
|
|
48
|
+
description="Created by nexusflow-sdk",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
source = model.add_component("Source", position=(0, 0))
|
|
52
|
+
pipe = model.add_component("Pipe", position=(240, 0))
|
|
53
|
+
load = model.add_component("Load", position=(480, 0))
|
|
54
|
+
|
|
55
|
+
model.connect(source, "0", pipe, "0")
|
|
56
|
+
model.connect(pipe, "1", load, "0")
|
|
57
|
+
|
|
58
|
+
model.save()
|
|
59
|
+
job = model.run(timeout=120, image="nexusflow")
|
|
60
|
+
|
|
61
|
+
for message in job.stream_results(timeout=2):
|
|
62
|
+
print(message.get("type"), message.get("key"))
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Public API
|
|
66
|
+
|
|
67
|
+
The package currently exports these main entry points:
|
|
68
|
+
|
|
69
|
+
- `NexusFlowClient`
|
|
70
|
+
- `Model`
|
|
71
|
+
- `Component`
|
|
72
|
+
- `Connection`
|
|
73
|
+
- `Job`
|
|
74
|
+
- `MetaService`
|
|
75
|
+
- `ResultStream`
|
|
76
|
+
- `LogMessage`
|
|
77
|
+
- `PlotMessage`
|
|
78
|
+
- `TableMessage`
|
|
79
|
+
- `InspectMessage`
|
|
80
|
+
- `ContainerMessage`
|
|
81
|
+
- `NexusFlowError`
|
|
82
|
+
- `AuthError`
|
|
83
|
+
- `APIError`
|
|
84
|
+
- `NotFoundError`
|
|
85
|
+
- `ValidationError`
|
|
86
|
+
|
|
87
|
+
## Examples
|
|
88
|
+
|
|
89
|
+
- `examples/quickstart.py`: create, save, run, and fetch results
|
|
90
|
+
- `examples/transient_streaming.py`: stream transient online results and send commands during execution
|
|
91
|
+
- `examples/run_existing_model.py`: open an existing model by ID and run it
|
|
92
|
+
|
|
93
|
+
## Release Validation
|
|
94
|
+
|
|
95
|
+
Build and metadata check:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
python -m build
|
|
99
|
+
python -m twine check dist/*
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Minimal install smoke test with the built wheel:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
python -m venv .smoke-venv
|
|
106
|
+
.smoke-venv\Scripts\python -m pip install dist\nexusflow_sdk-0.1.2-py3-none-any.whl
|
|
107
|
+
.smoke-venv\Scripts\python -c "from nexusflow_sdk import NexusFlowClient, Model, Job, ResultStream; print('smoke ok')"
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Release Workflow
|
|
111
|
+
|
|
112
|
+
If you want to publish a new version yourself, the repository now includes a reusable PowerShell script:
|
|
113
|
+
|
|
114
|
+
```powershell
|
|
115
|
+
.\scripts\release.ps1 -Repository pypi -Token "pypi-..."
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Recommended process:
|
|
119
|
+
|
|
120
|
+
1. Update `version` in `pyproject.toml`.
|
|
121
|
+
2. Run the release script from the `nexusflow-sdk` directory.
|
|
122
|
+
3. Wait for PyPI index propagation if the published install smoke test fails on the first try.
|
|
123
|
+
|
|
124
|
+
The script performs these steps automatically:
|
|
125
|
+
|
|
126
|
+
- clean `dist/`
|
|
127
|
+
- `python -m build`
|
|
128
|
+
- `python -m twine check dist/*`
|
|
129
|
+
- `python -m unittest tests.test_public_api`
|
|
130
|
+
- install the built wheel into a temporary virtual environment for a local smoke test
|
|
131
|
+
- upload to PyPI or TestPyPI
|
|
132
|
+
- create a fresh temporary virtual environment and install the published package for a post-publish smoke test
|
|
133
|
+
|
|
134
|
+
Examples:
|
|
135
|
+
|
|
136
|
+
```powershell
|
|
137
|
+
# Full publish to PyPI
|
|
138
|
+
.\scripts\release.ps1 -Repository pypi -Token "pypi-..."
|
|
139
|
+
|
|
140
|
+
# Full publish to TestPyPI
|
|
141
|
+
.\scripts\release.ps1 -Repository testpypi -Token "pypi-..."
|
|
142
|
+
|
|
143
|
+
# Local validation only, without upload
|
|
144
|
+
.\scripts\release.ps1 -SkipUpload -SkipSmokeTest
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
You can also avoid passing tokens on the command line by using environment variables:
|
|
148
|
+
|
|
149
|
+
```powershell
|
|
150
|
+
$env:PYPI_TOKEN = "pypi-..."
|
|
151
|
+
.\scripts\release.ps1 -Repository pypi
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
This package is currently distributed as proprietary software.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "nexusflow-sdk"
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "Python SDK for NexusFlow model editing and simulation workflows"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "LicenseRef-Proprietary"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "NexusFlow Team"},
|
|
14
|
+
]
|
|
15
|
+
keywords = ["nexusflow", "pipeline-simulation", "oil-gas", "sdk", "digital-twin"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Topic :: Scientific/Engineering",
|
|
25
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
"requests>=2.31.0",
|
|
29
|
+
"websocket-client>=1.8.0",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
dev = [
|
|
34
|
+
"build>=1.2.2",
|
|
35
|
+
"twine>=5.1.1",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[tool.setuptools]
|
|
39
|
+
package-dir = {"" = "src"}
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.packages.find]
|
|
42
|
+
where = ["src"]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from .client import NexusFlowClient
|
|
2
|
+
from .component import Component, PortRef
|
|
3
|
+
from .connection import Connection
|
|
4
|
+
from .exceptions import APIError, AuthError, NexusFlowError, NotFoundError, ValidationError
|
|
5
|
+
from .job import Job
|
|
6
|
+
from .meta import MetaService
|
|
7
|
+
from .model import Model
|
|
8
|
+
from .result import (
|
|
9
|
+
ContainerMessage,
|
|
10
|
+
InspectMessage,
|
|
11
|
+
LogMessage,
|
|
12
|
+
PlotMessage,
|
|
13
|
+
ResultStream,
|
|
14
|
+
TableMessage,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"APIError",
|
|
19
|
+
"AuthError",
|
|
20
|
+
"Component",
|
|
21
|
+
"Connection",
|
|
22
|
+
"ContainerMessage",
|
|
23
|
+
"NexusFlowClient",
|
|
24
|
+
"NexusFlowError",
|
|
25
|
+
"InspectMessage",
|
|
26
|
+
"Job",
|
|
27
|
+
"LogMessage",
|
|
28
|
+
"MetaService",
|
|
29
|
+
"Model",
|
|
30
|
+
"NotFoundError",
|
|
31
|
+
"PlotMessage",
|
|
32
|
+
"PortRef",
|
|
33
|
+
"ResultStream",
|
|
34
|
+
"TableMessage",
|
|
35
|
+
"ValidationError",
|
|
36
|
+
]
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Mapping
|
|
4
|
+
|
|
5
|
+
from ._expression import evaluate_condition, evaluate_expression
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _constraint_scope(
|
|
9
|
+
*,
|
|
10
|
+
value: Any,
|
|
11
|
+
params: Mapping[str, Any] | None,
|
|
12
|
+
global_params: Mapping[str, Any] | None,
|
|
13
|
+
context: Mapping[str, Any] | None,
|
|
14
|
+
) -> dict[str, Any]:
|
|
15
|
+
return {
|
|
16
|
+
"value": value,
|
|
17
|
+
"params": dict(params or {}),
|
|
18
|
+
"global": dict(global_params or params or {}),
|
|
19
|
+
"context": dict(context or {}),
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def validate_constraints(
|
|
24
|
+
arg_def: Mapping[str, Any],
|
|
25
|
+
value: Any,
|
|
26
|
+
*,
|
|
27
|
+
params: Mapping[str, Any] | None,
|
|
28
|
+
global_params: Mapping[str, Any] | None = None,
|
|
29
|
+
context: Mapping[str, Any] | None = None,
|
|
30
|
+
) -> list[str]:
|
|
31
|
+
constraints = arg_def.get("constraints", [])
|
|
32
|
+
if not isinstance(constraints, list):
|
|
33
|
+
return []
|
|
34
|
+
|
|
35
|
+
issues: list[str] = []
|
|
36
|
+
scope = _constraint_scope(
|
|
37
|
+
value=value,
|
|
38
|
+
params=params,
|
|
39
|
+
global_params=global_params,
|
|
40
|
+
context={
|
|
41
|
+
**dict(context or {}),
|
|
42
|
+
"key": arg_def.get("key", ""),
|
|
43
|
+
"type": arg_def.get("type", ""),
|
|
44
|
+
},
|
|
45
|
+
)
|
|
46
|
+
for constraint in constraints:
|
|
47
|
+
if not isinstance(constraint, Mapping):
|
|
48
|
+
continue
|
|
49
|
+
expr = str(constraint.get("expr", "")).strip()
|
|
50
|
+
if not expr:
|
|
51
|
+
continue
|
|
52
|
+
message = str(constraint.get("message") or "constraint failed")
|
|
53
|
+
when = str(constraint.get("when", "")).strip()
|
|
54
|
+
try:
|
|
55
|
+
if when and not evaluate_condition(when, scope):
|
|
56
|
+
continue
|
|
57
|
+
if not bool(evaluate_expression(expr, scope)):
|
|
58
|
+
issues.append(message)
|
|
59
|
+
except Exception as exc:
|
|
60
|
+
issues.append(f"constraint rule failed: {message} ({exc})")
|
|
61
|
+
return issues
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from types import SimpleNamespace
|
|
5
|
+
from typing import Any, Mapping
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
_NOT_PATTERN = re.compile(r"(?<![=!<>])!(?!=)")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _to_namespace(value: Any) -> Any:
|
|
12
|
+
if isinstance(value, Mapping):
|
|
13
|
+
return SimpleNamespace(**{key: _to_namespace(item) for key, item in value.items()})
|
|
14
|
+
if isinstance(value, list):
|
|
15
|
+
return [_to_namespace(item) for item in value]
|
|
16
|
+
return value
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _equal_text(left: Any, right: Any) -> bool:
|
|
20
|
+
return str(left) == str(right)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _as_list(value: Any) -> list[Any]:
|
|
24
|
+
if value is None:
|
|
25
|
+
return []
|
|
26
|
+
if isinstance(value, list):
|
|
27
|
+
return value
|
|
28
|
+
if isinstance(value, tuple):
|
|
29
|
+
return list(value)
|
|
30
|
+
return [value]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _stable_marker(value: Any) -> str:
|
|
34
|
+
if isinstance(value, Mapping):
|
|
35
|
+
return "dict:" + repr(sorted((str(key), _stable_marker(item)) for key, item in value.items()))
|
|
36
|
+
if isinstance(value, (list, tuple)):
|
|
37
|
+
return "list:" + repr([_stable_marker(item) for item in value])
|
|
38
|
+
return f"{type(value).__name__}:{value!r}"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _column(rows: Any, index: Any) -> list[Any]:
|
|
42
|
+
col_index = int(index)
|
|
43
|
+
result: list[Any] = []
|
|
44
|
+
for row in _as_list(rows):
|
|
45
|
+
if isinstance(row, Mapping):
|
|
46
|
+
result.append(row.get(col_index, row.get(str(col_index))))
|
|
47
|
+
continue
|
|
48
|
+
if isinstance(row, (list, tuple)) and -len(row) <= col_index < len(row):
|
|
49
|
+
result.append(row[col_index])
|
|
50
|
+
else:
|
|
51
|
+
result.append(None)
|
|
52
|
+
return result
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _unique(items: Any) -> bool:
|
|
56
|
+
seen: set[str] = set()
|
|
57
|
+
for item in _as_list(items):
|
|
58
|
+
marker = _stable_marker(item)
|
|
59
|
+
if marker in seen:
|
|
60
|
+
return False
|
|
61
|
+
seen.add(marker)
|
|
62
|
+
return True
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _empty(value: Any) -> bool:
|
|
66
|
+
if value is None:
|
|
67
|
+
return True
|
|
68
|
+
if isinstance(value, (str, list, tuple, dict, set)):
|
|
69
|
+
return len(value) == 0
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _not_empty(value: Any) -> bool:
|
|
74
|
+
return not _empty(value)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _matches(value: Any, pattern: Any) -> bool:
|
|
78
|
+
return re.search(str(pattern), "" if value is None else str(value)) is not None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _number(value: Any) -> float:
|
|
82
|
+
return float(value)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _string(value: Any) -> str:
|
|
86
|
+
return "" if value is None else str(value)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _transform(expr: str) -> str:
|
|
90
|
+
transformed = expr.strip()
|
|
91
|
+
transformed = transformed.replace("&&", " and ")
|
|
92
|
+
transformed = transformed.replace("||", " or ")
|
|
93
|
+
transformed = re.sub(r"\btrue\b", "True", transformed, flags=re.IGNORECASE)
|
|
94
|
+
transformed = re.sub(r"\bfalse\b", "False", transformed, flags=re.IGNORECASE)
|
|
95
|
+
transformed = re.sub(r"\bnull\b", "None", transformed, flags=re.IGNORECASE)
|
|
96
|
+
transformed = re.sub(r"\bglobal\b", "global_", transformed)
|
|
97
|
+
transformed = _NOT_PATTERN.sub(" not ", transformed)
|
|
98
|
+
return transformed
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def evaluate_expression(expr: str | None, scope: Mapping[str, Any] | None = None) -> Any:
|
|
102
|
+
if expr is None:
|
|
103
|
+
raise ValueError("expression is empty")
|
|
104
|
+
text = expr.strip()
|
|
105
|
+
if not text:
|
|
106
|
+
raise ValueError("expression is empty")
|
|
107
|
+
transformed = _transform(text)
|
|
108
|
+
locals_scope = {key: _to_namespace(value) for key, value in (scope or {}).items()}
|
|
109
|
+
if "global" in locals_scope and "global_" not in locals_scope:
|
|
110
|
+
locals_scope["global_"] = locals_scope["global"]
|
|
111
|
+
return eval(
|
|
112
|
+
transformed,
|
|
113
|
+
{
|
|
114
|
+
"__builtins__": {},
|
|
115
|
+
"equalText": _equal_text,
|
|
116
|
+
"min": min,
|
|
117
|
+
"max": max,
|
|
118
|
+
"abs": abs,
|
|
119
|
+
"len": len,
|
|
120
|
+
"empty": _empty,
|
|
121
|
+
"notEmpty": _not_empty,
|
|
122
|
+
"column": _column,
|
|
123
|
+
"unique": _unique,
|
|
124
|
+
"matches": _matches,
|
|
125
|
+
"number": _number,
|
|
126
|
+
"string": _string,
|
|
127
|
+
},
|
|
128
|
+
locals_scope,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def evaluate_condition(expr: str | None, scope: Mapping[str, Any] | None = None) -> bool:
|
|
133
|
+
try:
|
|
134
|
+
result = evaluate_expression(expr, scope)
|
|
135
|
+
except Exception:
|
|
136
|
+
return False
|
|
137
|
+
return bool(result)
|