ionworks-api 0.1.0__py3-none-any.whl → 0.1.2__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.
@@ -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
- def __init__(self, client: Any):
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
- Get a specific cell specification by ID.
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
- List all cell specs.
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
- Create a new cell spec.
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
- Create a new cell spec if it doesn't exist, otherwise get the
58
- existing one.
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 e
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
- Delete a cell specification by ID.
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
- Get a specific cell specification by slug.
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
- def __init__(self, api_key: str | None = None, api_url: str | None = None):
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, optional
23
- API key. If not provided, will look for IONWORKS_API_KEY env var
24
- api_url : str, optional
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": {str(e)}"
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
- dict[str, Any]: Health check response
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
- from typing import Any, Optional
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: Optional[str] = None # Optional callback
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: Optional[dict[str, Any]]
24
- error: Optional[str]
25
- result: Optional[dict[str, Any]]
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
- def __init__(self, client: Any):
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
- raise ValueError(
63
- f"Invalid response format received from {endpoint}: {e}"
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
- Get the status and details of a specific job.
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
- raise ValueError(
77
- f"Invalid response format received from {endpoint}: {e}"
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
- List all jobs.
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
- raise ValueError(
97
- f"Invalid job data format received from {endpoint}: {e}"
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
- Cancel a job.
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
- raise ValueError(
110
- f"Invalid response format received from {endpoint}: {e}"
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: pd.DataFrame
99
- time_series: pd.DataFrame
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
- Response model for all measurements + steps associated with a specific cell
131
- instance.
132
- Returns normalized data: separate lists for instance, measurements, and steps.
133
- Includes parent information for consistency with other group endpoints.
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[pd.DataFrame]
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
- Response model for all instances associated with a cell specification.
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
- Response model for all instances + measurements + steps associated with a cell
167
- specification.
168
- Returns normalized data: separate lists for specification, instances, measurements,
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[pd.DataFrame]
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