prisma-api 0.3.8__tar.gz → 0.3.10__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.
- {prisma_api-0.3.8 → prisma_api-0.3.10}/PKG-INFO +1 -1
- {prisma_api-0.3.8 → prisma_api-0.3.10}/prisma_api/__init__.py +1 -1
- {prisma_api-0.3.8 → prisma_api-0.3.10}/prisma_api/prisma_api_v2.py +85 -13
- {prisma_api-0.3.8 → prisma_api-0.3.10}/prisma_api.egg-info/PKG-INFO +1 -1
- {prisma_api-0.3.8 → prisma_api-0.3.10}/pyproject.toml +1 -1
- {prisma_api-0.3.8 → prisma_api-0.3.10}/tests/test_prisma_api_v2.py +77 -0
- {prisma_api-0.3.8 → prisma_api-0.3.10}/LICENSE +0 -0
- {prisma_api-0.3.8 → prisma_api-0.3.10}/README.md +0 -0
- {prisma_api-0.3.8 → prisma_api-0.3.10}/prisma_api/config.py +0 -0
- {prisma_api-0.3.8 → prisma_api-0.3.10}/prisma_api/prisma_api.py +0 -0
- {prisma_api-0.3.8 → prisma_api-0.3.10}/prisma_api.egg-info/SOURCES.txt +0 -0
- {prisma_api-0.3.8 → prisma_api-0.3.10}/prisma_api.egg-info/dependency_links.txt +0 -0
- {prisma_api-0.3.8 → prisma_api-0.3.10}/prisma_api.egg-info/requires.txt +0 -0
- {prisma_api-0.3.8 → prisma_api-0.3.10}/prisma_api.egg-info/top_level.txt +0 -0
- {prisma_api-0.3.8 → prisma_api-0.3.10}/setup.cfg +0 -0
- {prisma_api-0.3.8 → prisma_api-0.3.10}/tests/test_prisma_api.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: prisma_api
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.10
|
|
4
4
|
Summary: A package for interacting with PrISMa Platform APIs. PrISMa is a holistic system for numerical synthesis and assessment of Metal-Organic Frameworks and their applications, for Direct Air Capture of CO2. For more information, see: https://prisma.hw.ac.uk.
|
|
5
5
|
Author-email: RCCS-CaptureTeam <team@prisma-platform.org>
|
|
6
6
|
License: GNUv3
|
|
@@ -22,6 +22,9 @@ from typing import Any
|
|
|
22
22
|
from pathlib import Path
|
|
23
23
|
from urllib.parse import urlencode
|
|
24
24
|
import warnings
|
|
25
|
+
import json
|
|
26
|
+
import re
|
|
27
|
+
from datetime import datetime
|
|
25
28
|
|
|
26
29
|
|
|
27
30
|
_BASE_PROD = "https://prisma-platform.org/api/v2"
|
|
@@ -141,6 +144,76 @@ class PrismaAPIv2:
|
|
|
141
144
|
"raw": cif_text,
|
|
142
145
|
}
|
|
143
146
|
|
|
147
|
+
def _coerce_scalar(self, value: Any) -> Any:
|
|
148
|
+
"""Best-effort conversion of scalar API values to native Python types."""
|
|
149
|
+
if not isinstance(value, str):
|
|
150
|
+
return value
|
|
151
|
+
|
|
152
|
+
text = value.strip()
|
|
153
|
+
if text == "":
|
|
154
|
+
return value
|
|
155
|
+
|
|
156
|
+
lowered = text.lower()
|
|
157
|
+
if lowered == "true":
|
|
158
|
+
return True
|
|
159
|
+
if lowered == "false":
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
# Strict integer parsing first to avoid converting floats like '1.0' to int.
|
|
163
|
+
if re.fullmatch(r"[+-]?\d+", text):
|
|
164
|
+
try:
|
|
165
|
+
return int(text)
|
|
166
|
+
except ValueError:
|
|
167
|
+
pass
|
|
168
|
+
|
|
169
|
+
# Support decimal/scientific notation.
|
|
170
|
+
if re.fullmatch(r"[+-]?(?:\d+\.\d*|\.\d+|\d+)(?:[eE][+-]?\d+)?", text):
|
|
171
|
+
try:
|
|
172
|
+
return float(text)
|
|
173
|
+
except ValueError:
|
|
174
|
+
pass
|
|
175
|
+
|
|
176
|
+
# Parse JSON list/dict payloads often returned as strings.
|
|
177
|
+
if (text.startswith("[") and text.endswith("]")) or (
|
|
178
|
+
text.startswith("{") and text.endswith("}")
|
|
179
|
+
):
|
|
180
|
+
try:
|
|
181
|
+
parsed = json.loads(text)
|
|
182
|
+
if isinstance(parsed, (list, dict)):
|
|
183
|
+
return parsed
|
|
184
|
+
except json.JSONDecodeError:
|
|
185
|
+
pass
|
|
186
|
+
|
|
187
|
+
# Parse ISO-ish datetime strings.
|
|
188
|
+
if any(sep in text for sep in ("-", "T", ":")):
|
|
189
|
+
try:
|
|
190
|
+
return datetime.fromisoformat(text.replace("Z", "+00:00"))
|
|
191
|
+
except ValueError:
|
|
192
|
+
pass
|
|
193
|
+
|
|
194
|
+
return value
|
|
195
|
+
|
|
196
|
+
def _coerce_scope_types(self, payload: Any) -> Any:
|
|
197
|
+
"""Normalize scope endpoint payload values to useful runtime types."""
|
|
198
|
+
if isinstance(payload, dict):
|
|
199
|
+
return {k: self._coerce_scalar(v) for k, v in payload.items()}
|
|
200
|
+
|
|
201
|
+
if isinstance(payload, list):
|
|
202
|
+
if payload and all(isinstance(item, dict) for item in payload):
|
|
203
|
+
return [{k: self._coerce_scalar(v) for k, v in row.items()} for row in payload]
|
|
204
|
+
return [self._coerce_scalar(v) for v in payload]
|
|
205
|
+
|
|
206
|
+
if isinstance(payload, pd.DataFrame):
|
|
207
|
+
df = payload.copy()
|
|
208
|
+
for col in df.columns:
|
|
209
|
+
df[col] = df[col].map(self._coerce_scalar)
|
|
210
|
+
non_null = df[col].dropna()
|
|
211
|
+
if len(non_null) > 0 and non_null.map(lambda v: isinstance(v, datetime)).all():
|
|
212
|
+
df[col] = pd.to_datetime(df[col], errors="coerce")
|
|
213
|
+
return df
|
|
214
|
+
|
|
215
|
+
return payload
|
|
216
|
+
|
|
144
217
|
# ── Health ────────────────────────────────────────────────────────────────
|
|
145
218
|
|
|
146
219
|
def health(self) -> dict:
|
|
@@ -275,7 +348,7 @@ class PrismaAPIv2:
|
|
|
275
348
|
"""
|
|
276
349
|
GET /api/v2/list_cifs/
|
|
277
350
|
|
|
278
|
-
|
|
351
|
+
List CIF-backed MOF records.
|
|
279
352
|
|
|
280
353
|
Args:
|
|
281
354
|
tag: Optional tag to filter CIFs by. If omitted, all CIFs are returned.
|
|
@@ -283,11 +356,6 @@ class PrismaAPIv2:
|
|
|
283
356
|
Returns:
|
|
284
357
|
List of MOF names matching ``tag``.
|
|
285
358
|
"""
|
|
286
|
-
if not self._dev:
|
|
287
|
-
raise RuntimeError(
|
|
288
|
-
"list_cifs() is only available on the dev server. "
|
|
289
|
-
"Initialise with dev=True (e.g. prisma_api.init(local_dev=True))."
|
|
290
|
-
)
|
|
291
359
|
response = self._get("/cifs/", _compact(tag=tag))
|
|
292
360
|
records = response.get("results", response) if isinstance(response, dict) else response
|
|
293
361
|
records = records or []
|
|
@@ -300,7 +368,7 @@ class PrismaAPIv2:
|
|
|
300
368
|
"""
|
|
301
369
|
GET /api/v2/cifs/files/
|
|
302
370
|
|
|
303
|
-
|
|
371
|
+
Fetch CIF payload(s) for one or more MOF names.
|
|
304
372
|
|
|
305
373
|
Args:
|
|
306
374
|
mof: One MOF name (str) or list of MOF names.
|
|
@@ -319,12 +387,6 @@ class PrismaAPIv2:
|
|
|
319
387
|
If ``structured=False``: streamed CIF response(s), or saved path(s)
|
|
320
388
|
when ``save_dir`` is provided.
|
|
321
389
|
"""
|
|
322
|
-
if not self._dev:
|
|
323
|
-
raise RuntimeError(
|
|
324
|
-
"get_cifs() is only available on the dev server. "
|
|
325
|
-
"Initialise with dev=True (e.g. prisma_api.init(local_dev=True))."
|
|
326
|
-
)
|
|
327
|
-
|
|
328
390
|
if structured and save_dir is not None:
|
|
329
391
|
raise ValueError("save_dir is only supported when structured=False.")
|
|
330
392
|
|
|
@@ -1256,6 +1318,16 @@ class PrismaAPIv2:
|
|
|
1256
1318
|
"""GET /api/v2/sources/{source_id}/"""
|
|
1257
1319
|
return self._get(f"/sources/{source_id}/")
|
|
1258
1320
|
|
|
1321
|
+
def get_scopes(self, name: str | None = None,
|
|
1322
|
+
limit: int = 500, offset: int = 0) -> pd.DataFrame:
|
|
1323
|
+
"""GET /api/v2/scopes/"""
|
|
1324
|
+
params = _compact(name=name, limit=limit, offset=offset)
|
|
1325
|
+
return self._coerce_scope_types(self._to_df(self._get("/scopes/", params)))
|
|
1326
|
+
|
|
1327
|
+
def get_scope(self, scope_id: int) -> dict:
|
|
1328
|
+
"""GET /api/v2/scopes/{scope_id}/"""
|
|
1329
|
+
return self._coerce_scope_types(self._get(f"/scopes/{scope_id}/"))
|
|
1330
|
+
|
|
1259
1331
|
def get_sinks(self, name: str | None = None,
|
|
1260
1332
|
limit: int = 500, offset: int = 0) -> pd.DataFrame:
|
|
1261
1333
|
"""GET /api/v2/sinks/"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: prisma_api
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.10
|
|
4
4
|
Summary: A package for interacting with PrISMa Platform APIs. PrISMa is a holistic system for numerical synthesis and assessment of Metal-Organic Frameworks and their applications, for Direct Air Capture of CO2. For more information, see: https://prisma.hw.ac.uk.
|
|
5
5
|
Author-email: RCCS-CaptureTeam <team@prisma-platform.org>
|
|
6
6
|
License: GNUv3
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "prisma_api"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.10"
|
|
8
8
|
description = "A package for interacting with PrISMa Platform APIs. PrISMa is a holistic system for numerical synthesis and assessment of Metal-Organic Frameworks and their applications, for Direct Air Capture of CO2. For more information, see: https://prisma.hw.ac.uk."
|
|
9
9
|
authors = [
|
|
10
10
|
{name = "RCCS-CaptureTeam", email = "team@prisma-platform.org"}
|
|
@@ -14,6 +14,9 @@ import os
|
|
|
14
14
|
import pytest
|
|
15
15
|
import responses as resp_lib
|
|
16
16
|
from responses import matchers
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
|
|
19
|
+
import pandas as pd
|
|
17
20
|
|
|
18
21
|
from prisma_api.prisma_api_v2 import PrismaAPIv2, _BASE_PROD
|
|
19
22
|
|
|
@@ -461,6 +464,7 @@ def test_get_region_detail(api):
|
|
|
461
464
|
|
|
462
465
|
@pytest.mark.parametrize("method,path,fixture", [
|
|
463
466
|
("get_sources", "/sources/", [{"id": 1, "name": "Coal Plant", "short_name": "CP"}]),
|
|
467
|
+
("get_scopes", "/scopes/", [{"id": 7, "name": "Point Source"}]),
|
|
464
468
|
("get_sinks", "/sinks/", [{"id": 2, "name": "North Sea"}]),
|
|
465
469
|
("get_transport_scenarios", "/transport-scenarios/", [{"id": 3, "name": "Pipeline 200km"}]),
|
|
466
470
|
("get_utilities", "/utilities/", [{"id": 4, "name": "Steam"}]),
|
|
@@ -476,6 +480,7 @@ def test_catalog_list_endpoints_return_dataframe(api, method, path, fixture):
|
|
|
476
480
|
|
|
477
481
|
@pytest.mark.parametrize("method,path,record", [
|
|
478
482
|
("get_source", "/sources/1/", {"id": 1, "name": "Coal Plant"}),
|
|
483
|
+
("get_scope", "/scopes/7/", {"id": 7, "name": "Point Source"}),
|
|
479
484
|
("get_sink", "/sinks/2/", {"id": 2, "name": "North Sea"}),
|
|
480
485
|
("get_transport_scenario", "/transport-scenarios/3/",{"id": 3, "name": "Pipeline"}),
|
|
481
486
|
("get_utility", "/utilities/4/", {"id": 4, "name": "Steam"}),
|
|
@@ -820,6 +825,7 @@ def test_get_mea_kpi_detail(api):
|
|
|
820
825
|
|
|
821
826
|
@pytest.mark.parametrize("method,path,fixture", [
|
|
822
827
|
("get_sources", "/sources/", [{"id": 1, "name": "Coal Plant", "short_name": "CP"}]),
|
|
828
|
+
("get_scopes", "/scopes/", [{"id": 7, "name": "Point Source"}]),
|
|
823
829
|
("get_sinks", "/sinks/", [{"id": 2, "name": "North Sea"}]),
|
|
824
830
|
("get_transport_scenarios", "/transport-scenarios/", [{"id": 3, "name": "Pipeline 200km"}]),
|
|
825
831
|
("get_utilities", "/utilities/", [{"id": 4, "name": "Steam"}]),
|
|
@@ -835,6 +841,7 @@ def test_catalog_list_endpoints_return_dataframe(api, method, path, fixture):
|
|
|
835
841
|
|
|
836
842
|
@pytest.mark.parametrize("method,path,record", [
|
|
837
843
|
("get_source", "/sources/1/", {"id": 1, "name": "Coal Plant"}),
|
|
844
|
+
("get_scope", "/scopes/7/", {"id": 7, "name": "Point Source"}),
|
|
838
845
|
("get_sink", "/sinks/2/", {"id": 2, "name": "North Sea"}),
|
|
839
846
|
("get_transport_scenario", "/transport-scenarios/3/",{"id": 3, "name": "Pipeline"}),
|
|
840
847
|
("get_utility", "/utilities/4/", {"id": 4, "name": "Steam"}),
|
|
@@ -865,6 +872,76 @@ def test_get_isotherm_returns_dataframe(api):
|
|
|
865
872
|
assert_df_columns(df, "id", "mof", "molecule", "T_ref_K", "sim_or_exp", "good_structure")
|
|
866
873
|
|
|
867
874
|
|
|
875
|
+
# ── Scopes ───────────────────────────────────────────────────────────────────
|
|
876
|
+
|
|
877
|
+
@resp_lib.activate
|
|
878
|
+
def test_get_scopes_name_filter(api):
|
|
879
|
+
resp_lib.add(
|
|
880
|
+
resp_lib.GET,
|
|
881
|
+
f"{PROD_BASE}/scopes/",
|
|
882
|
+
match=[matchers.query_param_matcher({"name": "point", "limit": "500", "offset": "0"})],
|
|
883
|
+
json=_envelope([{"id": 7, "name": "Point Source"}]),
|
|
884
|
+
status=200,
|
|
885
|
+
)
|
|
886
|
+
df = api.get_scopes(name="point")
|
|
887
|
+
assert df.iloc[0]["name"] == "Point Source"
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
@resp_lib.activate
|
|
891
|
+
def test_get_scopes_coerces_types(api):
|
|
892
|
+
resp_lib.add(
|
|
893
|
+
resp_lib.GET,
|
|
894
|
+
f"{PROD_BASE}/scopes/",
|
|
895
|
+
json=_envelope([
|
|
896
|
+
{
|
|
897
|
+
"id": "7",
|
|
898
|
+
"name": "Point Source",
|
|
899
|
+
"active": "true",
|
|
900
|
+
"capture_efficiency": "0.935",
|
|
901
|
+
"created_at": "2026-07-01T12:34:56Z",
|
|
902
|
+
"tags": "[\"pilot\", \"uk\"]",
|
|
903
|
+
}
|
|
904
|
+
]),
|
|
905
|
+
status=200,
|
|
906
|
+
)
|
|
907
|
+
|
|
908
|
+
df = api.get_scopes()
|
|
909
|
+
row = df.iloc[0]
|
|
910
|
+
|
|
911
|
+
assert isinstance(row["id"], int)
|
|
912
|
+
assert isinstance(row["name"], str)
|
|
913
|
+
assert isinstance(row["active"], bool)
|
|
914
|
+
assert isinstance(row["capture_efficiency"], float)
|
|
915
|
+
assert pd.api.types.is_datetime64_any_dtype(df["created_at"])
|
|
916
|
+
assert isinstance(row["tags"], list)
|
|
917
|
+
|
|
918
|
+
|
|
919
|
+
@resp_lib.activate
|
|
920
|
+
def test_get_scope_detail_coerces_types(api):
|
|
921
|
+
resp_lib.add(
|
|
922
|
+
resp_lib.GET,
|
|
923
|
+
f"{PROD_BASE}/scopes/7/",
|
|
924
|
+
json={
|
|
925
|
+
"id": "7",
|
|
926
|
+
"name": "Point Source",
|
|
927
|
+
"active": "false",
|
|
928
|
+
"capture_efficiency": "1.05",
|
|
929
|
+
"created_at": "2026-07-01T12:34:56Z",
|
|
930
|
+
"tags": "[\"a\", \"b\"]",
|
|
931
|
+
},
|
|
932
|
+
status=200,
|
|
933
|
+
)
|
|
934
|
+
|
|
935
|
+
scope = api.get_scope(7)
|
|
936
|
+
|
|
937
|
+
assert isinstance(scope["id"], int)
|
|
938
|
+
assert isinstance(scope["name"], str)
|
|
939
|
+
assert isinstance(scope["active"], bool)
|
|
940
|
+
assert isinstance(scope["capture_efficiency"], float)
|
|
941
|
+
assert isinstance(scope["created_at"], datetime)
|
|
942
|
+
assert isinstance(scope["tags"], list)
|
|
943
|
+
|
|
944
|
+
|
|
868
945
|
@resp_lib.activate
|
|
869
946
|
def test_get_isotherm_all_filters_passed(api):
|
|
870
947
|
expected_params = {
|
|
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
|