Models

Pydantic models for API request and response validation.

Pydantic models for the Ionworks API client.

These models use extra=”allow” to accept any fields from the API response, letting the API handle validation. Required fields are kept minimal.

Bases: BaseModel

Links a detail get() returns alongside the resource.

Prefer these over Ionworks.urls: the server builds them from the record itself, so they are right even when you do not know the record’s project. Both are None on objects not fetched by get() (list items, create responses).

url: str | None

The resource’s page in the Ionworks web app, or None if it has none.

api_url: str | None

The API endpoint that returns this resource.

model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

ionworks.api.models.collect_all_pages(fetch_page, page_size: int = 100) → list[source]

Page through a paginated endpoint and return every item.

Advances offset by the number of items actually returned (not by page_size) and stops on an empty page, so a short or empty intermediate page can neither skip records nor loop forever.

Parameters:
  • fetch_page (callable) – Called as fetch_page(limit, offset), returning a PaginatedList.

  • page_size (int, optional) – Rows to request per call. Routes cap this at 100.

Returns:

Every item across all pages, in page order.

Return type:

list

class ionworks.api.models.PaginatedList(items: list[_T], total: int)[source]

Bases: Generic[_T]

A list-like container that also carries pagination metadata.

Returned by list() methods when limit or offset is provided. Behaves like a regular list for iteration, indexing, truthiness, and len() so callers can treat it interchangeably with list[T].

Parameters:
  • items (list[_T]) – The page of results.

  • total (int) – Total number of matching records across all pages.

items
total
property count: int

Number of items in this page (same as len(self)).

ionworks.api.models.create_or_get(*, create: Callable[[], _T], get_by_id: Callable[[str], _T], find_by_name: Callable[[str], _T | None], name: str | None, resource_label: str) → _T[source]

Create a resource, or return the existing one on a name conflict.

Shared conflict-resolution used by the equipment sub-clients. Calls create; on an IonworksError that is a CONFLICT (error_code == "CONFLICT" or HTTP 409), resolves the existing resource — first by the existing_id echoed in the error detail, then by find_by_name as a fallback. Any non-conflict error propagates unchanged.

Parameters:
  • create (Callable[[], _T]) – Zero-arg thunk that performs the create and returns the new resource.

  • get_by_id (Callable[[str], _T]) – Fetch a resource by id (used with the conflict’s existing_id).

  • find_by_name (Callable[[str], _T | None]) – Look up the existing resource by its (conflicting) name; returns None if not found.

  • name (str | None) – The name that was being created, used for the fallback lookup and the error message.

  • resource_label (str) – Human-readable resource name for the “duplicate but not found” error (e.g. "Cycler").

Returns:

The newly created resource, or the pre-existing one on conflict.

Return type:

_T

Raises:

ValueError – If the create reported a duplicate but the existing resource could not be resolved by id or name.

class ionworks.api.models.CellSpecification(*, url: str | None = None, api_url: str | None = None, id: str, name: str, **extra_data: Any)[source]

Bases: ResourceLinks

Cell specification model - accepts any fields from the API.

The API returns nested component/material data and ratings objects. This model is permissive to allow the API to define the schema.

model_config = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

id: str
name: str
class ionworks.api.models.Site(*, id: str, name: str, timezone: str | None = None, **extra_data: Any)[source]

Bases: BaseModel

Site model - accepts any fields from the API.

A site is an organization-scoped physical location that owns cyclers.

model_config = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

id: str
name: str
timezone: str | None

IANA zone of the lab’s wall clock; server default “America/New_York”.

class ionworks.api.models.Cycler(*, id: str, name: str, site_id: str, project_id: str, manufacturer: str | None = None, model: str | None = None, version: str | None = None, serial_number: str | None = None, firmware_version: str | None = None, hostname: str | None = None, ip_address: str | None = None, port: int | None = None, last_calibrated_at: datetime | None = None, calibration_interval_days: int | None = None, calibration_due_at: datetime | None = None, **extra_data: Any)[source]

Bases: BaseModel

Cycler model - accepts any fields from the API.

A cycler is battery test equipment that belongs to a site, is owned by one project, and owns channels.

model_config = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

id: str
name: str
site_id: str
project_id: str
manufacturer: str | None

Manufacturer (make) of the cycler, e.g. “Arbin”. Nullable.

model: str | None

Model (type) of the cycler, e.g. “LBT-5V”. Nullable.

version: str | None

Hardware/product version. Distinct from firmware_version, which changes without the hardware changing. Nullable.

serial_number: str | None

Manufacturer serial number. Nullable.

firmware_version: str | None

Firmware revision currently installed. Nullable.

hostname: str | None

DNS name of the control host on the lab network. Nullable.

ip_address: str | None

IP address of the control host. Nullable.

port: int | None

TCP port the control software listens on (1-65535). Nullable.

last_calibrated_at: datetime | None

When the cycler was last calibrated. Null means never calibrated or not tracked — read as unknown, not as overdue.

calibration_interval_days: int | None

Maximum time allowed between calibrations, in days. Null means the cycler is not on a calibration schedule.

calibration_due_at: datetime | None

