albert 1.13.0b1__py3-none-any.whl → 1.14.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.
- albert/__init__.py +1 -1
- albert/collections/attachments.py +17 -11
- albert/collections/projects.py +9 -9
- albert/collections/worksheets.py +165 -0
- albert/core/auth/_listener.py +4 -2
- albert/core/shared/enums.py +1 -0
- albert/resources/acls.py +26 -2
- albert/resources/notebooks.py +26 -2
- albert/utils/property_data.py +61 -1
- albert/utils/worksheets.py +90 -0
- {albert-1.13.0b1.dist-info → albert-1.14.0.dist-info}/METADATA +1 -1
- {albert-1.13.0b1.dist-info → albert-1.14.0.dist-info}/RECORD +14 -13
- {albert-1.13.0b1.dist-info → albert-1.14.0.dist-info}/WHEEL +0 -0
- {albert-1.13.0b1.dist-info → albert-1.14.0.dist-info}/licenses/LICENSE +0 -0
albert/__init__.py
CHANGED
|
@@ -152,20 +152,30 @@ class AttachmentCollection(BaseCollection):
|
|
|
152
152
|
The name of the file, by default ""
|
|
153
153
|
upload_key : str | None, optional
|
|
154
154
|
Override the storage key used when signing and uploading the file.
|
|
155
|
-
Defaults to
|
|
155
|
+
Defaults to ``{parent_id}/{note_id}/{file_name}``.
|
|
156
156
|
|
|
157
157
|
Returns
|
|
158
158
|
-------
|
|
159
159
|
Note
|
|
160
160
|
The created note.
|
|
161
161
|
"""
|
|
162
|
-
|
|
163
|
-
if not upload_name:
|
|
162
|
+
if not (upload_key or file_name):
|
|
164
163
|
raise ValueError("A file name or upload key must be provided for attachment upload.")
|
|
165
164
|
|
|
166
|
-
file_type = mimetypes.guess_type(file_name or upload_name)[0]
|
|
167
|
-
file_collection = self._get_file_collection()
|
|
168
165
|
note_collection = self._get_note_collection()
|
|
166
|
+
note = Note(
|
|
167
|
+
parent_id=parent_id,
|
|
168
|
+
note=note_text,
|
|
169
|
+
)
|
|
170
|
+
registered_note = note_collection.create(note=note)
|
|
171
|
+
if upload_key:
|
|
172
|
+
attachment_name = file_name or Path(upload_key).name
|
|
173
|
+
upload_name = upload_key
|
|
174
|
+
else:
|
|
175
|
+
attachment_name = file_name
|
|
176
|
+
upload_name = f"{parent_id}/{registered_note.id}/{file_name}"
|
|
177
|
+
file_type = mimetypes.guess_type(attachment_name or upload_name)[0]
|
|
178
|
+
file_collection = self._get_file_collection()
|
|
169
179
|
|
|
170
180
|
file_collection.sign_and_upload_file(
|
|
171
181
|
data=file_data,
|
|
@@ -176,16 +186,12 @@ class AttachmentCollection(BaseCollection):
|
|
|
176
186
|
file_info = file_collection.get_by_name(
|
|
177
187
|
name=upload_name, namespace=FileNamespace.RESULT.value
|
|
178
188
|
)
|
|
179
|
-
note = Note(
|
|
180
|
-
parent_id=parent_id,
|
|
181
|
-
note=note_text,
|
|
182
|
-
)
|
|
183
|
-
registered_note = note_collection.create(note=note)
|
|
184
189
|
self.attach_file_to_note(
|
|
185
190
|
note_id=registered_note.id,
|
|
186
|
-
file_name=
|
|
191
|
+
file_name=attachment_name,
|
|
187
192
|
file_key=file_info.name,
|
|
188
193
|
)
|
|
194
|
+
|
|
189
195
|
return note_collection.get_by_id(id=registered_note.id)
|
|
190
196
|
|
|
191
197
|
@validate_call
|
albert/collections/projects.py
CHANGED
|
@@ -142,17 +142,17 @@ class ProjectCollection(BaseCollection):
|
|
|
142
142
|
----------
|
|
143
143
|
text : str, optional
|
|
144
144
|
Full-text search query.
|
|
145
|
-
status : list
|
|
145
|
+
status : list[str], optional
|
|
146
146
|
Filter by project statuses.
|
|
147
|
-
market_segment : list
|
|
147
|
+
market_segment : list[str], optional
|
|
148
148
|
Filter by market segment.
|
|
149
|
-
application : list
|
|
149
|
+
application : list[str], optional
|
|
150
150
|
Filter by application.
|
|
151
|
-
technology : list
|
|
151
|
+
technology : list[str], optional
|
|
152
152
|
Filter by technology tags.
|
|
153
|
-
created_by : list
|
|
153
|
+
created_by : list[str], optional
|
|
154
154
|
Filter by user names who created the project.
|
|
155
|
-
location : list
|
|
155
|
+
location : list[str], optional
|
|
156
156
|
Filter by location(s).
|
|
157
157
|
from_created_at : str, optional
|
|
158
158
|
Earliest creation date in 'YYYY-MM-DD' format.
|
|
@@ -162,15 +162,15 @@ class ProjectCollection(BaseCollection):
|
|
|
162
162
|
Facet field to filter on.
|
|
163
163
|
facet_text : str, optional
|
|
164
164
|
Facet text to search for.
|
|
165
|
-
contains_field : list
|
|
165
|
+
contains_field : list[str], optional
|
|
166
166
|
Fields to search inside.
|
|
167
|
-
contains_text : list
|
|
167
|
+
contains_text : list[str], optional
|
|
168
168
|
Values to search for within the `contains_field`.
|
|
169
169
|
linked_to : str, optional
|
|
170
170
|
Entity ID the project is linked to.
|
|
171
171
|
my_project : bool, optional
|
|
172
172
|
If True, return only projects owned by current user.
|
|
173
|
-
my_role : list
|
|
173
|
+
my_role : list[str], optional
|
|
174
174
|
User roles to filter by.
|
|
175
175
|
order_by : OrderBy, optional
|
|
176
176
|
Sort order. Default is DESCENDING.
|
albert/collections/worksheets.py
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
from pydantic import validate_call
|
|
2
2
|
|
|
3
3
|
from albert.collections.base import BaseCollection
|
|
4
|
+
from albert.collections.custom_templates import CustomTemplatesCollection
|
|
4
5
|
from albert.core.session import AlbertSession
|
|
5
6
|
from albert.core.shared.identifiers import ProjectId
|
|
7
|
+
from albert.resources.acls import ACLContainer
|
|
8
|
+
from albert.resources.custom_templates import CustomTemplate
|
|
6
9
|
from albert.resources.worksheets import Worksheet
|
|
10
|
+
from albert.utils.worksheets import (
|
|
11
|
+
get_columns_to_copy,
|
|
12
|
+
get_prg_rows_to_copy,
|
|
13
|
+
get_sheet_from_worksheet,
|
|
14
|
+
get_task_rows_to_copy,
|
|
15
|
+
)
|
|
7
16
|
|
|
8
17
|
|
|
9
18
|
class WorksheetCollection(BaseCollection):
|
|
@@ -116,3 +125,159 @@ class WorksheetCollection(BaseCollection):
|
|
|
116
125
|
url = f"{self.base_path}/project/{project_id}/sheets"
|
|
117
126
|
self.session.put(url=url, json=payload)
|
|
118
127
|
return self.get_by_project_id(project_id=project_id)
|
|
128
|
+
|
|
129
|
+
@validate_call
|
|
130
|
+
def duplicate_sheet(
|
|
131
|
+
self,
|
|
132
|
+
*,
|
|
133
|
+
project_id: ProjectId,
|
|
134
|
+
source_sheet_name: str,
|
|
135
|
+
new_sheet_name: str,
|
|
136
|
+
copy_all_pd_rows: bool = True,
|
|
137
|
+
copy_all_pinned_columns: bool = True,
|
|
138
|
+
copy_all_unpinned_columns: bool = True,
|
|
139
|
+
column_names: list[str] | None = None,
|
|
140
|
+
task_row_names: list[str] | None = None,
|
|
141
|
+
) -> Worksheet:
|
|
142
|
+
"""Duplicate an existing sheet within the same project.
|
|
143
|
+
|
|
144
|
+
This creates a new sheet based on the specified source sheet. You can control
|
|
145
|
+
which Product Design (PD) & Results rows and columns are copied using the available options.
|
|
146
|
+
The final list of columns copied is the union of:
|
|
147
|
+
- all pinned columns (if copy_all_pinned_columns is True)
|
|
148
|
+
- all unpinned columns (if copy_all_unpinned_columns is True)
|
|
149
|
+
- explicitly listed column names (column_names)
|
|
150
|
+
|
|
151
|
+
Parameters
|
|
152
|
+
----------
|
|
153
|
+
project_id : str
|
|
154
|
+
The project ID under which the sheet exists.
|
|
155
|
+
source_sheet_name : str
|
|
156
|
+
The name of the existing sheet to duplicate.
|
|
157
|
+
new_sheet_name : str
|
|
158
|
+
The name of the new sheet to create.
|
|
159
|
+
copy_all_pd_rows : bool, optional
|
|
160
|
+
When True, all PD (Product Design) rows from the source sheet are copied.
|
|
161
|
+
When False, only rows corresponding to the selected columns will be copied.
|
|
162
|
+
Default is True.
|
|
163
|
+
copy_all_pinned_columns : bool, optional
|
|
164
|
+
If True, includes all pinned columns from the source sheet. Default is True.
|
|
165
|
+
copy_all_unpinned_columns : bool, optional
|
|
166
|
+
If True, includes all unpinned columns from the source sheet. Default is True.
|
|
167
|
+
column_names : list[str], optional
|
|
168
|
+
A list of column names to explicitly copy. These are resolved internally
|
|
169
|
+
to column IDs using the sheet's product design grid.
|
|
170
|
+
task_row_names : list[str], optional
|
|
171
|
+
List of task row names to include from the tasks.
|
|
172
|
+
|
|
173
|
+
Returns
|
|
174
|
+
-------
|
|
175
|
+
Worksheet
|
|
176
|
+
The Worksheet entity containing newly created sheet.
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
worksheet = self.get_by_project_id(project_id=project_id)
|
|
180
|
+
sheet = get_sheet_from_worksheet(sheet_name=source_sheet_name, worksheet=worksheet)
|
|
181
|
+
columns = get_columns_to_copy(
|
|
182
|
+
sheet=sheet,
|
|
183
|
+
copy_all_pinned_columns=copy_all_pinned_columns,
|
|
184
|
+
copy_all_unpinned_columns=copy_all_unpinned_columns,
|
|
185
|
+
input_column_names=column_names,
|
|
186
|
+
)
|
|
187
|
+
task_rows = get_task_rows_to_copy(sheet=sheet, input_row_names=task_row_names)
|
|
188
|
+
|
|
189
|
+
payload = {
|
|
190
|
+
"name": new_sheet_name,
|
|
191
|
+
"sourceData": {
|
|
192
|
+
"projectId": project_id,
|
|
193
|
+
"sheetId": sheet.id,
|
|
194
|
+
"Columns": [{"id": col_id} for col_id in columns],
|
|
195
|
+
"copyAllPDRows": copy_all_pd_rows,
|
|
196
|
+
"TaskRows": [{"id": row_id} for row_id in task_rows],
|
|
197
|
+
},
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
path = f"{self.base_path}/project/{project_id}/sheets"
|
|
201
|
+
self.session.put(path, json=payload)
|
|
202
|
+
return self.get_by_project_id(project_id=project_id)
|
|
203
|
+
|
|
204
|
+
@validate_call
|
|
205
|
+
def create_sheet_template(
|
|
206
|
+
self,
|
|
207
|
+
*,
|
|
208
|
+
project_id: ProjectId,
|
|
209
|
+
source_sheet_name: str,
|
|
210
|
+
template_name: str,
|
|
211
|
+
copy_all_pd_rows: bool = True,
|
|
212
|
+
copy_all_pinned_columns: bool = True,
|
|
213
|
+
copy_all_unpinned_columns: bool = True,
|
|
214
|
+
column_names: list[str] | None = None,
|
|
215
|
+
task_row_names: list[str] | None = None,
|
|
216
|
+
prg_row_names: list[str] | None = None,
|
|
217
|
+
acl: ACLContainer | None = None,
|
|
218
|
+
) -> CustomTemplate:
|
|
219
|
+
"""Create a new sheet template from an existing sheet.
|
|
220
|
+
|
|
221
|
+
Parameters
|
|
222
|
+
----------
|
|
223
|
+
project_id : str
|
|
224
|
+
The project ID under which the sheet exists.
|
|
225
|
+
source_sheet_name : str
|
|
226
|
+
The name of the existing sheet to use as the template source.
|
|
227
|
+
template_name : str
|
|
228
|
+
The name of the new template.
|
|
229
|
+
copy_all_pd_rows : bool, optional
|
|
230
|
+
When True, all PD (Product Design) rows from the source sheet are copied.
|
|
231
|
+
When False, only rows corresponding to the selected columns will be copied.
|
|
232
|
+
copy_all_pinned_columns : bool, optional
|
|
233
|
+
If True, includes all pinned columns from the source sheet. Default is True.
|
|
234
|
+
copy_all_unpinned_columns : bool, optional
|
|
235
|
+
If True, includes all unpinned columns from the source sheet. Default is True.
|
|
236
|
+
column_names : list[str], optional
|
|
237
|
+
A list of column names to explicitly copy. These are resolved internally
|
|
238
|
+
to column IDs using the sheet's product design grid.
|
|
239
|
+
task_row_names : list[str], optional
|
|
240
|
+
List of task row names to include from the tasks.
|
|
241
|
+
prg_row_names : list[str], optional
|
|
242
|
+
List of parameter group row names to include.
|
|
243
|
+
acl : ACLContainer, optional
|
|
244
|
+
ACL for the template.
|
|
245
|
+
|
|
246
|
+
Returns
|
|
247
|
+
-------
|
|
248
|
+
CustomTemplate
|
|
249
|
+
The CustomTemplate for the created sheet template.
|
|
250
|
+
"""
|
|
251
|
+
worksheet = self.get_by_project_id(project_id=project_id)
|
|
252
|
+
sheet = get_sheet_from_worksheet(sheet_name=source_sheet_name, worksheet=worksheet)
|
|
253
|
+
columns = get_columns_to_copy(
|
|
254
|
+
sheet=sheet,
|
|
255
|
+
copy_all_pinned_columns=copy_all_pinned_columns,
|
|
256
|
+
copy_all_unpinned_columns=copy_all_unpinned_columns,
|
|
257
|
+
input_column_names=column_names,
|
|
258
|
+
)
|
|
259
|
+
if not columns:
|
|
260
|
+
raise ValueError("At least one column must be selected to create a template.")
|
|
261
|
+
task_rows = get_task_rows_to_copy(sheet=sheet, input_row_names=task_row_names)
|
|
262
|
+
prg_rows = get_prg_rows_to_copy(sheet=sheet, input_row_names=prg_row_names)
|
|
263
|
+
|
|
264
|
+
payload = {
|
|
265
|
+
"name": template_name,
|
|
266
|
+
"sourceData": {
|
|
267
|
+
"projectId": project_id,
|
|
268
|
+
"sheetId": sheet.id,
|
|
269
|
+
"Columns": [{"id": col_id} for col_id in columns],
|
|
270
|
+
"copyAllPDRows": copy_all_pd_rows,
|
|
271
|
+
"TaskRows": [{"id": row_id} for row_id in task_rows],
|
|
272
|
+
"PRGRows": [{"id": row_id} for row_id in prg_rows],
|
|
273
|
+
},
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if acl is not None:
|
|
277
|
+
payload["ACL"] = acl.model_dump(exclude_none=True, by_alias=True, mode="json")
|
|
278
|
+
|
|
279
|
+
path = f"{self.base_path}/sheet/template"
|
|
280
|
+
response = self.session.post(path, json=payload)
|
|
281
|
+
response_json = response.json()
|
|
282
|
+
ctp_id = response_json.get("ctpId")
|
|
283
|
+
return CustomTemplatesCollection(session=self.session).get_by_id(id=ctp_id)
|
albert/core/auth/_listener.py
CHANGED
|
@@ -31,6 +31,10 @@ class RequestHandler(BaseHTTPRequestHandler):
|
|
|
31
31
|
status = "successful" if self.server.token else "failed (no token found)"
|
|
32
32
|
self.send_response(200)
|
|
33
33
|
self.send_header("Content-Type", "text/html")
|
|
34
|
+
self.send_header(
|
|
35
|
+
"Content-Security-Policy",
|
|
36
|
+
"default-src 'none'; frame-ancestors 'none'; base-uri 'none';",
|
|
37
|
+
)
|
|
34
38
|
self.end_headers()
|
|
35
39
|
self.wfile.write(
|
|
36
40
|
f"""
|
|
@@ -38,8 +42,6 @@ class RequestHandler(BaseHTTPRequestHandler):
|
|
|
38
42
|
<body>
|
|
39
43
|
<h1>Authentication {status}</h1>
|
|
40
44
|
<p>You can close this window now.</p>
|
|
41
|
-
<script>window.close()</script>
|
|
42
|
-
<button onclick="window.close()">Close Window</button>
|
|
43
45
|
</body>
|
|
44
46
|
</html>
|
|
45
47
|
""".encode()
|
albert/core/shared/enums.py
CHANGED
albert/resources/acls.py
CHANGED
|
@@ -3,10 +3,11 @@ from enum import Enum
|
|
|
3
3
|
from pydantic import Field
|
|
4
4
|
|
|
5
5
|
from albert.core.base import BaseAlbertModel
|
|
6
|
+
from albert.core.shared.models.base import BaseResource
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
class AccessControlLevel(str, Enum):
|
|
9
|
-
"""
|
|
10
|
+
"""Access control levels you can grant users."""
|
|
10
11
|
|
|
11
12
|
PROJECT_OWNER = "ProjectOwner"
|
|
12
13
|
PROJECT_EDITOR = "ProjectEditor"
|
|
@@ -22,9 +23,32 @@ class AccessControlLevel(str, Enum):
|
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
class ACL(BaseAlbertModel):
|
|
25
|
-
"""
|
|
26
|
+
"""A single access rule for a user.
|
|
27
|
+
|
|
28
|
+
Attributes
|
|
29
|
+
----------
|
|
30
|
+
id : str
|
|
31
|
+
The user or team this rule applies to.
|
|
32
|
+
fgc : AccessControlLevel | None
|
|
33
|
+
The access level for that user or team.
|
|
34
|
+
"""
|
|
26
35
|
|
|
27
36
|
id: str = Field(description="The id of the user for which this ACL applies")
|
|
28
37
|
fgc: AccessControlLevel | None = Field(
|
|
29
38
|
default=None, description="The Fine-Grain Control Level"
|
|
30
39
|
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ACLContainer(BaseResource):
|
|
43
|
+
"""Access settings with a default class and a list of rules.
|
|
44
|
+
|
|
45
|
+
Attributes
|
|
46
|
+
----------
|
|
47
|
+
acl_class : str | None
|
|
48
|
+
The default access class (for example, "restricted" or "confidential").
|
|
49
|
+
fgclist : list[ACL] | None
|
|
50
|
+
Specific access rules for users or teams.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
acl_class: str | None = Field(default=None, alias="class")
|
|
54
|
+
fgclist: list[ACL] | None = Field(default=None, alias="fgclist")
|
albert/resources/notebooks.py
CHANGED
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
4
|
import uuid
|
|
5
|
+
import warnings
|
|
5
6
|
from datetime import datetime
|
|
6
7
|
from enum import Enum
|
|
7
8
|
from pathlib import Path
|
|
@@ -14,7 +15,7 @@ from albert.core.base import BaseAlbertModel
|
|
|
14
15
|
from albert.core.shared.identifiers import LinkId, NotebookId, ProjectId, SynthesisId, TaskId
|
|
15
16
|
from albert.core.shared.models.base import BaseResource, EntityLink
|
|
16
17
|
from albert.exceptions import AlbertException
|
|
17
|
-
from albert.resources.acls import ACL
|
|
18
|
+
from albert.resources.acls import ACL, ACLContainer
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
class ListBlockStyle(str, Enum):
|
|
@@ -297,13 +298,36 @@ class PutBlockPayload(BaseAlbertModel):
|
|
|
297
298
|
|
|
298
299
|
|
|
299
300
|
class NotebookCopyACL(BaseResource):
|
|
301
|
+
"""
|
|
302
|
+
Access settings applied to a copied notebook.
|
|
303
|
+
|
|
304
|
+
Warning
|
|
305
|
+
-----
|
|
306
|
+
Deprecated and will be removed in 2.0. Use ``ACLContainer`` instead.
|
|
307
|
+
|
|
308
|
+
Attributes
|
|
309
|
+
----------
|
|
310
|
+
fgclist : list[ACL]
|
|
311
|
+
Specific access rules for users or teams.
|
|
312
|
+
acl_class : str
|
|
313
|
+
Default access class (for example, "restricted" or "confidential").
|
|
314
|
+
"""
|
|
315
|
+
|
|
300
316
|
fgclist: list[ACL] = Field(default=None)
|
|
301
317
|
acl_class: str = Field(alias="class")
|
|
302
318
|
|
|
319
|
+
def __init__(self, **data):
|
|
320
|
+
warnings.warn(
|
|
321
|
+
"NotebookCopyACL is deprecated and will be removed in 2.0; use ACLContainer instead.",
|
|
322
|
+
DeprecationWarning,
|
|
323
|
+
stacklevel=2,
|
|
324
|
+
)
|
|
325
|
+
super().__init__(**data)
|
|
326
|
+
|
|
303
327
|
|
|
304
328
|
class NotebookCopyInfo(BaseAlbertModel):
|
|
305
329
|
id: NotebookId
|
|
306
330
|
parent_id: str = Field(alias="parentId")
|
|
307
331
|
notebook_name: str | None = Field(default=None, alias="notebookName")
|
|
308
332
|
name: str | None = Field(default=None)
|
|
309
|
-
acl: NotebookCopyACL | None = Field(default=None)
|
|
333
|
+
acl: ACLContainer | NotebookCopyACL | None = Field(default=None)
|
albert/utils/property_data.py
CHANGED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import ast
|
|
6
|
+
import math
|
|
5
7
|
import mimetypes
|
|
8
|
+
import operator
|
|
6
9
|
import re
|
|
7
10
|
import uuid
|
|
8
11
|
from collections.abc import Callable
|
|
@@ -573,6 +576,63 @@ def get_all_columns_used_in_calculations(*, first_row_data_column: list):
|
|
|
573
576
|
return used_columns
|
|
574
577
|
|
|
575
578
|
|
|
579
|
+
_ALLOWED_BINOPS = {
|
|
580
|
+
ast.Add: operator.add,
|
|
581
|
+
ast.Sub: operator.sub,
|
|
582
|
+
ast.Mult: operator.mul,
|
|
583
|
+
ast.Div: operator.truediv,
|
|
584
|
+
ast.Mod: operator.mod,
|
|
585
|
+
ast.Pow: operator.pow,
|
|
586
|
+
}
|
|
587
|
+
_ALLOWED_UNARYOPS = {
|
|
588
|
+
ast.UAdd: operator.pos,
|
|
589
|
+
ast.USub: operator.neg,
|
|
590
|
+
}
|
|
591
|
+
_ALLOWED_FUNCS: dict[str, tuple[Callable[..., float], int]] = {
|
|
592
|
+
"log10": (math.log10, 1),
|
|
593
|
+
"ln": (math.log, 1),
|
|
594
|
+
"sqrt": (math.sqrt, 1),
|
|
595
|
+
"pi": (lambda: math.pi, 0),
|
|
596
|
+
}
|
|
597
|
+
_ALLOWED_NAMES = {"pi": math.pi}
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def _safe_eval_math(*, expression: str) -> float:
|
|
601
|
+
"""Safely evaluate supported math expressions."""
|
|
602
|
+
parsed = ast.parse(expression, mode="eval")
|
|
603
|
+
|
|
604
|
+
def _eval(node: ast.AST) -> float:
|
|
605
|
+
if isinstance(node, ast.Expression):
|
|
606
|
+
return _eval(node.body)
|
|
607
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, (int | float)):
|
|
608
|
+
return node.value
|
|
609
|
+
if isinstance(node, ast.BinOp) and type(node.op) in _ALLOWED_BINOPS:
|
|
610
|
+
return _ALLOWED_BINOPS[type(node.op)](_eval(node.left), _eval(node.right))
|
|
611
|
+
if isinstance(node, ast.UnaryOp) and type(node.op) in _ALLOWED_UNARYOPS:
|
|
612
|
+
return _ALLOWED_UNARYOPS[type(node.op)](_eval(node.operand))
|
|
613
|
+
if isinstance(node, ast.Call):
|
|
614
|
+
if not isinstance(node.func, ast.Name):
|
|
615
|
+
raise ValueError("Unsupported function call.")
|
|
616
|
+
func_name = node.func.id
|
|
617
|
+
if func_name not in _ALLOWED_FUNCS:
|
|
618
|
+
raise ValueError("Unsupported function.")
|
|
619
|
+
if node.keywords:
|
|
620
|
+
raise ValueError("Keyword arguments are not supported.")
|
|
621
|
+
func, arity = _ALLOWED_FUNCS[func_name]
|
|
622
|
+
if len(node.args) != arity:
|
|
623
|
+
raise ValueError("Unsupported function arity.")
|
|
624
|
+
if arity == 0:
|
|
625
|
+
return func()
|
|
626
|
+
return func(_eval(node.args[0]))
|
|
627
|
+
if isinstance(node, ast.Name):
|
|
628
|
+
if node.id in _ALLOWED_NAMES:
|
|
629
|
+
return _ALLOWED_NAMES[node.id]
|
|
630
|
+
raise ValueError("Unsupported name.")
|
|
631
|
+
raise ValueError("Unsupported expression.")
|
|
632
|
+
|
|
633
|
+
return _eval(parsed)
|
|
634
|
+
|
|
635
|
+
|
|
576
636
|
def evaluate_calculation(*, calculation: str, column_values: dict) -> float | None:
|
|
577
637
|
"""Evaluate a calculation expression against column values."""
|
|
578
638
|
calculation = calculation.lstrip("=")
|
|
@@ -589,7 +649,7 @@ def evaluate_calculation(*, calculation: str, column_values: dict) -> float | No
|
|
|
589
649
|
calculation = pattern.sub(repl, calculation)
|
|
590
650
|
|
|
591
651
|
calculation = calculation.replace("^", "**")
|
|
592
|
-
return
|
|
652
|
+
return _safe_eval_math(expression=calculation)
|
|
593
653
|
except Exception as e:
|
|
594
654
|
logger.info(
|
|
595
655
|
"Error evaluating calculation '%s': %s. Likely do not have all values needed.",
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from albert.resources.sheets import CellType, Sheet
|
|
4
|
+
from albert.resources.worksheets import Worksheet
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_sheet_from_worksheet(*, sheet_name: str, worksheet: Worksheet) -> Sheet:
|
|
8
|
+
sheet = next((s for s in worksheet.sheets if s.name == sheet_name), None)
|
|
9
|
+
if not sheet:
|
|
10
|
+
raise ValueError(f"Sheet with name {sheet_name!r} not found in the Worksheet.")
|
|
11
|
+
return sheet
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_columns_to_copy(
|
|
15
|
+
*,
|
|
16
|
+
sheet: Sheet,
|
|
17
|
+
copy_all_pinned_columns: bool,
|
|
18
|
+
copy_all_unpinned_columns: bool,
|
|
19
|
+
input_column_names: list[str] | None,
|
|
20
|
+
) -> list[str]:
|
|
21
|
+
sheet_columns = sheet.columns
|
|
22
|
+
all_columns = {col.name: col.column_id for col in sheet_columns}
|
|
23
|
+
|
|
24
|
+
# If both flags are true, copy everything
|
|
25
|
+
if copy_all_pinned_columns and copy_all_unpinned_columns:
|
|
26
|
+
columns_to_copy: set[str] = {col.column_id for col in sheet_columns}
|
|
27
|
+
else:
|
|
28
|
+
columns_to_copy = set()
|
|
29
|
+
# Copy pinned columns
|
|
30
|
+
if copy_all_pinned_columns:
|
|
31
|
+
columns_to_copy.update(
|
|
32
|
+
col.column_id for col in sheet_columns if getattr(col, "pinned", False)
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Copy unpinned columns
|
|
36
|
+
if copy_all_unpinned_columns:
|
|
37
|
+
columns_to_copy.update(
|
|
38
|
+
col.column_id for col in sheet_columns if not getattr(col, "pinned", False)
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Add any explicitly specified columns
|
|
42
|
+
if input_column_names:
|
|
43
|
+
for name in input_column_names:
|
|
44
|
+
if name not in all_columns:
|
|
45
|
+
raise ValueError(f"Column name {name!r} not found in sheet {sheet.name!r}")
|
|
46
|
+
columns_to_copy.add(all_columns[name])
|
|
47
|
+
|
|
48
|
+
return list(columns_to_copy)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_task_rows_to_copy(*, sheet: Sheet, input_row_names: list[str] | None) -> list[str]:
|
|
52
|
+
task_rows = []
|
|
53
|
+
|
|
54
|
+
sheet_rows = sheet.rows
|
|
55
|
+
if not input_row_names:
|
|
56
|
+
# Copy all task rows if no input rows specified
|
|
57
|
+
for row in sheet_rows:
|
|
58
|
+
if row.type == CellType.TAS:
|
|
59
|
+
task_rows.append(row.row_id)
|
|
60
|
+
return task_rows
|
|
61
|
+
|
|
62
|
+
name_to_id = {row.name: row.row_id for row in sheet_rows if row.name}
|
|
63
|
+
for name in input_row_names:
|
|
64
|
+
row_id = name_to_id.get(name)
|
|
65
|
+
if row_id:
|
|
66
|
+
task_rows.append(row_id)
|
|
67
|
+
else:
|
|
68
|
+
raise ValueError(f"Task row name '{name}' not found in the grid.")
|
|
69
|
+
return task_rows
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_prg_rows_to_copy(*, sheet: Sheet, input_row_names: list[str] | None) -> list[str]:
|
|
73
|
+
prg_rows = []
|
|
74
|
+
|
|
75
|
+
sheet_rows = sheet.rows
|
|
76
|
+
if not input_row_names:
|
|
77
|
+
# Copy all PRG rows if no input rows specified
|
|
78
|
+
for row in sheet_rows:
|
|
79
|
+
if row.type == CellType.PRG:
|
|
80
|
+
prg_rows.append(row.row_id)
|
|
81
|
+
return prg_rows
|
|
82
|
+
|
|
83
|
+
name_to_id = {row.name: row.row_id for row in sheet_rows if row.name}
|
|
84
|
+
for name in input_row_names:
|
|
85
|
+
row_id = name_to_id.get(name)
|
|
86
|
+
if row_id:
|
|
87
|
+
prg_rows.append(row_id)
|
|
88
|
+
else:
|
|
89
|
+
raise ValueError(f"PRG row name '{name}' not found in the grid.")
|
|
90
|
+
return prg_rows
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: albert
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.14.0
|
|
4
4
|
Summary: The official Python SDK for the Albert Invent platform.
|
|
5
5
|
Project-URL: Homepage, https://www.albertinvent.com/
|
|
6
6
|
Project-URL: Documentation, https://docs.developer.albertinvent.com/albert-python
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
albert/__init__.py,sha256=
|
|
1
|
+
albert/__init__.py,sha256=XHvC39FCxseMBwu9xH2Vj24U9im2WtUiaBANFsmIiI0,239
|
|
2
2
|
albert/client.py,sha256=9SUy9AJpnFEUlSfxvbP1gJI776WcOoZykgPHx0EcF8g,12038
|
|
3
3
|
albert/exceptions.py,sha256=-oxOJGE0A__aPUhri3qqb5YQ5qanECcTqamS73vGajM,3172
|
|
4
4
|
albert/collections/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
5
|
albert/collections/activities.py,sha256=vvV5-f9KH52HarVSzUNAL7o4hz9mKWEZiE1MreqiH1g,3373
|
|
6
|
-
albert/collections/attachments.py,sha256=
|
|
6
|
+
albert/collections/attachments.py,sha256=Lq5q2sMUKMBargfCM7xqFMbaZLRNRvyhjCUT5sWdhUk,10169
|
|
7
7
|
albert/collections/base.py,sha256=MwBHndy5mXHvieozseaT1YF_RDevJWYu4IFGpLCBfww,8590
|
|
8
8
|
albert/collections/batch_data.py,sha256=esMttDTCY8TqSEOfMFQQ6Um_77Mf-SY3g75t1b6THjQ,3388
|
|
9
9
|
albert/collections/btdataset.py,sha256=rhjO4RtGSzAZj4hgA4M9BKG-eyhl111IDwZr_JW4t_E,4461
|
|
@@ -29,7 +29,7 @@ albert/collections/parameter_groups.py,sha256=8j5X3WJ7dcLFVLdtM7xg-iZzTijK2GCrWK
|
|
|
29
29
|
albert/collections/parameters.py,sha256=buvW_2d2MvK0iadaFgaPt5ibJw3rGexXMO-AErlKgvs,6930
|
|
30
30
|
albert/collections/pricings.py,sha256=hrlNOwt3rdmDvdfAMwepTem6iTpo5h6mZzyDlaYIsPE,6089
|
|
31
31
|
albert/collections/product_design.py,sha256=hr3hijrNcE8OPFF5e-Ke2FB39KvyNz4CKoaN_UDj6s4,1855
|
|
32
|
-
albert/collections/projects.py,sha256=
|
|
32
|
+
albert/collections/projects.py,sha256=vKfE52laYijYsAN_igJJIrGEOK3M29SUoJHPIzP1OJI,9685
|
|
33
33
|
albert/collections/property_data.py,sha256=tnP4jlOpKrvUkZUyXPITG27XOfvQZjmyToCqpbXQ2o0,39685
|
|
34
34
|
albert/collections/report_templates.py,sha256=7Alsl-6zqbw_HQpDX89c-DjrpUk_3fb_jgAOMGk_8hU,2208
|
|
35
35
|
albert/collections/reports.py,sha256=GzlXO_HH5EMXzzrFruHZVMhTMMQ8Lh55P_xf0yXb-xo,6278
|
|
@@ -44,26 +44,26 @@ albert/collections/un_numbers.py,sha256=C0RcmvUy0fOMZMe7s0cO1kAVHWArCvDNy3bPRyer
|
|
|
44
44
|
albert/collections/units.py,sha256=3KKfZKBUavu5nTEhyN9rAM2ibFUucpZzqbnXkNoCUlg,7347
|
|
45
45
|
albert/collections/users.py,sha256=A5LMsZkoeIZh8wqgmVQjRR38gCf1mCXxhW-WuXI3urc,8606
|
|
46
46
|
albert/collections/workflows.py,sha256=dY5q3DdBUCo6BL0vCK7Ls40AvRdZMSrmF1hLc-qJVBA,6645
|
|
47
|
-
albert/collections/worksheets.py,sha256=
|
|
47
|
+
albert/collections/worksheets.py,sha256=kvYpmMXK7EFcmfzPf4rlQ2Un7KOclWfta1_td5Qyhj0,10997
|
|
48
48
|
albert/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
49
49
|
albert/core/base.py,sha256=vG7O665y4ck51baipoWO71qHHmAS1zxYyNocRf3KcNY,426
|
|
50
50
|
albert/core/logging.py,sha256=sqNbIC3CZyaTyLnoV9mn0NCkxKH-jUNDJkAVMxgPSFY,820
|
|
51
51
|
albert/core/pagination.py,sha256=aK8wUHknptP0lfLoNT5qsxlkR8UA0xXghZroFzgA2rY,4440
|
|
52
52
|
albert/core/session.py,sha256=kCeTsjc4k-6bwqByc3R-tpG1ikvc17a_IRBKnflrCYY,3860
|
|
53
53
|
albert/core/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
54
|
-
albert/core/auth/_listener.py,sha256=
|
|
54
|
+
albert/core/auth/_listener.py,sha256=0RJnqofGrAQWkQ_UMWzk68_wGkzLmeX07Yn6NqWAeck,2293
|
|
55
55
|
albert/core/auth/_manager.py,sha256=g4PUxADWJTTfwEP0-ob33ckjU_6mrFOBA4MWxaulsv4,1061
|
|
56
56
|
albert/core/auth/credentials.py,sha256=uaN4RFIUsnCKPHgLnaZbNe9p-hhEDMOU6naE6vSrp8M,4234
|
|
57
57
|
albert/core/auth/sso.py,sha256=4NEDN8wVT1fXvPEEgJNj20VCb5P5XZqG_6b8ovmloMI,7526
|
|
58
58
|
albert/core/shared/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
59
|
-
albert/core/shared/enums.py,sha256=
|
|
59
|
+
albert/core/shared/enums.py,sha256=faIN6ps8Ug4T4gFH62hCw2H249P-vhQaPYj3R8GGAVY,498
|
|
60
60
|
albert/core/shared/identifiers.py,sha256=f34UIXssyX-U3sCUda2Lxicc5zsz0PSoJoVmxoqadv0,8936
|
|
61
61
|
albert/core/shared/types.py,sha256=iFlia55akIm_Wi4dhBrpJ2TquoOAqSGWh1slSS5FM_k,1462
|
|
62
62
|
albert/core/shared/models/base.py,sha256=Jk7HkcSOTiVCRcdNp_7pJeZc_ZzIqNIpdfXGjL0yD1Y,2853
|
|
63
63
|
albert/core/shared/models/patch.py,sha256=Y1sHc4-Zu32PQhF3361_2AdynZS9JQkG0RHxhw3IOqI,1787
|
|
64
64
|
albert/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
65
65
|
albert/resources/_mixins.py,sha256=rTHSzhAzJgj5nLQNdUTPAJt9abd7gwYavSF0Qs3H0cg,1026
|
|
66
|
-
albert/resources/acls.py,sha256=
|
|
66
|
+
albert/resources/acls.py,sha256=OC-RA8EzOPO6-pWc16YFM1c7DlGjR4iFNA1hgcI4WF4,1627
|
|
67
67
|
albert/resources/activities.py,sha256=eUmDoV-877Mm4JLLjI-s1lNKjgiNRycbJgyzWmlf_HA,1171
|
|
68
68
|
albert/resources/attachments.py,sha256=uK9U54g6oUO4F4zx0A_rxdW0L2I35SFT81IEuAOe3-E,1597
|
|
69
69
|
albert/resources/batch_data.py,sha256=CECWb7vzDb0Zq7NnXK-uBbz9J06oXzBDo3od3bgnVRs,3312
|
|
@@ -85,7 +85,7 @@ albert/resources/links.py,sha256=te7KIO7vfXrA-Qjngvb4JHy--SUVTG5M7GBFz8XdKto,103
|
|
|
85
85
|
albert/resources/lists.py,sha256=UinEaBEEo80Q1Vf5Ys4ViPcNTSvAeCRaZiW41CHY4f0,1861
|
|
86
86
|
albert/resources/locations.py,sha256=xqbSoO-IjLSyK74W1jSdsNL9kUN5jD6Wf9XNxw8czYA,821
|
|
87
87
|
albert/resources/lots.py,sha256=8YC6fHv08uYvhh09ULzNtFmdyfLeM1z6Hkm9wcTvcX4,6839
|
|
88
|
-
albert/resources/notebooks.py,sha256=
|
|
88
|
+
albert/resources/notebooks.py,sha256=gpVTlOmnHa6Dt9xXczkUDJgu6xXqIscxU6KhgS9LcuM,10246
|
|
89
89
|
albert/resources/notes.py,sha256=QLB3D18H8g7VZoqvuv_EwQe4-YlMvBr86AExUNQuYxI,946
|
|
90
90
|
albert/resources/parameter_groups.py,sha256=EMMuynGIl1fxLxM3QsGhqXWPdPkKp9iAzGXOBzQAxVQ,6344
|
|
91
91
|
albert/resources/parameters.py,sha256=f4s2-IhkbzQ2frAwCbkAZNw8YAWqqJxBZvGX1_KwSeY,1162
|
|
@@ -115,9 +115,10 @@ albert/utils/_auth.py,sha256=YjzaGIzI9qP53nwdyE2Ezs-9UokzA38kgdE7Sxnyjd8,124
|
|
|
115
115
|
albert/utils/_patch.py,sha256=e-bD2x6W3Wt4FaKFK477h3kZeyucn3DEB9m5DR7LzaA,24273
|
|
116
116
|
albert/utils/data_template.py,sha256=AUwzfQ-I2HY-osq_Tme5KLwXfMzW2pJpiud7HAMh148,27874
|
|
117
117
|
albert/utils/inventory.py,sha256=hhL1wCn2vtF2z5FGwlX-3XIla4paB26QbmbStkG3yTQ,7433
|
|
118
|
-
albert/utils/property_data.py,sha256=
|
|
118
|
+
albert/utils/property_data.py,sha256=xb0CZAh_0zFiN5yQwcw3Mjk4En8MxMrJF9DKQI19UhM,25074
|
|
119
119
|
albert/utils/tasks.py,sha256=ejhXD9yQR_ReDKLJ3k8ZxWxkYl-Mta_4OPXrz_lQiBI,19878
|
|
120
|
-
albert
|
|
121
|
-
albert-1.
|
|
122
|
-
albert-1.
|
|
123
|
-
albert-1.
|
|
120
|
+
albert/utils/worksheets.py,sha256=yJXoDBcjtT11fOz6q9D4gRR6NZECFJKwHyBpm27O5Ac,3040
|
|
121
|
+
albert-1.14.0.dist-info/METADATA,sha256=HdX4VmfEnqA_XjJiVPZli3eRGZRjbJV2ckW5egVNJ9c,15427
|
|
122
|
+
albert-1.14.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
123
|
+
albert-1.14.0.dist-info/licenses/LICENSE,sha256=S7_vRdIhQmG7PmTlU8-BCCveuEcFZ6_3IUVdcoaJMuA,11348
|
|
124
|
+
albert-1.14.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|