albert 1.9.5__py3-none-any.whl → 1.10.0__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/collections/attachments.py +25 -8
  3. albert/collections/custom_templates.py +3 -0
  4. albert/collections/data_templates.py +121 -99
  5. albert/collections/entity_types.py +9 -2
  6. albert/collections/inventory.py +1 -1
  7. albert/collections/parameters.py +1 -0
  8. albert/collections/property_data.py +384 -279
  9. albert/collections/reports.py +4 -0
  10. albert/collections/tasks.py +266 -191
  11. albert/collections/worksheets.py +3 -0
  12. albert/core/shared/models/base.py +3 -1
  13. albert/core/shared/models/patch.py +1 -1
  14. albert/resources/attachments.py +8 -2
  15. albert/resources/batch_data.py +4 -2
  16. albert/resources/cas.py +3 -1
  17. albert/resources/custom_fields.py +3 -1
  18. albert/resources/data_templates.py +126 -9
  19. albert/resources/inventory.py +6 -4
  20. albert/resources/lists.py +3 -1
  21. albert/resources/notebooks.py +4 -2
  22. albert/resources/notes.py +5 -3
  23. albert/resources/parameter_groups.py +3 -1
  24. albert/resources/property_data.py +64 -5
  25. albert/resources/sheets.py +16 -14
  26. albert/resources/tags.py +3 -1
  27. albert/resources/tasks.py +156 -0
  28. albert/resources/worker_jobs.py +60 -0
  29. albert/resources/workflows.py +4 -2
  30. albert/utils/_patch.py +44 -1
  31. albert/utils/data_template.py +791 -0
  32. albert/utils/property_data.py +638 -0
  33. albert/utils/tasks.py +555 -0
  34. {albert-1.9.5.dist-info → albert-1.10.0.dist-info}/METADATA +2 -1
  35. {albert-1.9.5.dist-info → albert-1.10.0.dist-info}/RECORD +37 -33
  36. {albert-1.9.5.dist-info → albert-1.10.0.dist-info}/WHEEL +0 -0
  37. {albert-1.9.5.dist-info → albert-1.10.0.dist-info}/licenses/LICENSE +0 -0
albert/__init__.py CHANGED
@@ -4,4 +4,4 @@ from albert.core.auth.sso import AlbertSSOClient
4
4
 
5
5
  __all__ = ["Albert", "AlbertClientCredentials", "AlbertSSOClient"]
6
6
 
7
- __version__ = "1.9.5"
7
+ __version__ = "1.10.0"
@@ -9,7 +9,7 @@ from pydantic import validate_call
9
9
  from albert.collections.base import BaseCollection
10
10
  from albert.collections.files import FileCollection
11
11
  from albert.collections.notes import NotesCollection
12
- from albert.core.shared.identifiers import AttachmentId, InventoryId
12
+ from albert.core.shared.identifiers import AttachmentId, DataColumnId, InventoryId
13
13
  from albert.core.shared.types import MetadataItem
14
14
  from albert.resources.attachments import Attachment, AttachmentCategory
15
15
  from albert.resources.files import FileCategory, FileNamespace
@@ -49,7 +49,9 @@ class AttachmentCollection(BaseCollection):
49
49
  response = self.session.get(url=f"{self.base_path}/{id}")
50
50
  return Attachment(**response.json())
51
51
 
52
- def get_by_parent_ids(self, *, parent_ids: list[str]) -> dict[str, list[Attachment]]:
52
+ def get_by_parent_ids(
53
+ self, *, parent_ids: list[str], data_column_ids: list[DataColumnId] | None = None
54
+ ) -> dict[str, list[Attachment]]:
53
55
  """Retrieves attachments by their parent IDs.
54
56
 
55
57
  Note: This method returns a dictionary where the keys are parent IDs
@@ -69,7 +71,10 @@ class AttachmentCollection(BaseCollection):
69
71
  dict[str, list[Attachment]]
70
72
  A dictionary mapping parent IDs to lists of Attachment objects associated with each parent ID.
71
73
  """
72
- response = self.session.get(url=f"{self.base_path}/parents", params={"id": parent_ids})
74
+ response = self.session.get(
75
+ url=f"{self.base_path}/parents",
76
+ params={"id": parent_ids, "dataColumnId": data_column_ids},
77
+ )
73
78
  response_data = response.json()
