albert 1.10.0rc2__py3-none-any.whl → 1.11.1__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.
Files changed (37) hide show
  1. albert/__init__.py +1 -1
  2. albert/client.py +5 -0
  3. albert/collections/custom_templates.py +3 -0
  4. albert/collections/data_templates.py +118 -264
  5. albert/collections/entity_types.py +19 -3
  6. albert/collections/inventory.py +1 -1
  7. albert/collections/notebooks.py +154 -26
  8. albert/collections/parameters.py +1 -0
  9. albert/collections/property_data.py +384 -280
  10. albert/collections/reports.py +4 -0
  11. albert/collections/synthesis.py +292 -0
  12. albert/collections/tasks.py +2 -1
  13. albert/collections/worksheets.py +3 -0
  14. albert/core/shared/models/base.py +3 -1
  15. albert/core/shared/models/patch.py +1 -1
  16. albert/resources/batch_data.py +4 -2
  17. albert/resources/cas.py +3 -1
  18. albert/resources/custom_fields.py +3 -1
  19. albert/resources/data_templates.py +60 -12
  20. albert/resources/entity_types.py +15 -4
  21. albert/resources/inventory.py +6 -4
  22. albert/resources/lists.py +3 -1
  23. albert/resources/notebooks.py +12 -7
  24. albert/resources/parameter_groups.py +3 -1
  25. albert/resources/property_data.py +64 -5
  26. albert/resources/sheets.py +16 -14
  27. albert/resources/synthesis.py +61 -0
  28. albert/resources/tags.py +3 -1
  29. albert/resources/tasks.py +4 -7
  30. albert/resources/workflows.py +4 -2
  31. albert/utils/data_template.py +392 -37
  32. albert/utils/property_data.py +638 -0
  33. albert/utils/tasks.py +3 -3
  34. {albert-1.10.0rc2.dist-info → albert-1.11.1.dist-info}/METADATA +1 -1
  35. {albert-1.10.0rc2.dist-info → albert-1.11.1.dist-info}/RECORD +37 -34
  36. {albert-1.10.0rc2.dist-info → albert-1.11.1.dist-info}/WHEEL +0 -0
  37. {albert-1.10.0rc2.dist-info → albert-1.11.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,5 +1,7 @@
1
1
  from typing import Any
2
2
 
3
+ from pydantic import validate_call
4
+
3
5
  from albert.collections.base import BaseCollection
4
6
  from albert.core.session import AlbertSession
5
7
  from albert.core.shared.identifiers import ReportId
@@ -136,6 +138,7 @@ class ReportCollection(BaseCollection):
136
138
  input_data=input_data,
137
139
  )
138
140
 
141
+ @validate_call
139
142
  def get_full_report(self, *, report_id: ReportId) -> FullAnalyticalReport:
140
143
  """Get a full analytical report by its ID.
141
144
 
@@ -192,6 +195,7 @@ class ReportCollection(BaseCollection):
192
195
  response = self.session.post(path, json=report_data)
193
196
  return FullAnalyticalReport(**response.json())
194
197
 
198
+ @validate_call
195
199
  def delete(self, *, id: ReportId) -> None:
196
200
  """Delete a report.
197
201
 
