prisma-api 0.3.0__py3-none-any.whl

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.
@@ -0,0 +1,539 @@
1
+ import os
2
+ from .config import get_or_create_config, update_dev_mode as _update_dev_mode
3
+ from .prisma_api_v2 import PrismaAPIv2
4
+ from pathlib import Path
5
+ import pandas as pd
6
+ import requests
7
+
8
+ import numpy as np
9
+ import json
10
+ import math
11
+
12
+ def _safe_nan_check(x):
13
+ if x is None:
14
+ return None
15
+ try:
16
+ if isinstance(x, (int, float)) and math.isnan(x):
17
+ return None
18
+ except (TypeError, ValueError):
19
+ pass
20
+ return x
21
+
22
+
23
+
24
+ # prisma_api main class
25
+ class prisma_api():
26
+
27
+ def __init__(self, use_config_file=True):
28
+
29
+ # Initialise `prisma_api` object with api_key location
30
+ self.verbose = False
31
+ # Initialise `prisma_api` object with api_key location
32
+ if use_config_file:
33
+ 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']
39
+ 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', '')
44
+
45
+ self.v2 = PrismaAPIv2(
46
+ key=self.key,
47
+ dev=self.dev,
48
+ dev_host_port=getattr(self, 'dev_host_port', ''),
49
+ return_format=getattr(self, '_return_format', 'json'),
50
+ )
51
+
52
+ def set_return_format(self, fmt: str) -> None:
53
+ """
54
+ Set the output format for all v2 list endpoints.
55
+
56
+ Args:
57
+ fmt: ``'dataframe'`` (default) — return ``pd.DataFrame``.
58
+ ``'json'`` — return a plain ``list[dict]``.
59
+ """
60
+ self._return_format = fmt
61
+ self.v2.set_return_format(fmt)
62
+
63
+ def update_dev_mode(self, dev: bool):
64
+ """Update the dev flag in config.yaml.
65
+
66
+ Args:
67
+ dev: Boolean flag to enable/disable dev mode.
68
+
69
+ Returns:
70
+ dict: Updated config.
71
+ """
72
+ return _update_dev_mode(dev)
73
+
74
+ def get_mofs(self, payload={}):
75
+
76
+ api = self
77
+
78
+ if self.dev:
79
+ url = f"http://localhost:{self.dev_host_port}/api/get_mofs/"
80
+ else:
81
+ url = "https://www.dun-eideann-labs.co.uk/prisma_cloud/api/get_mofs/"
82
+
83
+ headers = {
84
+ "X-API-Key": api.key,
85
+ "Content-Type": "application/json"
86
+ }
87
+
88
+ response = requests.post(url, json=payload, headers=headers, timeout=60)
89
+ data = response.json()['data']
90
+
91
+ data = pd.DataFrame.from_dict(data)
92
+
93
+ return data
94
+
95
+
96
+ def get_carbon_isotherms(self, payload={}):
97
+
98
+ api = self
99
+
100
+ if self.dev:
101
+ url = f"http://localhost:{self.dev_host_port}/api/get_carbon_isotherms/"
102
+ else:
103
+ url = "https://www.prisma-platform.org/api/get_carbon_isotherms/"
104
+
105
+ headers = {
106
+ "X-API-Key": api.key,
107
+ "Content-Type": "application/json"
108
+ }
109
+
110
+ try:
111
+ response = requests.post(url, json=payload, headers=headers, timeout=60)
112
+ data = response.json()['data']
113
+
114
+ data = pd.DataFrame.from_dict(data)
115
+
116
+ # Flatten nested mof fields
117
+ if 'mof' in data.columns and not data.empty:
118
+ mof_df = pd.json_normalize(data['mof'])
119
+ mof_df.columns = ['mof_' + col for col in mof_df.columns]
120
+ data = pd.concat([data.drop('mof', axis=1), mof_df], axis=1)
121
+
122
+ # Flatten nested molecule fields
123
+ if 'molecule' in data.columns and not data.empty:
124
+ molecule_df = pd.json_normalize(data['molecule'])
125
+ molecule_df.columns = ['molecule_' + col for col in molecule_df.columns]
126
+ data = pd.concat([data.drop('molecule', axis=1), molecule_df], axis=1)
127
+
128
+ return data
129
+
130
+ except Exception as e:
131
+ print("Error retrieving carbon isotherms: check that the query parameter names are correct.")
132
+ return pd.DataFrame()
133
+
134
+ def get_carbon_data_nested(self, payload={}, safe_names=False):
135
+ """
136
+ Get carbon data with nested structure, returned as separate DataFrames.
137
+
138
+ Args:
139
+ payload: Dictionary containing query parameters for filtering
140
+ safe_names: If True (default), keep API-safe column names (e.g. 'Pressure_bar').
141
+ If False, rename columns to original names (e.g. 'Pressure [bar]').
142
+
143
+ Returns:
144
+ dict: {
145
+ 'Simulated': {'isotherm': pd.DataFrame, 'geometry': pd.DataFrame},
146
+ 'Experimental': {'isotherm': pd.DataFrame, 'geometry': pd.DataFrame},
147
+ 'meta': dict
148
+ }
149
+ """
150
+ api = self
151
+
152
+ if self.dev:
153
+ url = f"http://localhost:{self.dev_host_port}/api/get_carbon_data_nested/"
154
+ else:
155
+ url = "https://www.dun-eideann-labs.co.uk/prisma_cloud/api/get_carbon_data_nested/"
156
+
157
+ headers = {
158
+ "X-API-Key": api.key,
159
+ "Content-Type": "application/json"
160
+ }
161
+
162
+ try:
163
+ response = requests.post(url, json=payload, headers=headers, timeout=60)
164
+ data = response.json()
165
+
166
+ col_names_carbon = data.get('meta', {}).get('original_column_names', {})
167
+ col_names_water = data.get('meta', {}).get('Water', {}).get('original_column_names', {})
168
+
169
+ # col_names maps: api_field_name -> original_column_name
170
+ # Use directly as a rename map for each DataFrame
171
+ sim_iso = pd.DataFrame(data.get('Simulated', {}).get('isotherm', []))
172
+ sim_geo = pd.DataFrame(data.get('Simulated', {}).get('geometry', []))
173
+ exp_iso = pd.DataFrame(data.get('Experimental', {}).get('isotherm', []))
174
+ exp_geo = pd.DataFrame(data.get('Experimental', {}).get('geometry', []))
175
+
176
+ sim_water_dac = pd.DataFrame(data.get('Water', {}).get('Simulated', {}).get('DAC', []))
177
+ sim_water_cement = pd.DataFrame(data.get('Water', {}).get('Simulated', {}).get('cement', []))
178
+ sim_water_coal = pd.DataFrame(data.get('Water', {}).get('Simulated', {}).get('coal', []))
179
+ sim_water_ngcc = pd.DataFrame(data.get('Water', {}).get('Simulated', {}).get('NGCC-onshore', []))
180
+ exp_water_dac = pd.DataFrame(data.get('Water', {}).get('Experimental', {}).get('DAC', []))
181
+ exp_water_cement = pd.DataFrame(data.get('Water', {}).get('Experimental', {}).get('cement', []))
182
+ exp_water_coal = pd.DataFrame(data.get('Water', {}).get('Experimental', {}).get('coal', []))
183
+ exp_water_ngcc = pd.DataFrame(data.get('Water', {}).get('Experimental', {}).get('NGCC-onshore', []))
184
+
185
+ if not safe_names:
186
+ if col_names_carbon.get('isotherm'):
187
+ sim_iso = sim_iso.rename(columns=col_names_carbon['isotherm'])
188
+ exp_iso = exp_iso.rename(columns=col_names_carbon['isotherm'])
189
+
190
+ if col_names_carbon.get('simulated_geometry'):
191
+ sim_geo = sim_geo.rename(columns=col_names_carbon['simulated_geometry'])
192
+
193
+ if col_names_carbon.get('experimental_geometry'):
194
+ exp_geo = exp_geo.rename(columns=col_names_carbon['experimental_geometry'])
195
+
196
+ if col_names_water:
197
+ sim_water_dac = sim_water_dac.rename(columns=col_names_water)
198
+ exp_water_dac = exp_water_dac.rename(columns=col_names_water)
199
+ sim_water_cement = sim_water_cement.rename(columns=col_names_water)
200
+ exp_water_cement = exp_water_cement.rename(columns=col_names_water)
201
+ sim_water_coal = sim_water_coal.rename(columns=col_names_water)
202
+ exp_water_coal = exp_water_coal.rename(columns=col_names_water)
203
+ sim_water_ngcc = sim_water_ngcc.rename(columns=col_names_water)
204
+ exp_water_ngcc = exp_water_ngcc.rename(columns=col_names_water)
205
+
206
+ return {
207
+ 'Simulated': {'isotherm': sim_iso, 'geometry': sim_geo},
208
+ 'Experimental': {'isotherm': exp_iso, 'geometry': exp_geo},
209
+ 'meta': data.get('meta', {}),
210
+ 'Water': {
211
+ 'Simulated': {'DAC': sim_water_dac, 'cement': sim_water_cement, 'coal': sim_water_coal, 'NGCC-onshore': sim_water_ngcc},
212
+ 'Experimental': {'DAC': exp_water_dac, 'cement': exp_water_cement, 'coal': exp_water_coal, 'NGCC-onshore': exp_water_ngcc},
213
+ 'meta': data.get('meta', {}).get('water', {}),
214
+ }
215
+ }
216
+
217
+ except Exception as e:
218
+ print(f"Error retrieving carbon data nested: {e}")
219
+ return {}
220
+
221
+ def get_materials_data(self, payload={}, separate_experimental=True):
222
+ """
223
+ Args:
224
+ payload: Dictionary containing query parameters for filtering.
225
+ separate_experimental: If True, split the result into separate 'simulated' and
226
+ 'experimental' DataFrames based on the sim_or_exp flag,
227
+ before unpacking. Requires unpack=True.
228
+ """
229
+ api = self
230
+
231
+ # Deprecated argument, forced by-pass as unpack=False format not likely needed.
232
+ # Can be removed after Apr-2026.
233
+ unpack=True
234
+
235
+ if self.dev:
236
+ urls = {
237
+ 'localhost': f"http://localhost:{self.dev_host_port}/api/get_materials_data/"
238
+ }
239
+ base_urls = {
240
+ 'localhost': f"http://localhost:{self.dev_host_port}"
241
+ }
242
+ else:
243
+ urls = {
244
+ 'prisma-platform.org': "https://prisma-platform.org/api/get_materials_data/",
245
+ 'dun-eideann-labs.co.uk': "https://www.dun-eideann-labs.co.uk/prisma_cloud/api/get_materials_data/"
246
+ }
247
+ base_urls = {
248
+ 'prisma-platform.org': "https://prisma-platform.org",
249
+ 'dun-eideann-labs.co.uk': "https://www.dun-eideann-labs.co.uk"
250
+ }
251
+
252
+ headers = {
253
+ "X-API-Key": api.key,
254
+ "Content-Type": "application/json"
255
+ }
256
+
257
+ try:
258
+ data_raw = None
259
+ source_key = None
260
+ for name, endpoint in urls.items():
261
+ try:
262
+ response = requests.post(endpoint, json=payload, headers=headers, timeout=60)
263
+ data_raw = response.json()
264
+ if data_raw.get('data'):
265
+ source_key = name
266
+ break
267
+ except Exception:
268
+ continue
269
+
270
+ if data_raw is None:
271
+ raise RuntimeError("All endpoints failed to return data.")
272
+
273
+ df = pd.DataFrame(data_raw.get('data', []))
274
+
275
+ # Prepend source base URL to cif_file column
276
+ if source_key and 'cif_file' in df.columns:
277
+ base = base_urls.get(source_key, '')
278
+ df['cif_file'] = df['cif_file'].apply(
279
+ lambda v: f"{base}{v}" if isinstance(v, str) and v else v
280
+ )
281
+
282
+ if unpack and not df.empty:
283
+ # Unpack carbon_isotherm independently (keep prefixed columns)
284
+ if 'carbon_isotherm' in df.columns:
285
+ unpacked = pd.json_normalize(
286
+ df['carbon_isotherm'].apply(lambda x: x[0] if isinstance(x, list) and len(x) > 0 else (x if isinstance(x, dict) else {}))
287
+ )
288
+ unpacked.columns = [f"carbon_isotherm__{c}" for c in unpacked.columns]
289
+ df = df.drop(columns=['carbon_isotherm']).join(unpacked)
290
+
291
+ # Unpack carbon_zeopp and carbon_zeopp_experimental, then combine shared fields
292
+ zeopp_cols = [c for c in ['carbon_zeopp', 'carbon_zeopp_experimental'] if c in df.columns]
293
+ if zeopp_cols:
294
+ unpacked_frames = {}
295
+ for col in zeopp_cols:
296
+ unpacked = pd.json_normalize(
297
+ df[col].apply(lambda x: x[0] if isinstance(x, list) and len(x) > 0 else (x if isinstance(x, dict) else {}))
298
+ )
299
+ unpacked_frames[col] = unpacked
300
+ df = df.drop(columns=[col])
301
+
302
+ # Derive sim_or_exp flag: 'exp' if carbon_zeopp_experimental has data, else 'sim'
303
+ if 'carbon_zeopp_experimental' in unpacked_frames:
304
+ exp_has_data = unpacked_frames['carbon_zeopp_experimental'].notna().any(axis=1)
305
+ else:
306
+ exp_has_data = pd.Series(False, index=df.index)
307
+
308
+ zeopp_sim_or_exp = exp_has_data.map({True: 'exp', False: 'sim'})
309
+
310
+ # Collect all field names across both unpacked frames
311
+ all_fields = set()
312
+ for unpacked in unpacked_frames.values():
313
+ all_fields.update(unpacked.columns)
314
+ all_fields.discard('sim_or_exp')
315
+
316
+ # Coalesce: for shared fields prefer carbon_zeopp, fall back to carbon_zeopp_experimental
317
+ combined = pd.DataFrame(index=df.index)
318
+ for field in sorted(all_fields):
319
+ series_list = [unpacked_frames[col][field] for col in zeopp_cols if field in unpacked_frames[col].columns]
320
+ if len(series_list) == 1:
321
+ combined[f"carbon_zeopp__{field}"] = series_list[0].values
322
+ else:
323
+ coalesced = series_list[0].copy()
324
+ for fallback in series_list[1:]:
325
+ coalesced = coalesced.combine_first(fallback.rename(coalesced.name))
326
+ combined[f"carbon_zeopp__{field}"] = coalesced.values
327
+
328
+ df = df.join(combined)
329
+
330
+ # Build a single top-level sim_or_exp column, coalescing sources in priority order
331
+ sim_or_exp = pd.Series(index=df.index, dtype=object)
332
+ for source_col in ['carbon_isotherm__sim_or_exp', 'carbon_zeopp__sim_or_exp']:
333
+ if source_col in df.columns:
334
+ sim_or_exp = sim_or_exp.combine_first(df[source_col])
335
+ df = df.drop(columns=[source_col])
336
+ # Fall back to the zeopp-derived flag if still null
337
+ if 'zeopp_sim_or_exp' in locals():
338
+ sim_or_exp = sim_or_exp.combine_first(zeopp_sim_or_exp)
339
+
340
+ # Insert sim_or_exp as the first column after unpacking
341
+ df.insert(0, 'sim_or_exp', sim_or_exp)
342
+
343
+ # Drop internal ID columns not needed in output
344
+ df = df.drop(columns=[c for c in ['carbon_isotherm__id'] if c in df.columns])
345
+ df = df.drop(columns=[c for c in ['carbon_zeopp__id'] if c in df.columns])
346
+ df = df.drop(columns=[c for c in ['carbon_zeopp__Molecule'] if c in df.columns])
347
+ df = df.drop(columns=[c for c in ['carbon_zeopp__good_structure'] if c in df.columns])
348
+ df = df.drop(columns=[c for c in ['id'] if c in df.columns])
349
+
350
+ # Rename unpacked columns to friendlier names
351
+ rename_map = {
352
+ 'carbon_isotherm__Molecule': 'Molecule',
353
+ 'carbon_isotherm__good_structure': 'Good Structure',
354
+ 'carbon_isotherm__Henry_mol_per_kg_Pa': 'CO2 Henry (mol/kg/Pa)',
355
+ 'carbon_isotherm__Pressure_bar': 'CO2 Pressure (bar)',
356
+ 'carbon_isotherm__Uptake_mol_per_kg': 'CO2 Uptake (mol/kg)',
357
+ 'carbon_isotherm__Heat_kJ_per_mol': 'CO2 Heat (kJ/mol)',
358
+ 'carbon_isotherm__T_ref_K': 'CO2 T_ref (K)',
359
+ 'carbon_zeopp__Binder': 'Zeo++ Binder',
360
+ 'carbon_zeopp__Cp_J_per_gK': 'Zeo++ Cp_J_per_gK',
361
+ 'carbon_zeopp__DOI': 'Zeo++ DOI',
362
+ 'carbon_zeopp__Density_g_per_cm3': 'Zeo++ Density_g_per_cm3',
363
+ 'carbon_zeopp__Formula': 'Zeo++ Formula',
364
+ 'carbon_zeopp__Macroporosity': 'Zeo++ Macroporosity',
365
+ 'carbon_zeopp__Molecule': 'Zeo++ Molecule',
366
+ 'carbon_zeopp__POAVF': 'Zeo++ POAVF',
367
+ 'carbon_zeopp__Pellet_Density_g_per_cm3': 'Zeo++ Pellet_Density_g_per_cm3',
368
+ 'carbon_zeopp__Round': 'Zeo++ Round',
369
+ }
370
+ rename_map_ = {k: v for k, v in rename_map.items() if k in df.columns}
371
+ if rename_map_:
372
+ df = df.rename(columns=rename_map_)
373
+
374
+ if separate_experimental and unpack and not df.empty and 'sim_or_exp' in df.columns:
375
+ df_sim = df[df['sim_or_exp'] == 'sim'].reset_index(drop=True)
376
+ df_exp = df[df['sim_or_exp'] == 'exp'].reset_index(drop=True)
377
+
378
+ return {
379
+ 'simulated': df_sim,
380
+ 'experimental': df_exp,
381
+ 'meta': {'source': source_key},
382
+ }
383
+ else:
384
+ return {
385
+ 'data': df,
386
+ 'meta': {'source': source_key},
387
+ }
388
+
389
+ except Exception as e:
390
+ print(f"Error retrieving materials data: {e}")
391
+ return {}
392
+
393
+ #### -------------------------- AutoPrism -------------------------- ####
394
+
395
+ def update_adsorption_singlepoint(self, df):
396
+ """
397
+ Update adsorption singlepoint data via PUT request.
398
+
399
+ Args:
400
+ df: DataFrame containing the adsorption singlepoint data to update
401
+
402
+ Returns:
403
+ pd.DataFrame: Response data from the API
404
+ """
405
+ api = self
406
+
407
+ if self.dev:
408
+ url = f"http://localhost:{self.dev_host_port}/api/update_adsorption_singlepoint/"
409
+ else:
410
+ url = "https://www.dun-eideann-labs.co.uk/prisma_cloud/api/update_adsorption_singlepoint/"
411
+
412
+ headers = {
413
+ "X-API-Key": api.key,
414
+ "Content-Type": "application/json"
415
+ }
416
+
417
+ # Convert dataframe to JSON payload, handling NaN and infinite values
418
+ json_data = self._clean_dataframe_for_json(df)
419
+
420
+ response = requests.put(url, data=json_data, headers=headers, timeout=300) # 300sec (5 minutes)
421
+
422
+ return response.json()
423
+
424
+ def _clean_dataframe_for_json(self, df):
425
+ """Helper method to clean DataFrame for JSON serialization."""
426
+ if df.empty:
427
+ return "[]"
428
+
429
+ import numpy as np
430
+ import json
431
+
432
+ # Use pandas to_json which handles NaN properly, then parse back
433
+ df_clean = df.copy()
434
+ df_clean = df_clean.replace([np.nan, np.inf, -np.inf], None)
435
+
436
+ # Convert using pandas to_json (which handles NaN correctly) then back to dict
437
+ json_str = df_clean.to_json(orient='records', force_ascii=False)
438
+
439
+ return json_str
440
+
441
+ def update_heat_capacity_all_tidy(self, df):
442
+ """
443
+ Update heat capacity all tidy data via PUT request.
444
+
445
+ Args:
446
+ df: DataFrame containing the heat capacity data to update
447
+
448
+ Returns:
449
+ dict: Response data from the API
450
+ """
451
+ if self.dev:
452
+ url = f"http://localhost:{self.dev_host_port}/api/update_heat_capacity_all_tidy/"
453
+ else:
454
+ url = "https://www.dun-eideann-labs.co.uk/prisma_cloud/api/update_heat_capacity_all_tidy/"
455
+
456
+ headers = {
457
+ "X-API-Key": self.key,
458
+ "Content-Type": "application/json"
459
+ }
460
+
461
+ json_data = self._clean_dataframe_for_json(df)
462
+ response = requests.put(url, data=json_data, headers=headers, timeout=300)
463
+
464
+ return response.json()
465
+
466
+ def update_isotherm_h2(self, df):
467
+ """
468
+ Update H2 isotherm data via PUT request.
469
+
470
+ Args:
471
+ df: DataFrame containing the H2 isotherm data to update
472
+
473
+ Returns:
474
+ dict: Response data from the API
475
+ """
476
+ if self.dev:
477
+ url = f"http://localhost:{self.dev_host_port}/api/update_isotherm_h2/"
478
+ else:
479
+ url = "https://www.dun-eideann-labs.co.uk/prisma_cloud/api/update_isotherm_h2/"
480
+
481
+ headers = {
482
+ "X-API-Key": self.key,
483
+ "Content-Type": "application/json"
484
+ }
485
+
486
+ json_data = self._clean_dataframe_for_json(df)
487
+ response = requests.put(url, data=json_data, headers=headers, timeout=300)
488
+
489
+ return response.json()
490
+
491
+ def update_mofchecker(self, df):
492
+ """
493
+ Update MOF checker data via PUT request.
494
+
495
+ Args:
496
+ df: DataFrame containing the MOF checker data to update
497
+
498
+ Returns:
499
+ dict: Response data from the API
500
+ """
501
+ if self.dev:
502
+ url = f"http://localhost:{self.dev_host_port}/api/update_mofchecker/"
503
+ else:
504
+ url = "https://www.dun-eideann-labs.co.uk/prisma_cloud/api/update_mofchecker/"
505
+
506
+ headers = {
507
+ "X-API-Key": self.key,
508
+ "Content-Type": "application/json"
509
+ }
510
+
511
+ json_data = self._clean_dataframe_for_json(df)
512
+ response = requests.put(url, data=json_data, headers=headers, timeout=300)
513
+
514
+ return response.json()
515
+
516
+ def update_zeopp_metrics(self, df):
517
+ """
518
+ Update Zeo++ metrics data via PUT request.
519
+
520
+ Args:
521
+ df: DataFrame containing the Zeo++ metrics data to update
522
+
523
+ Returns:
524
+ dict: Response data from the API
525
+ """
526
+ if self.dev:
527
+ url = f"http://localhost:{self.dev_host_port}/api/update_zeopp_metrics/"
528
+ else:
529
+ url = "https://www.dun-eideann-labs.co.uk/prisma_cloud/api/update_zeopp_metrics/"
530
+
531
+ headers = {
532
+ "X-API-Key": self.key,
533
+ "Content-Type": "application/json"
534
+ }
535
+
536
+ json_data = self._clean_dataframe_for_json(df)
537
+ response = requests.put(url, data=json_data, headers=headers, timeout=300)
538
+
539
+ return response.json()