pastastore 1.7.1__py3-none-any.whl → 1.8.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.
pastastore/base.py CHANGED
@@ -2,20 +2,15 @@
2
2
  """Base classes for PastaStore Connectors."""
3
3
 
4
4
  import functools
5
- import json
6
5
  import warnings
7
6
 
8
7
  # import weakref
9
8
  from abc import ABC, abstractmethod
10
- from collections.abc import Iterable
11
9
  from itertools import chain
12
- from typing import Dict, List, Optional, Tuple, Union
10
+ from typing import Callable, Dict, List, Optional, Tuple, Union
13
11
 
14
12
  import pandas as pd
15
13
  import pastas as ps
16
- from numpy import isin
17
- from packaging.version import parse as parse_version
18
- from pastas.io.pas import PastasEncoder
19
14
  from tqdm.auto import tqdm
20
15
 
21
16
  from pastastore.util import ItemInLibraryException, _custom_warning, validate_names
@@ -30,7 +25,7 @@ class BaseConnector(ABC):
30
25
 
31
26
  Class holds base logic for dealing with time series and Pastas Models. Create your
32
27
  own Connector to a data source by writing a a class that inherits from this
33
- BaseConnector. Your class has to override each abstractmethod and abstractproperty.
28
+ BaseConnector. Your class has to override each abstractmethod and property.
34
29
  """
35
30
 
36
31
  _default_library_names = [
@@ -47,6 +42,10 @@ class BaseConnector(ABC):
47
42
  # True for pastas>=0.23.0 and False for pastas<=0.22.0
48
43
  USE_PASTAS_VALIDATE_SERIES = False if PASTAS_LEQ_022 else True
49
44
 
45
+ # set series equality comparison settings (using assert_series_equal)
46
+ SERIES_EQUALITY_ABSOLUTE_TOLERANCE = 1e-10
47
+ SERIES_EQUALITY_RELATIVE_TOLERANCE = 0.0
48
+
50
49
  def __repr__(self):
51
50
  """Representation string of the object."""
52
51
  return (
@@ -65,7 +64,7 @@ class BaseConnector(ABC):
65
64
  def _get_library(self, libname: str):
66
65
  """Get library handle.
67
66
 
68
- Must be overriden by subclass.
67
+ Must be overridden by subclass.
69
68
 
70
69
  Parameters
71
70
  ----------
@@ -89,7 +88,7 @@ class BaseConnector(ABC):
89
88
  ) -> None:
90
89
  """Add item for both time series and pastas.Models (internal method).
91
90
 
92
- Must be overriden by subclass.
91
+ Must be overridden by subclass.
93
92
 
94
93
  Parameters
95
94
  ----------
@@ -107,7 +106,7 @@ class BaseConnector(ABC):
107
106
  def _get_item(self, libname: str, name: str) -> Union[FrameorSeriesUnion, Dict]:
108
107
  """Get item (series or pastas.Models) (internal method).
109
108
 
110
- Must be overriden by subclass.
109
+ Must be overridden by subclass.
111
110
 
112
111
  Parameters
113
112
  ----------
@@ -126,7 +125,7 @@ class BaseConnector(ABC):
126
125
  def _del_item(self, libname: str, name: str) -> None:
127
126
  """Delete items (series or models) (internal method).
128
127
 
129
- Must be overriden by subclass.
128
+ Must be overridden by subclass.
130
129
 
131
130
  Parameters
132
131
  ----------
@@ -140,7 +139,7 @@ class BaseConnector(ABC):
140
139
  def _get_metadata(self, libname: str, name: str) -> Dict:
141
140
  """Get metadata (internal method).
142
141
 
143
- Must be overriden by subclass.
142
+ Must be overridden by subclass.
144
143
 
145
144
  Parameters
146
145
  ----------
@@ -160,7 +159,7 @@ class BaseConnector(ABC):
160
159
  def oseries_names(self):
161
160
  """List of oseries names.
162
161
 
163
- Property must be overriden by subclass.
162
+ Property must be overridden by subclass.
164
163
  """
165
164
 
166
165
  @property
@@ -168,7 +167,7 @@ class BaseConnector(ABC):
168
167
  def stresses_names(self):