@@ -0,0 +1,292 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import validate_call
6
+
7
+ from albert.collections.base import BaseCollection
8
+ from albert.core.session import AlbertSession
9
+ from albert.core.shared.identifiers import NotebookId, SynthesisId
10
+ from albert.exceptions import AlbertException
11
+ from albert.resources.synthesis import ReactantValues, RowSequence, Synthesis
12
+
13
+
14
+ class SynthesisCollection(BaseCollection):
15
+ """
16
+ Collection for interacting with synthesis records used by notebook Ketcher blocks.
17
+ """
18
+
19
+ _api_version = "v3"
20
+ _updatable_attributes = {"name", "status", "hide_reaction_worksheet"}
21
+
22
+ def __init__(self, *, session: AlbertSession):
23
+ """
24
+ Initialize the SynthesisCollection.
25
+
26
+ Parameters
27
+ ----------
28
+ session : AlbertSession
29
+ The Albert session information.
30
+ """
31
+ super().__init__(session=session)
32
+ self.base_path = f"/api/{SynthesisCollection._api_version}/synthesis"
33
+
34
+ @validate_call
35
+ def create(
36
+ self, *, parent_id: NotebookId | str, name: str, block_id: str, smiles: str | None = None
37
+ ) -> Synthesis:
38
+ """
39
+ Create a synthesis record for a notebook Ketcher block.
40
+
41
+ Parameters
42
+ ----------
43
+ parent_id : NotebookId | str
44
+ The notebook ID that owns the synthesis record.
45
+ name : str
46
+ The synthesis name.
47
+ block_id : str
48
+ The Ketcher block ID associated with the synthesis.
49
+ smiles : str | None, optional
50
+ The initial SMILES string for the synthesis.
51
+
52
+ Returns
53
+ -------
54
+ Synthesis
55
+ The created synthesis record.
56
+ """
57
+ payload = {"name": name, "blockId": block_id, "smiles": smiles}
58
+ response = self.session.post(
59
+ url=self.base_path,
60
+ params={"parentId": parent_id},
61
+ json=payload,
62
+ )
63
+ return Synthesis(**response.json())
64
+
65
+ @validate_call
66
+ def get_by_id(
67
+ self,
68
+ *,
69
+ id: SynthesisId,
70
+ include_recommendations: bool = False,
71
+ include_predictions: bool = False,
72
+ version: str | None = None,
73
+ ) -> Synthesis:
74
+ """
75
+ Retrieve a synthesis record by ID.
76
+
77
+ Parameters
78
+ ----------
79
+ id : SynthesisId
80
+ The synthesis ID.
81
+ include_recommendations : bool, optional
82
+ Whether to include recommendations in the response.
83
+ include_predictions : bool, optional
84
+ Whether to include predictions in the response.
85
+ version : str | None, optional
86
+ The specific version to retrieve.
87
+
88
+ Returns
89
+ -------
90
+ Synthesis
91
+ The requested synthesis record.
92
+ """
93
+ params: dict[str, Any] = {
94
+ "recommendations": include_recommendations,
95
+ "predictions": include_predictions,
96
+ }
97
+ if version:
98
+ params["version"] = version
99
+ response = self.session.get(
100
+ url=f"{self.base_path}/{id}",
101
+ params=params,
102
+ )
103
+ return Synthesis(**response.json())
104
+
105
+ @validate_call
106
+ def update_canvas_data(
107
+ self, *, synthesis_id: SynthesisId, smiles: str, data: str, png: str
108
+ ) -> Synthesis:
109
+ """
110
+ Update the Ketcher canvas data for a synthesis record.
111
+
112
+ Parameters
113
+ ----------
114
+ synthesis_id : SynthesisId
115
+ The synthesis ID.
116
+ smiles : str
117
+ The updated SMILES string.
118
+ data : str
119
+ The serialized canvas data.
120
+ png : str
121
+ The base64-encoded PNG for the canvas.
122
+
123
+ Returns
124
+ -------
125
+ Synthesis
126
+ The updated synthesis record.
127
+ """
128
+ payload = {
129
+ "smiles": smiles,
130
+ "canvasData": {"data": data, "png": png},
131
+ }
132
+ response = self.session.put(
133
+ url=f"{self.base_path}/{synthesis_id}",
134
+ json=payload,
135
+ )
136
+ return Synthesis(**response.json())
137
+
138
+ @validate_call
139
+ def update(self, *, synthesis: Synthesis) -> Synthesis:
140
+ """
141
+ Update a synthesis record.
142
+
143
+ Parameters
144
+ ----------
145
+ synthesis : Synthesis
146
+ The synthesis record containing updated fields.
147
+
148
+ Returns
149
+ -------
150
+ Synthesis
151
+ The refreshed synthesis record.
152
+
153
+ Raises
154
+ ------
155
+ AlbertException
156
+ If the synthesis record is missing an ID.
157
+ """
158
+ if synthesis.id is None:
159
+ msg = "Synthesis id is required to update the record."
160
+ raise AlbertException(msg)
161
+ existing = self.get_by_id(id=synthesis.id)
162
+ patch_data = self._generate_patch_payload(existing=existing, updated=synthesis)
163
+ if len(patch_data.data) == 0:
164
+ return existing
165
+ self.session.patch(
166
+ url=f"{self.base_path}/{synthesis.id}",
167
+ json=patch_data.model_dump(by_alias=True, mode="json"),
168
+ )
169
+ return self.get_by_id(id=synthesis.id)
170
+
171
+ @validate_call
172
+ def update_reactant_row_values(
173
+ self,
174
+ *,
175
+ synthesis_id: SynthesisId,
176
+ row_id: str,
177
+ values: ReactantValues,
178
+ ) -> Synthesis:
179
+ """
180
+ Update the values for a reactant row.
181
+
182
+ Parameters
183
+ ----------
184
+ synthesis_id : SynthesisId
185
+ The synthesis ID.
186
+ row_id : str
187
+ The reactant row ID to update.
188
+ values : ReactantValues
189
+ The values to apply to the reactant row.
190
+
191
+ Returns
192
+ -------
193
+ Synthesis
194
+ The updated synthesis record.
195
+ """
196
+ payload = {
197
+ "data": [
198
+ {
199
+ "rowId": row_id,
200
+ "operation": "update",
201
+ "attribute": "values",
202
+ "newValue": values.model_dump(by_alias=True, mode="json"),
203
+ }
204
+ ]
205
+ }
206
+ self.session.patch(
207
+ url=f"{self.base_path}/{synthesis_id}/reactants/rows",
208
+ json=payload,
209
+ )
210
+ return self.get_by_id(id=synthesis_id)
211
+
212
+ @validate_call
213
+ def create_reactant_productant_table(self, *, synthesis_id: SynthesisId) -> Synthesis:
214
+ """
215
+ Initialize the reactant/product table for a synthesis.
216
+
217
+ Parameters
218
+ ----------
219
+ synthesis_id : SynthesisId
220
+ The synthesis ID.
221
+
222
+ Returns
223
+ -------
224
+ Synthesis
225
+ The synthesis record.
226
+ """
227
+ synthesis = self.get_by_id(id=synthesis_id)
228
+ if synthesis.inventory_id is not None:
229
+ return synthesis
230
+ row_sequence: RowSequence | None = synthesis.row_sequence
231
+ reactant_row_ids = row_sequence.reactants if row_sequence else []
232
+ if not reactant_row_ids and synthesis.reactants:
233
+ reactant_row_ids = [r.row_id for r in synthesis.reactants if r.row_id]
234
+ if not reactant_row_ids:
235
+ return synthesis
236
+
237
+ self.update_reactant_row_values(
238
+ synthesis_id=synthesis_id,
239
+ row_id=reactant_row_ids[0],
240
+ values=ReactantValues(
241
+ mass=None,
242
+ moles=None,
243
+ eq=None,
244
+ concentration=100,
245
+ ),
246
+ )
247
+
248
+ self._send_patch(
249
+ synthesis_id=synthesis_id,
250
+ payload={
251
+ "data": [
252
+ {
253
+ "attribute": "hideReactionWorksheet",
254
+ "operation": "update",
255
+ "newValue": "false",
256
+ }
257
+ ]
258
+ },
259
+ )
260
+
261
+ self._send_patch(
262
+ synthesis_id=synthesis_id,
263
+ payload={
264
+ "data": [
265
+ {
266
+ "attribute": "inventoryId",
267
+ "operation": "add",
268
+ }
269
+ ]
270
+ },
271
+ )
272
+ return self.get_by_id(id=synthesis_id)
273
+
274
+ def _send_patch(self, *, synthesis_id: SynthesisId, payload: dict[str, Any]) -> None:
275
+ """
276
+ Send a PATCH request to the synthesis endpoint.
277
+
278
+ Parameters
279
+ ----------
280
+ synthesis_id : SynthesisId
281
+ The synthesis ID.
282
+ payload : dict[str, Any]
283
+ The patch payload to send.
284
+
285
+ Returns
286
+ -------
287
+ None
288
+ """
289
+ self.session.patch(
290
+ url=f"{self.base_path}/{synthesis_id}",
291
+ json=payload,
292
+ )
@@ -27,6 +27,7 @@ from albert.core.shared.identifiers import (
27
27
  )
