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/cell_specification.py
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
"""Cell specification client for managing cell type definitions.
|
|
2
|
+
|
|
3
|
+
This module provides the :class:`CellSpecificationClient` for creating,
|
|
4
|
+
reading, updating, and deleting cell specifications, which define the
|
|
5
|
+
properties of battery cell types (manufacturer, chemistry, ratings, etc.).
|
|
6
|
+
"""
|
|
7
|
+
|
|
1
8
|
from typing import Any
|
|
2
9
|
|
|
3
10
|
from pydantic import ValidationError
|
|
@@ -8,12 +15,42 @@ from .models import CellSpecification
|
|
|
8
15
|
|
|
9
16
|
|
|
10
17
|
class CellSpecificationClient:
|
|
11
|
-
|
|
18
|
+
"""Client for managing cell specifications.
|
|
19
|
+
|
|
20
|
+
Provides methods to create, read, update, and delete cell specifications,
|
|
21
|
+
which define the properties of battery cell types (manufacturer, chemistry,
|
|
22
|
+
ratings, etc.).
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, client: Any) -> None:
|
|
26
|
+
"""Initialize the CellSpecificationClient.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
client : Any
|
|
31
|
+
The HTTP client instance for making API requests.
|
|
32
|
+
"""
|
|
12
33
|
self.client = client
|
|
13
34
|
|
|
14
35
|
def get(self, cell_spec_id: str) -> CellSpecification:
|
|
15
|
-
"""
|
|
16
|
-
|
|
36
|
+
"""Get a specific cell specification by ID.
|
|
37
|
+
|
|
38
|
+
Parameters
|
|
39
|
+
----------
|
|
40
|
+
cell_spec_id : str
|
|
41
|
+
The ID of the cell specification to retrieve.
|
|
42
|
+
|
|
43
|
+
Returns
|
|
44
|
+
-------
|
|
45
|
+
CellSpecification
|
|
46
|
+
The requested cell specification object.
|
|
47
|
+
|
|
48
|
+
Raises
|
|
49
|
+
------
|
|
50
|
+
ValueError
|
|
51
|
+
If the response format is invalid.
|
|
52
|
+
RuntimeError
|
|
53
|
+
If the API call fails.
|
|
17
54
|
"""
|
|
18
55
|
endpoint = f"/cell_specifications/{cell_spec_id}"
|
|
19
56
|
try:
|
|
@@ -26,8 +63,19 @@ class CellSpecificationClient:
|
|
|
26
63
|
raise RuntimeError(f"API call to {endpoint} failed: {e}") from e
|
|
27
64
|
|
|
28
65
|
def list(self) -> list[CellSpecification]:
|
|
29
|
-
"""
|
|
30
|
-
|
|
66
|
+
"""List all cell specifications.
|
|
67
|
+
|
|
68
|
+
Returns
|
|
69
|
+
-------
|
|
70
|
+
list[CellSpecification]
|
|
71
|
+
A list of all cell specification objects.
|
|
72
|
+
|
|
73
|
+
Raises
|
|
74
|
+
------
|
|
75
|
+
ValueError
|
|
76
|
+
If the response format is not a list or items are invalid.
|
|
77
|
+
RuntimeError
|
|
78
|
+
If the API call fails.
|
|
31
79
|
"""
|
|
32
80
|
endpoint = "/cell_specifications"
|
|
33
81
|
try:
|
|
@@ -45,17 +93,37 @@ class CellSpecificationClient:
|
|
|
45
93
|
raise RuntimeError(f"API call to {endpoint} failed: {e}") from e
|
|
46
94
|
|
|
47
95
|
def create(self, data: dict[str, Any]) -> CellSpecification:
|
|
48
|
-
"""
|
|
49
|
-
|
|
96
|
+
"""Create a new cell specification.
|
|
97
|
+
|
|
98
|
+
Parameters
|
|
99
|
+
----------
|
|
100
|
+
data : dict[str, Any]
|
|
101
|
+
Dictionary containing the cell specification data.
|
|
102
|
+
|
|
103
|
+
Returns
|
|
104
|
+
-------
|
|
105
|
+
CellSpecification
|
|
106
|
+
The newly created cell specification object.
|
|
50
107
|
"""
|
|
51
108
|
endpoint = "/cell_specifications"
|
|
52
109
|
response_data = self.client.post(endpoint, data)
|
|
53
110
|
return CellSpecification(**response_data)
|
|
54
111
|
|
|
55
112
|
def create_or_get(self, data: dict[str, Any]) -> CellSpecification:
|
|
56
|
-
"""
|
|
57
|
-
|
|
58
|
-
|
|
113
|
+
"""Create a new cell specification or get an existing one.
|
|
114
|
+
|
|
115
|
+
Creates a new cell specification if it doesn't exist, otherwise returns
|
|
116
|
+
the existing one.
|
|
117
|
+
|
|
118
|
+
Parameters
|
|
119
|
+
----------
|
|
120
|
+
data : dict[str, Any]
|
|
121
|
+
Dictionary containing the cell specification data.
|
|
122
|
+
|
|
123
|
+
Returns
|
|
124
|
+
-------
|
|
125
|
+
CellSpecification
|
|
126
|
+
The cell specification object (newly created or existing).
|
|
59
127
|
"""
|
|
60
128
|
try:
|
|
61
129
|
return self.create(data)
|
|
@@ -64,39 +132,58 @@ class CellSpecificationClient:
|
|
|
64
132
|
existing_id = e.data.get("existing_cell_specification_id")
|
|
65
133
|
if existing_id:
|
|
66
134
|
return self.get(existing_id)
|
|
67
|
-
raise
|
|
135
|
+
raise
|
|
68
136
|
|
|
69
137
|
def update(self, cell_spec_id: str, data: dict[str, Any]) -> CellSpecification:
|
|
70
|
-
"""
|
|
71
|
-
Update an existing cell specification.
|
|
138
|
+
"""Update an existing cell specification.
|
|
72
139
|
|
|
73
140
|
Parameters
|
|
74
141
|
----------
|
|
75
142
|
cell_spec_id : str
|
|
76
143
|
The ID of the cell specification to update.
|
|
77
|
-
data : dict
|
|
144
|
+
data : dict[str, Any]
|
|
78
145
|
Dictionary containing the fields to update. Supports nested
|
|
79
146
|
component/material data for upsert.
|
|
80
147
|
|
|
81
148
|
Returns
|
|
82
149
|
-------
|
|
83
150
|
CellSpecification
|
|
84
|
-
The updated cell specification.
|
|
151
|
+
The updated cell specification object.
|
|
85
152
|
"""
|
|
86
153
|
endpoint = f"/cell_specifications/{cell_spec_id}"
|
|
87
154
|
response_data = self.client.put(endpoint, data)
|
|
88
155
|
return CellSpecification(**response_data)
|
|
89
156
|
|
|
90
157
|
def delete(self, cell_spec_id: str) -> None:
|
|
91
|
-
"""
|
|
92
|
-
|
|
158
|
+
"""Delete a cell specification by ID.
|
|
159
|
+
|
|
160
|
+
Parameters
|
|
161
|
+
----------
|
|
162
|
+
cell_spec_id : str
|
|
163
|
+
The ID of the cell specification to delete.
|
|
93
164
|
"""
|
|
94
165
|
endpoint = f"/cell_specifications/{cell_spec_id}"
|
|
95
166
|
self.client.delete(endpoint)
|
|
96
167
|
|
|
97
168
|
def get_by_slug(self, cell_spec_slug: str) -> CellSpecification:
|
|
98
|
-
"""
|
|
99
|
-
|
|
169
|
+
"""Get a specific cell specification by slug.
|
|
170
|
+
|
|
171
|
+
Parameters
|
|
172
|
+
----------
|
|
173
|
+
cell_spec_slug : str
|
|
174
|
+
The slug of the cell specification to retrieve.
|
|
175
|
+
|
|
176
|
+
Returns
|
|
177
|
+
-------
|
|
178
|
+
CellSpecification
|
|
179
|
+
The requested cell specification object.
|
|
180
|
+
|
|
181
|
+
Raises
|
|
182
|
+
------
|
|
183
|
+
ValueError
|
|
184
|
+
If the response format is invalid.
|
|
185
|
+
RuntimeError
|
|
186
|
+
If the API call fails.
|
|
100
187
|
"""
|
|
101
188
|
endpoint = f"/cell_specifications/slug/{cell_spec_slug}"
|
|
102
189
|
try:
|
ionworks/client.py
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
"""Main client module for the Ionworks API.
|
|
2
|
+
|
|
3
|
+
This module provides the :class:`Ionworks` client, which is the main entry point
|
|
4
|
+
for interacting with the Ionworks API. It handles authentication, request/response
|
|
5
|
+
processing, and provides access to all API resources through sub-clients.
|
|
6
|
+
"""
|
|
7
|
+
|
|
1
8
|
import os
|
|
2
9
|
from typing import Any, cast
|
|
3
10
|
|
|
@@ -11,21 +18,41 @@ from .errors import IonworksError
|
|
|
11
18
|
from .job import JobClient
|
|
12
19
|
from .pipeline import PipelineClient
|
|
13
20
|
from .simulation import SimulationClient
|
|
21
|
+
from .validators import set_dataframe_backend
|
|
14
22
|
|
|
15
23
|
|
|
16
24
|
class Ionworks:
|
|
17
|
-
|
|
25
|
+
"""Client for interacting with the Ionworks API.
|
|
26
|
+
|
|
27
|
+
Handles authentication, request/response processing, and provides access
|
|
28
|
+
to all API resources through sub-clients.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
api_key: str | None = None,
|
|
34
|
+
api_url: str | None = None,
|
|
35
|
+
dataframe_backend: str | None = None,
|
|
36
|
+
) -> None:
|
|
18
37
|
"""Initialize Ionworks client.
|
|
19
38
|
|
|
20
39
|
Parameters
|
|
21
40
|
----------
|
|
22
|
-
api_key : str
|
|
23
|
-
API key. If not provided, will look for IONWORKS_API_KEY env var
|
|
24
|
-
api_url : str
|
|
25
|
-
API URL. If not provided, will look for IONWORKS_API_URL env var
|
|
41
|
+
api_key : str | None
|
|
42
|
+
API key. If not provided, will look for IONWORKS_API_KEY env var.
|
|
43
|
+
api_url : str | None
|
|
44
|
+
API URL. If not provided, will look for IONWORKS_API_URL env var.
|
|
45
|
+
dataframe_backend : str | None
|
|
46
|
+
DataFrame backend for returned data: "polars" or "pandas".
|
|
47
|
+
If not provided, uses IONWORKS_DATAFRAME_BACKEND env var
|
|
48
|
+
(defaults to "polars").
|
|
26
49
|
"""
|
|
27
50
|
load_dotenv()
|
|
28
51
|
|
|
52
|
+
# Set DataFrame backend (explicit param > env var > default "polars")
|
|
53
|
+
if dataframe_backend is not None:
|
|
54
|
+
set_dataframe_backend(dataframe_backend)
|
|
55
|
+
|
|
29
56
|
self.api_key = api_key or os.getenv("IONWORKS_API_KEY")
|
|
30
57
|
if not self.api_key:
|
|
31
58
|
raise ValueError(
|
|
@@ -96,7 +123,7 @@ class Ionworks:
|
|
|
96
123
|
except requests.exceptions.JSONDecodeError:
|
|
97
124
|
error_msg += f": {e.response.text}"
|
|
98
125
|
else:
|
|
99
|
-
error_msg += f": {
|
|
126
|
+
error_msg += f": {e!s}"
|
|
100
127
|
# Raise the custom error for general request issues
|
|
101
128
|
raise IonworksError(error_msg) from None
|
|
102
129
|
|
|
@@ -119,8 +146,10 @@ class Ionworks:
|
|
|
119
146
|
def health_check(self) -> dict[str, Any]:
|
|
120
147
|
"""Check the health of the Ionworks API.
|
|
121
148
|
|
|
122
|
-
Returns
|
|
123
|
-
|
|
149
|
+
Returns
|
|
150
|
+
-------
|
|
151
|
+
dict[str, Any]
|
|
152
|
+
Health check response.
|
|
124
153
|
"""
|
|
125
154
|
response_data = self.get("/healthz")
|
|
126
155
|
return cast(dict[str, Any], response_data)
|
ionworks/errors.py
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom exception classes for the Ionworks API client.
|
|
3
|
+
|
|
4
|
+
This module defines :class:`IonworksError`, which is raised when API requests
|
|
5
|
+
fail or return error responses.
|
|
6
|
+
"""
|
|
7
|
+
|
|
1
8
|
from typing import Any
|
|
2
9
|
|
|
3
10
|
|
|
@@ -7,18 +14,27 @@ class IonworksError(Exception):
|
|
|
7
14
|
Attributes
|
|
8
15
|
----------
|
|
9
16
|
message : str
|
|
10
|
-
A string description of the error
|
|
17
|
+
A string description of the error.
|
|
11
18
|
data : dict[str, Any] | None
|
|
12
|
-
Structured error data if available (e.g., from API error response)
|
|
19
|
+
Structured error data if available (e.g., from API error response).
|
|
13
20
|
status_code : int | None
|
|
14
|
-
HTTP status code if applicable
|
|
21
|
+
HTTP status code if applicable.
|
|
15
22
|
"""
|
|
16
23
|
|
|
17
24
|
def __init__(
|
|
18
25
|
self,
|
|
19
26
|
message: str | dict[str, Any],
|
|
20
27
|
status_code: int | None = None,
|
|
21
|
-
):
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Initialize the IonworksError.
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
message : str | dict[str, Any]
|
|
34
|
+
Error message string or dict containing error details.
|
|
35
|
+
status_code : int | None
|
|
36
|
+
Optional HTTP status code.
|
|
37
|
+
"""
|
|
22
38
|
self.status_code = status_code
|
|
23
39
|
|
|
24
40
|
# Parse message into string and optional data dict
|
|
@@ -31,5 +47,6 @@ class IonworksError(Exception):
|
|
|
31
47
|
|
|
32
48
|
super().__init__(self.message)
|
|
33
49
|
|
|
34
|
-
def __str__(self):
|
|
50
|
+
def __str__(self) -> str:
|
|
51
|
+
"""Return string representation of the error."""
|
|
35
52
|
return f"error code: {self.status_code}, message: {self.message}"
|
ionworks/job.py
CHANGED
|
@@ -1,18 +1,26 @@
|
|
|
1
|
-
|
|
1
|
+
"""Job client for managing asynchronous jobs.
|
|
2
|
+
|
|
3
|
+
This module provides the :class:`JobClient` for submitting, monitoring, and
|
|
4
|
+
managing background jobs in the Ionworks platform.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any
|
|
2
8
|
|
|
3
9
|
from pydantic import BaseModel, ValidationError
|
|
4
10
|
|
|
5
11
|
|
|
6
|
-
# Model for the data required to create ANY job
|
|
7
12
|
class JobCreationPayload(BaseModel):
|
|
13
|
+
"""Payload for creating a job."""
|
|
14
|
+
|
|
8
15
|
job_type: str
|
|
9
16
|
params: dict[str, Any]
|
|
10
17
|
priority: int = 5 # Default priority
|
|
11
|
-
callback_url:
|
|
18
|
+
callback_url: str | None = None # Optional callback
|
|
12
19
|
|
|
13
20
|
|
|
14
|
-
# Model for the detailed job response
|
|
15
21
|
class JobResponse(BaseModel):
|
|
22
|
+
"""Response model for job details."""
|
|
23
|
+
|
|
16
24
|
job_id: str
|
|
17
25
|
status: str
|
|
18
26
|
job_type: str
|
|
@@ -20,18 +28,30 @@ class JobResponse(BaseModel):
|
|
|
20
28
|
priority: int
|
|
21
29
|
created_at: str
|
|
22
30
|
updated_at: str
|
|
23
|
-
metadata:
|
|
24
|
-
error:
|
|
25
|
-
result:
|
|
31
|
+
metadata: dict[str, Any] | None
|
|
32
|
+
error: str | None
|
|
33
|
+
result: dict[str, Any] | None
|
|
26
34
|
|
|
27
35
|
|
|
28
36
|
class JobClient:
|
|
29
|
-
|
|
37
|
+
"""Client for managing asynchronous jobs.
|
|
38
|
+
|
|
39
|
+
This class provides methods to create, retrieve, list, and cancel jobs
|
|
40
|
+
in the Ionworks platform.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, client: Any) -> None:
|
|
44
|
+
"""Initialize the JobClient.
|
|
45
|
+
|
|
46
|
+
Parameters
|
|
47
|
+
----------
|
|
48
|
+
client : Any
|
|
49
|
+
The HTTP client instance used for API requests.
|
|
50
|
+
"""
|
|
30
51
|
self.client = client
|
|
31
52
|
|
|
32
53
|
def create(self, payload: JobCreationPayload) -> JobResponse:
|
|
33
|
-
"""
|
|
34
|
-
Submit a job using the provided payload.
|
|
54
|
+
"""Submit a job using the provided payload.
|
|
35
55
|
|
|
36
56
|
Parameters
|
|
37
57
|
----------
|
|
@@ -59,53 +79,87 @@ class JobClient:
|
|
|
59
79
|
return JobResponse(**response_data)
|
|
60
80
|
except ValidationError as e:
|
|
61
81
|
# Catch Pydantic validation errors specifically
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
) from e
|
|
82
|
+
msg = f"Invalid response format received from {endpoint}: {e}"
|
|
83
|
+
raise ValueError(msg) from e
|
|
65
84
|
# RequestExceptions (including HTTPError) are handled by client._post
|
|
66
85
|
|
|
67
86
|
def get(self, job_id: str) -> JobResponse:
|
|
68
|
-
"""
|
|
69
|
-
|
|
87
|
+
"""Get the status and details of a specific job.
|
|
88
|
+
|
|
89
|
+
Parameters
|
|
90
|
+
----------
|
|
91
|
+
job_id : str
|
|
92
|
+
The ID of the job to retrieve.
|
|
93
|
+
|
|
94
|
+
Returns
|
|
95
|
+
-------
|
|
96
|
+
JobResponse
|
|
97
|
+
Job details and current status.
|
|
98
|
+
|
|
99
|
+
Raises
|
|
100
|
+
------
|
|
101
|
+
ValueError
|
|
102
|
+
If the response parsing fails.
|
|
70
103
|
"""
|
|
71
104
|
endpoint = f"/jobs/{job_id}"
|
|
72
105
|
try:
|
|
73
106
|
response_data = self.client.get(endpoint)
|
|
74
107
|
return JobResponse(**response_data)
|
|
75
108
|
except ValidationError as e:
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
) from e
|
|
109
|
+
msg = f"Invalid response format received from {endpoint}: {e}"
|
|
110
|
+
raise ValueError(msg) from e
|
|
79
111
|
|
|
80
112
|
def list(self) -> list[JobResponse]:
|
|
81
|
-
"""
|
|
82
|
-
|
|
113
|
+
"""List all jobs.
|
|
114
|
+
|
|
115
|
+
Returns
|
|
116
|
+
-------
|
|
117
|
+
list[JobResponse]
|
|
118
|
+
List of all jobs with their details.
|
|
119
|
+
|
|
120
|
+
Raises
|
|
121
|
+
------
|
|
122
|
+
ValueError
|
|
123
|
+
If the response is not a list or job data format is invalid.
|
|
83
124
|
"""
|
|
84
125
|
endpoint = "/jobs/"
|
|
126
|
+
response_data = self.client.get(endpoint)
|
|
127
|
+
# Ensure response_data is a list before list comprehension
|
|
128
|
+
if not isinstance(response_data, list):
|
|
129
|
+
msg = (
|
|
130
|
+
f"Unexpected response format from {endpoint}: expected a list, "
|
|
131
|
+
f"got {type(response_data).__name__}"
|
|
132
|
+
)
|
|
133
|
+
raise ValueError(msg)
|
|
134
|
+
# Apply validation within list comprehension
|
|
85
135
|
try:
|
|
86
|
-
response_data = self.client.get(endpoint)
|
|
87
|
-
# Ensure response_data is a list before list comprehension
|
|
88
|
-
if not isinstance(response_data, list):
|
|
89
|
-
raise ValueError(
|
|
90
|
-
f"Unexpected response format from {endpoint}: expected a list, "
|
|
91
|
-
"got {type(response_data).__name__}"
|
|
92
|
-
)
|
|
93
|
-
# Apply validation within list comprehension
|
|
94
136
|
return [JobResponse(**job) for job in response_data]
|
|
95
137
|
except ValidationError as e:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
) from e
|
|
138
|
+
msg = f"Invalid job data format received from {endpoint}: {e}"
|
|
139
|
+
raise ValueError(msg) from e
|
|
99
140
|
|
|
100
141
|
def cancel(self, job_id: str) -> JobResponse:
|
|
101
|
-
"""
|
|
102
|
-
|
|
142
|
+
"""Cancel a job.
|
|
143
|
+
|
|
144
|
+
Parameters
|
|
145
|
+
----------
|
|
146
|
+
job_id : str
|
|
147
|
+
The ID of the job to cancel.
|
|
148
|
+
|
|
149
|
+
Returns
|
|
150
|
+
-------
|
|
151
|
+
JobResponse
|
|
152
|
+
Updated job details with cancelled status.
|
|
153
|
+
|
|
154
|
+
Raises
|
|
155
|
+
------
|
|
156
|
+
ValueError
|
|
157
|
+
If the response parsing fails.
|
|
103
158
|
"""
|
|
104
159
|
endpoint = f"/jobs/{job_id}/cancel"
|
|
105
160
|
try:
|
|
106
161
|
response_data = self.client.post(endpoint, json_payload={})
|
|
107
162
|
return JobResponse(**response_data)
|
|
108
163
|
except ValidationError as e:
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
) from e
|
|
164
|
+
msg = f"Invalid response format received from {endpoint}: {e}"
|
|
165
|
+
raise ValueError(msg) from e
|
ionworks/models.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Pydantic models for the Ionworks API client.
|
|
1
|
+
"""Pydantic models for the Ionworks API client.
|
|
3
2
|
|
|
4
3
|
These models use extra="allow" to accept any fields from the API response,
|
|
5
4
|
letting the API handle validation. Required fields are kept minimal.
|
|
@@ -7,17 +6,15 @@ letting the API handle validation. Required fields are kept minimal.
|
|
|
7
6
|
|
|
8
7
|
from typing import Any
|
|
9
8
|
|
|
10
|
-
import pandas as pd # type: ignore
|
|
11
9
|
from pydantic import BaseModel, ConfigDict, field_validator
|
|
12
10
|
|
|
13
|
-
from .validators import dict_to_df_validator
|
|
11
|
+
from .validators import DataFrame, dict_to_df_validator
|
|
14
12
|
|
|
15
13
|
# --- Cell Specification Models --- #
|
|
16
14
|
|
|
17
15
|
|
|
18
16
|
class CellSpecification(BaseModel):
|
|
19
|
-
"""
|
|
20
|
-
Cell specification model - accepts any fields from the API.
|
|
17
|
+
"""Cell specification model - accepts any fields from the API.
|
|
21
18
|
|
|
22
19
|
The API returns nested component/material data and ratings objects.
|
|
23
20
|
This model is permissive to allow the API to define the schema.
|
|
@@ -34,9 +31,7 @@ class CellSpecification(BaseModel):
|
|
|
34
31
|
|
|
35
32
|
|
|
36
33
|
class CellInstance(BaseModel):
|
|
37
|
-
"""
|
|
38
|
-
Cell instance model - accepts any fields from the API.
|
|
39
|
-
"""
|
|
34
|
+
"""Cell instance model - accepts any fields from the API."""
|
|
40
35
|
|
|
41
36
|
model_config = ConfigDict(extra="allow")
|
|
42
37
|
|
|
@@ -48,9 +43,7 @@ class CellInstance(BaseModel):
|
|
|
48
43
|
|
|
49
44
|
|
|
50
45
|
class CellMeasurement(BaseModel):
|
|
51
|
-
"""
|
|
52
|
-
Cell measurement model - accepts any fields from the API.
|
|
53
|
-
"""
|
|
46
|
+
"""Cell measurement model - accepts any fields from the API."""
|
|
54
47
|
|
|
55
48
|
model_config = ConfigDict(extra="allow")
|
|
56
49
|
|
|
@@ -94,18 +87,18 @@ class ConfirmUploadResponse(BaseModel):
|
|
|
94
87
|
class CellMeasurementDetailBase(BaseModel):
|
|
95
88
|
"""Base model for measurement detail with steps and time series."""
|
|
96
89
|
|
|
90
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
91
|
+
|
|
97
92
|
measurement: CellMeasurement
|
|
98
|
-
steps:
|
|
99
|
-
time_series:
|
|
93
|
+
steps: DataFrame
|
|
94
|
+
time_series: DataFrame
|
|
100
95
|
|
|
101
96
|
@field_validator("steps", "time_series", mode="before")
|
|
102
97
|
@classmethod
|
|
103
98
|
def convert_dict_to_df(cls, v: Any) -> Any:
|
|
99
|
+
"""Convert dictionary to DataFrame (polars or pandas based on config)."""
|
|
104
100
|
return dict_to_df_validator(v)
|
|
105
101
|
|
|
106
|
-
class Config:
|
|
107
|
-
arbitrary_types_allowed = True # Allow pandas DataFrame
|
|
108
|
-
|
|
109
102
|
|
|
110
103
|
class CellInstanceDetail(BaseModel):
|
|
111
104
|
"""Detail model for a cell instance with all measurements."""
|
|
@@ -126,32 +119,32 @@ class CellMeasurementDetail(CellMeasurementDetailBase):
|
|
|
126
119
|
|
|
127
120
|
|
|
128
121
|
class CellInstanceMeasurementsWithSteps(BaseModel):
|
|
129
|
-
"""
|
|
130
|
-
|
|
131
|
-
instance
|
|
132
|
-
|
|
133
|
-
|
|
122
|
+
"""Response model for measurements + steps for a cell instance.
|
|
123
|
+
|
|
124
|
+
Returns normalized data: separate lists for instance, measurements,
|
|
125
|
+
and steps. Includes parent information for consistency with other
|
|
126
|
+
group endpoints.
|
|
134
127
|
"""
|
|
135
128
|
|
|
129
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
130
|
+
|
|
136
131
|
specification: CellSpecification
|
|
137
132
|
instance: CellInstance
|
|
138
133
|
measurements: list[CellMeasurement]
|
|
139
|
-
steps: list[
|
|
134
|
+
steps: list[DataFrame]
|
|
140
135
|
|
|
141
136
|
@field_validator("steps", mode="before")
|
|
142
137
|
@classmethod
|
|
143
138
|
def convert_dict_to_df(cls, v: Any) -> Any:
|
|
139
|
+
"""Convert dictionary or list of dictionaries to DataFrames."""
|
|
144
140
|
if isinstance(v, list):
|
|
145
141
|
return [dict_to_df_validator(item) for item in v]
|
|
146
142
|
return dict_to_df_validator(v)
|
|
147
143
|
|
|
148
|
-
class Config:
|
|
149
|
-
arbitrary_types_allowed = True
|
|
150
|
-
|
|
151
144
|
|
|
152
145
|
class CellSpecificationInstances(BaseModel):
|
|
153
|
-
"""
|
|
154
|
-
|
|
146
|
+
"""Response model for all instances associated with a cell specification.
|
|
147
|
+
|
|
155
148
|
Returns normalized data: separate lists for specification, instances,
|
|
156
149
|
and measurements.
|
|
157
150
|
"""
|
|
@@ -162,24 +155,23 @@ class CellSpecificationInstances(BaseModel):
|
|
|
162
155
|
|
|
163
156
|
|
|
164
157
|
class CellSpecificationInstancesWithSteps(BaseModel):
|
|
165
|
-
"""
|
|
166
|
-
|
|
167
|
-
specification
|
|
168
|
-
|
|
169
|
-
and steps.
|
|
158
|
+
"""Response model for instances + measurements + steps for a cell spec.
|
|
159
|
+
|
|
160
|
+
Returns normalized data: separate lists for specification, instances,
|
|
161
|
+
measurements, and steps.
|
|
170
162
|
"""
|
|
171
163
|
|
|
164
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
165
|
+
|
|
172
166
|
specification: CellSpecification
|
|
173
167
|
instances: list[CellInstance]
|
|
174
168
|
measurements: list[CellMeasurement]
|
|
175
|
-
steps: list[
|
|
169
|
+
steps: list[DataFrame]
|
|
176
170
|
|
|
177
171
|
@field_validator("steps", mode="before")
|
|
178
172
|
@classmethod
|
|
179
173
|
def convert_dict_to_df(cls, v: Any) -> Any:
|
|
174
|
+
"""Convert dictionary or list of dictionaries to DataFrames."""
|
|
180
175
|
if isinstance(v, list):
|
|
181
176
|
return [dict_to_df_validator(item) for item in v]
|
|
182
177
|
return dict_to_df_validator(v)
|
|
183
|
-
|
|
184
|
-
class Config:
|
|
185
|
-
arbitrary_types_allowed = True
|