last_calibrated_at plus calibration_interval_days. Read-only — derived by the server, so it cannot disagree with those two fields. Null when either is unset.

Type:

When calibration next falls due

class ionworks.api.models.CyclerServiceEvent(*, id: str, cycler_id: str, project_id: str, event_type: str, scheduled_for: datetime | None = None, scheduled_until: datetime | None = None, performed_at: datetime | None = None, notes: str | None = None, created_by: str | None = None, performed_by: str | None = None, **extra_data: Any)[source]

Bases: BaseModel

One instrument-level service on a cycler.

Calibration and preventive maintenance are performed on the instrument, so a 40-channel cycler going in for its annual calibration is one event, not 40 unrelated channel outages. Opening an event takes every channel out of service together; completing it returns them together.

A cycler has at most one open event (performed_at is None) at a time.

model_config = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

id: str
cycler_id: str
project_id: str
event_type: str

One of “calibration”, “preventive_maintenance”, “firmware”, “repair”, “other”.

scheduled_for: datetime | None

When the visit is booked. None on an event recorded after the fact.

scheduled_until: datetime | None

When the booked visit is expected to end, making the booking a span rather than a start instant. None when no end was given. A plan, not a guarantee — a visit may run past it.

performed_at: datetime | None

When the work was done. None means the event is still open.

notes: str | None
created_by: str | None
performed_by: str | None
class ionworks.api.models.Channel(*, id: str, name: str, cycler_id: str, project_id: str, notes: str | None = None, out_of_commission: bool = False, max_amps: float | None = None, min_volts: float | None = None, max_volts: float | None = None, **extra_data: Any)[source]

Bases: BaseModel

Channel model - accepts any fields from the API.

A channel is an individual test channel belonging to a cycler; it inherits its cycler’s project.

model_config = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

id: str
name: str
cycler_id: str
project_id: str
notes: str | None

Free-text notes on the channel. Nullable.

out_of_commission: bool

Whether the channel is deliberately out of service (broken / maintenance). Shown as out-of-service in the Lab view.

max_amps: float | None

Rated maximum current in amps (A). Nullable = unrated.

min_volts: float | None

Rated voltage window in volts (V). Nullable = unrated.

max_volts: float | None
class ionworks.api.models.ChannelIncident(*, id: str, channel_id: str, project_id: str, service_event_id: str | None = None, category: str = 'other', notes: str | None = None, service_scheduled_at: datetime | None = None, service_scheduled_until: datetime | None = None, started_at: datetime, resolved_at: datetime | None = None, resolution_notes: str | None = None, started_by: str | None = None, resolved_by: str | None = None, is_estimated: bool = False, **extra_data: Any)[source]

Bases: BaseModel

One span during which a channel was out of service.

Opened when a channel is marked out of commission and closed when it returns, so a channel’s downtime is a history of spans rather than just the current out_of_commission flag. A channel has at most one open incident (resolved_at is None) at a time.

An outage is either instrument-level or channel-local, and service_event_id is what tells them apart: it is set for every channel taken down by a cycler calibration or PM visit, and None for a fault the channel had on its own (a failed relay is not a cycler event).

model_config = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

id: str
channel_id: str
project_id: str
service_event_id: str | None

The instrument-level service this outage belongs to, when it is one. None for a channel-local fault.

category: str

One of “hardware_failure”, “maintenance”, “calibration”, “decommissioned”, “other”.

notes: str | None
service_scheduled_at: datetime | None

When the booked service is due to happen. None on unplanned outages and on backfilled rows, whose service was never scheduled.

service_scheduled_until: datetime | None

When the booked service is expected to end, making the outage a span rather than a start instant. A plan, not a guarantee — an outage may run past it, which is what is_overdue_to_return reports.

started_at: datetime

When the channel went out of service.

resolved_at: datetime | None

When the channel returned. None means the outage is still open.

resolution_notes: str | None
started_by: str | None
resolved_by: str | None
is_estimated: bool

True for backfilled rows whose started_at is derived from the channel’s last-modified time and is only an upper bound. Exclude these from mean-time-to-repair arithmetic.

property is_overdue_to_return: bool

Whether an open outage has run past the end its service was booked to.

Derived when you ask rather than stored, because overrunning a booked window is a plan being wrong, not a record being invalid — the platform deliberately accepts it. Only an open outage can be overdue: once the channel is back it is back, however late.

Returns:

True when the outage is still open, has a booked end, and that end is in the past.

Return type:

bool

class ionworks.api.models.ChannelState(*values)[source]

Bases: StrEnum

Derived occupancy state of a channel in the lab view.

free = 'free'
occupied = 'occupied'
stale = 'stale'
out_of_commission = 'out_of_commission'
class ionworks.api.models.LabMeasurementSummary(*, id: str, name: str, start_time: str | None = None, estimated_end_time: str | None = None, updated_at: str, cell_instance_id: str, cell_instance_name: str | None = None, cell_specification_name: str | None = None, protocol_name: str | None = None, watched: bool = False, **extra_data: Any)[source]

Bases: BaseModel

The measurement occupying a channel (slim view for the lab wall).

model_config = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

