PyGeoModel 1.0.7__tar.gz → 1.0.9__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.
- {pygeomodel-1.0.7 → pygeomodel-1.0.9}/PKG-INFO +2 -27
- {pygeomodel-1.0.7 → pygeomodel-1.0.9}/PyGeoModel.egg-info/PKG-INFO +2 -27
- {pygeomodel-1.0.7 → pygeomodel-1.0.9}/README.md +1 -25
- {pygeomodel-1.0.7 → pygeomodel-1.0.9}/pygeomodel/__init__.py +1 -1
- {pygeomodel-1.0.7 → pygeomodel-1.0.9}/pygeomodel/modeler.py +1 -0
- {pygeomodel-1.0.7 → pygeomodel-1.0.9}/pygeomodel/results.py +383 -43
- {pygeomodel-1.0.7 → pygeomodel-1.0.9}/setup.py +1 -2
- {pygeomodel-1.0.7 → pygeomodel-1.0.9}/tests/test_core_api.py +137 -0
- {pygeomodel-1.0.7 → pygeomodel-1.0.9}/LICENSE +0 -0
- {pygeomodel-1.0.7 → pygeomodel-1.0.9}/MANIFEST.in +0 -0
- {pygeomodel-1.0.7 → pygeomodel-1.0.9}/PyGeoModel.egg-info/SOURCES.txt +0 -0
- {pygeomodel-1.0.7 → pygeomodel-1.0.9}/PyGeoModel.egg-info/dependency_links.txt +0 -0
- {pygeomodel-1.0.7 → pygeomodel-1.0.9}/PyGeoModel.egg-info/requires.txt +0 -0
- {pygeomodel-1.0.7 → pygeomodel-1.0.9}/PyGeoModel.egg-info/top_level.txt +0 -0
- {pygeomodel-1.0.7 → pygeomodel-1.0.9}/ogmsServer2/__init__.py +0 -0
- {pygeomodel-1.0.7 → pygeomodel-1.0.9}/ogmsServer2/base.py +0 -0
- {pygeomodel-1.0.7 → pygeomodel-1.0.9}/ogmsServer2/constants.py +0 -0
- {pygeomodel-1.0.7 → pygeomodel-1.0.9}/ogmsServer2/openModel.py +0 -0
- {pygeomodel-1.0.7 → pygeomodel-1.0.9}/ogmsServer2/openUtils/__init__.py +0 -0
- {pygeomodel-1.0.7 → pygeomodel-1.0.9}/ogmsServer2/openUtils/exceptions.py +0 -0
- {pygeomodel-1.0.7 → pygeomodel-1.0.9}/ogmsServer2/openUtils/http_client.py +0 -0
- {pygeomodel-1.0.7 → pygeomodel-1.0.9}/ogmsServer2/openUtils/mdlUtils.py +0 -0
- {pygeomodel-1.0.7 → pygeomodel-1.0.9}/ogmsServer2/openUtils/parameterValidator.py +0 -0
- {pygeomodel-1.0.7 → pygeomodel-1.0.9}/ogmsServer2/openUtils/stateManager.py +0 -0
- {pygeomodel-1.0.7 → pygeomodel-1.0.9}/pygeomodel/client.py +0 -0
- {pygeomodel-1.0.7 → pygeomodel-1.0.9}/pygeomodel/config.py +0 -0
- {pygeomodel-1.0.7 → pygeomodel-1.0.9}/pygeomodel/consensus.py +0 -0
- {pygeomodel-1.0.7 → pygeomodel-1.0.9}/pygeomodel/context.py +0 -0
- {pygeomodel-1.0.7 → pygeomodel-1.0.9}/pygeomodel/data/__init__.py +0 -0
- {pygeomodel-1.0.7 → pygeomodel-1.0.9}/pygeomodel/data/computeModel.json +0 -0
- {pygeomodel-1.0.7 → pygeomodel-1.0.9}/pygeomodel/data/modelContext.txt +0 -0
- {pygeomodel-1.0.7 → pygeomodel-1.0.9}/pygeomodel/models.py +0 -0
- {pygeomodel-1.0.7 → pygeomodel-1.0.9}/pygeomodel/notebook.py +0 -0
- {pygeomodel-1.0.7 → pygeomodel-1.0.9}/pygeomodel/qa.py +0 -0
- {pygeomodel-1.0.7 → pygeomodel-1.0.9}/pygeomodel/recommendation.py +0 -0
- {pygeomodel-1.0.7 → pygeomodel-1.0.9}/scripts.py +0 -0
- {pygeomodel-1.0.7 → pygeomodel-1.0.9}/setup.cfg +0 -0
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyGeoModel
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.9
|
|
4
4
|
Summary: A Python package for integrating OpenGMS geographic model services.
|
|
5
5
|
Home-page: https://github.com/MpLebron/PyGeoModel
|
|
6
6
|
Author: Peilong Ma
|
|
7
7
|
Author-email: mpl_gis@nnu.edu.cn
|
|
8
8
|
Keywords: geographic modeling,GIS,OpenGMS,model services,geospatial analysis,jupyter
|
|
9
|
-
Classifier: Development Status :: 4 - Beta
|
|
10
9
|
Classifier: Intended Audience :: Science/Research
|
|
11
10
|
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
12
11
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
@@ -91,20 +90,6 @@ result = modeler.invoke(
|
|
|
91
90
|
result.to_json("execution_record.json")
|
|
92
91
|
```
|
|
93
92
|
|
|
94
|
-
OpenGMS execution uses the bundled public demo token by default. For stable or private use, configure your own OpenGMS token:
|
|
95
|
-
|
|
96
|
-
```bash
|
|
97
|
-
export OGMS_TOKEN="your-token"
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
Optional endpoint overrides:
|
|
101
|
-
|
|
102
|
-
```bash
|
|
103
|
-
export OGMS_BASE_PORTAL_URL="http://222.192.7.75"
|
|
104
|
-
export OGMS_BASE_MANAGER_URL="http://222.192.7.75/managerServer"
|
|
105
|
-
export OGMS_BASE_DATA_URL="http://222.192.7.75/dataTransferServer"
|
|
106
|
-
```
|
|
107
|
-
|
|
108
93
|
## Notebook Interface
|
|
109
94
|
|
|
110
95
|
```python
|
|
@@ -124,18 +109,8 @@ answer = modeler.ask_model("SWAT_Model", "What input data are required?")
|
|
|
124
109
|
answer.to_json("qa_record.json")
|
|
125
110
|
```
|
|
126
111
|
|
|
127
|
-
The recommendation service automatically builds notebook/data context and calls the configured
|
|
112
|
+
The recommendation service automatically builds notebook/data context and calls the configured recommendation workflow. Q&A uses OpenGMS model metadata and an OpenAI-compatible web-enabled model. The main notebook workflow is designed to run out of the box for demonstration use.
|
|
128
113
|
|
|
129
114
|
## Relation to OpenGMS
|
|
130
115
|
|
|
131
116
|
OpenGMS provides the model-service platform and online execution infrastructure. PyGeoModel is a Python client package that exposes OpenGMS model-service discovery, metadata inspection, task invocation, and result records to Python and notebook workflows.
|
|
132
|
-
|
|
133
|
-
## Development
|
|
134
|
-
|
|
135
|
-
Run the lightweight test suite with:
|
|
136
|
-
|
|
137
|
-
```bash
|
|
138
|
-
python -m unittest discover -s tests
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
The source distribution should not include real API keys, local `.env` files, bytecode caches, generated C files, `.pyd` binaries, or previous build artifacts.
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyGeoModel
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.9
|
|
4
4
|
Summary: A Python package for integrating OpenGMS geographic model services.
|
|
5
5
|
Home-page: https://github.com/MpLebron/PyGeoModel
|
|
6
6
|
Author: Peilong Ma
|
|
7
7
|
Author-email: mpl_gis@nnu.edu.cn
|
|
8
8
|
Keywords: geographic modeling,GIS,OpenGMS,model services,geospatial analysis,jupyter
|
|
9
|
-
Classifier: Development Status :: 4 - Beta
|
|
10
9
|
Classifier: Intended Audience :: Science/Research
|
|
11
10
|
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
12
11
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
@@ -91,20 +90,6 @@ result = modeler.invoke(
|
|
|
91
90
|
result.to_json("execution_record.json")
|
|
92
91
|
```
|
|
93
92
|
|
|
94
|
-
OpenGMS execution uses the bundled public demo token by default. For stable or private use, configure your own OpenGMS token:
|
|
95
|
-
|
|
96
|
-
```bash
|
|
97
|
-
export OGMS_TOKEN="your-token"
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
Optional endpoint overrides:
|
|
101
|
-
|
|
102
|
-
```bash
|
|
103
|
-
export OGMS_BASE_PORTAL_URL="http://222.192.7.75"
|
|
104
|
-
export OGMS_BASE_MANAGER_URL="http://222.192.7.75/managerServer"
|
|
105
|
-
export OGMS_BASE_DATA_URL="http://222.192.7.75/dataTransferServer"
|
|
106
|
-
```
|
|
107
|
-
|
|
108
93
|
## Notebook Interface
|
|
109
94
|
|
|
110
95
|
```python
|
|
@@ -124,18 +109,8 @@ answer = modeler.ask_model("SWAT_Model", "What input data are required?")
|
|
|
124
109
|
answer.to_json("qa_record.json")
|
|
125
110
|
```
|
|
126
111
|
|
|
127
|
-
The recommendation service automatically builds notebook/data context and calls the configured
|
|
112
|
+
The recommendation service automatically builds notebook/data context and calls the configured recommendation workflow. Q&A uses OpenGMS model metadata and an OpenAI-compatible web-enabled model. The main notebook workflow is designed to run out of the box for demonstration use.
|
|
128
113
|
|
|
129
114
|
## Relation to OpenGMS
|
|
130
115
|
|
|
131
116
|
OpenGMS provides the model-service platform and online execution infrastructure. PyGeoModel is a Python client package that exposes OpenGMS model-service discovery, metadata inspection, task invocation, and result records to Python and notebook workflows.
|
|
132
|
-
|
|
133
|
-
## Development
|
|
134
|
-
|
|
135
|
-
Run the lightweight test suite with:
|
|
136
|
-
|
|
137
|
-
```bash
|
|
138
|
-
python -m unittest discover -s tests
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
The source distribution should not include real API keys, local `.env` files, bytecode caches, generated C files, `.pyd` binaries, or previous build artifacts.
|
|
@@ -45,20 +45,6 @@ result = modeler.invoke(
|
|
|
45
45
|
result.to_json("execution_record.json")
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
OpenGMS execution uses the bundled public demo token by default. For stable or private use, configure your own OpenGMS token:
|
|
49
|
-
|
|
50
|
-
```bash
|
|
51
|
-
export OGMS_TOKEN="your-token"
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
Optional endpoint overrides:
|
|
55
|
-
|
|
56
|
-
```bash
|
|
57
|
-
export OGMS_BASE_PORTAL_URL="http://222.192.7.75"
|
|
58
|
-
export OGMS_BASE_MANAGER_URL="http://222.192.7.75/managerServer"
|
|
59
|
-
export OGMS_BASE_DATA_URL="http://222.192.7.75/dataTransferServer"
|
|
60
|
-
```
|
|
61
|
-
|
|
62
48
|
## Notebook Interface
|
|
63
49
|
|
|
64
50
|
```python
|
|
@@ -78,18 +64,8 @@ answer = modeler.ask_model("SWAT_Model", "What input data are required?")
|
|
|
78
64
|
answer.to_json("qa_record.json")
|
|
79
65
|
```
|
|
80
66
|
|
|
81
|
-
The recommendation service automatically builds notebook/data context and calls the configured
|
|
67
|
+
The recommendation service automatically builds notebook/data context and calls the configured recommendation workflow. Q&A uses OpenGMS model metadata and an OpenAI-compatible web-enabled model. The main notebook workflow is designed to run out of the box for demonstration use.
|
|
82
68
|
|
|
83
69
|
## Relation to OpenGMS
|
|
84
70
|
|
|
85
71
|
OpenGMS provides the model-service platform and online execution infrastructure. PyGeoModel is a Python client package that exposes OpenGMS model-service discovery, metadata inspection, task invocation, and result records to Python and notebook workflows.
|
|
86
|
-
|
|
87
|
-
## Development
|
|
88
|
-
|
|
89
|
-
Run the lightweight test suite with:
|
|
90
|
-
|
|
91
|
-
```bash
|
|
92
|
-
python -m unittest discover -s tests
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
The source distribution should not include real API keys, local `.env` files, bytecode caches, generated C files, `.pyd` binaries, or previous build artifacts.
|
|
@@ -90,6 +90,7 @@ class GeoModeler:
|
|
|
90
90
|
outputs=response.get("outputs", []),
|
|
91
91
|
execution_time=response.get("execution_time", time.time() - started),
|
|
92
92
|
endpoint=getattr(self.client, "manager_url", None),
|
|
93
|
+
record_path=str(record_path) if record_path else None,
|
|
93
94
|
)
|
|
94
95
|
if output_dir:
|
|
95
96
|
result.download(output_dir)
|
|
@@ -23,26 +23,47 @@ class TaskResult:
|
|
|
23
23
|
model_name: str
|
|
24
24
|
status: str = "unknown"
|
|
25
25
|
outputs: list[dict[str, Any]] = field(default_factory=list)
|
|
26
|
+
downloaded_outputs: list[str] = field(default_factory=list)
|
|
26
27
|
task_id: str | None = None
|
|
27
28
|
model_id: str | None = None
|
|
28
29
|
model_md5: str | None = None
|
|
29
30
|
params: dict[str, Any] = field(default_factory=dict)
|
|
30
31
|
uploaded_inputs: dict[str, Any] = field(default_factory=dict)
|
|
31
32
|
endpoint: str | None = None
|
|
32
|
-
pygeomodel_version: str = "1.0.
|
|
33
|
+
pygeomodel_version: str = "1.0.9"
|
|
33
34
|
execution_time: float | None = None
|
|
35
|
+
record_path: str | None = None
|
|
34
36
|
created_at: float = field(default_factory=time.time)
|
|
35
37
|
|
|
36
38
|
def to_dict(self) -> dict[str, Any]:
|
|
37
39
|
return asdict(self)
|
|
38
40
|
|
|
39
41
|
def to_json(self, path: str | Path) -> str:
|
|
42
|
+
self.record_path = str(path)
|
|
40
43
|
return _write_json(self.to_dict(), path)
|
|
41
44
|
|
|
42
45
|
def download(self, output_dir: str | Path) -> list[str]:
|
|
43
46
|
from .client import download_output_files
|
|
44
47
|
|
|
45
|
-
|
|
48
|
+
self.downloaded_outputs = download_output_files(self.outputs, output_dir)
|
|
49
|
+
return self.downloaded_outputs
|
|
50
|
+
|
|
51
|
+
def save(
|
|
52
|
+
self,
|
|
53
|
+
output_dir: str | Path | None = None,
|
|
54
|
+
record_path: str | Path | None = None,
|
|
55
|
+
) -> "TaskResult":
|
|
56
|
+
if output_dir is not None:
|
|
57
|
+
self.download(output_dir)
|
|
58
|
+
if record_path is not None:
|
|
59
|
+
self.to_json(record_path)
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
def _repr_markdown_(self) -> str:
|
|
63
|
+
return f"Model run: {self.model_name}\n\nStatus: {self.status}\n\nTask ID: {self.task_id or 'Not returned'}"
|
|
64
|
+
|
|
65
|
+
def _repr_html_(self) -> str:
|
|
66
|
+
return _task_result_to_html(self)
|
|
46
67
|
|
|
47
68
|
|
|
48
69
|
@dataclass
|
|
@@ -207,13 +228,287 @@ class QAResult:
|
|
|
207
228
|
"""
|
|
208
229
|
|
|
209
230
|
|
|
231
|
+
def _task_result_to_html(result: TaskResult) -> str:
|
|
232
|
+
status = html.escape(str(result.status or "unknown"))
|
|
233
|
+
status_class = "success" if status.lower() == "completed" else "neutral"
|
|
234
|
+
task_id = html.escape(str(result.task_id or "Not returned"))
|
|
235
|
+
runtime = _format_seconds(result.execution_time)
|
|
236
|
+
record_path = html.escape(str(result.record_path or "Not saved"))
|
|
237
|
+
endpoint = html.escape(str(result.endpoint or "Not recorded"))
|
|
238
|
+
output_count = len(result.outputs or [])
|
|
239
|
+
downloaded_count = len(result.downloaded_outputs or [])
|
|
240
|
+
outputs_html = _task_outputs_to_html(result.outputs or [])
|
|
241
|
+
downloaded_html = _downloaded_outputs_to_html(result.downloaded_outputs or [])
|
|
242
|
+
params_json = html.escape(json.dumps(result.params or {}, ensure_ascii=False, indent=2), quote=False)
|
|
243
|
+
uploaded_json = html.escape(json.dumps(result.uploaded_inputs or {}, ensure_ascii=False, indent=2), quote=False)
|
|
244
|
+
|
|
245
|
+
return f"""
|
|
246
|
+
<style>
|
|
247
|
+
.pygeomodel-task-card {{
|
|
248
|
+
font-family: PingFang SC, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
249
|
+
color: #1e293b;
|
|
250
|
+
border: 1px solid #dbe3ef;
|
|
251
|
+
border-radius: 8px;
|
|
252
|
+
background: #ffffff;
|
|
253
|
+
overflow: hidden;
|
|
254
|
+
max-width: 100%;
|
|
255
|
+
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
|
256
|
+
}}
|
|
257
|
+
.pygeomodel-task-card * {{ box-sizing: border-box; }}
|
|
258
|
+
.pygeomodel-task-head {{
|
|
259
|
+
display: flex;
|
|
260
|
+
align-items: flex-start;
|
|
261
|
+
gap: 14px;
|
|
262
|
+
padding: 16px 18px;
|
|
263
|
+
border-bottom: 1px solid #e5edf6;
|
|
264
|
+
background: #fbfdff;
|
|
265
|
+
}}
|
|
266
|
+
.pygeomodel-task-status {{
|
|
267
|
+
flex: 0 0 auto;
|
|
268
|
+
min-width: 88px;
|
|
269
|
+
text-align: center;
|
|
270
|
+
border-radius: 999px;
|
|
271
|
+
padding: 4px 10px;
|
|
272
|
+
font-size: 12px;
|
|
273
|
+
line-height: 1.5;
|
|
274
|
+
font-weight: 750;
|
|
275
|
+
border: 1px solid #cbd5e1;
|
|
276
|
+
color: #475569;
|
|
277
|
+
background: #f8fafc;
|
|
278
|
+
}}
|
|
279
|
+
.pygeomodel-task-status.success {{
|
|
280
|
+
border-color: #bbf7d0;
|
|
281
|
+
color: #15803d;
|
|
282
|
+
background: #f0fdf4;
|
|
283
|
+
}}
|
|
284
|
+
.pygeomodel-task-title {{
|
|
285
|
+
min-width: 0;
|
|
286
|
+
}}
|
|
287
|
+
.pygeomodel-task-title h4 {{
|
|
288
|
+
margin: 0;
|
|
289
|
+
color: #0f172a;
|
|
290
|
+
font-size: 15px;
|
|
291
|
+
line-height: 1.4;
|
|
292
|
+
font-weight: 750;
|
|
293
|
+
}}
|
|
294
|
+
.pygeomodel-task-title p {{
|
|
295
|
+
margin: 4px 0 0 0;
|
|
296
|
+
color: #64748b;
|
|
297
|
+
font-size: 12px;
|
|
298
|
+
line-height: 1.5;
|
|
299
|
+
}}
|
|
300
|
+
.pygeomodel-task-body {{
|
|
301
|
+
padding: 14px 18px 16px 18px;
|
|
302
|
+
}}
|
|
303
|
+
.pygeomodel-task-meta {{
|
|
304
|
+
display: grid;
|
|
305
|
+
grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
|
|
306
|
+
gap: 10px;
|
|
307
|
+
margin-bottom: 16px;
|
|
308
|
+
}}
|
|
309
|
+
.pygeomodel-task-meta div {{
|
|
310
|
+
border-top: 1px solid #e5edf6;
|
|
311
|
+
padding-top: 8px;
|
|
312
|
+
}}
|
|
313
|
+
.pygeomodel-task-meta b {{
|
|
314
|
+
display: block;
|
|
315
|
+
color: #475569;
|
|
316
|
+
font-size: 11px;
|
|
317
|
+
line-height: 1.4;
|
|
318
|
+
font-weight: 750;
|
|
319
|
+
text-transform: uppercase;
|
|
320
|
+
letter-spacing: 0.02em;
|
|
321
|
+
}}
|
|
322
|
+
.pygeomodel-task-meta span {{
|
|
323
|
+
display: block;
|
|
324
|
+
margin-top: 3px;
|
|
325
|
+
color: #0f172a;
|
|
326
|
+
font-size: 12px;
|
|
327
|
+
line-height: 1.45;
|
|
328
|
+
overflow-wrap: anywhere;
|
|
329
|
+
}}
|
|
330
|
+
.pygeomodel-task-section {{
|
|
331
|
+
margin-top: 14px;
|
|
332
|
+
}}
|
|
333
|
+
.pygeomodel-task-section h4 {{
|
|
334
|
+
margin: 0 0 8px 0;
|
|
335
|
+
color: #0f172a;
|
|
336
|
+
font-size: 14px;
|
|
337
|
+
line-height: 1.4;
|
|
338
|
+
font-weight: 750;
|
|
339
|
+
}}
|
|
340
|
+
.pygeomodel-task-table {{
|
|
341
|
+
display: grid;
|
|
342
|
+
grid-template-columns: minmax(180px, 1.1fr) minmax(140px, 0.8fr) minmax(100px, 0.55fr) minmax(150px, 0.8fr);
|
|
343
|
+
border: 1px solid #dbe3ef;
|
|
344
|
+
border-radius: 8px;
|
|
345
|
+
overflow: hidden;
|
|
346
|
+
font-size: 12px;
|
|
347
|
+
line-height: 1.45;
|
|
348
|
+
}}
|
|
349
|
+
.pygeomodel-task-table > div {{
|
|
350
|
+
padding: 9px 11px;
|
|
351
|
+
border-bottom: 1px solid #e8eef6;
|
|
352
|
+
overflow-wrap: anywhere;
|
|
353
|
+
}}
|
|
354
|
+
.pygeomodel-task-table .head {{
|
|
355
|
+
background: #f8fafc;
|
|
356
|
+
color: #475569;
|
|
357
|
+
font-weight: 750;
|
|
358
|
+
text-transform: uppercase;
|
|
359
|
+
letter-spacing: 0.02em;
|
|
360
|
+
font-size: 11px;
|
|
361
|
+
}}
|
|
362
|
+
.pygeomodel-task-table .muted {{
|
|
363
|
+
color: #94a3b8;
|
|
364
|
+
}}
|
|
365
|
+
.pygeomodel-task-table a {{
|
|
366
|
+
color: #1d4ed8;
|
|
367
|
+
text-decoration: none;
|
|
368
|
+
}}
|
|
369
|
+
.pygeomodel-task-table a:hover {{ text-decoration: underline; }}
|
|
370
|
+
.pygeomodel-task-details {{
|
|
371
|
+
margin-top: 14px;
|
|
372
|
+
border-top: 1px solid #e5edf6;
|
|
373
|
+
padding-top: 10px;
|
|
374
|
+
color: #334155;
|
|
375
|
+
font-size: 12px;
|
|
376
|
+
}}
|
|
377
|
+
.pygeomodel-task-details summary {{
|
|
378
|
+
cursor: pointer;
|
|
379
|
+
font-weight: 700;
|
|
380
|
+
}}
|
|
381
|
+
.pygeomodel-task-details pre {{
|
|
382
|
+
margin: 10px 0 0 0;
|
|
383
|
+
padding: 10px;
|
|
384
|
+
border-radius: 6px;
|
|
385
|
+
background: #f8fafc;
|
|
386
|
+
border: 1px solid #e5edf6;
|
|
387
|
+
color: #0f172a;
|
|
388
|
+
font-size: 11px;
|
|
389
|
+
line-height: 1.45;
|
|
390
|
+
overflow-x: auto;
|
|
391
|
+
white-space: pre-wrap;
|
|
392
|
+
}}
|
|
393
|
+
@media (max-width: 840px) {{
|
|
394
|
+
.pygeomodel-task-head {{
|
|
395
|
+
display: block;
|
|
396
|
+
}}
|
|
397
|
+
.pygeomodel-task-status {{
|
|
398
|
+
display: inline-block;
|
|
399
|
+
margin-bottom: 10px;
|
|
400
|
+
}}
|
|
401
|
+
.pygeomodel-task-table {{
|
|
402
|
+
grid-template-columns: minmax(120px, 1fr) minmax(120px, 1fr);
|
|
403
|
+
}}
|
|
404
|
+
}}
|
|
405
|
+
</style>
|
|
406
|
+
<div class="pygeomodel-task-card">
|
|
407
|
+
<div class="pygeomodel-task-head">
|
|
408
|
+
<span class="pygeomodel-task-status {status_class}">{status}</span>
|
|
409
|
+
<div class="pygeomodel-task-title">
|
|
410
|
+
<h4>Model Execution Summary</h4>
|
|
411
|
+
<p>{html.escape(result.model_name)}</p>
|
|
412
|
+
</div>
|
|
413
|
+
</div>
|
|
414
|
+
<div class="pygeomodel-task-body">
|
|
415
|
+
<div class="pygeomodel-task-meta">
|
|
416
|
+
<div><b>Task ID</b><span>{task_id}</span></div>
|
|
417
|
+
<div><b>Runtime</b><span>{html.escape(runtime)}</span></div>
|
|
418
|
+
<div><b>Output resources</b><span>{output_count}</span></div>
|
|
419
|
+
<div><b>Saved output files</b><span>{downloaded_count}</span></div>
|
|
420
|
+
<div><b>Execution record</b><span>{record_path}</span></div>
|
|
421
|
+
<div><b>Endpoint</b><span>{endpoint}</span></div>
|
|
422
|
+
<div><b>PyGeoModel</b><span>{html.escape(str(result.pygeomodel_version))}</span></div>
|
|
423
|
+
</div>
|
|
424
|
+
<div class="pygeomodel-task-section">
|
|
425
|
+
<h4>Output resources</h4>
|
|
426
|
+
{outputs_html}
|
|
427
|
+
</div>
|
|
428
|
+
<div class="pygeomodel-task-section">
|
|
429
|
+
<h4>Saved output files</h4>
|
|
430
|
+
{downloaded_html}
|
|
431
|
+
</div>
|
|
432
|
+
<details class="pygeomodel-task-details">
|
|
433
|
+
<summary>Input parameters</summary>
|
|
434
|
+
<pre>{params_json}</pre>
|
|
435
|
+
</details>
|
|
436
|
+
<details class="pygeomodel-task-details">
|
|
437
|
+
<summary>Uploaded input structure</summary>
|
|
438
|
+
<pre>{uploaded_json}</pre>
|
|
439
|
+
</details>
|
|
440
|
+
</div>
|
|
441
|
+
</div>
|
|
442
|
+
"""
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _task_outputs_to_html(outputs: list[dict[str, Any]]) -> str:
|
|
446
|
+
if not outputs:
|
|
447
|
+
return '<div class="pygeomodel-task-table"><div class="muted">No output resources were returned.</div></div>'
|
|
448
|
+
|
|
449
|
+
rows = [
|
|
450
|
+
'<div class="head">Name</div>',
|
|
451
|
+
'<div class="head">State</div>',
|
|
452
|
+
'<div class="head">Format</div>',
|
|
453
|
+
'<div class="head">Access</div>',
|
|
454
|
+
]
|
|
455
|
+
for output in outputs:
|
|
456
|
+
name = html.escape(str(output.get("tag") or output.get("event") or "Output"))
|
|
457
|
+
state = html.escape(str(output.get("statename") or ""))
|
|
458
|
+
suffix = html.escape(str(output.get("suffix") or "Not specified"))
|
|
459
|
+
url = str(output.get("url") or "")
|
|
460
|
+
if url:
|
|
461
|
+
safe_url = html.escape(url)
|
|
462
|
+
access = f'<a href="{safe_url}" target="_blank" rel="noopener noreferrer">Open output</a>'
|
|
463
|
+
else:
|
|
464
|
+
access = '<span class="muted">No downloadable URL returned</span>'
|
|
465
|
+
rows.extend([f"<div>{name}</div>", f"<div>{state}</div>", f"<div>{suffix}</div>", f"<div>{access}</div>"])
|
|
466
|
+
return f'<div class="pygeomodel-task-table">{"".join(rows)}</div>'
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _downloaded_outputs_to_html(paths: list[str]) -> str:
|
|
470
|
+
if not paths:
|
|
471
|
+
return (
|
|
472
|
+
'<div class="pygeomodel-task-table">'
|
|
473
|
+
'<div class="muted">No output files were downloaded. This can happen when the OpenGMS service returns output metadata without a downloadable URL.</div>'
|
|
474
|
+
'</div>'
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
rows = [
|
|
478
|
+
'<div class="head">File</div>',
|
|
479
|
+
'<div class="head">Location</div>',
|
|
480
|
+
'<div class="head">Status</div>',
|
|
481
|
+
'<div class="head">Access</div>',
|
|
482
|
+
]
|
|
483
|
+
for path in paths:
|
|
484
|
+
safe_path = html.escape(str(path))
|
|
485
|
+
name = html.escape(Path(path).name)
|
|
486
|
+
rows.extend([
|
|
487
|
+
f"<div>{name}</div>",
|
|
488
|
+
f"<div>{safe_path}</div>",
|
|
489
|
+
"<div>Saved</div>",
|
|
490
|
+
"<div>Local file</div>",
|
|
491
|
+
])
|
|
492
|
+
return f'<div class="pygeomodel-task-table">{"".join(rows)}</div>'
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _format_seconds(value: float | None) -> str:
|
|
496
|
+
if value is None:
|
|
497
|
+
return "Not recorded"
|
|
498
|
+
if value < 60:
|
|
499
|
+
return f"{value:.2f} s"
|
|
500
|
+
minutes = int(value // 60)
|
|
501
|
+
seconds = value - minutes * 60
|
|
502
|
+
return f"{minutes} min {seconds:.1f} s"
|
|
503
|
+
|
|
504
|
+
|
|
210
505
|
def _recommendation_to_html(result: RecommendationResult) -> str:
|
|
211
506
|
candidates = result.candidates or []
|
|
212
507
|
recommended_data = result.recommended_data or {}
|
|
213
508
|
|
|
214
|
-
|
|
215
|
-
if not
|
|
216
|
-
|
|
509
|
+
candidate_rows = "".join(_recommendation_candidate_row(candidate) for candidate in candidates)
|
|
510
|
+
if not candidate_rows:
|
|
511
|
+
candidate_rows = '<div class="pygeomodel-rec-empty">No candidate models were returned.</div>'
|
|
217
512
|
|
|
218
513
|
local_data_html = _recommendation_data_items(recommended_data.get("local_data", []), label_key="location")
|
|
219
514
|
kb_data_html = _recommendation_data_items(recommended_data.get("knowledge_base_data", []), label_key="url")
|
|
@@ -247,36 +542,50 @@ def _recommendation_to_html(result: RecommendationResult) -> str:
|
|
|
247
542
|
line-height: 1.4;
|
|
248
543
|
font-weight: 750;
|
|
249
544
|
}}
|
|
250
|
-
.pygeomodel-rec-
|
|
251
|
-
display: grid;
|
|
252
|
-
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
|
253
|
-
gap: 10px;
|
|
254
|
-
}}
|
|
255
|
-
.pygeomodel-rec-candidate {{
|
|
545
|
+
.pygeomodel-rec-list {{
|
|
256
546
|
border: 1px solid #dbe3ef;
|
|
257
547
|
border-radius: 8px;
|
|
258
|
-
|
|
548
|
+
overflow: hidden;
|
|
259
549
|
background: #ffffff;
|
|
260
550
|
}}
|
|
261
|
-
.pygeomodel-rec-
|
|
262
|
-
|
|
263
|
-
|
|
551
|
+
.pygeomodel-rec-list-head,
|
|
552
|
+
.pygeomodel-rec-row {{
|
|
553
|
+
display: grid;
|
|
554
|
+
grid-template-columns: 90px minmax(220px, 0.95fr) minmax(280px, 1.25fr) minmax(220px, 1fr);
|
|
555
|
+
column-gap: 18px;
|
|
556
|
+
align-items: start;
|
|
264
557
|
}}
|
|
265
|
-
.pygeomodel-rec-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
558
|
+
.pygeomodel-rec-list-head {{
|
|
559
|
+
padding: 9px 14px;
|
|
560
|
+
background: #f8fafc;
|
|
561
|
+
border-bottom: 1px solid #dbe3ef;
|
|
562
|
+
color: #475569;
|
|
563
|
+
font-size: 11px;
|
|
564
|
+
font-weight: 750;
|
|
565
|
+
letter-spacing: 0.02em;
|
|
566
|
+
text-transform: uppercase;
|
|
271
567
|
}}
|
|
272
|
-
.pygeomodel-rec-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
568
|
+
.pygeomodel-rec-row {{
|
|
569
|
+
padding: 13px 14px;
|
|
570
|
+
border-bottom: 1px solid #e8eef6;
|
|
571
|
+
}}
|
|
572
|
+
.pygeomodel-rec-row:last-child {{
|
|
573
|
+
border-bottom: 0;
|
|
574
|
+
}}
|
|
575
|
+
.pygeomodel-rec-row.primary {{
|
|
576
|
+
background: #f8fbff;
|
|
577
|
+
box-shadow: inset 3px 0 0 #60a5fa;
|
|
578
|
+
}}
|
|
579
|
+
.pygeomodel-rec-rank {{
|
|
580
|
+
color: #64748b;
|
|
581
|
+
font-size: 12px;
|
|
276
582
|
font-weight: 750;
|
|
583
|
+
line-height: 1.4;
|
|
584
|
+
white-space: nowrap;
|
|
277
585
|
}}
|
|
278
586
|
.pygeomodel-rec-badge {{
|
|
279
|
-
|
|
587
|
+
display: inline-block;
|
|
588
|
+
margin-top: 6px;
|
|
280
589
|
border-radius: 999px;
|
|
281
590
|
border: 1px solid #bfdbfe;
|
|
282
591
|
background: #dbeafe;
|
|
@@ -285,18 +594,28 @@ def _recommendation_to_html(result: RecommendationResult) -> str:
|
|
|
285
594
|
font-size: 11px;
|
|
286
595
|
font-weight: 700;
|
|
287
596
|
line-height: 1.4;
|
|
597
|
+
text-transform: none;
|
|
598
|
+
letter-spacing: 0;
|
|
288
599
|
}}
|
|
289
|
-
.pygeomodel-rec-
|
|
290
|
-
color: #
|
|
291
|
-
font-size:
|
|
292
|
-
|
|
600
|
+
.pygeomodel-rec-name {{
|
|
601
|
+
color: #0f172a;
|
|
602
|
+
font-size: 13px;
|
|
603
|
+
line-height: 1.35;
|
|
604
|
+
font-weight: 750;
|
|
293
605
|
}}
|
|
294
|
-
.pygeomodel-rec-
|
|
295
|
-
|
|
606
|
+
.pygeomodel-rec-description,
|
|
607
|
+
.pygeomodel-rec-text {{
|
|
608
|
+
margin: 0;
|
|
296
609
|
color: #475569;
|
|
297
610
|
font-size: 12px;
|
|
298
611
|
line-height: 1.5;
|
|
299
612
|
}}
|
|
613
|
+
.pygeomodel-rec-description {{
|
|
614
|
+
margin-top: 5px;
|
|
615
|
+
}}
|
|
616
|
+
.pygeomodel-rec-muted {{
|
|
617
|
+
color: #94a3b8;
|
|
618
|
+
}}
|
|
300
619
|
.pygeomodel-rec-data-grid {{
|
|
301
620
|
display: grid;
|
|
302
621
|
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
|
@@ -332,11 +651,32 @@ def _recommendation_to_html(result: RecommendationResult) -> str:
|
|
|
332
651
|
padding: 12px;
|
|
333
652
|
background: #f8fafc;
|
|
334
653
|
}}
|
|
654
|
+
@media (max-width: 900px) {{
|
|
655
|
+
.pygeomodel-rec-list-head {{
|
|
656
|
+
display: none;
|
|
657
|
+
}}
|
|
658
|
+
.pygeomodel-rec-row {{
|
|
659
|
+
grid-template-columns: 76px minmax(0, 1fr);
|
|
660
|
+
row-gap: 8px;
|
|
661
|
+
}}
|
|
662
|
+
.pygeomodel-rec-row > div:nth-child(3),
|
|
663
|
+
.pygeomodel-rec-row > div:nth-child(4) {{
|
|
664
|
+
grid-column: 2;
|
|
665
|
+
}}
|
|
666
|
+
}}
|
|
335
667
|
</style>
|
|
336
668
|
<div class="pygeomodel-rec-card">
|
|
337
669
|
<div class="pygeomodel-rec-body">
|
|
338
670
|
<div class="pygeomodel-rec-section">
|
|
339
|
-
<div class="pygeomodel-rec-
|
|
671
|
+
<div class="pygeomodel-rec-list">
|
|
672
|
+
<div class="pygeomodel-rec-list-head">
|
|
673
|
+
<div>Rank</div>
|
|
674
|
+
<div>Candidate model</div>
|
|
675
|
+
<div>Recommendation rationale</div>
|
|
676
|
+
<div>Input data</div>
|
|
677
|
+
</div>
|
|
678
|
+
{candidate_rows}
|
|
679
|
+
</div>
|
|
340
680
|
</div>
|
|
341
681
|
<div class="pygeomodel-rec-section">
|
|
342
682
|
<h4>Relevant Data</h4>
|
|
@@ -356,7 +696,7 @@ def _recommendation_to_html(result: RecommendationResult) -> str:
|
|
|
356
696
|
"""
|
|
357
697
|
|
|
358
698
|
|
|
359
|
-
def
|
|
699
|
+
def _recommendation_candidate_row(candidate: dict[str, Any]) -> str:
|
|
360
700
|
name = html.escape(str(candidate.get("name") or "Unnamed model"))
|
|
361
701
|
description = html.escape(str(candidate.get("description") or candidate.get("desc") or ""))
|
|
362
702
|
reason = html.escape(str(candidate.get("reason") or candidate.get("recommendation_reason") or ""))
|
|
@@ -367,17 +707,17 @@ def _recommendation_candidate_card(candidate: dict[str, Any]) -> str:
|
|
|
367
707
|
badge = '<span class="pygeomodel-rec-badge">Recommended</span>' if is_primary else ""
|
|
368
708
|
|
|
369
709
|
return f"""
|
|
370
|
-
<div class="pygeomodel-rec-
|
|
371
|
-
<div class="pygeomodel-rec-
|
|
372
|
-
|
|
373
|
-
<div class="pygeomodel-rec-rank">Rank {rank}</div>
|
|
374
|
-
<div class="pygeomodel-rec-candidate-name">{name}</div>
|
|
375
|
-
</div>
|
|
710
|
+
<div class="pygeomodel-rec-row{primary_class}">
|
|
711
|
+
<div class="pygeomodel-rec-rank">
|
|
712
|
+
Rank {rank}
|
|
376
713
|
{badge}
|
|
377
714
|
</div>
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
715
|
+
<div>
|
|
716
|
+
<div class="pygeomodel-rec-name">{name}</div>
|
|
717
|
+
{f'<p class="pygeomodel-rec-description">{description}</p>' if description else ''}
|
|
718
|
+
</div>
|
|
719
|
+
<p class="pygeomodel-rec-text">{reason if reason else '<span class="pygeomodel-rec-muted">No recommendation rationale recorded.</span>'}</p>
|
|
720
|
+
<p class="pygeomodel-rec-text">{input_desc if input_desc else '<span class="pygeomodel-rec-muted">No input-data description recorded.</span>'}</p>
|
|
381
721
|
</div>
|
|
382
722
|
"""
|
|
383
723
|
|
|
@@ -11,7 +11,7 @@ def read_readme():
|
|
|
11
11
|
|
|
12
12
|
setup(
|
|
13
13
|
name="PyGeoModel",
|
|
14
|
-
version="1.0.
|
|
14
|
+
version="1.0.9",
|
|
15
15
|
author="Peilong Ma",
|
|
16
16
|
author_email="mpl_gis@nnu.edu.cn",
|
|
17
17
|
description="A Python package for integrating OpenGMS geographic model services.",
|
|
@@ -41,7 +41,6 @@ setup(
|
|
|
41
41
|
},
|
|
42
42
|
python_requires=">=3.8",
|
|
43
43
|
classifiers=[
|
|
44
|
-
"Development Status :: 4 - Beta",
|
|
45
44
|
"Intended Audience :: Science/Research",
|
|
46
45
|
"Topic :: Scientific/Engineering :: GIS",
|
|
47
46
|
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
@@ -97,6 +97,36 @@ class CoreApiTests(unittest.TestCase):
|
|
|
97
97
|
self.assertEqual(normalized["SpatialAnalysis"]["end_time"], 201812)
|
|
98
98
|
self.assertTrue(normalized["SolarCalculation"]["roof_vector_path"].endswith("rooftops.zip"))
|
|
99
99
|
|
|
100
|
+
def test_model_invocation_records_downloaded_output_paths(self):
|
|
101
|
+
import pygeomodel.client as client_module
|
|
102
|
+
from pygeomodel import GeoModeler
|
|
103
|
+
|
|
104
|
+
original_downloader = client_module.download_output_files
|
|
105
|
+
try:
|
|
106
|
+
client_module.download_output_files = lambda outputs, output_dir: [
|
|
107
|
+
str(Path(output_dir) / "SolarCalculation-roofSloar.zip")
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
111
|
+
modeler = GeoModeler(client=FakeClient())
|
|
112
|
+
result = modeler.invoke(
|
|
113
|
+
"Roof Photovoltaic Carbon Emission Reduction Potential Assessment Model",
|
|
114
|
+
params={
|
|
115
|
+
"system_efficiency": 0.8,
|
|
116
|
+
"start_time": 201801,
|
|
117
|
+
"end_time": 201812,
|
|
118
|
+
"roof_vector_path": str(Path(tmpdir) / "rooftops.zip"),
|
|
119
|
+
},
|
|
120
|
+
output_dir=Path(tmpdir) / "outputs",
|
|
121
|
+
)
|
|
122
|
+
finally:
|
|
123
|
+
client_module.download_output_files = original_downloader
|
|
124
|
+
|
|
125
|
+
self.assertEqual(
|
|
126
|
+
result.downloaded_outputs,
|
|
127
|
+
[str(Path(tmpdir) / "outputs" / "SolarCalculation-roofSloar.zip")],
|
|
128
|
+
)
|
|
129
|
+
|
|
100
130
|
def test_model_invocation_preserves_multi_child_internal_params(self):
|
|
101
131
|
from pygeomodel import GeoModeler
|
|
102
132
|
|
|
@@ -298,6 +328,113 @@ class CoreApiTests(unittest.TestCase):
|
|
|
298
328
|
qa_path = qa.to_json(tmpdir / "qa.json")
|
|
299
329
|
self.assertEqual(json.loads(Path(qa_path).read_text())["answer"], "a")
|
|
300
330
|
|
|
331
|
+
def test_task_result_save_records_metadata_and_outputs(self):
|
|
332
|
+
import pygeomodel.client as client_module
|
|
333
|
+
from pygeomodel import TaskResult
|
|
334
|
+
|
|
335
|
+
original_downloader = client_module.download_output_files
|
|
336
|
+
try:
|
|
337
|
+
client_module.download_output_files = lambda outputs, output_dir: [
|
|
338
|
+
str(Path(output_dir) / "SolarCalculation-roofSloar.zip")
|
|
339
|
+
]
|
|
340
|
+
|
|
341
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
342
|
+
tmpdir = Path(tmpdir)
|
|
343
|
+
result = TaskResult(
|
|
344
|
+
model_name="m",
|
|
345
|
+
status="completed",
|
|
346
|
+
task_id="task-123",
|
|
347
|
+
outputs=[{"url": "http://example.test/result.zip", "tag": "roofSloar", "suffix": "zip"}],
|
|
348
|
+
params={"system_efficiency": 0.8},
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
returned = result.save(
|
|
352
|
+
output_dir=tmpdir / "outputs",
|
|
353
|
+
record_path=tmpdir / "records" / "execution_record.json",
|
|
354
|
+
)
|
|
355
|
+
record = json.loads((tmpdir / "records" / "execution_record.json").read_text())
|
|
356
|
+
finally:
|
|
357
|
+
client_module.download_output_files = original_downloader
|
|
358
|
+
|
|
359
|
+
self.assertIs(returned, result)
|
|
360
|
+
self.assertEqual(
|
|
361
|
+
result.downloaded_outputs,
|
|
362
|
+
[str(tmpdir / "outputs" / "SolarCalculation-roofSloar.zip")],
|
|
363
|
+
)
|
|
364
|
+
self.assertEqual(record["task_id"], "task-123")
|
|
365
|
+
self.assertEqual(record["record_path"], str(tmpdir / "records" / "execution_record.json"))
|
|
366
|
+
self.assertEqual(record["downloaded_outputs"], result.downloaded_outputs)
|
|
367
|
+
|
|
368
|
+
def test_task_result_save_only_outputs_when_no_record_path_is_given(self):
|
|
369
|
+
import pygeomodel.client as client_module
|
|
370
|
+
from pygeomodel import TaskResult
|
|
371
|
+
|
|
372
|
+
original_downloader = client_module.download_output_files
|
|
373
|
+
try:
|
|
374
|
+
client_module.download_output_files = lambda outputs, output_dir: [
|
|
375
|
+
str(Path(output_dir) / "SolarCalculation-roofSloar.zip")
|
|
376
|
+
]
|
|
377
|
+
|
|
378
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
379
|
+
tmpdir = Path(tmpdir)
|
|
380
|
+
result = TaskResult(
|
|
381
|
+
model_name="m",
|
|
382
|
+
status="completed",
|
|
383
|
+
task_id="task-123",
|
|
384
|
+
outputs=[{"url": "http://example.test/result.zip", "tag": "roofSloar", "suffix": "zip"}],
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
returned = result.save(output_dir=tmpdir / "outputs")
|
|
388
|
+
implicit_record = tmpdir / "outputs" / "execution_record.json"
|
|
389
|
+
finally:
|
|
390
|
+
client_module.download_output_files = original_downloader
|
|
391
|
+
|
|
392
|
+
self.assertIs(returned, result)
|
|
393
|
+
self.assertEqual(
|
|
394
|
+
result.downloaded_outputs,
|
|
395
|
+
[str(tmpdir / "outputs" / "SolarCalculation-roofSloar.zip")],
|
|
396
|
+
)
|
|
397
|
+
self.assertIsNone(result.record_path)
|
|
398
|
+
self.assertFalse(implicit_record.exists())
|
|
399
|
+
|
|
400
|
+
def test_task_result_has_notebook_rich_display(self):
|
|
401
|
+
from pygeomodel import TaskResult
|
|
402
|
+
|
|
403
|
+
result = TaskResult(
|
|
404
|
+
model_name="Roof Photovoltaic Carbon Emission Reduction Potential Assessment Model",
|
|
405
|
+
status="completed",
|
|
406
|
+
task_id="task-123",
|
|
407
|
+
outputs=[
|
|
408
|
+
{
|
|
409
|
+
"statename": "SolarCalculation",
|
|
410
|
+
"event": "roofSloar",
|
|
411
|
+
"tag": "SolarCalculation-roofSloar",
|
|
412
|
+
"suffix": "",
|
|
413
|
+
"url": "",
|
|
414
|
+
}
|
|
415
|
+
],
|
|
416
|
+
params={
|
|
417
|
+
"system_efficiency": 0.8,
|
|
418
|
+
"start_time": 201801,
|
|
419
|
+
"end_time": 201812,
|
|
420
|
+
"roof_vector_path": "data/xuanwu_rooftop.zip",
|
|
421
|
+
},
|
|
422
|
+
record_path="records/execution_record.json",
|
|
423
|
+
execution_time=103.39,
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
html = result._repr_html_()
|
|
427
|
+
|
|
428
|
+
self.assertIn("Model Execution Summary", html)
|
|
429
|
+
self.assertIn("completed", html)
|
|
430
|
+
self.assertIn("task-123", html)
|
|
431
|
+
self.assertIn("records/execution_record.json", html)
|
|
432
|
+
self.assertIn("Input parameters", html)
|
|
433
|
+
self.assertIn("roof_vector_path", html)
|
|
434
|
+
self.assertIn("Output resources", html)
|
|
435
|
+
self.assertIn("No downloadable URL returned", html)
|
|
436
|
+
self.assertNotIn("TaskResult(", html)
|
|
437
|
+
|
|
301
438
|
def test_opengms_config_uses_public_demo_token_as_fallback(self):
|
|
302
439
|
import pygeomodel.config as config_module
|
|
303
440
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|