74
79
  return {
75
80
  parent["parentId"]: [
@@ -125,7 +130,12 @@ class AttachmentCollection(BaseCollection):
125
130
  self.session.delete(f"{self.base_path}/{id}")
126
131
 
127
132
  def upload_and_attach_file_as_note(
128
- self, parent_id: str, file_data: IO, note_text: str = "", file_name: str = ""
133
+ self,
134
+ parent_id: str,
135
+ file_data: IO,
136
+ note_text: str = "",
137
+ file_name: str = "",
138
+ upload_key: str | None = None,
129
139
  ) -> Note:
130
140
  """Uploads a file and attaches it to a new note. A user can be tagged in the note_text string by using f-string and the User.to_note_mention() method.
131
141
  This allows for easy tagging and referencing of users within notes. example: f"Hello {tagged_user.to_note_mention()}!"
@@ -140,24 +150,31 @@ class AttachmentCollection(BaseCollection):
140
150
  Any additional text to add to the note, by default ""
141
151
  file_name : str, optional
142
152
  The name of the file, by default ""
153
+ upload_key : str | None, optional
154
+ Override the storage key used when signing and uploading the file.
155
+ Defaults to the provided ``file_name``.
143
156
 
144
157
  Returns
145
158
  -------
146
159
  Note
147
160
  The created note.
148
161
  """
149
- file_type = mimetypes.guess_type(file_name)[0]
162
+ upload_name = upload_key or file_name
163
+ if not upload_name:
164
+ raise ValueError("A file name or upload key must be provided for attachment upload.")
165
+
166
+ file_type = mimetypes.guess_type(file_name or upload_name)[0]
150
167
  file_collection = self._get_file_collection()
151
168
  note_collection = self._get_note_collection()
152
169
 
153
170
  file_collection.sign_and_upload_file(
154
171
  data=file_data,
155
- name=file_name,
172
+ name=upload_name,
156
173
  namespace=FileNamespace.RESULT.value,
157
174
  content_type=file_type,
158
175
  )
159
176
  file_info = file_collection.get_by_name(
160
- name=file_name, namespace=FileNamespace.RESULT.value
177
+ name=upload_name, namespace=FileNamespace.RESULT.value
161
178
  )
162
179
  note = Note(
163
180
  parent_id=parent_id,
@@ -166,7 +183,7 @@ class AttachmentCollection(BaseCollection):
166
183
  registered_note = note_collection.create(note=note)
167
184
  self.attach_file_to_note(
168
185
  note_id=registered_note.id,
169
- file_name=file_name,
186
+ file_name=file_name or Path(upload_name).name,
170
187
  file_key=file_info.name,
171
188
  )
172
189
  return note_collection.get_by_id(id=registered_note.id)
@@ -1,5 +1,7 @@
1
1
  from collections.abc import Iterator
2
2
 
3
+ from pydantic import validate_call
4
+
3
5
  from albert.collections.base import BaseCollection
4
6
  from albert.core.logging import logger
5
7
  from albert.core.pagination import AlbertPaginator
@@ -28,6 +30,7 @@ class CustomTemplatesCollection(BaseCollection):
28
30
  super().__init__(session=session)
29
31
  self.base_path = f"/api/{CustomTemplatesCollection._api_version}/customtemplates"
30
32
 
33
+ @validate_call
31
34
  def get_by_id(self, *, id: CustomTemplateId) -> CustomTemplate:
32
35
  """Get a Custom Template by ID
33
36
 
@@ -8,7 +8,7 @@ from albert.core.logging import logger
8
8
  from albert.core.pagination import AlbertPaginator
9
9
  from albert.core.session import AlbertSession
10
10
  from albert.core.shared.enums import OrderBy, PaginationMode
11
- from albert.core.shared.identifiers import DataTemplateId, UserId
11
+ from albert.core.shared.identifiers import DataColumnId, DataTemplateId, UserId
12
12
  from albert.core.shared.models.patch import (
13
13
  GeneralPatchDatum,
14
14
  GeneralPatchPayload,
@@ -17,13 +17,21 @@ from albert.core.shared.models.patch import (
17
17
  )
18
18
  from albert.exceptions import AlbertHTTPError
19
19
  from albert.resources.data_templates import (
20
+ CurveExample,
20
21
  DataColumnValue,
21
22
  DataTemplate,
22
23
  DataTemplateSearchItem,
24
+ ImageExample,
23
25
  ParameterValue,
24
26
  )
25
27
  from albert.resources.parameter_groups import DataType, EnumValidationValue, ValueValidation
26
28
  from albert.utils._patch import generate_data_template_patches
29
+ from albert.utils.data_template import (
30
+ add_parameter_enums,
31
+ build_curve_example,
32
+ build_image_example,
33
+ get_target_data_column,
34
+ )
27
35
 
28
36
 
29
37
  class DCPatchDatum(PGPatchPayload):
@@ -82,102 +90,6 @@ class DataTemplateCollection(BaseCollection):
82
90
  else:
83
91
  return self.add_parameters(data_template_id=dt.id, parameters=parameter_values)
84
92
 
85
- def _add_param_enums(
86
- self,
87
- *,
88
- data_template_id: DataTemplateId,
89
- new_parameters: list[ParameterValue],
90
- ) -> list[EnumValidationValue]:
91
- """Adds enum values to a parameter."""
92
-
93
- data_template = self.get_by_id(id=data_template_id)
94
- existing_parameters = data_template.parameter_values
95
- enums_by_sequence = {}
96
- for parameter in new_parameters:
97
- this_sequence = next(
98
- (
99
- p.sequence
100
- for p in existing_parameters
101
- if p.id == parameter.id and p.short_name == parameter.short_name
102
- ),
103
- None,
104
- )
105
- enum_patches = []
106
- if (
107
- parameter.validation
108
- and len(parameter.validation) > 0
109
- and isinstance(parameter.validation[0].value, list)
110
- ):
111
- existing_validation = (
112
- [x for x in existing_parameters if x.sequence == parameter.sequence]
113
- if existing_parameters
114
- else []
115
- )
116
- existing_enums = (
117
- [
118
- x
119
- for x in existing_validation[0].validation[0].value
120
- if isinstance(x, EnumValidationValue) and x.id is not None
121
- ]
122
- if (
123
- existing_validation
124
- and len(existing_validation) > 0
125
- and existing_validation[0].validation
126
- and len(existing_validation[0].validation) > 0
127
- and existing_validation[0].validation[0].value
128
- and isinstance(existing_validation[0].validation[0].value, list)
129
- )
130
- else []
131
- )
132
- updated_enums = (
133
- [
134
- x
135
- for x in parameter.validation[0].value
136
- if isinstance(x, EnumValidationValue)
137
- ]
138
- if parameter.validation[0].value
139
- else []
140
- )
141
-
142
- deleted_enums = [
143
- x for x in existing_enums if x.id not in [y.id for y in updated_enums]
144
- ]
145
-
146
- new_enums = [
147
- x for x in updated_enums if x.id not in [y.id for y in existing_enums]
148
- ]
149
-
150
- matching_enums = [
151
- x for x in updated_enums if x.id in [y.id for y in existing_enums]
152
- ]
153
-
154
- for new_enum in new_enums:
155
- enum_patches.append({"operation": "add", "text": new_enum.text})
156
- for deleted_enum in deleted_enums:
157
- enum_patches.append({"operation": "delete", "id": deleted_enum.id})
158
- for matching_enum in matching_enums:
159
- if (
160
- matching_enum.text
161
- != [x for x in existing_enums if x.id == matching_enum.id][0].text
162
- ):
163
- enum_patches.append(
164
- {
165
- "operation": "update",
166
- "id": matching_enum.id,
167
- "text": matching_enum.text,
168
- }
169
- )
170
-
171
- if len(enum_patches) > 0:
172
- enum_response = self.session.put(
173
- f"{self.base_path}/{data_template_id}/parameters/{this_sequence}/enums",
174
- json=enum_patches,
175
- )
176
- enums_by_sequence[this_sequence] = [
177
- EnumValidationValue(**x) for x in enum_response.json()
178
- ]
179
- return enums_by_sequence
180
-
181
93
  @validate_call
182
94
  def get_by_id(self, *, id: DataTemplateId) -> DataTemplate:
183
95
  """Get a data template by its ID.
@@ -195,6 +107,7 @@ class DataTemplateCollection(BaseCollection):
195
107
  response = self.session.get(f"{self.base_path}/{id}")
196
108
  return DataTemplate(**response.json())
197
109
 
110
+ @validate_call
198
111
  def get_by_ids(self, *, ids: list[DataTemplateId]) -> list[DataTemplate]:
199
112
  """Get a list of data templates by their IDs.
200
113
 
@@ -234,6 +147,7 @@ class DataTemplateCollection(BaseCollection):
234
147
  return t.hydrate()
235
148
  return None
236
149
 
150
+ @validate_call
237
151
  def add_data_columns(
238
152
  self, *, data_template_id: DataTemplateId, data_columns: list[DataColumnValue]
239
153
  ) -> DataTemplate:
@@ -277,6 +191,7 @@ class DataTemplateCollection(BaseCollection):
277
191
  )
278
192
  return self.get_by_id(id=data_template_id)
279
193
 
194
+ @validate_call
280
195
  def add_parameters(
281
196
  self, *, data_template_id: DataTemplateId, parameters: list[ParameterValue]
282
197
  ) -> DataTemplate:
@@ -325,7 +240,9 @@ class DataTemplateCollection(BaseCollection):
325
240
  if param.id in initial_enum_values:
326
241
  param.validation[0].value = initial_enum_values[param.id]
327
242
  param.validation[0].datatype = DataType.ENUM
328
- self._add_param_enums(
243
+ add_parameter_enums(
244
+ session=self.session,
245
+ base_path=self.base_path,
329
246
  data_template_id=data_template_id,
330
247
  new_parameters=[param],
331
248
  )
@@ -396,6 +313,12 @@ class DataTemplateCollection(BaseCollection):
396
313
  -------
397
314
  DataTemplate
398
315
  The Updated DataTemplate object.
316
+
317
+ Warnings
318
+ --------
319
+ Only scalar data column values (text, number, dropdown) can be updated using this function. Use
320
+ `set_curve_example` / `set_image_example` to set example values for other data column types.
321
+
399
322
  """
400
323
 
401
324
  existing = self.get_by_id(id=data_template.id)
@@ -471,7 +394,9 @@ class DataTemplateCollection(BaseCollection):
471
394
  param.validation[0].datatype = DataType.ENUM # Add this line
472
395
 
473
396
  # Add enum values to newly created parameters
474
- self._add_param_enums(
397
+ add_parameter_enums(
398
+ session=self.session,
399
+ base_path=self.base_path,
475
400
  data_template_id=existing.id,
476
401
  new_parameters=returned_parameters,
477
402
  )
@@ -555,6 +480,7 @@ class DataTemplateCollection(BaseCollection):
555
480
  )
556
481
  return self.get_by_id(id=data_template.id)
557
482
 
483
+ @validate_call
558
484
  def delete(self, *, id: DataTemplateId) -> None:
559
485
  """Deletes a data template by its ID.
560
486
 
@@ -623,3 +549,99 @@ class DataTemplateCollection(BaseCollection):
623
549
  yield from hydrated_templates
624
550
  except AlbertHTTPError as e:
625
551
  logger.warning(f"Error hydrating batch {batch}: {e}")
552
+
553
+ @validate_call
554
+ def set_curve_example(
555
+ self,
556
+ *,
557
+ data_template_id: DataTemplateId,
558
+ data_column_id: DataColumnId | None = None,
559
+ data_column_name: str | None = None,
560
+ example: CurveExample,
561
+ ) -> DataTemplate:
562
+ """Set a curve example on a Curve data column.
563
+
564
+ Parameters
565
+ ----------
566
+ data_template_id : DataTemplateId
567
+ Target data template ID.
568
+ data_column_id : DataColumnId, optional
569
+ Target curve column ID (provide exactly one of id or name).
570
+ data_column_name : str, optional
571
+ Target curve column name (provide exactly one of id or name).
572
+ example : CurveExample
573
+ Curve example payload
574
+
575
+ Returns
576
+ -------
577
+ DataTemplate
578
+ The updated data template after the example is applied.
579
+ """
580
+ data_template = self.get_by_id(id=data_template_id)
581
+ target_column = get_target_data_column(
582
+ data_template=data_template,
583
+ data_template_id=data_template_id,
584
+ data_column_id=data_column_id,
585
+ data_column_name=data_column_name,
586
+ )
587
+ payload = build_curve_example(
588
+ session=self.session,
589
+ data_template_id=data_template_id,
590
+ example=example,
591
+ target_column=target_column,
592
+ )
593
+ if not payload.data:
594
+ return data_template
595
+ self.session.patch(
596
+ f"{self.base_path}/{data_template_id}",
597
+ json=payload.model_dump(mode="json", by_alias=True, exclude_none=True),
598
+ )
599
+ return self.get_by_id(id=data_template_id)
600
+
601
+ @validate_call
602
+ def set_image_example(
603
+ self,
604
+ *,
605
+ data_template_id: DataTemplateId,
606
+ data_column_id: DataColumnId | None = None,
607
+ data_column_name: str | None = None,
608
+ example: ImageExample,
609
+ ) -> DataTemplate:
610
+ """Set an image example on a Image data column.
611
+
612
+ Parameters
613
+ ----------
614
+ data_template_id : DataTemplateId
615
+ Target data template ID.
616
+ data_column_id : DataColumnId, optional
617
+ Target image column ID (provide exactly one of id or name).
618
+ data_column_name : str, optional
619
+ Target image column name (provide exactly one of id or name).
620
+ example : ImageExample
621
+ Image example payload
622
+
623
+ Returns
624
+ -------
625
+ DataTemplate
626
+ The updated data template after the example is applied.
627
+ """
628
+ data_template = self.get_by_id(id=data_template_id)
629
+ target_column = get_target_data_column(
630
+ data_template=data_template,
631
+ data_template_id=data_template_id,
632
+ data_column_id=data_column_id,
633
+ data_column_name=data_column_name,
634
+ )
635
+ payload = build_image_example(
636
+ session=self.session,
637
+ data_template_id=data_template_id,
638
+ example=example,
639
+ target_column=target_column,
640
+ )
641
+ if not payload.data:
642
+ return data_template
643
+ self.session.patch(
644
+ f"{self.base_path}/{data_template_id}",
645
+ json=payload.model_dump(mode="json", by_alias=True, exclude_none=True),
646
+ )
647
+ return self.get_by_id(id=data_template_id)
@@ -1,5 +1,7 @@
1
1
  from collections.abc import Iterator
2
2
 
3
+ from pydantic import validate_call
4
+
3
5
  from albert.collections.base import BaseCollection
4
6
  from albert.core.pagination import AlbertPaginator, PaginationMode
5
7
  from albert.core.session import AlbertSession
@@ -41,6 +43,7 @@ class EntityTypeCollection(BaseCollection):
41
43
  super().__init__(session=session)
42
44
  self.base_path = f"/api/{EntityTypeCollection._api_version}/entitytypes"
43
45
 
46
+ @validate_call
44
47
  def get_by_id(self, *, id: EntityTypeId) -> EntityType:
45
48
  """Get an entity type by its ID.
46
49
  Parameters
@@ -203,6 +206,7 @@ class EntityTypeCollection(BaseCollection):
203
206
 
204
207
  return patches
205
208
 
209
+ @validate_call
206
210
  def delete(self, *, id: EntityTypeId) -> None:
207
211
  """Delete an entity type.
208
212
  Parameters
@@ -212,6 +216,7 @@ class EntityTypeCollection(BaseCollection):
212
216
  """
213
217
  self.session.delete(f"{self.base_path}/{id}")
214
218
 
219
+ @validate_call
215
220
  def get_rules(self, *, id: EntityTypeId) -> list[EntityTypeRule]:
216
221
  """Get the rules for an entity type.
217
222
  Parameters
@@ -222,6 +227,7 @@ class EntityTypeCollection(BaseCollection):
222
227
  response = self.session.get(f"{self.base_path}/rules/{id}")
223
228
  return [EntityTypeRule(**rule) for rule in response.json()]
224
229
 
230
+ @validate_call
225
231
  def set_rules(self, *, id: EntityTypeId, rules: list[EntityTypeRule]) -> list[EntityTypeRule]:
226
232
  """Create or update the rules for an entity type.
227
233
  Parameters
@@ -241,6 +247,7 @@ class EntityTypeCollection(BaseCollection):
241
247
  )
242
248
  return [EntityTypeRule(**rule) for rule in response.json()]
243
249
 
250
+ @validate_call
244
251
  def delete_rules(self, *, id: EntityTypeId) -> None:
245
252
  """Delete the rules for an entity type.
246
253
  Parameters
@@ -263,12 +270,12 @@ class EntityTypeCollection(BaseCollection):
263
270
  ----------
264
271
  service : EntityServiceType | None, optional
265
272
  The service type the entity type is associated with, by default None
266
- limit : int, optional
267
- Maximum number of results to return, by default 100
268
273
  start_key : str | None, optional
269
274
  Key to start pagination from, by default None
270
275
  order : OrderBy | None, optional
271
276
  Sort order (ascending/descending), by default None
277
+ max_items : int | None, optional
278
+ Maximum number of items to return, by default None
272
279
  Yields
273
280
  ------
274
281
  Iterator[EntityType]
@@ -281,7 +281,7 @@ class InventoryCollection(BaseCollection):
281
281
  """Add inventory specs to the inventory item.
282
282
 
283
283
  An `InventorySpec` is a property that was not directly measured via a task,
284
- but is a generic property of that inentory item.
284
+ but is a generic property of that inventory item.
285
285
 
286
286
  Parameters
287
287
  ----------
@@ -98,6 +98,7 @@ class ParameterCollection(BaseCollection):
98
98
  url = f"{self.base_path}/{id}"
99
99
  self.session.delete(url)
100
100
 
101
+ @validate_call
101
102
  def get_all(
102
103
  self,
103
104
  *,