prisma-api 0.3.4__tar.gz → 0.3.6__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: prisma_api
3
- Version: 0.3.4
3
+ Version: 0.3.6
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
@@ -20,6 +20,9 @@ Dynamic: license-file
20
20
 
21
21
  # PrISMa API
22
22
 
23
+ ![Tests](https://img.shields.io/badge/tests-passing-brightgreen)
24
+ ![Coverage](https://img.shields.io/badge/test%20coverage-passing-brightgreen)
25
+
23
26
  Python client for the PrISMa platform APIs.
24
27
 
25
28
  This package provides:
@@ -200,6 +203,7 @@ All methods below are available on `api.v2`.
200
203
 
201
204
  ### Case bundles and pack builders
202
205
 
206
+ - `list_case_studies(name=None, limit=500, offset=0)`
203
207
  - `get_cases_bundle(name=None, source=None, sink=None, region=None, limit_cases=100, limit_props=2000)`
204
208
  - `build_case_spec(case_id)`
205
209
  - `build_scenario_spec(scenario_id)`
@@ -275,6 +279,7 @@ All methods below are available on `api.v2`.
275
279
  - `get_case(case_id)`
276
280
  - `get_scenarios(case_id=None, name=None, type=None, limit=500, offset=0)`
277
281
  - `get_scenario(scenario_id)`
282
+ - `get_screening_analysis_bundle(analysis_id)`
278
283
  - `get_screening_summaries(scenario_id=None, limit=500, offset=0)`
279
284
  - `get_screening_summary(summary_id)`
280
285
 
@@ -1,5 +1,8 @@
1
1
  # PrISMa API
2
2
 
3
+ ![Tests](https://img.shields.io/badge/tests-passing-brightgreen)
4
+ ![Coverage](https://img.shields.io/badge/test%20coverage-passing-brightgreen)
5
+
3
6
  Python client for the PrISMa platform APIs.
4
7
 
5
8
  This package provides:
@@ -180,6 +183,7 @@ All methods below are available on `api.v2`.
180
183
 
181
184
  ### Case bundles and pack builders
182
185
 
186
+ - `list_case_studies(name=None, limit=500, offset=0)`
183
187
  - `get_cases_bundle(name=None, source=None, sink=None, region=None, limit_cases=100, limit_props=2000)`
184
188
  - `build_case_spec(case_id)`
185
189
  - `build_scenario_spec(scenario_id)`
@@ -255,6 +259,7 @@ All methods below are available on `api.v2`.
255
259
  - `get_case(case_id)`
256
260
  - `get_scenarios(case_id=None, name=None, type=None, limit=500, offset=0)`
257
261
  - `get_scenario(scenario_id)`
262
+ - `get_screening_analysis_bundle(analysis_id)`
258
263
  - `get_screening_summaries(scenario_id=None, limit=500, offset=0)`
259
264
  - `get_screening_summary(summary_id)`
260
265
 
@@ -1,4 +1,4 @@
1
- __version__ = "0.3.4"
1
+ __version__ = "0.3.6"
2
2
 
3
3
 
4
4
  from prisma_api.prisma_api import prisma_api as init # Main prisma_api class for initialisation
@@ -1,4 +1,5 @@
1
1
  import os
2
+ import warnings
2
3
  from .config import get_or_create_config, update_dev_mode as _update_dev_mode
3
4
  from .prisma_api_v2 import PrismaAPIv2
4
5
  from pathlib import Path
@@ -24,23 +25,25 @@ def _safe_nan_check(x):
24
25
  # prisma_api main class
25
26
  class prisma_api():
26
27
 
27
- def __init__(self, use_config_file=True):
28
+ def __init__(self, use_config_file=True, local_dev: bool = False):
28
29
 
29
30
  # Initialise `prisma_api` object with api_key location
30
31
  self.verbose = False
31
32
  # Initialise `prisma_api` object with api_key location
32
33
  if use_config_file:
33
34
  cfg = get_or_create_config()
34
- self.key = cfg['api_key']
35
- self.dev = cfg.get('dev', False)
36
- if self.dev:
37
- self.dev_host_port = cfg['dev_host_port']
38
- self.key = cfg['dev_api_key']
35
+ prod_key = cfg.get('api_key', '')
36
+ dev_key = cfg.get('dev_api_key', prod_key)
37
+ dev_host_port = cfg.get('dev_host_port', '8000')
39
38
  else:
40
- self.key = os.getenv('PRISMA_API_KEY', '')
41
- self.dev = os.getenv('PRISMA_API_DEV', 'False').lower() in ('true', '1', 't')
42
- if self.dev:
43
- self.dev_host_port = os.getenv('PRISMA_API_DEV_HOST_PORT', '')
39
+ prod_key = os.getenv('PRISMA_API_KEY', '')
40
+ dev_key = os.getenv('PRISMA_API_DEV_API_KEY', prod_key)
41
+ dev_host_port = os.getenv('PRISMA_API_DEV_HOST_PORT', '8000')
42
+
43
+ # local_dev now controls target DB explicitly at init time.
44
+ self.dev = bool(local_dev)
45
+ self.dev_host_port = dev_host_port
46
+ self.key = dev_key if self.dev else prod_key
44
47
 
45
48
  self.v2 = PrismaAPIv2(
46
49
  key=self.key,
@@ -69,6 +72,12 @@ class prisma_api():
69
72
  Returns:
70
73
  dict: Updated config.
71
74
  """
75
+ warnings.warn(
76
+ "update_dev_mode() is deprecated and will be removed in a future release. "
77
+ "Use prisma_api.init(local_dev=True|False) instead.",
78
+ FutureWarning,
79
+ stacklevel=2,
80
+ )
72
81
  return _update_dev_mode(dev)
73
82
 
74
83
  def get_mofs(self, payload={}):
@@ -19,6 +19,7 @@ from __future__ import annotations
19
19
  import pandas as pd
20
20
  import requests
21
21
  from typing import Any
22
+ from urllib.parse import urlencode
22
23
 
23
24
 
24
25
  _BASE_PROD = "https://prisma-platform.org/api/v2"
@@ -140,6 +141,60 @@ class PrismaAPIv2:
140
141
  """
141
142
  return self._get(f"/flowsheets/{name}/bundle/")
142
143
 
144
+ def upsert_flowsheets(
145
+ self,
146
+ flowsheets: pd.DataFrame | list[dict],
147
+ screening_analysis_name: str,
148
+ on_exists: str = "append",
149
+ appendix: str = "_v4",
150
+ ) -> dict:
151
+ """
152
+ PUT /api/v2/flowsheets/upsert/
153
+
154
+ Modes:
155
+ * append (default): ``?on_exists=append&appendix=<suffix>``
156
+ * overwrite: ``?on_exists=overwrite``
157
+
158
+ Args:
159
+ flowsheets: DataFrame or list of dict payload records.
160
+ screening_analysis_name: Required analysis identifier forwarded to the
161
+ API for upsert attribution/routing.
162
+ on_exists: Conflict mode; one of ``'append'`` or ``'overwrite'``.
163
+ appendix: Suffix used only in append mode (default ``'_v4'``).
164
+
165
+ Returns:
166
+ API response dict.
167
+ """
168
+ if on_exists not in ("append", "overwrite"):
169
+ raise ValueError("on_exists must be 'append' or 'overwrite'")
170
+ if not screening_analysis_name or not screening_analysis_name.strip():
171
+ raise ValueError("screening_analysis_name must be a non-empty string")
172
+
173
+ records = (
174
+ flowsheets.to_dict(orient="records")
175
+ if isinstance(flowsheets, pd.DataFrame)
176
+ else flowsheets
177
+ )
178
+
179
+ if on_exists == "overwrite":
180
+ query = urlencode(
181
+ {
182
+ "on_exists": "overwrite",
183
+ "screening_analysis_name": screening_analysis_name,
184
+ }
185
+ )
186
+ else:
187
+ query = urlencode(
188
+ {
189
+ "on_exists": "append",
190
+ "appendix": appendix,
191
+ "screening_analysis_name": screening_analysis_name,
192
+ }
193
+ )
194
+ path = f"/flowsheets/upsert/?{query}"
195
+
196
+ return self._put(path, records)
197
+
143
198
  # ── Catalog ───────────────────────────────────────────────────────────────
144
199
 
145
200
  def list_materials(self, name: str | None = None,
@@ -1228,6 +1283,21 @@ class PrismaAPIv2:
1228
1283
  """GET /api/v2/cases/{case_id}/"""
1229
1284
  return self._get(f"/cases/{case_id}/")
1230
1285
 
1286
+ def list_case_studies(self,
1287
+ name: str | None = None,
1288
+ limit: int = 500,
1289
+ offset: int = 0) -> pd.DataFrame:
1290
+ """
1291
+ GET /api/v2/case-studies/
1292
+
1293
+ Args:
1294
+ name: Optional substring filter on case-study name.
1295
+ limit: Maximum number of records to return.
1296
+ offset: Pagination offset.
1297
+ """
1298
+ params = _compact(name=name, limit=limit, offset=offset)
1299
+ return self._to_df(self._get("/case-studies/", params))
1300
+
1231
1301
  def get_scenarios(self,
1232
1302
  case_id: int | None = None,
1233
1303
  name: str | None = None,
@@ -1250,6 +1320,10 @@ class PrismaAPIv2:
1250
1320
  """GET /api/v2/scenarios/{scenario_id}/"""
1251
1321
  return self._get(f"/scenarios/{scenario_id}/")
1252
1322
 
1323
+ def get_screening_analysis_bundle(self, analysis_id: int) -> dict:
1324
+ """GET /api/v2/screening-analyses/{analysis_id}/bundle/"""
1325
+ return self._get(f"/screening-analyses/{analysis_id}/bundle/")
1326
+
1253
1327
  # ── Case-pack builders (ImportedCasePack spec) ────────────────────────────
1254
1328
 
1255
1329
  @staticmethod
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: prisma_api
3
- Version: 0.3.4
3
+ Version: 0.3.6
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
@@ -20,6 +20,9 @@ Dynamic: license-file
20
20
 
21
21
  # PrISMa API
22
22
 
23
+ ![Tests](https://img.shields.io/badge/tests-passing-brightgreen)
24
+ ![Coverage](https://img.shields.io/badge/test%20coverage-passing-brightgreen)
25
+
23
26
  Python client for the PrISMa platform APIs.
24
27
 
25
28
  This package provides:
@@ -200,6 +203,7 @@ All methods below are available on `api.v2`.
200
203
 
201
204
  ### Case bundles and pack builders
202
205
 
206
+ - `list_case_studies(name=None, limit=500, offset=0)`
203
207
  - `get_cases_bundle(name=None, source=None, sink=None, region=None, limit_cases=100, limit_props=2000)`
204
208
  - `build_case_spec(case_id)`
205
209
  - `build_scenario_spec(scenario_id)`
@@ -275,6 +279,7 @@ All methods below are available on `api.v2`.
275
279
  - `get_case(case_id)`
276
280
  - `get_scenarios(case_id=None, name=None, type=None, limit=500, offset=0)`
277
281
  - `get_scenario(scenario_id)`
282
+ - `get_screening_analysis_bundle(analysis_id)`
278
283
  - `get_screening_summaries(scenario_id=None, limit=500, offset=0)`
279
284
  - `get_screening_summary(summary_id)`
280
285
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "prisma_api"
7
- version = "0.3.4"
7
+ version = "0.3.6"
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"}
@@ -94,6 +94,66 @@ def test_v2_is_attached():
94
94
  assert isinstance(api.v2, PrismaAPIv2)
95
95
 
96
96
 
97
+ @patch("prisma_api.prisma_api.get_or_create_config")
98
+ def test_init_local_dev_false_uses_prod_config(mock_cfg):
99
+ from prisma_api.prisma_api import prisma_api
100
+
101
+ mock_cfg.return_value = {
102
+ "api_key": "prod-key",
103
+ "dev_api_key": "dev-key",
104
+ "dev_host_port": "8000",
105
+ "dev": True,
106
+ }
107
+ api = prisma_api(local_dev=False)
108
+ assert api.dev is False
109
+ assert api.key == "prod-key"
110
+ assert api.v2._dev is False
111
+
112
+
113
+ @patch("prisma_api.prisma_api.get_or_create_config")
114
+ def test_init_local_dev_true_uses_dev_config(mock_cfg):
115
+ from prisma_api.prisma_api import prisma_api
116
+
117
+ mock_cfg.return_value = {
118
+ "api_key": "prod-key",
119
+ "dev_api_key": "dev-key",
120
+ "dev_host_port": "8000",
121
+ "dev": False,
122
+ }
123
+ api = prisma_api(local_dev=True)
124
+ assert api.dev is True
125
+ assert api.key == "dev-key"
126
+ assert api.dev_host_port == "8000"
127
+ assert api.v2._dev is True
128
+
129
+
130
+ def test_init_local_dev_true_uses_env_when_no_config(monkeypatch):
131
+ from prisma_api.prisma_api import prisma_api
132
+
133
+ monkeypatch.setenv("PRISMA_API_KEY", "prod-env-key")
134
+ monkeypatch.setenv("PRISMA_API_DEV_API_KEY", "dev-env-key")
135
+ monkeypatch.setenv("PRISMA_API_DEV_HOST_PORT", "9001")
136
+
137
+ api = prisma_api(use_config_file=False, local_dev=True)
138
+ assert api.dev is True
139
+ assert api.key == "dev-env-key"
140
+ assert api.dev_host_port == "9001"
141
+
142
+
143
+ @patch("prisma_api.prisma_api._update_dev_mode")
144
+ def test_update_dev_mode_emits_deprecation_warning(mock_update):
145
+ from prisma_api.prisma_api import prisma_api
146
+
147
+ mock_update.return_value = {"dev": False}
148
+ api = prisma_api.__new__(prisma_api)
149
+
150
+ with pytest.warns(FutureWarning, match="deprecated"):
151
+ result = api.update_dev_mode(False)
152
+
153
+ assert result == {"dev": False}
154
+ mock_update.assert_called_once_with(False)
155
+
156
+
97
157
  # ── get_materials_data: basic response ────────────────────────────────────────
98
158
 
99
159
  @resp_lib.activate
@@ -1188,6 +1188,24 @@ def test_get_case_detail(api):
1188
1188
  assert result["name"] == "UK Coal CCS 2030"
1189
1189
 
1190
1190
 
1191
+ @resp_lib.activate
1192
+ def test_list_case_studies_returns_dataframe(api):
1193
+ records = [{"id": 1, "name": "Alpha 2030"}, {"id": 2, "name": "Beta 2040"}]
1194
+ resp_lib.add(resp_lib.GET, f"{PROD_BASE}/case-studies/",
1195
+ json=_envelope(records), status=200)
1196
+ df = api.list_case_studies()
1197
+ assert_df_columns(df, "id", "name")
1198
+
1199
+
1200
+ @resp_lib.activate
1201
+ def test_list_case_studies_name_filter_passed(api):
1202
+ expected = {"name": "Alpha", "limit": "500", "offset": "0"}
1203
+ resp_lib.add(resp_lib.GET, f"{PROD_BASE}/case-studies/",
1204
+ match=[matchers.query_param_matcher(expected)],
1205
+ json=_envelope([]), status=200)
1206
+ api.list_case_studies(name="Alpha")
1207
+
1208
+
1191
1209
  @resp_lib.activate
1192
1210
  def test_get_scenarios_returns_dataframe(api):
1193
1211
  records = [{"id": 830, "name": "baseline_2030", "print_name": "Baseline 2030",
@@ -1216,6 +1234,15 @@ def test_get_scenario_detail(api):
1216
1234
  assert result["type"] == "TEA"
1217
1235
 
1218
1236
 
1237
+ @resp_lib.activate
1238
+ def test_get_screening_analysis_bundle_detail(api):
1239
+ bundle = {"analysis_id": 123, "case_study": {"name": "Alpha 2030"}, "results": []}
1240
+ resp_lib.add(resp_lib.GET, f"{PROD_BASE}/screening-analyses/123/bundle/",
1241
+ json=bundle, status=200)
1242
+ result = api.get_screening_analysis_bundle(123)
1243
+ assert result["analysis_id"] == 123
1244
+
1245
+
1219
1246
  # ── ImportedCasePack builders ─────────────────────────────────────────────────
1220
1247
 
1221
1248
  _CASE_DETAIL = {
@@ -1423,6 +1450,13 @@ def test_api_key_header_sent(api):
1423
1450
 
1424
1451
  # ── Flowsheets ────────────────────────────────────────────────────────────────
1425
1452
 
1453
+ _UPSERT_FLOWSHEET_FIXTURE = "reference_data/prisma_v2/dac_min_2026-07-01.json"
1454
+
1455
+
1456
+ def _load_upsert_flowsheet_payload() -> list[dict]:
1457
+ with open(_UPSERT_FLOWSHEET_FIXTURE, "r", encoding="utf-8") as f:
1458
+ return [json.load(f)]
1459
+
1426
1460
  @resp_lib.activate
1427
1461
  @pytest.mark.skipif(
1428
1462
  os.getenv("CI", "").lower() == "true",
@@ -1478,3 +1512,70 @@ def test_get_flowsheet_bundle_custom_name_routes_path(api):
1478
1512
  json={"template_id": "custom_case"}, status=200)
1479
1513
  result = api.get_flowsheet_bundle(name="custom_case")
1480
1514
  assert result["template_id"] == "custom_case"
1515
+
1516
+
1517
+ @resp_lib.activate
1518
+ def test_upsert_flowsheets_default_append_mode(api):
1519
+ resp_lib.add(
1520
+ resp_lib.PUT,
1521
+ f"{PROD_BASE}/flowsheets/upsert/?on_exists=append&appendix=_v4&screening_analysis_name=screening_case",
1522
+ json={"created": 1, "updated": 0},
1523
+ status=200,
1524
+ )
1525
+ payload = _load_upsert_flowsheet_payload()
1526
+ result = api.upsert_flowsheets(payload, screening_analysis_name="screening_case")
1527
+ assert result["created"] == 1
1528
+
1529
+
1530
+ @resp_lib.activate
1531
+ def test_upsert_flowsheets_overwrite_mode(api):
1532
+ resp_lib.add(
1533
+ resp_lib.PUT,
1534
+ f"{PROD_BASE}/flowsheets/upsert/?on_exists=overwrite&screening_analysis_name=screening_case",
1535
+ json={"created": 0, "updated": 1},
1536
+ status=200,
1537
+ )
1538
+ payload = _load_upsert_flowsheet_payload()
1539
+ result = api.upsert_flowsheets(
1540
+ payload,
1541
+ screening_analysis_name="screening_case",
1542
+ on_exists="overwrite",
1543
+ )
1544
+ assert result["updated"] == 1
1545
+
1546
+
1547
+ @resp_lib.activate
1548
+ def test_upsert_flowsheets_uses_dev_mode_base_url(dev_api):
1549
+ resp_lib.add(
1550
+ resp_lib.PUT,
1551
+ "http://localhost:8000/api/v2/flowsheets/upsert/?on_exists=append&appendix=_dev&screening_analysis_name=screening_dev",
1552
+ json={"created": 1, "updated": 0},
1553
+ status=200,
1554
+ )
1555
+ payload = _load_upsert_flowsheet_payload()
1556
+ result = dev_api.upsert_flowsheets(
1557
+ payload,
1558
+ screening_analysis_name="screening_dev",
1559
+ appendix="_dev",
1560
+ )
1561
+ assert result["created"] == 1
1562
+
1563
+
1564
+ def test_upsert_flowsheets_rejects_invalid_on_exists(api):
1565
+ payload = _load_upsert_flowsheet_payload()
1566
+ with pytest.raises(ValueError):
1567
+ api.upsert_flowsheets(
1568
+ payload,
1569
+ screening_analysis_name="screening_case",
1570
+ on_exists="replace",
1571
+ )
1572
+
1573
+
1574
+ @pytest.mark.skipif(
1575
+ os.getenv("PRISMA_API_PRODUCTION_BYPASS", "").lower() == "true",
1576
+ reason="Bypassed in production mode",
1577
+ )
1578
+ def test_upsert_flowsheets_rejects_empty_screening_analysis_name(api):
1579
+ payload = _load_upsert_flowsheet_payload()
1580
+ with pytest.raises(ValueError):
1581
+ api.upsert_flowsheets(payload, screening_analysis_name=" ")
File without changes
File without changes