prisma-api 0.3.9__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.9 → prisma_api-0.3.10}/PKG-INFO +1 -1
- {prisma_api-0.3.9 → prisma_api-0.3.10}/prisma_api/prisma_api_v2.py +83 -0
- {prisma_api-0.3.9 → prisma_api-0.3.10}/prisma_api.egg-info/PKG-INFO +1 -1
- {prisma_api-0.3.9 → prisma_api-0.3.10}/pyproject.toml +1 -1
- {prisma_api-0.3.9 → prisma_api-0.3.10}/tests/test_prisma_api_v2.py +77 -0
- {prisma_api-0.3.9 → prisma_api-0.3.10}/LICENSE +0 -0
- {prisma_api-0.3.9 → prisma_api-0.3.10}/README.md +0 -0
- {prisma_api-0.3.9 → prisma_api-0.3.10}/prisma_api/__init__.py +0 -0
- {prisma_api-0.3.9 → prisma_api-0.3.10}/prisma_api/config.py +0 -0
- {prisma_api-0.3.9 → prisma_api-0.3.10}/prisma_api/prisma_api.py +0 -0
- {prisma_api-0.3.9 → prisma_api-0.3.10}/prisma_api.egg-info/SOURCES.txt +0 -0
- {prisma_api-0.3.9 → prisma_api-0.3.10}/prisma_api.egg-info/dependency_links.txt +0 -0
- {prisma_api-0.3.9 → prisma_api-0.3.10}/prisma_api.egg-info/requires.txt +0 -0
- {prisma_api-0.3.9 → prisma_api-0.3.10}/prisma_api.egg-info/top_level.txt +0 -0
- {prisma_api-0.3.9 → prisma_api-0.3.10}/setup.cfg +0 -0
- {prisma_api-0.3.9 → 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:
|
|
@@ -1245,6 +1318,16 @@ class PrismaAPIv2:
|
|
|
1245
1318
|
"""GET /api/v2/sources/{source_id}/"""
|
|
1246
1319
|
return self._get(f"/sources/{source_id}/")
|
|
1247
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
|
+
|
|
1248
1331
|
def get_sinks(self, name: str | None = None,
|
|
1249
1332
|
limit: int = 500, offset: int = 0) -> pd.DataFrame:
|
|
1250
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
|
|
File without changes
|