albert 1.10.0rc2__py3-none-any.whl → 1.11.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/client.py +5 -0
- albert/collections/custom_templates.py +3 -0
- albert/collections/data_templates.py +118 -264
- albert/collections/entity_types.py +19 -3
- albert/collections/inventory.py +1 -1
- albert/collections/notebooks.py +154 -26
- albert/collections/parameters.py +1 -0
- albert/collections/property_data.py +384 -280
- albert/collections/reports.py +4 -0
- albert/collections/synthesis.py +292 -0
- albert/collections/tasks.py +2 -1
- albert/collections/worksheets.py +3 -0
- albert/core/shared/models/base.py +3 -1
- albert/core/shared/models/patch.py +1 -1
- albert/resources/batch_data.py +4 -2
- albert/resources/cas.py +3 -1
- albert/resources/custom_fields.py +3 -1
- albert/resources/data_templates.py +60 -12
- albert/resources/inventory.py +6 -4
- albert/resources/lists.py +3 -1
- albert/resources/notebooks.py +12 -7
- albert/resources/parameter_groups.py +3 -1
- albert/resources/property_data.py +64 -5
- albert/resources/sheets.py +16 -14
- albert/resources/synthesis.py +61 -0
- albert/resources/tags.py +3 -1
- albert/resources/tasks.py +4 -7
- albert/resources/workflows.py +4 -2
- albert/utils/data_template.py +392 -37
- albert/utils/property_data.py +638 -0
- albert/utils/tasks.py +3 -3
- {albert-1.10.0rc2.dist-info → albert-1.11.0.dist-info}/METADATA +1 -1
- {albert-1.10.0rc2.dist-info → albert-1.11.0.dist-info}/RECORD +36 -33
- {albert-1.10.0rc2.dist-info → albert-1.11.0.dist-info}/WHEEL +0 -0
- {albert-1.10.0rc2.dist-info → albert-1.11.0.dist-info}/licenses/LICENSE +0 -0
albert/collections/notebooks.py
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
|
+
import mimetypes
|
|
2
|
+
from pathlib import Path, PurePosixPath
|
|
3
|
+
|
|
1
4
|
from pydantic import TypeAdapter, validate_call
|
|
2
5
|
|
|
3
6
|
from albert.collections.base import BaseCollection
|
|
7
|
+
from albert.collections.files import FileCollection
|
|
8
|
+
from albert.collections.synthesis import SynthesisCollection
|
|
9
|
+
from albert.core.base import BaseAlbertModel
|
|
4
10
|
from albert.core.session import AlbertSession
|
|
5
|
-
from albert.core.shared.identifiers import NotebookId, ProjectId, TaskId
|
|
6
|
-
from albert.exceptions import AlbertException
|
|
11
|
+
from albert.core.shared.identifiers import NotebookId, ProjectId, SynthesisId, TaskId
|
|
12
|
+
from albert.exceptions import AlbertException
|
|
13
|
+
from albert.resources.files import FileNamespace
|
|
7
14
|
from albert.resources.notebooks import (
|
|
15
|
+
AttachesBlock,
|
|
16
|
+
ImageBlock,
|
|
17
|
+
KetcherBlock,
|
|
8
18
|
Notebook,
|
|
9
19
|
NotebookBlock,
|
|
10
20
|
NotebookCopyInfo,
|
|
@@ -15,6 +25,13 @@ from albert.resources.notebooks import (
|
|
|
15
25
|
)
|
|
16
26
|
|
|
17
27
|
|
|
28
|
+
class _KetcherUpdateAction(BaseAlbertModel):
|
|
29
|
+
synthesis_id: SynthesisId
|
|
30
|
+
data: str
|
|
31
|
+
png: str
|
|
32
|
+
smiles: str
|
|
33
|
+
|
|
34
|
+
|
|
18
35
|
class NotebookCollection(BaseCollection):
|
|
19
36
|
"""NotebookCollection is a collection class for managing Notebook entities in the Albert platform."""
|
|
20
37
|
|
|
@@ -32,6 +49,8 @@ class NotebookCollection(BaseCollection):
|
|
|
32
49
|
"""
|
|
33
50
|
super().__init__(session=session)
|
|
34
51
|
self.base_path = f"/api/{NotebookCollection._api_version}/notebooks"
|
|
52
|
+
self._files = FileCollection(session=session)
|
|
53
|
+
self._synthesis = SynthesisCollection(session=session)
|
|
35
54
|
|
|
36
55
|
@validate_call
|
|
37
56
|
def get_by_id(self, *, id: NotebookId) -> Notebook:
|
|
@@ -139,6 +158,10 @@ class NotebookCollection(BaseCollection):
|
|
|
139
158
|
If a block in the Notebook does not already exist on Albert, it will be created.
|
|
140
159
|
*Note: The order of the Blocks in your Notebook matter and will be used in the updated Notebook!*
|
|
141
160
|
|
|
161
|
+
!!! warning
|
|
162
|
+
Updating existing Ketcher blocks is not supported. To change a Ketcher block, delete it and
|
|
163
|
+
create a new one instead.
|
|
164
|
+
|
|
142
165
|
|
|
143
166
|
Parameters
|
|
144
167
|
----------
|
|
@@ -149,12 +172,33 @@ class NotebookCollection(BaseCollection):
|
|
|
149
172
|
-------
|
|
150
173
|
Notebook
|
|
151
174
|
The updated notebook object as returned by the server.
|
|
175
|
+
|
|
176
|
+
Examples
|
|
177
|
+
--------
|
|
178
|
+
!!! example "Add a Ketcher block from SMILES"
|
|
179
|
+
```python
|
|
180
|
+
notebook = client.notebooks.get_by_id(id="NTB123")
|
|
181
|
+
notebook.blocks.append(
|
|
182
|
+
KetcherBlock(content=KetcherContent(smiles="CCO"))
|
|
183
|
+
)
|
|
184
|
+
notebook = client.notebooks.update_block_content(notebook=notebook)
|
|
185
|
+
```
|
|
152
186
|
"""
|
|
153
|
-
|
|
187
|
+
if notebook.id is None:
|
|
188
|
+
raise AlbertException("Notebook id is required to update block content.")
|
|
189
|
+
put_data, ketcher_updates = self._generate_put_block_payload(notebook=notebook)
|
|
154
190
|
url = f"{self.base_path}/{notebook.id}/content"
|
|
155
191
|
|
|
156
192
|
self.session.put(url, json=put_data.model_dump(mode="json", by_alias=True))
|
|
157
193
|
|
|
194
|
+
for action in ketcher_updates:
|
|
195
|
+
self._synthesis.update_canvas_data(
|
|
196
|
+
synthesis_id=action.synthesis_id,
|
|
197
|
+
smiles=action.smiles,
|
|
198
|
+
data=action.data,
|
|
199
|
+
png=action.png,
|
|
200
|
+
)
|
|
201
|
+
self._synthesis.create_reactant_productant_table(synthesis_id=action.synthesis_id)
|
|
158
202
|
return self.get_by_id(id=notebook.id)
|
|
159
203
|
|
|
160
204
|
@validate_call
|
|
@@ -176,29 +220,33 @@ class NotebookCollection(BaseCollection):
|
|
|
176
220
|
response = self.session.get(f"{self.base_path}/{notebook_id}/blocks/{block_id}")
|
|
177
221
|
return TypeAdapter(NotebookBlock).validate_python(response.json())
|
|
178
222
|
|
|
179
|
-
def _generate_put_block_payload(
|
|
180
|
-
|
|
181
|
-
|
|
223
|
+
def _generate_put_block_payload(
|
|
224
|
+
self, *, notebook: Notebook
|
|
225
|
+
) -> tuple[PutBlockPayload, list[_KetcherUpdateAction]]:
|
|
226
|
+
data: list[PutBlockDatum] = []
|
|
227
|
+
seen_ids: set[str] = set()
|
|
182
228
|
previous_block_id = ""
|
|
183
|
-
|
|
229
|
+
ketcher_updates: list[_KetcherUpdateAction] = []
|
|
230
|
+
existing_blocks = {b.id: b for b in self.get_by_id(id=notebook.id).blocks}
|
|
184
231
|
for block in notebook.blocks:
|
|
185
232
|
if block.id in seen_ids:
|
|
186
|
-
# This check keeps a user from corrupting the Notebook data.
|
|
187
233
|
msg = f"You have Notebook blocks with duplicate ids. [id={block.id}]"
|
|
188
234
|
raise AlbertException(msg)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
235
|
+
existing_block = existing_blocks.get(block.id)
|
|
236
|
+
if existing_block and type(block) is not type(existing_block):
|
|
237
|
+
msg = (
|
|
238
|
+
f"Cannot convert an existing block type into another block type. "
|
|
239
|
+
f"Instead, please instantiate a new block, and remove the old block "
|
|
240
|
+
f"from the Notebook object. [existing_block_type={type(existing_block)}, "
|
|
241
|
+
f"new_block_type={type(block)}]"
|
|
242
|
+
)
|
|
243
|
+
raise AlbertException(msg)
|
|
244
|
+
|
|
245
|
+
if isinstance(block, KetcherBlock) and existing_block is None:
|
|
246
|
+
ketcher_updates.append(self._prepare_ketcher_block(notebook=notebook, block=block))
|
|
247
|
+
elif isinstance(block, (AttachesBlock | ImageBlock)):
|
|
248
|
+
self._prepare_file_block(notebook=notebook, block=block)
|
|
249
|
+
|
|
202
250
|
put_datum = PutBlockDatum(
|
|
203
251
|
id=block.id,
|
|
204
252
|
type=block.type,
|
|
@@ -207,16 +255,96 @@ class NotebookCollection(BaseCollection):
|
|
|
207
255
|
previous_block_id=previous_block_id,
|
|
208
256
|
)
|
|
209
257
|
seen_ids.add(put_datum.id)
|
|
210
|
-
previous_block_id = put_datum.id
|
|
258
|
+
previous_block_id = put_datum.id
|
|
211
259
|
data.append(put_datum)
|
|
212
260
|
|
|
213
|
-
|
|
214
|
-
existing_notebook = self.get_by_id(id=notebook.id)
|
|
215
|
-
for block in existing_notebook.blocks:
|
|
261
|
+
for block in existing_blocks.values():
|
|
216
262
|
if block.id not in seen_ids:
|
|
217
263
|
data.append(PutBlockDatum(id=block.id, operation=PutOperation.DELETE))
|
|
218
264
|
|
|
219
|
-
return PutBlockPayload(data=data)
|
|
265
|
+
return PutBlockPayload(data=data), ketcher_updates
|
|
266
|
+
|
|
267
|
+
def _prepare_file_block(
|
|
268
|
+
self, *, notebook: Notebook, block: AttachesBlock | ImageBlock
|
|
269
|
+
) -> None:
|
|
270
|
+
content = block.content
|
|
271
|
+
file_path = content.file_path
|
|
272
|
+
file_key = content.file_key
|
|
273
|
+
if file_path is None:
|
|
274
|
+
if file_key:
|
|
275
|
+
file_name = PurePosixPath(file_key).name
|
|
276
|
+
if "/" not in file_key:
|
|
277
|
+
content.file_key = f"{notebook.id}/{block.id}/{file_key}"
|
|
278
|
+
if content.format is None:
|
|
279
|
+
content.format = (
|
|
280
|
+
mimetypes.guess_type(file_name)[0] or "application/octet-stream"
|
|
281
|
+
)
|
|
282
|
+
if isinstance(block, AttachesBlock) and content.title is None:
|
|
283
|
+
content.title = file_name or None
|
|
284
|
+
return
|
|
285
|
+
|
|
286
|
+
path = Path(file_path)
|
|
287
|
+
if file_key and "/" not in file_key:
|
|
288
|
+
file_key = f"{notebook.id}/{block.id}/{file_key}"
|
|
289
|
+
elif not file_key:
|
|
290
|
+
file_key = f"{notebook.id}/{block.id}/{path.name}"
|
|
291
|
+
|
|
292
|
+
content.file_key = file_key
|
|
293
|
+
file_name = PurePosixPath(file_key).name
|
|
294
|
+
if content.format is None:
|
|
295
|
+
content.format = mimetypes.guess_type(file_name)[0] or "application/octet-stream"
|
|
296
|
+
if isinstance(block, AttachesBlock) and content.title is None:
|
|
297
|
+
content.title = file_name or None
|
|
298
|
+
|
|
299
|
+
with path.open("rb") as handle:
|
|
300
|
+
self._files.sign_and_upload_file(
|
|
301
|
+
data=handle,
|
|
302
|
+
name=file_key,
|
|
303
|
+
namespace=FileNamespace(content.namespace),
|
|
304
|
+
content_type=content.format,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
def _prepare_ketcher_block(
|
|
308
|
+
self, *, notebook: Notebook, block: KetcherBlock
|
|
309
|
+
) -> _KetcherUpdateAction:
|
|
310
|
+
"""
|
|
311
|
+
Prepare a Ketcher block for creation.
|
|
312
|
+
|
|
313
|
+
Updates to existing Ketcher blocks are not supported. To change a Ketcher
|
|
314
|
+
block, delete it and create a new one instead.
|
|
315
|
+
"""
|
|
316
|
+
content = block.content
|
|
317
|
+
smiles = content.smiles or ""
|
|
318
|
+
data = content.data
|
|
319
|
+
png = content.png
|
|
320
|
+
|
|
321
|
+
if content.synthesis_id is None:
|
|
322
|
+
if not smiles:
|
|
323
|
+
raise AlbertException("smiles is required to create a Ketcher block.")
|
|
324
|
+
name = "Chemical Draw Block"
|
|
325
|
+
synthesis = self._synthesis.create(
|
|
326
|
+
parent_id=notebook.id, name=name, block_id=block.id, smiles=smiles or None
|
|
327
|
+
)
|
|
328
|
+
content.synthesis_id = synthesis.id
|
|
329
|
+
content.s3_key = synthesis.s3_key or content.s3_key
|
|
330
|
+
|
|
331
|
+
canvas_data = synthesis.canvas_data or {}
|
|
332
|
+
data = data or canvas_data.get("data")
|
|
333
|
+
png = png or canvas_data.get("png")
|
|
334
|
+
|
|
335
|
+
content.id = block.id
|
|
336
|
+
content.block_id = block.id
|
|
337
|
+
content.state_type = "project"
|
|
338
|
+
content.smiles = smiles
|
|
339
|
+
content.data = data
|
|
340
|
+
content.png = png
|
|
341
|
+
|
|
342
|
+
return _KetcherUpdateAction(
|
|
343
|
+
synthesis_id=content.synthesis_id,
|
|
344
|
+
data=data or "",
|
|
345
|
+
png=png or "",
|
|
346
|
+
smiles=smiles or "",
|
|
347
|
+
)
|
|
220
348
|
|
|
221
349
|
def copy(self, *, notebook_copy_info: NotebookCopyInfo, type: NotebookCopyType) -> Notebook:
|
|
222
350
|
"""Create a copy of a Notebook into a specified parent
|