ionworks-api 0.1.0__py3-none-any.whl → 0.1.3__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.
- ionworks/__init__.py +27 -2
- ionworks/cell_instance.py +54 -48
- ionworks/cell_measurement.py +95 -58
- ionworks/cell_specification.py +106 -19
- ionworks/client.py +37 -8
- ionworks/errors.py +22 -5
- ionworks/job.py +90 -36
- ionworks/models.py +29 -37
- ionworks/pipeline.py +113 -4
- ionworks/simulation.py +127 -74
- ionworks/validators.py +553 -32
- {ionworks_api-0.1.0.dist-info → ionworks_api-0.1.3.dist-info}/METADATA +36 -17
- ionworks_api-0.1.3.dist-info/RECORD +15 -0
- ionworks_api-0.1.3.dist-info/licenses/LICENSE.md +21 -0
- ionworks_api-0.1.0.dist-info/RECORD +0 -14
- {ionworks_api-0.1.0.dist-info → ionworks_api-0.1.3.dist-info}/WHEEL +0 -0
ionworks/__init__.py
CHANGED
|
@@ -1,3 +1,28 @@
|
|
|
1
|
-
|
|
1
|
+
"""
|
|
2
|
+
Ionworks API Client.
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
A Python client for interacting with the Ionworks platform for battery cell
|
|
5
|
+
testing, simulation, and modeling.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
-------
|
|
9
|
+
>>> from ionworks import Ionworks
|
|
10
|
+
>>> client = Ionworks()
|
|
11
|
+
>>> specs = client.cell_spec.list()
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from .client import Ionworks
|
|
15
|
+
from .errors import IonworksError
|
|
16
|
+
from .validators import (
|
|
17
|
+
MeasurementValidationError,
|
|
18
|
+
get_dataframe_backend,
|
|
19
|
+
set_dataframe_backend,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"Ionworks",
|
|
24
|
+
"IonworksError",
|
|
25
|
+
"MeasurementValidationError",
|
|
26
|
+
"get_dataframe_backend",
|
|
27
|
+
"set_dataframe_backend",
|
|
28
|
+
]
|
ionworks/cell_instance.py
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cell instance client for managing individual cell records.
|
|
3
|
+
|
|
4
|
+
This module provides the :class:`CellInstanceClient` for creating, reading,
|
|
5
|
+
updating, and deleting cell instances, which represent individual physical
|
|
6
|
+
battery cells linked to a cell specification.
|
|
7
|
+
"""
|
|
8
|
+
|
|
1
9
|
from typing import Any
|
|
2
10
|
|
|
3
11
|
from pydantic import ValidationError
|
|
@@ -12,13 +20,20 @@ from .models import (
|
|
|
12
20
|
|
|
13
21
|
|
|
14
22
|
class CellInstanceClient:
|
|
15
|
-
|
|
23
|
+
"""Client for managing cell instances."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, client: Any) -> None:
|
|
26
|
+
"""Initialize the CellInstanceClient.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
client : Any
|
|
31
|
+
The HTTP client instance used for API calls.
|
|
32
|
+
"""
|
|
16
33
|
self.client = client
|
|
17
34
|
|
|
18
35
|
def get(self, cell_instance_id: str) -> CellInstance:
|
|
19
|
-
"""
|
|
20
|
-
Get a specific cell instance by ID.
|
|
21
|
-
"""
|
|
36
|
+
"""Get a specific cell instance by ID."""
|
|
22
37
|
endpoint = f"/cell_instances/{cell_instance_id}"
|
|
23
38
|
try:
|
|
24
39
|
response_data = self.client.get(endpoint)
|
|
@@ -30,9 +45,7 @@ class CellInstanceClient:
|
|
|
30
45
|
raise RuntimeError(f"API call to {endpoint} failed: {e}") from e
|
|
31
46
|
|
|
32
47
|
def get_by_slug(self, cell_instance_slug: str) -> CellInstance:
|
|
33
|
-
"""
|
|
34
|
-
Get a specific cell instance by slug.
|
|
35
|
-
"""
|
|
48
|
+
"""Get a specific cell instance by slug."""
|
|
36
49
|
endpoint = f"/cell_instances/slug/{cell_instance_slug}"
|
|
37
50
|
try:
|
|
38
51
|
response_data = self.client.get(endpoint)
|
|
@@ -43,9 +56,7 @@ class CellInstanceClient:
|
|
|
43
56
|
raise RuntimeError(f"API call to {endpoint} failed: {e}") from e
|
|
44
57
|
|
|
45
58
|
def list(self, cell_spec_id: str) -> list[CellInstance]:
|
|
46
|
-
"""
|
|
47
|
-
List all cell instances for a cell specification by specification ID.
|
|
48
|
-
"""
|
|
59
|
+
"""List all cell instances for a cell specification by specification ID."""
|
|
49
60
|
endpoint = f"/cell_specifications/{cell_spec_id}/cell_instances"
|
|
50
61
|
try:
|
|
51
62
|
response_data = self.client.get(endpoint)
|
|
@@ -74,14 +85,13 @@ class CellInstanceClient:
|
|
|
74
85
|
# return [CellMeasurementDetailBase(**item) for item in response_data]
|
|
75
86
|
|
|
76
87
|
def update(self, cell_instance_id: str, data: dict[str, Any]) -> CellInstance:
|
|
77
|
-
"""
|
|
78
|
-
Update an existing cell instance.
|
|
88
|
+
"""Update an existing cell instance.
|
|
79
89
|
|
|
80
90
|
Parameters
|
|
81
91
|
----------
|
|
82
92
|
cell_instance_id : str
|
|
83
93
|
The ID of the cell instance to update.
|
|
84
|
-
data : dict
|
|
94
|
+
data : dict[str, Any]
|
|
85
95
|
Dictionary containing the fields to update.
|
|
86
96
|
|
|
87
97
|
Returns
|
|
@@ -94,30 +104,36 @@ class CellInstanceClient:
|
|
|
94
104
|
return CellInstance(**response_data)
|
|
95
105
|
|
|
96
106
|
def delete(self, cell_instance_id: str) -> None:
|
|
97
|
-
"""
|
|
98
|
-
Delete a cell instance by ID.
|
|
99
|
-
"""
|
|
107
|
+
"""Delete a cell instance by ID."""
|
|
100
108
|
endpoint = f"/cell_instances/{cell_instance_id}"
|
|
101
109
|
self.client.delete(endpoint)
|
|
102
110
|
|
|
103
111
|
# Renamed and moved method from Data class
|
|
104
112
|
def detail(self, cell_instance_id: str) -> CellInstanceDetail:
|
|
105
|
-
"""
|
|
106
|
-
Loads the full details for a cell instance from a single API endpoint,
|
|
107
|
-
including its spec, instance data, steps DataFrame, and time series DataFrame.
|
|
108
|
-
Uses the /detail endpoint.
|
|
113
|
+
"""Load the full details for a cell instance.
|
|
109
114
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
115
|
+
Loads the full details for a cell instance from a single API endpoint,
|
|
116
|
+
including its spec, instance data, steps DataFrame, and time series
|
|
117
|
+
DataFrame. Uses the /detail endpoint.
|
|
113
118
|
|
|
114
|
-
|
|
115
|
-
|
|
119
|
+
Parameters
|
|
120
|
+
----------
|
|
121
|
+
cell_instance_id : str
|
|
122
|
+
The cell instance ID.
|
|
116
123
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
124
|
+
Returns
|
|
125
|
+
-------
|
|
126
|
+
CellInstanceDetail
|
|
127
|
+
Object containing validated cell instance data.
|
|
128
|
+
|
|
129
|
+
Raises
|
|
130
|
+
------
|
|
131
|
+
RuntimeError
|
|
132
|
+
If the API call fails.
|
|
133
|
+
ValueError
|
|
134
|
+
If the API response format is unexpected or parsing fails.
|
|
135
|
+
ValidationError
|
|
136
|
+
If the response data doesn't match expected models.
|
|
121
137
|
"""
|
|
122
138
|
endpoint = f"/cell_instances/{cell_instance_id}/detail"
|
|
123
139
|
try:
|
|
@@ -127,9 +143,9 @@ class CellInstanceClient:
|
|
|
127
143
|
except ValidationError as e:
|
|
128
144
|
# Pydantic validation error during parsing
|
|
129
145
|
raise ValueError(f"Response validation failed for {endpoint}: {e}") from e
|
|
130
|
-
except ValueError
|
|
146
|
+
except ValueError:
|
|
131
147
|
# Re-raise our explicit format validation errors
|
|
132
|
-
raise
|
|
148
|
+
raise
|
|
133
149
|
except Exception as e:
|
|
134
150
|
# Catch client errors or any other unexpected issues
|
|
135
151
|
raise RuntimeError(
|
|
@@ -137,19 +153,13 @@ class CellInstanceClient:
|
|
|
137
153
|
) from e
|
|
138
154
|
|
|
139
155
|
def create(self, cell_spec_id: str, data: dict[str, Any]) -> CellInstance:
|
|
140
|
-
"""
|
|
141
|
-
Create a new cell instance under a cell specification by
|
|
142
|
-
specification ID.
|
|
143
|
-
"""
|
|
156
|
+
"""Create a new cell instance under a cell specification by specification ID."""
|
|
144
157
|
endpoint = f"/cell_specifications/{cell_spec_id}/cell_instances"
|
|
145
158
|
response_data = self.client.post(endpoint, data)
|
|
146
159
|
return CellInstance(**response_data)
|
|
147
160
|
|
|
148
161
|
def create_or_get(self, cell_spec_id: str, data: dict[str, Any]) -> CellInstance:
|
|
149
|
-
"""
|
|
150
|
-
Create a new cell instance if it doesn't exist, otherwise get the
|
|
151
|
-
existing one.
|
|
152
|
-
"""
|
|
162
|
+
"""Create a new cell instance if it doesn't exist, otherwise get it."""
|
|
153
163
|
try:
|
|
154
164
|
return self.create(cell_spec_id, data)
|
|
155
165
|
except IonworksError as e:
|
|
@@ -157,11 +167,11 @@ class CellInstanceClient:
|
|
|
157
167
|
existing_id = e.data.get("existing_cell_instance_id")
|
|
158
168
|
if existing_id:
|
|
159
169
|
return self.get(existing_id)
|
|
160
|
-
raise
|
|
170
|
+
raise
|
|
161
171
|
|
|
162
172
|
def list_with_measurements(self, cell_spec_id: str) -> CellSpecificationInstances:
|
|
163
|
-
"""
|
|
164
|
-
|
|
173
|
+
"""List all instances and measurements for a cell specification.
|
|
174
|
+
|
|
165
175
|
Returns normalized data: separate lists for specification, instances, and
|
|
166
176
|
measurements.
|
|
167
177
|
"""
|
|
@@ -179,8 +189,8 @@ class CellInstanceClient:
|
|
|
179
189
|
def list_with_measurements_and_steps(
|
|
180
190
|
self, cell_spec_id: str
|
|
181
191
|
) -> CellSpecificationInstancesWithSteps:
|
|
182
|
-
"""
|
|
183
|
-
|
|
192
|
+
"""List all instances, measurements, and steps for a cell specification.
|
|
193
|
+
|
|
184
194
|
Returns normalized data: separate lists for specification, instances,
|
|
185
195
|
measurements, and steps.
|
|
186
196
|
"""
|
|
@@ -195,7 +205,3 @@ class CellInstanceClient:
|
|
|
195
205
|
raise ValueError(f"Response validation failed for {endpoint}: {e}") from e
|
|
196
206
|
except Exception as e:
|
|
197
207
|
raise RuntimeError(f"API call to {endpoint} failed: {e}") from e
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
class CellMeasurementClient:
|
|
201
|
-
pass
|
ionworks/cell_measurement.py
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
|
+
"""Cell measurement client for managing time series test data.
|
|
2
|
+
|
|
3
|
+
This module provides the :class:`CellMeasurementClient` for uploading,
|
|
4
|
+
retrieving, and managing measurement data from battery cell testing. It
|
|
5
|
+
supports efficient upload of large datasets using signed URLs and parquet
|
|
6
|
+
format.
|
|
7
|
+
"""
|
|
8
|
+
|
|
1
9
|
from __future__ import annotations
|
|
2
10
|
|
|
3
11
|
import io
|
|
4
12
|
from typing import Any
|
|
5
13
|
|
|
6
14
|
import pandas as pd
|
|
7
|
-
import
|
|
8
|
-
import pyarrow.parquet as pq
|
|
15
|
+
import polars as pl
|
|
9
16
|
from pydantic import ValidationError
|
|
10
17
|
import requests
|
|
11
18
|
|
|
@@ -18,46 +25,73 @@ from .models import (
|
|
|
18
25
|
ConfirmUploadResponse,
|
|
19
26
|
InitiateUploadResponse,
|
|
20
27
|
)
|
|
21
|
-
from .validators import df_to_dict_validator
|
|
28
|
+
from .validators import DataFrame, df_to_dict_validator, validate_measurement_data
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _to_polars(df: DataFrame | dict) -> pl.DataFrame:
|
|
32
|
+
"""Convert pandas DataFrame or dict to polars DataFrame.
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
df : DataFrame | dict
|
|
37
|
+
Input data as pandas DataFrame, polars DataFrame, or dict.
|
|
38
|
+
|
|
39
|
+
Returns
|
|
40
|
+
-------
|
|
41
|
+
pl.DataFrame
|
|
42
|
+
Polars DataFrame.
|
|
43
|
+
"""
|
|
44
|
+
if isinstance(df, pl.DataFrame):
|
|
45
|
+
return df
|
|
46
|
+
if isinstance(df, pd.DataFrame):
|
|
47
|
+
return pl.from_pandas(df)
|
|
48
|
+
if isinstance(df, dict):
|
|
49
|
+
return pl.DataFrame(df)
|
|
50
|
+
raise TypeError(f"Expected DataFrame or dict, got {type(df).__name__}")
|
|
22
51
|
|
|
23
52
|
|
|
24
53
|
class CellMeasurementClient:
|
|
25
|
-
|
|
54
|
+
"""Client for managing cell measurement data."""
|
|
55
|
+
|
|
56
|
+
#: Default timeout for signed URL uploads as (connect, read) in seconds.
|
|
57
|
+
UPLOAD_TIMEOUT: tuple[float, float] = (10, 300)
|
|
58
|
+
|
|
59
|
+
def __init__(self, client: Any) -> None:
|
|
60
|
+
"""Initialize the CellMeasurementClient.
|
|
61
|
+
|
|
62
|
+
Parameters
|
|
63
|
+
----------
|
|
64
|
+
client : Any
|
|
65
|
+
The HTTP client instance for making API calls.
|
|
66
|
+
"""
|
|
26
67
|
self.client = client
|
|
27
68
|
|
|
28
69
|
def list(self, cell_instance_id: str) -> list[CellMeasurement]:
|
|
29
|
-
"""
|
|
30
|
-
List all cell measurements for a cell instance by instance ID.
|
|
31
|
-
"""
|
|
70
|
+
"""List all cell measurements for a cell instance by instance ID."""
|
|
32
71
|
endpoint = f"/cell_instances/{cell_instance_id}/cell_measurements"
|
|
33
72
|
response_data = self.client.get(endpoint)
|
|
34
73
|
return [CellMeasurement(**item) for item in response_data]
|
|
35
74
|
|
|
36
75
|
def get(self, measurement_id: str) -> CellMeasurement:
|
|
37
|
-
"""
|
|
38
|
-
Get a specific cell measurement by its ID only.
|
|
39
|
-
"""
|
|
76
|
+
"""Get a specific cell measurement by its ID only."""
|
|
40
77
|
endpoint = f"/cell_measurements/{measurement_id}"
|
|
41
78
|
response_data = self.client.get(endpoint)
|
|
42
79
|
return CellMeasurement(**response_data)
|
|
43
80
|
|
|
44
81
|
def detail(self, measurement_id: str) -> CellMeasurementDetail:
|
|
45
|
-
"""
|
|
46
|
-
Get the full details for a cell measurement by measurement ID only.
|
|
47
|
-
"""
|
|
82
|
+
"""Get the full details for a cell measurement by measurement ID only."""
|
|
48
83
|
endpoint = f"/cell_measurements/{measurement_id}/detail"
|
|
49
84
|
response_data = self.client.get(endpoint)
|
|
50
85
|
return CellMeasurementDetail(**response_data)
|
|
51
86
|
|
|
52
87
|
def update(self, measurement_id: str, data: dict[str, Any]) -> CellMeasurement:
|
|
53
|
-
"""
|
|
54
|
-
Update an existing cell measurement.
|
|
88
|
+
"""Update an existing cell measurement.
|
|
55
89
|
|
|
56
90
|
Parameters
|
|
57
91
|
----------
|
|
58
92
|
measurement_id : str
|
|
59
93
|
The ID of the cell measurement to update.
|
|
60
|
-
data : dict
|
|
94
|
+
data : dict[str, Any]
|
|
61
95
|
Dictionary containing the fields to update.
|
|
62
96
|
|
|
63
97
|
Returns
|
|
@@ -70,17 +104,15 @@ class CellMeasurementClient:
|
|
|
70
104
|
return CellMeasurement(**response_data)
|
|
71
105
|
|
|
72
106
|
def delete(self, measurement_id: str) -> None:
|
|
73
|
-
"""
|
|
74
|
-
Delete a cell measurement by measurement ID only.
|
|
75
|
-
"""
|
|
107
|
+
"""Delete a cell measurement by measurement ID only."""
|
|
76
108
|
endpoint = f"/cell_measurements/{measurement_id}"
|
|
77
109
|
self.client.delete(endpoint)
|
|
78
110
|
|
|
79
111
|
def list_with_steps(
|
|
80
112
|
self, cell_instance_id: str
|
|
81
113
|
) -> CellInstanceMeasurementsWithSteps:
|
|
82
|
-
"""
|
|
83
|
-
|
|
114
|
+
"""List all measurements and steps for a specific cell instance.
|
|
115
|
+
|
|
84
116
|
Returns normalized data: separate lists for instance, measurements, and
|
|
85
117
|
steps.
|
|
86
118
|
"""
|
|
@@ -96,8 +128,7 @@ class CellMeasurementClient:
|
|
|
96
128
|
def get_by_slugs(
|
|
97
129
|
self, cell_instance_slug: str, measurement_slug: str
|
|
98
130
|
) -> CellMeasurement:
|
|
99
|
-
"""
|
|
100
|
-
Get a specific cell measurement by instance slug and measurement slug.
|
|
131
|
+
"""Get a specific cell measurement by instance slug and measurement slug.
|
|
101
132
|
|
|
102
133
|
Returns metadata only (no steps or time series).
|
|
103
134
|
"""
|
|
@@ -108,23 +139,21 @@ class CellMeasurementClient:
|
|
|
108
139
|
response_data = self.client.get(endpoint)
|
|
109
140
|
return CellMeasurement(**response_data)
|
|
110
141
|
|
|
111
|
-
def _dataframe_to_parquet(self, df:
|
|
112
|
-
"""
|
|
113
|
-
Convert a pandas DataFrame to parquet bytes.
|
|
142
|
+
def _dataframe_to_parquet(self, df: pl.DataFrame) -> bytes:
|
|
143
|
+
"""Convert a polars DataFrame to parquet bytes.
|
|
114
144
|
|
|
115
145
|
Parameters
|
|
116
146
|
----------
|
|
117
|
-
df :
|
|
147
|
+
df : pl.DataFrame
|
|
118
148
|
The DataFrame to convert.
|
|
119
149
|
|
|
120
150
|
Returns
|
|
121
151
|
-------
|
|
122
152
|
bytes
|
|
123
|
-
Parquet file content (uses
|
|
153
|
+
Parquet file content (uses zstd compression).
|
|
124
154
|
"""
|
|
125
|
-
table = pa.Table.from_pandas(df)
|
|
126
155
|
buffer = io.BytesIO()
|
|
127
|
-
|
|
156
|
+
df.write_parquet(buffer, compression="zstd")
|
|
128
157
|
return buffer.getvalue()
|
|
129
158
|
|
|
130
159
|
def _initiate_upload(
|
|
@@ -133,8 +162,7 @@ class CellMeasurementClient:
|
|
|
133
162
|
name: str,
|
|
134
163
|
notes: str | None = None,
|
|
135
164
|
) -> InitiateUploadResponse:
|
|
136
|
-
"""
|
|
137
|
-
Initiate a signed URL upload for a new measurement.
|
|
165
|
+
"""Initiate a signed URL upload for a new measurement.
|
|
138
166
|
|
|
139
167
|
Parameters
|
|
140
168
|
----------
|
|
@@ -142,7 +170,7 @@ class CellMeasurementClient:
|
|
|
142
170
|
The ID of the cell instance.
|
|
143
171
|
name : str
|
|
144
172
|
Name for the new measurement.
|
|
145
|
-
notes : str
|
|
173
|
+
notes : str | None
|
|
146
174
|
Optional notes for the measurement.
|
|
147
175
|
|
|
148
176
|
Returns
|
|
@@ -162,8 +190,7 @@ class CellMeasurementClient:
|
|
|
162
190
|
return InitiateUploadResponse(**response_data)
|
|
163
191
|
|
|
164
192
|
def _upload_to_signed_url(self, signed_url: str, parquet_bytes: bytes) -> None:
|
|
165
|
-
"""
|
|
166
|
-
Upload parquet bytes to a signed URL.
|
|
193
|
+
"""Upload parquet bytes to a signed URL.
|
|
167
194
|
|
|
168
195
|
Parameters
|
|
169
196
|
----------
|
|
@@ -185,6 +212,7 @@ class CellMeasurementClient:
|
|
|
185
212
|
url,
|
|
186
213
|
data=parquet_bytes,
|
|
187
214
|
headers={"Content-Type": "application/octet-stream"},
|
|
215
|
+
timeout=self.UPLOAD_TIMEOUT,
|
|
188
216
|
)
|
|
189
217
|
response.raise_for_status()
|
|
190
218
|
except requests.exceptions.RequestException as e:
|
|
@@ -197,8 +225,7 @@ class CellMeasurementClient:
|
|
|
197
225
|
measurement_data: dict[str, Any],
|
|
198
226
|
steps: dict[str, list[Any]] | None = None,
|
|
199
227
|
) -> ConfirmUploadResponse:
|
|
200
|
-
"""
|
|
201
|
-
Confirm a signed URL upload after successful file upload.
|
|
228
|
+
"""Confirm a signed URL upload after successful file upload.
|
|
202
229
|
|
|
203
230
|
This creates the measurement record in the database. The measurement
|
|
204
231
|
data must be passed again (same as initiate) to ensure no orphaned
|
|
@@ -210,10 +237,12 @@ class CellMeasurementClient:
|
|
|
210
237
|
The pre-generated ID for the measurement (from initiate).
|
|
211
238
|
cell_instance_id : str
|
|
212
239
|
The ID of the parent cell instance.
|
|
213
|
-
measurement_data : dict
|
|
214
|
-
Measurement metadata (name, notes, etc.)
|
|
215
|
-
|
|
216
|
-
|
|
240
|
+
measurement_data : dict[str, Any]
|
|
241
|
+
Measurement metadata (name, notes, etc.) — same as passed to
|
|
242
|
+
initiate.
|
|
243
|
+
steps : dict[str, list[Any]] | None
|
|
244
|
+
Pre-calculated steps data. If not provided, will be
|
|
245
|
+
auto-calculated.
|
|
217
246
|
|
|
218
247
|
Returns
|
|
219
248
|
-------
|
|
@@ -237,9 +266,7 @@ class CellMeasurementClient:
|
|
|
237
266
|
cell_instance_id: str,
|
|
238
267
|
measurement_detail: dict[str, Any],
|
|
239
268
|
) -> CellMeasurementBundleResponse:
|
|
240
|
-
"""
|
|
241
|
-
Create a new cell measurement with steps and time series data
|
|
242
|
-
for a cell instance.
|
|
269
|
+
"""Create a new cell measurement with steps and time series data.
|
|
243
270
|
|
|
244
271
|
Uses signed URL upload for better performance with large datasets.
|
|
245
272
|
Data is uploaded directly to storage as parquet, bypassing backend
|
|
@@ -250,9 +277,10 @@ class CellMeasurementClient:
|
|
|
250
277
|
----------
|
|
251
278
|
cell_instance_id : str
|
|
252
279
|
The ID of the cell instance to create the measurement for.
|
|
253
|
-
measurement_detail : dict
|
|
280
|
+
measurement_detail : dict[str, Any]
|
|
254
281
|
Dictionary containing 'measurement', 'steps', and 'time_series'.
|
|
255
|
-
|
|
282
|
+
|
|
283
|
+
- measurement: dict with 'name' (required) and 'notes'
|
|
256
284
|
- time_series: pandas DataFrame or dict with time series data
|
|
257
285
|
- steps: optional dict with pre-calculated steps data
|
|
258
286
|
|
|
@@ -261,24 +289,35 @@ class CellMeasurementClient:
|
|
|
261
289
|
CellMeasurementBundleResponse
|
|
262
290
|
Response containing the created measurement, steps count, and
|
|
263
291
|
file path.
|
|
292
|
+
|
|
293
|
+
Raises
|
|
294
|
+
------
|
|
295
|
+
MeasurementValidationError
|
|
296
|
+
If data validation fails. Validation checks include:
|
|
297
|
+
- Positive current should correspond to discharge
|
|
298
|
+
- Cumulative values should reset at each step
|
|
264
299
|
"""
|
|
265
300
|
measurement_info = measurement_detail["measurement"]
|
|
266
301
|
name = measurement_info["name"]
|
|
267
302
|
notes = measurement_info.get("notes")
|
|
268
303
|
|
|
269
|
-
# Step 1:
|
|
304
|
+
# Step 1: Convert time_series to polars DataFrame
|
|
305
|
+
# Accepts pandas DataFrame, polars DataFrame, or dict
|
|
306
|
+
time_series = _to_polars(measurement_detail["time_series"])
|
|
307
|
+
|
|
308
|
+
# Step 2: Validate the data before any upload
|
|
309
|
+
validate_measurement_data(time_series)
|
|
310
|
+
|
|
311
|
+
# Step 3: Initiate upload (validates metadata, returns signed URL, no DB record)
|
|
270
312
|
initiate_result = self._initiate_upload(cell_instance_id, name, notes)
|
|
271
313
|
|
|
272
|
-
# Step
|
|
273
|
-
time_series = measurement_detail["time_series"]
|
|
274
|
-
if isinstance(time_series, dict):
|
|
275
|
-
time_series = pd.DataFrame(time_series)
|
|
314
|
+
# Step 4: Convert to parquet
|
|
276
315
|
parquet_bytes = self._dataframe_to_parquet(time_series)
|
|
277
316
|
|
|
278
|
-
# Step
|
|
317
|
+
# Step 5: Upload to signed URL
|
|
279
318
|
self._upload_to_signed_url(initiate_result.signed_url, parquet_bytes)
|
|
280
319
|
|
|
281
|
-
# Step
|
|
320
|
+
# Step 6: Confirm upload (creates the measurement record)
|
|
282
321
|
steps = measurement_detail.get("steps")
|
|
283
322
|
if steps is not None:
|
|
284
323
|
# Convert DataFrame to dict if needed
|
|
@@ -301,15 +340,13 @@ class CellMeasurementClient:
|
|
|
301
340
|
cell_instance_id: str,
|
|
302
341
|
measurement_detail: dict[str, Any],
|
|
303
342
|
) -> CellMeasurementBundleResponse | CellMeasurement:
|
|
304
|
-
"""
|
|
305
|
-
Create a new cell measurement if it doesn't exist, otherwise get the
|
|
306
|
-
existing one.
|
|
343
|
+
"""Create a new cell measurement or get existing one.
|
|
307
344
|
|
|
308
345
|
Parameters
|
|
309
346
|
----------
|
|
310
347
|
cell_instance_id : str
|
|
311
348
|
The ID of the cell instance to create the measurement for.
|
|
312
|
-
measurement_detail : dict
|
|
349
|
+
measurement_detail : dict[str, Any]
|
|
313
350
|
Dictionary containing 'measurement', 'steps', and 'time_series'.
|
|
314
351
|
|
|
315
352
|
Returns
|
|
@@ -335,5 +372,5 @@ class CellMeasurementClient:
|
|
|
335
372
|
raise ValueError(
|
|
336
373
|
f"Measurement '{measurement_name}' reported as duplicate "
|
|
337
374
|
"but could not be found"
|
|
338
|
-
)
|
|
375
|
+
) from e
|
|
339
376
|
raise
|