28
28
  from albert.exceptions import AlbertHTTPError
29
29
  from albert.resources.attachments import AttachmentCategory
30
+ from albert.resources.data_templates import ImportMode
30
31
  from albert.resources.tasks import (
31
32
  BaseTask,
32
33
  BatchTask,
@@ -34,7 +35,6 @@ from albert.resources.tasks import (
34
35
  CsvTableResponseItem,
35
36
  GeneralTask,
36
37
  HistoryEntity,
37
- ImportMode,
38
38
  PropertyTask,
39
39
  TaskAdapter,
40
40
  TaskCategory,
@@ -735,6 +735,7 @@ class TaskCollection(BaseCollection):
735
735
 
736
736
  return self.get_by_id(id=task.id)
737
737
 
738
+ @validate_call
738
739
  def get_history(
739
740
  self,
740
741
  *,
@@ -48,6 +48,7 @@ class WorksheetCollection(BaseCollection):
48
48
  response_json = self._add_session_to_sheets(response_json)
49
49
  return Worksheet(**response_json)
50
50
 
51
+ @validate_call
51
52
  def setup_worksheet(self, *, project_id: ProjectId, add_sheet=False) -> Worksheet:
52
53
  """Setup a new worksheet for a project.
53
54
 
@@ -69,6 +70,7 @@ class WorksheetCollection(BaseCollection):
69
70
  self.session.post(path, json=params)
70
71
  return self.get_by_project_id(project_id=project_id)
71
72
 
73
+ @validate_call
72
74
  def setup_new_sheet_from_template(
73
75
  self, *, project_id: ProjectId, sheet_template_id: str, sheet_name: str
74
76
  ) -> Worksheet:
@@ -94,6 +96,7 @@ class WorksheetCollection(BaseCollection):
94
96
  self.session.post(path, json=payload, params=params)
95
97
  return self.get_by_project_id(project_id=project_id)
96
98
 
99
+ @validate_call
97
100
  def add_sheet(self, *, project_id: ProjectId, sheet_name: str) -> Worksheet:
98
101
  """Create a new blank sheet in the Worksheet with the specified name.
99
102
 
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from datetime import datetime
2
4
 
3
5
  from pydantic import Field, PrivateAttr
@@ -21,7 +23,7 @@ class EntityLink(BaseAlbertModel):
21
23
  name: str | None = Field(default=None, exclude=True)
22
24
  category: str | None = Field(default=None, exclude=True)
23
25
 
24
- def to_entity_link(self) -> "EntityLink":
26
+ def to_entity_link(self) -> EntityLink:
25
27
  # Convience method to return self, so you can call this method on objects that are already entity links
26
28
  return self
27
29
 
@@ -43,7 +43,7 @@ class PGPatchDatum(PatchDatum):
43
43
 
44
44
  class GeneralPatchDatum(PGPatchDatum):
45
45
  colId: str | None = Field(default=None)
46
- actions: list[PGPatchDatum | DTPatchDatum] | None = None
46
+ actions: list[PGPatchDatum | DTPatchDatum | PatchDatum] | None = None
47
47
  operation: str | None = Field(default=None)
48
48
 
49
49
 
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from enum import Enum
2
4
 
3
5
  from pydantic import Field
@@ -53,7 +55,7 @@ class BatchDataRow(BaseAlbertModel):
53
55
  is_formula: bool | None = Field(default=None, alias="isFormula")
54
56
  is_lot_parent: bool | None = Field(default=None, alias="isLotParent")
55
57
  values: list[BatchDataValue] = Field(default_factory=list, alias="Values")
56
- child_rows: list["BatchDataRow"] = Field(default_factory=list, alias="ChildRows")
58
+ child_rows: list[BatchDataRow] = Field(default_factory=list, alias="ChildRows")
57
59
 
58
60
 
59
61
  class BatchDataColumn(BaseAlbertModel):
@@ -67,7 +69,7 @@ class BatchDataColumn(BaseAlbertModel):
67
69
  product_total: float | None = Field(default=None, alias="productTotal")
68
70
  parent_id: str | None = Field(default=None, alias="parentId")
69
71
  design_col_id: str | None = Field(default=None, alias="designColId")
70
- lots: list["BatchDataColumn"] = Field(default_factory=list, alias="Lots")
72
+ lots: list[BatchDataColumn] = Field(default_factory=list, alias="Lots")
71
73
 
72
74
 
73
75
  class BatchData(BaseResource):
albert/resources/cas.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from enum import Enum
2
4
 
3
5
  from pydantic import Field
@@ -53,7 +55,7 @@ class Cas(BaseResource):
53
55
  metadata: dict[str, MetadataItem] = Field(alias="Metadata", default_factory=dict)
54
56
 
55
57
  @classmethod
56
- def from_string(cls, *, number: str) -> "Cas":
58
+ def from_string(cls, *, number: str) -> Cas:
57
59
  """
58
60
  Creates a Cas object from a string.
59
61
 
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from enum import Enum
2
4
  from typing import Annotated, Any, Literal
3
5
 
@@ -171,7 +173,7 @@ class CustomField(BaseResource):
171
173
  api: CustomFieldAPI | None = Field(default=None)
172
174
 
173
175
  @model_validator(mode="after")
174
- def confirm_field_compatability(self) -> "CustomField":
176
+ def confirm_field_compatability(self) -> CustomField:
175
177
  if self.field_type == FieldType.LIST and self.category is None:
176
178
  raise ValueError("Category must be set for list fields")
177
179
  return self
@@ -1,10 +1,14 @@
1
+ from __future__ import annotations
2
+
1
3
  from enum import Enum
4
+ from pathlib import Path
5
+ from typing import Literal
2
6
 
3
7
  from pydantic import AliasChoices, Field, model_validator
4
8
 
5
9
  from albert.core.base import BaseAlbertModel
6
10
  from albert.core.shared.enums import SecurityClass
7
- from albert.core.shared.identifiers import DataColumnId, DataTemplateId
11
+ from albert.core.shared.identifiers import AttachmentId, DataColumnId, DataTemplateId
8
12
  from albert.core.shared.models.base import (
9
13
  AuditFields,
10
14
  BaseResource,
@@ -15,7 +19,7 @@ from albert.core.shared.models.base import (
15
19
  from albert.core.shared.types import MetadataItem, SerializeAsEntityLink
16
20
  from albert.resources._mixins import HydrationMixin
17
21
  from albert.resources.data_columns import DataColumn
18
- from albert.resources.parameter_groups import ParameterValue, ValueValidation
22
+ from albert.resources.parameter_groups import DataType, ParameterValue, ValueValidation
19
23
  from albert.resources.tagged_base import BaseTaggedResource
20
24
  from albert.resources.units import Unit
21
25
  from albert.resources.users import User
@@ -30,6 +34,11 @@ class CSVMapping(BaseAlbertModel):
30
34
  )
31
35
 
32
36
 
37
+ class Axis(str, Enum):
38
+ X = "X"
39
+ Y = "Y"
40
+
41
+
33
42
  class CurveDBMetadata(BaseAlbertModel):
34
43
  table_name: str | None = Field(default=None, alias="tableName")
35
44
  partition_key: str | None = Field(default=None, alias="partitionKey")
@@ -39,6 +48,9 @@ class StorageKeyReference(BaseAlbertModel):
39
48
  rawfile: str | None = None
40
49
  s3_input: str | None = Field(default=None, alias="s3Input")
41
50
  s3_output: str | None = Field(default=None, alias="s3Output")
51
+ preview: str | None = None
52
+ thumb: str | None = None
53
+ original: str | None = None
42
54
 
43
55
 
44
56
  class JobSummary(BaseAlbertModel):
@@ -46,11 +58,6 @@ class JobSummary(BaseAlbertModel):
46
58
  state: str | None = None
47
59
 
48
60
 
49
- class Axis(str, Enum):
50
- X = "X"
51
- Y = "Y"
52
-
53
-
54
61
  class CurveDataEntityLink(EntityLinkWithName):
55
62
  id: DataColumnId
56
63
  axis: Axis | None = Field(default=None)
@@ -102,11 +109,6 @@ class DataColumnValue(BaseResource):
102
109
  return self
103
110
 
104
111
 
105
- class Axis(str, Enum):
106
- X = "X"
107
- Y = "Y"
108
-
109
-
110
112
  class DataTemplate(BaseTaggedResource):
111
113
  name: str
112
114
  id: DataTemplateId | None = Field(None, alias="albertId")
@@ -131,6 +133,52 @@ class DataTemplate(BaseTaggedResource):
131
133
  full_name: str | None = Field(default=None, alias="fullName", exclude=True, frozen=True)
132
134
 
133
135
 
136
+ class ImportMode(str, Enum):
137
+ SCRIPT = "SCRIPT"
138
+ CSV = "CSV"
139
+
140
+
141
+ class CurveExample(BaseAlbertModel):
142
+ """
143
+ Curve example data for a data template column.
144
+
145
+ Attributes
146
+ ----------
147
+ mode : ImportMode
148
+ ``ImportMode.CSV`` ingests the CSV directly; ``ImportMode.SCRIPT`` runs the attached
149
+ script first (requires a script attachment on the column).
150
+ field_mapping : dict[str, str] | None
151
+ Optional header-to-curve-result mapping, e.g. ``{"visc": "Viscosity"}``. Overrides
152
+ auto-detected mappings.
153
+ file_path : str | Path | None
154
+ Local path to source CSV file.
155
+ attachment_id : AttachmentId | None
156
+ Existing attachment ID of source CSV file.
157
+ Provide exactly one source CSV (local path or existing attachment).
158
+ """
159
+
160
+ type: Literal[DataType.CURVE] = DataType.CURVE
161
+ mode: ImportMode = ImportMode.CSV
162
+ field_mapping: dict[str, str] | None = None
163
+ file_path: str | Path | None = None
164
+ attachment_id: AttachmentId | None = None
165
+
166
+ @model_validator(mode="after")
167
+ def _require_curve_source(self) -> CurveExample:
168
+ if (self.file_path is None) == (self.attachment_id is None):
169
+ raise ValueError(
170
+ "Provide exactly one of file_path or attachment_id for curve examples."
171
+ )
172
+ return self
173
+
174
+
175
+ class ImageExample(BaseAlbertModel):
176
+ """Example data for an image data column."""
177
+
178
+ type: Literal[DataType.IMAGE] = DataType.IMAGE
179
+ file_path: str | Path
180
+
181
+
134
182
  class DataTemplateSearchItemDataColumn(BaseAlbertModel):
135
183
  id: str
136
184
  name: str | None = None
@@ -1,7 +1,9 @@
1
+ from __future__ import annotations
2
+
1
3
  from enum import Enum
2
4
  from typing import Any
3
5
 
4
- from pydantic import Field
6
+ from pydantic import Field, model_validator
5
7
 
6
8
  from albert.core.shared.identifiers import CustomFieldId, EntityTypeId, RuleId
7
9
  from albert.core.shared.models.base import BaseAlbertModel, BaseResource, EntityLink
@@ -173,8 +175,8 @@ class EntityType(BaseResource):
173
175
  ----------
174
176
  id : EntityTypeId
175
177
  The unique identifier for the entity type.
176
- category : EntityCategory
177
- The category the entity type belongs to.
178
+ category : EntityCategory | None
179
+ The category the entity type belongs to. Required for tasks and inventories.
178
180
  custom_category : str | None, optional
179
181
  A custom category name for the entity type.
180
182
  label : str
@@ -194,7 +196,7 @@ class EntityType(BaseResource):
194
196
  """
195
197
 
196
198
  id: EntityTypeId | None = Field(alias="albertId", default=None)
197
- category: EntityCategory
199
+ category: EntityCategory | None = None
198
200
  custom_category: str | None = Field(
199
201
  default=None, max_length=100, min_length=1, alias="customCategory"
200
202
  )
@@ -215,6 +217,15 @@ class EntityType(BaseResource):
215
217
  alias="searchQueryString", default=None
216
218
  )
217
219
 
220
+ @model_validator(mode="after")
221
+ def validate_category(self) -> EntityType:
222
+ if (
223
+ self.service in {EntityServiceType.TASKS, EntityServiceType.INVENTORIES}
224
+ and self.category is None
225
+ ):
226
+ raise ValueError("category is required for tasks and inventories entity types.")
227
+ return self
228
+
218
229
 
219
230
  class EntityTypeOptionType(str, Enum):
220
231
  """Types of options that can be used in entity type fields.
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from enum import Enum
2
4
  from typing import Any
3
5
 
@@ -104,7 +106,7 @@ class CasAmount(BaseAlbertModel):
104
106
  )
105
107
 
106
108
  @model_validator(mode="after")
107
- def set_cas_attributes(self: "CasAmount") -> "CasAmount":
109
+ def set_cas_attributes(self: CasAmount) -> CasAmount:
108
110
  """Set attributes after model initialization from the Cas object, if provided."""
109
111
  if self.cas is not None:
110
112
  object.__setattr__(self, "id", self.cas.id)
@@ -132,7 +134,7 @@ class InventoryMinimum(BaseAlbertModel):
132
134
  minimum: float = Field(ge=0, le=1000000000000000)
133
135
 
134
136
  @model_validator(mode="after")
135
- def check_id_or_location(self: "InventoryMinimum") -> "InventoryMinimum":
137
+ def check_id_or_location(self: InventoryMinimum) -> InventoryMinimum:
136
138
  """
137
139
  Ensure that either an id or a location is provided.
138
140
  """
@@ -235,7 +237,7 @@ class InventoryItem(BaseTaggedResource):
235
237
  return value
236
238
 
237
239
  @model_validator(mode="after")
238
- def set_unit_category(self) -> "InventoryItem":
240
+ def set_unit_category(self) -> InventoryItem:
239
241
  """Set unit category from category if not defined."""
240
242
  if self.unit_category is None:
241
243
  if self.category in [InventoryCategory.RAW_MATERIALS, InventoryCategory.FORMULAS]:
@@ -245,7 +247,7 @@ class InventoryItem(BaseTaggedResource):
245
247
  return self
246
248
 
247
249
  @model_validator(mode="after")
248
- def validate_formula_fields(self) -> "InventoryItem":
250
+ def validate_formula_fields(self) -> InventoryItem:
249
251
  """Ensure required fields are present for formulas."""
250
252
  if self.category == InventoryCategory.FORMULAS and not self.project_id and not self.id:
251
253
  # Some legacy on platform formulas don't have a project_id so check if its already on platform
albert/resources/lists.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from enum import Enum
2
4
 
3
5
  from pydantic import Field, model_validator
@@ -34,7 +36,7 @@ class ListItem(BaseResource):
34
36
  list_type: str | None = Field(default=None, alias="listType")
35
37
 
36
38
  @model_validator(mode="after")
37
- def validate_list_type(self) -> "ListItem":
39
+ def validate_list_type(self) -> ListItem:
38
40
  if (
39
41
  self.category == ListItemCategory.PROJECTS
40
42
  and self.list_type is not None