id: str
name: str
start_time: str | None

ISO time the test started, if known. Nullable.

estimated_end_time: str | None

ISO forecast finish time, if an estimate has been computed. Nullable. Informational only – doesn’t affect the derived occupancy state or the staleness window. The note explaining the estimate and the timestamp it was computed at aren’t part of this slim summary; fetch the full measurement (client.cell_measurement.get(measurement_id)) for those.

updated_at: str

ISO time the measurement was last updated (drives staleness).

cell_instance_id: str
cell_instance_name: str | None
cell_specification_name: str | None

Name of the cell instance’s cell specification, if known. Nullable.

protocol_name: str | None
watched: bool

Whether the current user is watching this measurement. A watch on the live measurement reads as watching its channel (see LabClient.watch / list_watched / watched_channels).

class ionworks.api.models.LabChannel(*, id: str, name: str, state: ChannelState, measurement: LabMeasurementSummary | None = None, notes: str | None = None, out_of_commission: bool = False, max_amps: float | None = None, min_volts: float | None = None, max_volts: float | None = None, **extra_data: Any)[source]

Bases: BaseModel

A channel with its derived occupancy state.

model_config = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

id: str
name: str
state: ChannelState
measurement: LabMeasurementSummary | None

The measurement driving the state (occupied/stale); None when free/OOC.

notes: str | None
out_of_commission: bool
max_amps: float | None
min_volts: float | None
max_volts: float | None
class ionworks.api.models.LabCycler(*, id: str, name: str, manufacturer: str | None = None, model: str | None = None, channel_count: int, occupied: int, stale: int, free: int, out_of_commission: int = 0, channels: list[LabChannel] = [], **extra_data: Any)[source]

Bases: BaseModel

A cycler with its channels and per-cycler occupancy counts.

model_config = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

id: str
name: str
manufacturer: str | None
model: str | None
channel_count: int
occupied: int
stale: int
free: int
out_of_commission: int
channels: list[LabChannel]
class ionworks.api.models.LabSite(*, id: str, name: str, cyclers: list[LabCycler] = [], **extra_data: Any)[source]

Bases: BaseModel

A site grouping its cyclers for the lab wall.

model_config = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

id: str
name: str
cyclers: list[LabCycler]
class ionworks.api.models.LabStatus(*, sites: list[LabSite] = [], occupied: int = 0, stale: int = 0, free: int = 0, out_of_commission: int = 0, **extra_data: Any)[source]

Bases: BaseModel

The full lab-view tree for a project plus project-level counts.

model_config = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

sites: list[LabSite]
occupied: int
stale: int
free: int
out_of_commission: int
class ionworks.api.models.FlatChannel(*, channel: LabChannel, cycler_id: str, cycler_name: str, site_id: str, site_name: str)[source]

Bases: BaseModel

A lab channel flattened with its parent cycler and site context.

Returned by LabClient.free_channels / stale_channels so an answer reads as “CH3 on Maccor-1 at Boston Lab” without re-walking the tree.

channel: LabChannel
cycler_id: str
cycler_name: str
site_id: str
site_name: str
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class ionworks.api.models.Utilization(*, percent: int, occupied: int, stale: int, free: int, out_of_commission: int, total: int)[source]

Bases: BaseModel

Project lab utilization: the frontend headline percent plus raw counts.

percent is the busy (occupied + stale) share of all channels (out-of-commission included in the denominator), rounded to a whole number to match the lab wall. 0 when there are no channels.

percent: int
occupied: int
stale: int
free: int
out_of_commission: int
total: int
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class ionworks.api.models.CellInstance(*, url: str | None = None, api_url: str | None = None, id: str, name: str, cell_specification_id: str, batch_id: str | None = None, batch_name: str | None = None, **extra_data: Any)[source]

Bases: ResourceLinks

Cell instance model - accepts any fields from the API.

model_config = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

id: str
name: str
cell_specification_id: str
batch_id: str | None
batch_name: str | None

Read-only; set batch_id to change it.

class ionworks.api.models.CellBatch(*, id: str, name: str, cell_specification_id: str, project_id: str | None = None, organization_id: str | None = None, notes: str | None = None, date_manufactured: str | None = None, properties: dict[str, Any] | None = None, created_by: str | None = None, created_at: str | None = None, updated_at: str | None = None, instance_count: int = 0, **extra_data: Any)[source]

Bases: BaseModel

A manufacturing lot: one production run of one cell specification.

Instances join a lot through their batch_id. Lot names are unique per specification, compared case-insensitively.

model_config = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

id: str
name: str
cell_specification_id: str
project_id: str | None
organization_id: str | None
notes: str | None
date_manufactured: str | None

ISO date (YYYY-MM-DD).

properties: dict[str, Any] | None

Lot-level properties, e.g. datasheet values. Numeric values use the same Quantity format as other properties ({"value": ..., "unit": ...}).

created_by: str | None
created_at: str | None
updated_at: str | None
instance_count: int

Number of cell instances currently in the lot.

class ionworks.api.models.MeasurementType(*values)[source]

Bases: StrEnum

Type of data stored in a cell measurement.