169
168
  """List of stresses names.
170
169
 
171
- Property must be overriden by subclass.
170
+ Property must be overridden by subclass.
172
171
  """
173
172
 
174
173
  @property
@@ -176,7 +175,37 @@ class BaseConnector(ABC):
176
175
  def model_names(self):
177
176
  """List of model names.
178
177
 
179
- Property must be overriden by subclass.
178
+ Property must be overridden by subclass.
179
+ """
180
+
181
+ @abstractmethod
182
+ def _parallel(
183
+ self,
184
+ func: Callable,
185
+ names: List[str],
186
+ progressbar: Optional[bool] = True,
187
+ max_workers: Optional[int] = None,
188
+ chunksize: Optional[int] = None,
189
+ desc: str = "",
190
+ ) -> None:
191
+ """Parallel processing of function.
192
+
193
+ Must be overridden by subclass.
194
+
195
+ Parameters
196
+ ----------
197
+ func : function
198
+ function to apply in parallel
199
+ names : list
200
+ list of names to apply function to
201
+ progressbar : bool, optional
202
+ show progressbar, by default True
203
+ max_workers : int, optional
204
+ maximum number of workers, by default None
205
+ chunksize : int, optional
206
+ chunksize for parallel processing, by default None
207
+ desc : str, optional
208
+ description for progressbar, by default ""
180
209
  """
181
210
 
182
211
  def set_check_model_series_values(self, b: bool):
@@ -670,22 +699,27 @@ class BaseConnector(ABC):
670
699
  metadata["kind"] = kind
671
700
  self._upsert_series("stresses", series, name, metadata=metadata)
672
701
 
673
- def del_models(self, names: Union[list, str]) -> None:
702
+ def del_models(self, names: Union[list, str], verbose: bool = True) -> None:
674
703
  """Delete model(s) from the database.
675
704
 
676
705
  Parameters
677
706
  ----------
678
707
  names : str or list of str
679
708
  name(s) of the model to delete
709
+ verbose : bool, optional
710
+ print information about deleted models, by default True
680
711
  """
681
- for n in self._parse_names(names, libname="models"):
712
+ names = self._parse_names(names, libname="models")
713
+ for n in names:
682
714
  mldict = self.get_models(n, return_dict=True)
683
715
  oname = mldict["oseries"]["name"]
684
716
  self._del_item("models", n)
685
717
  self._del_oseries_model_link(oname, n)
686
718
  self._clear_cache("_modelnames_cache")
719
+ if verbose:
720
+ print(f"Deleted {len(names)} model(s) from database.")
687
721
 
688
- def del_model(self, names: Union[list, str]) -> None:
722
+ def del_model(self, names: Union[list, str], verbose: bool = True) -> None:
689
723
  """Delete model(s) from the database.
690
724
 
691
725
  Alias for del_models().
@@ -694,10 +728,14 @@ class BaseConnector(ABC):
694
728
  ----------
695
729
  names : str or list of str
696
730
  name(s) of the model to delete
731
+ verbose : bool, optional
732
+ print information about deleted models, by default True
697
733
  """
698
- self.del_models(names=names)
734
+ self.del_models(names=names, verbose=verbose)
699
735
 
700
- def del_oseries(self, names: Union[list, str], remove_models: bool = False):
736
+ def del_oseries(
737
+ self, names: Union[list, str], remove_models: bool = False, verbose: bool = True
738
+ ):
701
739
  """Delete oseries from the database.
702
740
 
703
741
  Parameters
@@ -706,29 +744,38 @@ class BaseConnector(ABC):
706
744
  name(s) of the oseries to delete
707
745
  remove_models : bool, optional
708
746
  also delete models for deleted oseries, default is False
747
+ verbose : bool, optional
748
+ print information about deleted oseries, by default True
709
749
  """
710
750
  names = self._parse_names(names, libname="oseries")
711
751
  for n in names:
712
752
  self._del_item("oseries", n)
713
753
  self._clear_cache("oseries")
754
+ if verbose:
755
+ print(f"Deleted {len(names)} oseries from database.")
714
756
  # remove associated models from database
715
757
  if remove_models:
716
758
  modelnames = list(
717
759
  chain.from_iterable([self.oseries_models.get(n, []) for n in names])
718
760
  )
719
- self.del_models(modelnames)
761
+ self.del_models(modelnames, verbose=verbose)
720
762
 
721
- def del_stress(self, names: Union[list, str]):
763
+ def del_stress(self, names: Union[list, str], verbose: bool = True):
722
764
  """Delete stress from the database.
723
765
 
724
766
  Parameters
725
767
  ----------
726
768
  names : str or list of str
727
769
  name(s) of the stress to delete
770
+ verbose : bool, optional
771
+ print information about deleted stresses, by default True
728
772
  """
729
- for n in self._parse_names(names, libname="stresses"):
773
+ names = self._parse_names(names, libname="stresses")
774
+ for n in names:
730
775
  self._del_item("stresses", n)
731
776
  self._clear_cache("stresses")
777
+ if verbose:
778
+ print(f"Deleted {len(names)} stress(es) from database.")
732
779
 
733
780
  def _get_series(
734
781
  self,
@@ -1088,8 +1135,8 @@ class BaseConnector(ABC):
1088
1135
  time series contained in library
1089
1136
  """
1090
1137
  names = self._parse_names(names, libname)
1091
- for nam in names:
1092
- yield self._get_series(libname, nam, progressbar=False)
1138
+ for name in names:
1139
+ yield self._get_series(libname, name, progressbar=False)
1093
1140
 
1094
1141
  def iter_oseries(self, names: Optional[List[str]] = None):
1095
1142
  """Iterate over oseries in library.
@@ -1315,611 +1362,6 @@ class BaseConnector(ABC):
1315
1362
  return d
1316
1363
 
1317
1364
 
1318
- class ConnectorUtil:
1319
- """Mix-in class for general Connector helper functions.
1320
-
1321
- Only for internal methods, and not methods that are related to CRUD operations on
1322
- database.
1323
- """
1324
-
1325
- def _parse_names(
1326
- self,
1327
- names: Optional[Union[list, str]] = None,
1328
- libname: Optional[str] = "oseries",
1329
- ) -> list:
1330
- """Parse names kwarg, returns iterable with name(s) (internal method).
1331
-
1332
- Parameters
1333
- ----------
1334
- names : Union[list, str], optional
1335
- str or list of str or None or 'all' (last two options
1336
- retrieves all names)
1337
- libname : str, optional
1338
- name of library, default is 'oseries'
1339
-
1340
- Returns
1341
- -------
1342
- list
1343
- list of names
1344
- """
1345
- if not isinstance(names, str) and isinstance(names, Iterable):
1346
- return names
1347
- elif isinstance(names, str) and names != "all":
1348
- return [names]
1349
- elif names is None or names == "all":
1350
- if libname == "oseries":
1351
- return self.oseries_names
1352
- elif libname == "stresses":
1353
- return self.stresses_names
1354
- elif libname == "models":
1355
- return self.model_names
1356
- elif libname == "oseries_models":
1357
- return self.oseries_with_models
1358
- else:
1359
- raise ValueError(f"No library '{libname}'!")
1360
- else:
1361
- raise NotImplementedError(f"Cannot parse 'names': {names}")
1362
-
1363
- @staticmethod
1364
- def _meta_list_to_frame(metalist: list, names: list):
1365
- """Convert list of metadata dictionaries to DataFrame.
1366
-
1367
- Parameters
1368
- ----------
1369
- metalist : list
1370
- list of metadata dictionaries
1371
- names : list
1372
- list of names corresponding to data in metalist
1373
-
1374
- Returns
1375
- -------
1376
- pandas.DataFrame
1377
- DataFrame containing overview of metadata
1378
- """
1379
- # convert to dataframe
1380
- if len(metalist) > 1:
1381
- meta = pd.DataFrame(metalist)
1382
- if len({"x", "y"}.difference(meta.columns)) == 0:
1383
- meta["x"] = meta["x"].astype(float)
1384
- meta["y"] = meta["y"].astype(float)
1385
- elif len(metalist) == 1:
1386
- meta = pd.DataFrame(metalist)
1387
- elif len(metalist) == 0:
1388
- meta = pd.DataFrame()
1389
-
1390
- meta.index = names
1391
- meta.index.name = "name"
1392
- return meta
1393
-
1394
- def _parse_model_dict(self, mdict: dict, update_ts_settings: bool = False):
1395
- """Parse dictionary describing pastas models (internal method).
1396
-
1397
- Parameters
1398
- ----------
1399
- mdict : dict
1400
- dictionary describing pastas.Model
1401
- update_ts_settings : bool, optional
1402
- update stored tmin and tmax in time series settings
1403
- based on time series loaded from store.
1404
-
1405
- Returns
1406
- -------
1407
- ml : pastas.Model
1408
- time series analysis model
1409
- """
1410
- PASFILE_LEQ_022 = parse_version(
1411
- mdict["file_info"]["pastas_version"]
1412
- ) <= parse_version("0.22.0")
1413
-
1414
- # oseries
1415
- if "series" not in mdict["oseries"]:
1416
- name = str(mdict["oseries"]["name"])
1417
- if name not in self.oseries.index:
1418
- msg = "oseries '{}' not present in library".format(name)
1419
- raise LookupError(msg)
1420
- mdict["oseries"]["series"] = self.get_oseries(name).squeeze()
1421
- # update tmin/tmax from time series
1422
- if update_ts_settings:
1423
- mdict["oseries"]["settings"]["tmin"] = mdict["oseries"]["series"].index[
1424
- 0
1425
- ]
1426
- mdict["oseries"]["settings"]["tmax"] = mdict["oseries"]["series"].index[
1427
- -1
1428
- ]
1429
-
1430
- # StressModel, WellModel
1431
- for ts in mdict["stressmodels"].values():
1432
- if "stress" in ts.keys():
1433
- # WellModel
1434
- classkey = "stressmodel" if PASFILE_LEQ_022 else "class"
1435
- if ts[classkey] == "WellModel":
1436
- for stress in ts["stress"]:
1437
- if "series" not in stress:
1438
- name = str(stress["name"])
1439
- if name in self.stresses.index:
1440
- stress["series"] = self.get_stresses(name).squeeze()
1441
- # update tmin/tmax from time series
1442
- if update_ts_settings:
1443
- stress["settings"]["tmin"] = stress["series"].index[
1444
- 0
1445
- ]
1446
- stress["settings"]["tmax"] = stress["series"].index[
1447
- -1
1448
- ]
1449
- # StressModel
1450
- else:
1451
- for stress in ts["stress"] if PASFILE_LEQ_022 else [ts["stress"]]:
1452
- if "series" not in stress:
1453
- name = str(stress["name"])
1454
- if name in self.stresses.index:
1455
- stress["series"] = self.get_stresses(name).squeeze()
1456
- # update tmin/tmax from time series
1457
- if update_ts_settings:
1458
- stress["settings"]["tmin"] = stress["series"].index[
1459
- 0
1460
- ]
1461
- stress["settings"]["tmax"] = stress["series"].index[
1462
- -1
1463
- ]
1464
-
1465
- # RechargeModel, TarsoModel
1466
- if ("prec" in ts.keys()) and ("evap" in ts.keys()):
1467
- for stress in [ts["prec"], ts["evap"]]:
1468
- if "series" not in stress:
1469
- name = str(stress["name"])
1470
- if name in self.stresses.index:
1471
- stress["series"] = self.get_stresses(name).squeeze()
1472
- # update tmin/tmax from time series
1473
- if update_ts_settings:
1474
- stress["settings"]["tmin"] = stress["series"].index[0]
1475
- stress["settings"]["tmax"] = stress["series"].index[-1]
1476
- else:
1477
- msg = "stress '{}' not present in library".format(name)
1478
- raise KeyError(msg)
1479
-
1480
- # hack for pcov w dtype object (when filled with NaNs on store?)
1481
- if "fit" in mdict:
1482
- if "pcov" in mdict["fit"]:
1483
- pcov = mdict["fit"]["pcov"]
1484
- if pcov.dtypes.apply(lambda dtyp: isinstance(dtyp, object)).any():
1485
- mdict["fit"]["pcov"] = pcov.astype(float)
1486
-
1487
- # check pastas version vs pas-file version
1488
- file_version = mdict["file_info"]["pastas_version"]
1489
-
1490
- # check file version and pastas version
1491
- # if file<0.23 and pastas>=1.0 --> error
1492
- PASTAS_GT_023 = parse_version(ps.__version__) > parse_version("0.23.1")
1493
- if PASFILE_LEQ_022 and PASTAS_GT_023:
1494
- raise UserWarning(
1495
- f"This file was created with Pastas v{file_version} "
1496
- f"and cannot be loaded with Pastas v{ps.__version__} Please load and "
1497
- "save the file with Pastas 0.23 first to update the file "
1498
- "format."
1499
- )
1500
-
1501
- try:
1502
- # pastas>=0.15.0
1503
- ml = ps.io.base._load_model(mdict)
1504
- except AttributeError:
1505
- # pastas<0.15.0
1506
- ml = ps.io.base.load_model(mdict)
1507
- return ml
1508
-
1509
- @staticmethod
1510
- def _validate_input_series(series):
1511
- """Check if series is pandas.DataFrame or pandas.Series.
1512
-
1513
- Parameters
1514
- ----------
1515
- series : object
1516
- object to validate
1517
-
1518
- Raises
1519
- ------
1520
- TypeError
1521
- if object is not of type pandas.DataFrame or pandas.Series
1522
- """
1523
- if not (isinstance(series, pd.DataFrame) or isinstance(series, pd.Series)):
1524
- raise TypeError("Please provide pandas.DataFrame or pandas.Series!")
1525
- if isinstance(series, pd.DataFrame):
1526
- if series.columns.size > 1:
1527
- raise ValueError("Only DataFrames with one column are supported!")
1528
-
1529
- @staticmethod
1530
- def _set_series_name(series, name):
1531
- """Set series name to match user defined name in store.
1532
-
1533
- Parameters
1534
- ----------
1535
- series : pandas.Series or pandas.DataFrame
1536
- set name for this time series
1537
- name : str
1538
- name of the time series (used in the pastastore)
1539
- """
1540
- if isinstance(series, pd.Series):
1541
- series.name = name
1542
- # empty string on index name causes trouble when reading
1543
- # data from ArcticDB: TODO: check if still an issue?
1544
- if series.index.name == "":
1545
- series.index.name = None
1546
-
1547
- if isinstance(series, pd.DataFrame):
1548
- series.columns = [name]
1549
- # check for hydropandas objects which are instances of DataFrame but
1550
- # do have a name attribute
1551
- if hasattr(series, "name"):
1552
- series.name = name
1553
- return series
1554
-
1555
- @staticmethod
1556
- def _check_stressmodels_supported(ml):
1557
- supported_stressmodels = [
1558
- "StressModel",
1559
- "StressModel2",
1560
- "RechargeModel",
1561
- "WellModel",
1562
- "TarsoModel",
1563
- "Constant",
1564
- "LinearTrend",
1565
- "StepModel",
1566
- ]
1567
- if isinstance(ml, ps.Model):
1568
- smtyps = [sm._name for sm in ml.stressmodels.values()]
1569
- elif isinstance(ml, dict):
1570
- classkey = "stressmodel" if PASTAS_LEQ_022 else "class"
1571
- smtyps = [sm[classkey] for sm in ml["stressmodels"].values()]
1572
- check = isin(smtyps, supported_stressmodels)
1573
- if not all(check):
1574
- unsupported = set(smtyps) - set(supported_stressmodels)
1575
- raise NotImplementedError(
1576
- "PastaStore does not support storing models with the "
1577
- f"following stressmodels: {unsupported}"
1578
- )
1579
-
1580
- @staticmethod
1581
- def _check_model_series_names_for_store(ml):
1582
- prec_evap_model = ["RechargeModel", "TarsoModel"]
1583
-
1584
- if isinstance(ml, ps.Model):
1585
- series_names = [
1586
- istress.series.name
1587
- for sm in ml.stressmodels.values()
1588
- for istress in sm.stress
1589
- ]
1590
-
1591
- elif isinstance(ml, dict):
1592
- # non RechargeModel, Tarsomodel, WellModel stressmodels
1593
- classkey = "stressmodel" if PASTAS_LEQ_022 else "class"
1594
- if PASTAS_LEQ_022:
1595
- series_names = [
1596
- istress["name"]
1597
- for sm in ml["stressmodels"].values()
1598
- if sm[classkey] not in (prec_evap_model + ["WellModel"])
1599
- for istress in sm["stress"]
1600
- ]
1601
- else:
1602
- series_names = [
1603
- sm["stress"]["name"]
1604
- for sm in ml["stressmodels"].values()
1605
- if sm[classkey] not in (prec_evap_model + ["WellModel"])
1606
- ]
1607
-
1608
- # WellModel
1609
- if isin(
1610
- ["WellModel"],
1611
- [i[classkey] for i in ml["stressmodels"].values()],
1612
- ).any():
1613
- series_names += [
1614
- istress["name"]
1615
- for sm in ml["stressmodels"].values()
1616
- if sm[classkey] in ["WellModel"]
1617
- for istress in sm["stress"]
1618
- ]
1619
-
1620
- # RechargeModel, TarsoModel
1621
- if isin(
1622
- prec_evap_model,
1623
- [i[classkey] for i in ml["stressmodels"].values()],
1624
- ).any():
1625
- series_names += [
1626
- istress["name"]
1627
- for sm in ml["stressmodels"].values()
1628
- if sm[classkey] in prec_evap_model
1629
- for istress in [sm["prec"], sm["evap"]]
1630
- ]
1631
-
1632
- else:
1633
- raise TypeError("Expected pastas.Model or dict!")
1634
- if len(series_names) - len(set(series_names)) > 0:
1635
- msg = (
1636
- "There are multiple stresses series with the same name! "
1637
- "Each series name must be unique for the PastaStore!"
1638
- )
1639
- raise ValueError(msg)
1640
-
1641
- def _check_oseries_in_store(self, ml: Union[ps.Model, dict]):
1642
- """Check if Model oseries are contained in PastaStore (internal method).
1643
-
1644
- Parameters
1645
- ----------
1646
- ml : Union[ps.Model, dict]
1647
- pastas Model
1648
- """
1649
- if isinstance(ml, ps.Model):
1650
- name = ml.oseries.name
1651
- elif isinstance(ml, dict):
1652
- name = str(ml["oseries"]["name"])
1653
- else:
1654
- raise TypeError("Expected pastas.Model or dict!")
1655
- if name not in self.oseries.index:
1656
- msg = (
1657
- f"Cannot add model because oseries '{name}' "
1658
- "is not contained in store."
1659
- )
1660
- raise LookupError(msg)
1661
- # expensive check
1662
- if self.CHECK_MODEL_SERIES_VALUES and isinstance(ml, ps.Model):
1663
- s_org = self.get_oseries(name).squeeze().dropna()
1664
- if PASTAS_LEQ_022:
1665
- so = ml.oseries.series_original
1666
- else:
1667
- so = ml.oseries._series_original
1668
- if not so.dropna().equals(s_org):
1669
- raise ValueError(
1670
- f"Cannot add model because model oseries '{name}'"
1671
- " is different from stored oseries!"
1672
- )
1673
-
1674
- def _check_stresses_in_store(self, ml: Union[ps.Model, dict]):
1675
- """Check if stresses time series are contained in PastaStore (internal method).
1676
-
1677
- Parameters
1678
- ----------
1679
- ml : Union[ps.Model, dict]
1680
- pastas Model
1681
- """
1682
- prec_evap_model = ["RechargeModel", "TarsoModel"]
1683
- if isinstance(ml, ps.Model):
1684
- for sm in ml.stressmodels.values():
1685
- if sm._name in prec_evap_model:
1686
- stresses = [sm.prec, sm.evap]
1687
- else:
1688
- stresses = sm.stress
1689
- for s in stresses:
1690
- if str(s.name) not in self.stresses.index:
1691
- msg = (
1692
- f"Cannot add model because stress '{s.name}' "
1693
- "is not contained in store."
1694
- )
1695
- raise LookupError(msg)
1696
- if self.CHECK_MODEL_SERIES_VALUES:
1697
- s_org = self.get_stresses(s.name).squeeze()
1698
- if PASTAS_LEQ_022:
1699
- so = s.series_original
1700
- else:
1701
- so = s._series_original
1702
- if not so.equals(s_org):
1703
- raise ValueError(
1704
- f"Cannot add model because model stress "
1705
- f"'{s.name}' is different from stored stress!"
1706
- )
1707
- elif isinstance(ml, dict):
1708
- for sm in ml["stressmodels"].values():
1709
- classkey = "stressmodel" if PASTAS_LEQ_022 else "class"
1710
- if sm[classkey] in prec_evap_model:
1711
- stresses = [sm["prec"], sm["evap"]]
1712
- elif sm[classkey] in ["WellModel"]:
1713
- stresses = sm["stress"]
1714
- else:
1715
- stresses = sm["stress"] if PASTAS_LEQ_022 else [sm["stress"]]
1716
- for s in stresses:
1717
- if str(s["name"]) not in self.stresses.index:
1718
- msg = (
1719
- f"Cannot add model because stress '{s['name']}' "
1720
- "is not contained in store."
1721
- )
1722
- raise LookupError(msg)
1723
- else:
1724
- raise TypeError("Expected pastas.Model or dict!")
1725
-
1726
- def _stored_series_to_json(
1727
- self,
1728
- libname: str,
1729
- names: Optional[Union[list, str]] = None,
1730
- squeeze: bool = True,
1731
- progressbar: bool = False,
1732
- ):
1733
- """Write stored series to JSON.
1734
-
1735
- Parameters
1736
- ----------
1737
- libname : str
1738
- library name
1739
- names : Optional[Union[list, str]], optional
1740
- names of series, by default None
1741
- squeeze : bool, optional
1742
- return single entry as json string instead
1743
- of list, by default True
1744
- progressbar : bool, optional
1745
- show progressbar, by default False
1746
-
1747
- Returns
1748
- -------
1749
- files : list or str
1750
- list of series converted to JSON string or single string
1751
- if single entry is returned and squeeze is True
1752
- """
1753
- names = self._parse_names(names, libname=libname)
1754
- files = []
1755
- for n in tqdm(names, desc=libname) if progressbar else names:
1756
- s = self._get_series(libname, n, progressbar=False)
1757
- if isinstance(s, pd.Series):
1758
- s = s.to_frame()
1759
- try:
1760
- sjson = s.to_json(orient="columns")
1761
- except ValueError as e:
1762
- msg = (
1763
- f"DatetimeIndex of '{n}' probably contains NaT "
1764
- "or duplicate timestamps!"
1765
- )
1766
- raise ValueError(msg) from e
1767
- files.append(sjson)
1768
- if len(files) == 1 and squeeze:
1769
- return files[0]
1770
- else:
1771
- return files
1772
-
1773
- def _stored_metadata_to_json(
1774
- self,
1775
- libname: str,
1776
- names: Optional[Union[list, str]] = None,
1777
- squeeze: bool = True,
1778
- progressbar: bool = False,
1779
- ):
1780
- """Write metadata from stored series to JSON.
1781
-
1782
- Parameters
1783
- ----------
1784
- libname : str
1785
- library containing series
1786
- names : Optional[Union[list, str]], optional
1787
- names to parse, by default None
1788
- squeeze : bool, optional
1789
- return single entry as json string instead of list, by default True
1790
- progressbar : bool, optional
1791
- show progressbar, by default False
1792
-
1793
- Returns
1794
- -------
1795
- files : list or str
1796
- list of json string
1797
- """
1798
- names = self._parse_names(names, libname=libname)
1799
- files = []
1800
- for n in tqdm(names, desc=libname) if progressbar else names:
1801
- meta = self.get_metadata(libname, n, as_frame=False)
1802
- meta_json = json.dumps(meta, cls=PastasEncoder, indent=4)
1803
- files.append(meta_json)
1804
- if len(files) == 1 and squeeze:
1805
- return files[0]
1806
- else:
1807
- return files
1808
-
1809
- def _series_to_archive(
1810
- self,
1811
- archive,
1812
- libname: str,
1813
- names: Optional[Union[list, str]] = None,
1814
- progressbar: bool = True,
1815
- ):
1816
- """Write DataFrame or Series to zipfile (internal method).
1817
-
1818
- Parameters
1819
- ----------
1820
- archive : zipfile.ZipFile
1821
- reference to an archive to write data to
1822
- libname : str
1823
- name of the library to write to zipfile
1824
- names : str or list of str, optional
1825
- names of the time series to write to archive, by default None,
1826
- which writes all time series to archive
1827
- progressbar : bool, optional
1828
- show progressbar, by default True
1829
- """
1830
- names = self._parse_names(names, libname=libname)
1831
- for n in tqdm(names, desc=libname) if progressbar else names:
1832
- sjson = self._stored_series_to_json(
1833
- libname, names=n, progressbar=False, squeeze=True
1834
- )
1835
- meta_json = self._stored_metadata_to_json(
1836
- libname, names=n, progressbar=False, squeeze=True
1837
- )
1838
- archive.writestr(f"{libname}/{n}.json", sjson)
1839
- archive.writestr(f"{libname}/{n}_meta.json", meta_json)
1840
-
1841
- def _models_to_archive(self, archive, names=None, progressbar=True):
1842
- """Write pastas.Model to zipfile (internal method).
1843
-
1844
- Parameters
1845
- ----------
1846
- archive : zipfile.ZipFile
1847
- reference to an archive to write data to
1848
- names : str or list of str, optional
1849
- names of the models to write to archive, by default None,
1850
- which writes all models to archive
1851
- progressbar : bool, optional
1852
- show progressbar, by default True
1853
- """
1854
- names = self._parse_names(names, libname="models")
1855
- for n in tqdm(names, desc="models") if progressbar else names:
1856
- m = self.get_models(n, return_dict=True)
1857
- jsondict = json.dumps(m, cls=PastasEncoder, indent=4)
1858
- archive.writestr(f"models/{n}.pas", jsondict)
1859
-
1860
- @staticmethod
1861
- def _series_from_json(fjson: str, squeeze: bool = True):
1862
- """Load time series from JSON.
1863
-
1864
- Parameters
1865
- ----------
1866
- fjson : str
1867
- path to file
1868
- squeeze : bool, optional
1869
- squeeze time series object to obtain pandas Series
1870
-
1871
- Returns
1872
- -------
1873
- s : pd.DataFrame
1874
- DataFrame containing time series
1875
- """
1876
- s = pd.read_json(fjson, orient="columns", precise_float=True, dtype=False)
1877
- if not isinstance(s.index, pd.DatetimeIndex):
1878
- s.index = pd.to_datetime(s.index, unit="ms")
1879
- s = s.sort_index() # needed for some reason ...
1880
- if squeeze:
1881
- return s.squeeze()
1882
- return s
1883
-
1884
- @staticmethod
1885
- def _metadata_from_json(fjson: str):
1886
- """Load metadata dictionary from JSON.
1887
-
1888
- Parameters
1889
- ----------
1890
- fjson : str
1891
- path to file
1892
-
1893
- Returns
1894
- -------
1895
- meta : dict
1896
- dictionary containing metadata
1897
- """
1898
- with open(fjson, "r") as f:
1899
- meta = json.load(f)
1900
- return meta
1901
-
1902
- def _get_model_orphans(self):
1903
- """Get models whose oseries no longer exist in database.
1904
-
1905
- Returns
1906
- -------
1907
- dict
1908
- dictionary with oseries names as keys and lists of model names
1909
- as values
1910
- """
1911
- d = {}
1912
- for mlnam in tqdm(self.model_names, desc="Identifying model orphans"):
1913
- mdict = self.get_models(mlnam, return_dict=True)
1914
- onam = mdict["oseries"]["name"]
1915
- if onam not in self.oseries_names:
1916
- if onam in d:
1917
- d[onam] = d[onam].append(mlnam)
1918
- else:
1919
- d[onam] = [mlnam]
1920
- return d
1921
-
1922
-
1923
1365
  class ModelAccessor:
1924
1366
  """Object for managing access to stored models.
1925
1367