time_series = 'time_series'
file = 'file'
properties = 'properties'
class ionworks.api.models.CellMeasurement(*, url: str | None = None, api_url: str | None = None, id: str, name: str, cell_instance_id: str, measurement_type: MeasurementType = MeasurementType.time_series, channel_id: str | None = None, protocol_id: str | None = None, start_time: str | None = None, end_time: str | None = None, estimated_end_time: str | None = None, estimated_end_time_note: str | None = None, estimated_end_time_calculated_at: str | None = None, processing_status: str | None = None, processing_error: str | None = None, tags: list[str] = <factory>, **extra_data: Any)[source]

Bases: ResourceLinks

Cell measurement model - accepts any fields from the API.

model_config = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

id: str
name: str
cell_instance_id: str
measurement_type: MeasurementType
channel_id: str | None

Optional channel the measurement was recorded on. Nullable. Only valid on time_series measurements, and requires start_time to be set.

protocol_id: str | None

Optional protocol the measurement ran against, in its own project. Preferred over the free-form protocol dict.

start_time: str | None

ISO-formatted time the test started. Nullable, but required when channel_id is set (it defines when the channel became occupied).

end_time: str | None

ISO-formatted time the test finished. Nullable — a null end_time means the test is still running (e.g. in the lab equipment view). Set it once the measurement is complete. Must not precede start_time.

estimated_end_time: str | None

ISO forecast finish time for a still-running test, if an estimate has been computed. Nullable. Distinct from end_time (the actual finish); informational only.

estimated_end_time_note: str | None

Free-text explanation of how estimated_end_time was derived. Nullable.

estimated_end_time_calculated_at: str | None

ISO-formatted time estimated_end_time was last computed. Nullable.

processing_status: str | None

Lifecycle of server-side step processing for uploaded time_series measurements: pending or running while the steps are still being derived, ready once they are available, failed if the upload could not be processed. file and properties measurements never process steps and are always ready.

processing_error: str | None

Why step processing failed, when processing_status is failed.

tags: list[str]

Free-form labels for organising and filtering measurements, stored normalised (trimmed, deduplicated case-insensitively, sorted).

class ionworks.api.models.MeasurementTagUsage(*, tag: str, usage_count: int)[source]

Bases: BaseModel

A tag used in a project and how many measurements carry it.

tag: str
usage_count: int
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class ionworks.api.models.CellMeasurementBundleResponse(*, url: str | None = None, api_url: str | None = None, id: str, name: str, cell_instance_id: str, measurement_type: MeasurementType = MeasurementType.time_series, channel_id: str | None = None, protocol_id: str | None = None, start_time: str | None = None, end_time: str | None = None, estimated_end_time: str | None = None, estimated_end_time_note: str | None = None, estimated_end_time_calculated_at: str | None = None, processing_status: str | None = None, processing_error: str | None = None, tags: list[str] = <factory>, steps_created: int, **extra_data: Any)[source]

Bases: CellMeasurement

Flat response from creating a measurement bundle.

Measurement fields (id, name, measurement_type, etc.) are at the top level alongside upload metadata.

steps_created: int
model_config = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class ionworks.api.models.MultipartUploadInfo(*, endpoint: str, region: str, bucket: str, object_path: str, access_key_id: str, secret_access_key: str, session_token: str, expires_at: str | None = None)[source]

Bases: BaseModel

S3 multipart credentials and target for a large direct-to-storage upload.

Returned inside UploadInfo only when the backend supports the multipart path. The SDK normally uses it at or above UPLOAD_MULTIPART_THRESHOLD_BYTES and PUTs smaller files to signed_url. A 504 or 524 signed-PUT failure falls forward to multipart once when the body is seekable.

endpoint: str
region: str
bucket: str
object_path: str
access_key_id: str
secret_access_key: str
session_token: str
expires_at: str | None
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class ionworks.api.models.UploadInfo(*, filename: str | None = None, signed_url: str, token: str, path: str, multipart: MultipartUploadInfo | None = None)[source]

Bases: BaseModel

Signed URL info for a single upload target.

filename: str | None
signed_url: str
token: str
path: str
multipart: MultipartUploadInfo | None

Present only when the backend offers an S3 multipart target.

model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class ionworks.api.models.InitiateUploadResponse(*, measurement_id: str, uploads: list[UploadInfo])[source]

Bases: BaseModel

Response from the initiate-upload endpoint for signed URL uploads.

measurement_id: str
uploads: list[UploadInfo]
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class ionworks.api.models.InitiateRawDataUploadResponse(*, raw_data_id: str, uploads: list[UploadInfo])[source]

Bases: BaseModel

Response from the raw-data initiate-upload endpoint.

raw_data_id: str
uploads: list[UploadInfo]
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class ionworks.api.models.CellMeasurementDetail(*, url: str | None = None, api_url: str | None = None, id: str, name: str, cell_instance_id: str, measurement_type: MeasurementType = MeasurementType.time_series, channel_id: str | None = None, protocol_id: str | None = None, start_time: str | None = None, end_time: str | None = None, estimated_end_time: str | None = None, estimated_end_time_note: str | None = None, estimated_end_time_calculated_at: str | None = None, processing_status: str | None = None, processing_error: str | None = None, tags: list[str] = <factory>, specification_id: str | None = None, instance_id: str | None = None, steps: DataFrame | DataFrame | None = None, time_series: DataFrame | DataFrame | None = None, cycles: DataFrame | DataFrame | None = None, files: dict[str, bytes] | None=None, **extra_data: Any)[source]

Bases: CellMeasurement

Flat detail model for a measurement with steps and time series.

Measurement fields (id, name, measurement_type, etc.) are at the top level alongside optional data payloads. Returns minimal data by default: foreign keys for parent objects rather than nested objects. Use the spec/instance clients to fetch parent objects if needed.

model_config = {'arbitrary_types_allowed': True, 'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

specification_id: str | None
instance_id: str | None
steps: DataFrame | None
time_series: DataFrame | None
cycles: DataFrame | None
files: dict[str, bytes] | None
classmethod convert_dict_to_df(v: Any) → Any[source]

Convert dictionary to DataFrame (polars or pandas based on config).

class ionworks.api.models.CellInstanceDetail(*, instance: CellInstance, specification_id: str, measurements: list[CellMeasurementDetail])[source]

Bases: BaseModel

Detail model for a cell instance with all measurements.

Returns a foreign key for the parent specification rather than a nested object. Use client.cell_spec.get(detail .specification_id) to fetch the full specification.

instance: CellInstance
specification_id: str
measurements: list[CellMeasurementDetail]
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class ionworks.api.models.CyclerDetail(*, cycler: Cycler, site_id: str, channels: list[Channel])[source]

Bases: BaseModel

Detail model for a cycler with all its channels.

Returns a foreign key for the parent site rather than a nested object. Use client.site.get(detail.site_id) to fetch the full site.

cycler: Cycler
site_id: str
channels: list[Channel]
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class ionworks.api.models.SiteDetail(*, site: Site, cyclers: list[CyclerDetail])[source]

Bases: BaseModel

Detail model for a site with all its cyclers.

Each cycler is expanded to a CyclerDetail, so its channels are included too.

site: Site
cyclers: list[CyclerDetail]
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class ionworks.api.models.ColumnSpec(*, name: str, unit: str = '', source_column_index: int, **extra_data: Any)[source]

Bases: BaseModel

Column descriptor for a material property dataset.

model_config = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

name: str
unit: str
source_column_index: int
class ionworks.api.models.MaterialPropertyDataset(*, id: str, name: str, columns: list[ColumnSpec] = [], data_version: int, nan_counts: dict[str, int] | None = None, created_at: str | None = None, source_pipeline_id: str | None = None, source_simple_pipeline_id: str | None = None, source_analysis_id: str | None = None, source_label: str | None = None, **extra_data: Any)[source]

Bases: BaseModel

Material property dataset record as returned by the API.

model_config = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

id: str
name: str
columns: list[ColumnSpec]
data_version: int
nan_counts: dict[str, int] | None
created_at: str | None
source_pipeline_id: str | None
source_simple_pipeline_id: str | None
source_analysis_id: str | None
source_label: str | None
class ionworks.api.models.AnalysisType(*values)[source]

Bases: StrEnum

Well-known analysis_type values.

Members are plain strings (StrEnum), so they can be passed directly wherever an analysis_type string is expected, e.g. client.analysis.create(..., analysis_type=AnalysisType.ECM_FROM_EIS).

This set is advisory, not exhaustive: the API does not restrict analysis_type to these values — any non-empty string is accepted, so a new extractor can use a raw string without waiting for an SDK release.

ECM_FROM_EIS = 'ecm_from_eis'
LAM_LLI_FROM_RPT = 'lam_lli_from_rpt'
DCIR_FROM_HPPC = 'dcir_from_hppc'
ionworks.api.models.KNOWN_ANALYSIS_TYPES: list[str] = ['ecm_from_eis', 'lam_lli_from_rpt', 'dcir_from_hppc']

The well-known analysis-type string values as a plain list, for iteration or display. Derived from AnalysisType; prefer the enum for authoring.

class ionworks.api.models.AnalysisColumnSpec(*, name: str, unit: str = '', dtype: str | None = None, **extra_data: Any)[source]

Bases: BaseModel

Column descriptor for an analysis parquet.

model_config = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

name: str
unit: str
dtype: str | None
class ionworks.api.models.Analysis(*, id: str, measurement_id: str, project_id: str | None = None, name: str, analysis_type: str, columns: list[AnalysisColumnSpec] = [], plots: list[dict] = [], metadata: dict = {}, notes: str | None = None, source_pipeline_id: str | None = None, source_simple_pipeline_id: str | None = None, source_analysis_id: str | None = None, source_label: str | None = None, created_at: str | None = None, **extra_data: Any)[source]

Bases: BaseModel

Analysis record as returned by the API.

An analysis holds features extracted from a single cell_measurement (e.g. ECM parameters from EIS, LLI/LAM from RPT), stored as a parquet table plus loose metadata.

model_config = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

id: str
measurement_id: str
project_id: str | None
name: str
analysis_type: str
columns: list[AnalysisColumnSpec]
plots: list[dict]

Plot presets over the table columns; see AnalysisClient.create.

metadata: dict
notes: str | None
source_pipeline_id: str | None
source_simple_pipeline_id: str | None
source_analysis_id: str | None
source_label: str | None
created_at: str | None
class ionworks.api.models.Material(*, url: str | None = None, api_url: str | None = None, id: str, name: str, manufacturer: str | None = None, product_id: str | None = None, **extra_data: Any)[source]

Bases: ResourceLinks

Material record as returned by the API.

model_config = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

id: str
name: str
manufacturer: str | None
product_id: str | None
class ionworks.api.models.Project(*, url: str | None = None, api_url: str | None = None, id: str, name: str, organization_id: str, my_role: str | None = None, my_permissions: list[str] = <factory>, **extra_data: Any)[source]

Bases: ResourceLinks

Project model - accepts any fields from the API.

model_config = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

id: str
name: str
organization_id: str
my_role: str | None

Your role in the project, or None if you are not a member.

my_permissions: list[str]

The permission actions your project role grants, e.g. "pipeline:read".

class ionworks.api.models.SearchResult(*, id: str, entity_type: str, name: str, project_id: str | None = None, parent_id: str | None = None, **extra_data: Any)[source]

Bases: BaseModel

A single result from the global search endpoint.

Accepts any extra fields the API may add in future.

model_config = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

id: str
entity_type: str
name: str
project_id: str | None

Project the entity belongs to (or a fallback project for org-scoped entities). None when the API could not associate a project.

parent_id: str | None

Parent entity ID needed for navigation (e.g. cell_specification_id for a cell_instance). None when the ID alone is sufficient.

class ionworks.api.models.SearchResponse(*, results: list[SearchResult], query: str, total: int, limit: int, offset: int, **extra_data: Any)[source]

Bases: BaseModel

Paginated response from the global search endpoint.

Iterates over results so callers can treat it like a list of SearchResult (for hit in response, response[0], len(response), if response:) while still reading the pagination metadata (total, limit, offset).

Notes

Because __iter__() yields SearchResult objects (not Pydantic’s default (field_name, value) pairs), dict(response) does not work. Use model_dump() to get a plain dict of fields.

model_config = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

results: list[SearchResult]
query: str
total: int
limit: int
offset: int
class ionworks.api.models.Model(*, url: str | None = None, api_url: str | None = None, id: str, name: str, project_id: str | None = None, source_model_id: str | None = None, config: dict[str, Any] | None = None, simulation_settings: dict[str, Any] | None = None, **extra_data: Any)[source]

Bases: ResourceLinks

Custom model model - accepts any fields from the API.

model_config = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

id: str
name: str
project_id: str | None

Project the model belongs to; None only for system templates.

source_model_id: str | None

The system model a project’s copy came from; None for its own models.

config: dict[str, Any] | None

Model config (e.g. {"type": "SPMe"}). The get response includes it; the create response may omit it, in which case this is None.

simulation_settings: dict[str, Any] | None

Persistent simulation settings (mesh + solver) re-applied whenever the model is simulated: a flat bag of var_pts / submesh_types / spatial_methods / solver. None uses the model defaults.

class ionworks.api.models.ParameterizedModel(*, url: str | None = None, api_url: str | None = None, id: str, name: str, simulation_settings: dict[str, Any] | None = None, **extra_data: Any)[source]

Bases: ResourceLinks

Parameterized model model - accepts any fields from the API.

model_config = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

id: str
name: str
simulation_settings: dict[str, Any] | None

Parameter-specific persistent simulation settings (mesh + solver); takes precedence over the base model’s settings when the model is simulated. None inherits the base model / defaults.

class ionworks.api.models.RawData(*, id: str, project_id: str, name: str, filename: str, source: str | None = None, **extra_data: Any)[source]

Bases: BaseModel

A raw-data record.

Represents an original uploaded file stored as-is, scoped to an organization and project. The API defines the full schema; extra fields are accepted.

model_config = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

id: str
project_id: str
name: str
filename: str
source: str | None
class ionworks.api.models.ProtocolGroup(*, id: str, name: str, organization_id: str, project_id: str, description: str | None = None, protocol_count: int | None = None, created_by: str | None = None, created_by_email: str | None = None, created_at: str | None = None, updated_at: str | None = None, **extra_data: Any)[source]

Bases: BaseModel

A named family of protocols within one project.

Deleting a group ungroups its protocols without deleting them.

model_config = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

id: str
name: str
organization_id: str
project_id: str
description: str | None
protocol_count: int | None
created_by: str | None
created_by_email: str | None
created_at: str | None
updated_at: str | None
class ionworks.api.models.Protocol(*, url: str | None = None, api_url: str | None = None, id: str, name: str, organization_id: str, project_id: str | None = None, protocol_group_id: str | None = None, description: str | None = None, protocol_config: dict[str, Any] | None = None, parameters_schema: dict[str, Any] | None = None, source_protocol: str | None = None, created_by_email: str | None = None, **extra_data: Any)[source]

Bases: ResourceLinks

A saved, project-scoped protocol.

Stored server-side as an experiment_template: a named UCP protocol plus the schema of any parameters it leaves open. Simulations and planned measurements both reference a protocol by id rather than carrying the protocol text, so the same protocol runs identically everywhere.

Heavy columns (protocol_config, parameters_schema, source_protocol, …) are omitted from list responses unless requested via include; on a listed protocol they are None rather than empty. Fetch the full record with get().

model_config = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

id: str
name: str
organization_id: str
project_id: str | None

Project that owns the protocol. Every protocol reachable through the API is project-scoped; None only appears on unmigrated legacy rows.

protocol_group_id: str | None
description: str | None
protocol_config: dict[str, Any] | None

The protocol itself, in UCP form. None when not included in a list.

parameters_schema: dict[str, Any] | None

Schema of the parameters the protocol leaves open (input[...] references). None when not included in a list.

source_protocol: str | None

Original protocol text as the user entered it, when one was recorded.

created_by_email: str | None
class ionworks.api.models.ParsedProtocol(*, parsed_protocol_ucp: str, parsed_protocol_human: str = '', raw_protocol: str = '', cycler_type: str | None = None, nominal_capacity_ah: float | None = None, required_drive_cycles: list[str] = <factory>, available_drive_cycles: list[str] = <factory>, required_subroutines: list[str] = <factory>, available_subroutines: list[str] = <factory>, error: str | None = None, **extra_data: Any)[source]

Bases: BaseModel

Result of parsing a vendor protocol file into UCP.

Returned by parse_file(). A parsed protocol is not saved — pass ucp to create() to store it.

Drive cycles and subroutines referenced by the file may not be embedded in it. Those the parser recovered are listed in available_drive_cycles / available_subroutines; those still needed are in the required_ lists and must be supplied before the protocol will simulate.

model_config = {'extra': 'allow', 'populate_by_name': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

ucp: str

The parsed protocol as UCP YAML.

human_readable: str

Human-readable rendering of the same protocol.

raw_protocol: str

The original file’s text, decoded with replacement for invalid bytes.

cycler_type: str | None

Detected cycler vendor, when the parser could identify one.

nominal_capacity_ah: float | None

Nominal cell capacity in A.h, when the file declared one (BioLogic headers do).

required_drive_cycles: list[str]
available_drive_cycles: list[str]
required_subroutines: list[str]
available_subroutines: list[str]
error: str | None

Set when the file parsed only partially; ucp may be incomplete.

class ionworks.api.models.Study(*, url: str | None = None, api_url: str | None = None, id: str, name: str, **extra_data: Any)[source]

Bases: ResourceLinks

Study model - accepts any fields from the API.

model_config = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

id: str
name: str
class ionworks.api.models.PlannedMeasurementStatus(*values)[source]

Bases: StrEnum

Lifecycle state of a planned measurement.

A measurement is first requested, then a scheduler assigns it a channel and time window (scheduled). Once the real run starts it becomes in_progress, then completed; it may be cancelled at any point before completion.

requested = 'requested'
scheduled = 'scheduled'
in_progress = 'in_progress'
completed = 'completed'
cancelled = 'cancelled'
class ionworks.api.models.PlannedMeasurement(*, id: str, name: str, project_id: str, status: PlannedMeasurementStatus, protocol_id: str | None = None, cell_specification_id: str | None = None, cell_instance_id: str | None = None, channel_id: str | None = None, planned_start_time: datetime | None = None, planned_end_time: datetime | None = None, estimated_duration_seconds: int | None = None, requested_by_email: str | None = None, scheduled_by_email: str | None = None, **extra_data: Any)[source]

Bases: BaseModel

Planned measurement model - accepts any fields from the API.

A planned measurement is a future, project-scoped test request. When status is scheduled it also reserves channel_id over the [planned_start_time, planned_end_time) window; a requested row carries only an estimated_duration_seconds and no channel.

model_config = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

id: str
name: str
project_id: str
status: PlannedMeasurementStatus
protocol_id: str | None

Named protocol (experiment_template) the measurement will run. Required on create; resolve its display name via the linked protocol.

cell_specification_id: str | None

Cell specification the requester wants tested. Required on create.

cell_instance_id: str | None

Cell instance the future measurement will run on. Nullable.

channel_id: str | None

Channel reserved once scheduled. Nullable until scheduled.

planned_start_time: datetime | None

Planned reservation window. Nullable until scheduled.

planned_end_time: datetime | None
estimated_duration_seconds: int | None

Expected run duration before scheduled times exist (requested rows).

requested_by_email: str | None

Email of the user who requested the plan (flattened from the embed).

scheduled_by_email: str | None

Email of the user who scheduled the plan; None until scheduled.

class ionworks.api.models.AutoScheduleAssignment(*, planned_measurement_id: str, planned_measurement_name: str, planned_measurement_updated_at: datetime, channel_id: str | None = None, channel_name: str | None = None, cycler_name: str | None = None, planned_start_time: datetime | None = None, planned_end_time: datetime | None = None, unscheduled_reason: str | None = None)[source]

Bases: BaseModel

One proposed planned-measurement reservation.

A proposal may include an unscheduled assignment when the project has no in-service channel able to take the test. Only assignments with is_scheduled set should be passed to ionworks.api.planned_measurement.PlannedMeasurementClient.apply_auto_schedule_proposal().

planned_measurement_id: str
planned_measurement_name: str
planned_measurement_updated_at: datetime
channel_id: str | None
channel_name: str | None
cycler_name: str | None

Owning cycler’s name. Channel names repeat across cyclers, so the pair identifies the hardware being reserved.

planned_start_time: datetime | None
planned_end_time: datetime | None
unscheduled_reason: str | None
property is_scheduled: bool

Whether this assignment is complete and can be applied.

model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class ionworks.api.models.AutoScheduleProposal(*, generated_at: datetime, assignments: list[AutoScheduleAssignment])[source]

Bases: BaseModel

A transient earliest-gap proposal for selected requested tests.

generated_at: datetime
assignments: list[AutoScheduleAssignment]
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class ionworks.api.models.Optimization(*, id: str, name: str | None = None, job_id: str, project_id: str, **extra_data: Any)[source]

Bases: BaseModel

Optimization model - accepts any fields from the API.

model_config = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

id: str
name: str | None
job_id: str
project_id: str
class ionworks.api.models.SimulationUsage(*, usage: float = 0, limit: float | None = None, **extra_data: Any)[source]

Bases: BaseModel

Current simulation usage and its configured limit, in hours.

model_config = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

usage: float

Simulated battery time consumed this period, in hours.

limit: float | None

Configured monthly limit in hours, or None when unconstrained.

class ionworks.api.models.ComputeUsage(*, usage: float = 0, usage_by_type: dict[str, float] = {}, limit: float | None = None, **extra_data: Any)[source]

Bases: BaseModel

Current compute usage and its configured limit, in hours.

usage is the total across all job types; usage_by_type breaks it down by job type (informational — the limit applies to the total).

model_config = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

usage: float

Total backend compute time consumed this period, in hours.

usage_by_type: dict[str, float]

Compute hours per job type (e.g. simulation, datafit). Sums to usage.

limit: float | None

Configured monthly limit in hours, or None when unconstrained.

class ionworks.api.models.OrganizationUsage(*, period_start: datetime, period_end: datetime, simulation: SimulationUsage = SimulationUsage(usage=0, limit=None), compute: ComputeUsage = ComputeUsage(usage=0, usage_by_type={}, limit=None), **extra_data: Any)[source]

Bases: BaseModel

An organization’s usage and limits for the current billing period.

Returned by usage(). Usage is aggregated across all members of the organization and resets on the first of each month. Simulation usage is a single figure; compute usage carries a per-job-type breakdown plus the total. All values are in hours. A None limit means that usage type is unconstrained.

model_config = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

period_start: datetime

Start of the current billing period (inclusive).

period_end: datetime

End of the current billing period (exclusive) — the next reset.

simulation: SimulationUsage
compute: ComputeUsage
class ionworks.api.models.ProjectMembership(*, project_id: str, project_role_id: str, **extra_data: Any)[source]

Bases: BaseModel

A member’s role in one project, as listed on OrganizationMember.

model_config = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

project_id: str
project_role_id: str
class ionworks.api.models.OrganizationMember(*, id: str, email: str | None = None, role: str | None = None, projects: list[ProjectMembership] = <factory>, **extra_data: Any)[source]

Bases: BaseModel

A user’s membership in the organization.

Returned by members(). The raw organization_role object is flattened into role ("Admin" or "Member"); projects lists the projects the member holds a role in.

model_config = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

id: str
email: str | None
role: str | None
projects: list[ProjectMembership]
classmethod from_api(data: dict[str, Any]) → OrganizationMember[source]

Build a member from a GET /organizations/{id}/users row.

class ionworks.api.models.InviteResult(*, user_id: str, invite_sent: bool, already_member: bool = False, role_name: str | None = None, **extra_data: Any)[source]

Bases: BaseModel

Outcome of adding someone to an organization or project by email.

model_config = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

user_id: str

ID of the invited or pre-existing user.

invite_sent: bool

True when a sign-up invitation email was sent; False when the email already belonged to an account, which was linked without an email.

already_member: bool

True when the user was already an organization member. Their membership, including its role, was left unchanged.

role_name: str | None

Organization role the user holds after the call, when reported.

class ionworks.api.models.ProjectRoleInfo(*, id: str, name: str, description: str | None = None, **extra_data: Any)[source]

Bases: BaseModel

A project role (e.g. "Project Contributor") and its permissions.

model_config = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

id: str
name: str
description: str | None
class ionworks.api.models.StepsAndCycles(*, steps: DataFrame | DataFrame, cycles: DataFrame | DataFrame)[source]

Bases: BaseModel

Steps and cycle metrics for a measurement.

Returned by the /steps_and_cycles endpoint which fetches both in one call (cycles are derived from steps).

model_config = {'arbitrary_types_allowed': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

steps: DataFrame
cycles: DataFrame
classmethod convert_dict_to_df(v: Any) → Any[source]

Convert dictionary to DataFrame.