matelab-python-sdk 0.1.0a1__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.
- matelab/__init__.py +372 -0
- matelab/_generated/__init__.py +1 -0
- matelab/_generated/models.py +3553 -0
- matelab/_transport.py +481 -0
- matelab/client.py +191 -0
- matelab/cloud_drive.py +789 -0
- matelab/errors.py +38 -0
- matelab/groups.py +78 -0
- matelab/literature.py +1243 -0
- matelab/models.py +27 -0
- matelab/notebooks.py +467 -0
- matelab/py.typed +1 -0
- matelab/records.py +2511 -0
- matelab/streaming.py +82 -0
- matelab/templates.py +737 -0
- matelab/uploads.py +247 -0
- matelab/users.py +77 -0
- matelab_python_sdk-0.1.0a1.dist-info/METADATA +538 -0
- matelab_python_sdk-0.1.0a1.dist-info/RECORD +22 -0
- matelab_python_sdk-0.1.0a1.dist-info/WHEEL +4 -0
- matelab_python_sdk-0.1.0a1.dist-info/licenses/LICENSE +202 -0
- matelab_python_sdk-0.1.0a1.dist-info/licenses/NOTICE +2 -0
matelab/models.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclass(frozen=True, slots=True)
|
|
5
|
+
class Token:
|
|
6
|
+
"""Immutable Provider token whose secret value is omitted from representations."""
|
|
7
|
+
|
|
8
|
+
value: str = field(repr=False)
|
|
9
|
+
expires_at_ms: int
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True, slots=True)
|
|
13
|
+
class Identity:
|
|
14
|
+
"""Immutable identity associated with a validated Matelab Session."""
|
|
15
|
+
|
|
16
|
+
userid: int
|
|
17
|
+
username: str | None
|
|
18
|
+
email: str | None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True, slots=True)
|
|
22
|
+
class Session:
|
|
23
|
+
"""Immutable snapshot of one client's current process-local Matelab authentication state."""
|
|
24
|
+
|
|
25
|
+
access: Token
|
|
26
|
+
refresh: Token
|
|
27
|
+
identity: Identity | None = None
|
matelab/notebooks.py
ADDED
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from collections.abc import Mapping, Sequence
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Literal, cast
|
|
5
|
+
|
|
6
|
+
from pydantic import ValidationError
|
|
7
|
+
|
|
8
|
+
from matelab._generated.models import AddNotebookShareRequest as WireAddNotebookShareRequest
|
|
9
|
+
from matelab._generated.models import BasicSuccessResponse as WireBasicSuccessResponse
|
|
10
|
+
from matelab._generated.models import DeleteNotebookShareRequest as WireDeleteNotebookShareRequest
|
|
11
|
+
from matelab._generated.models import ElnNotebook
|
|
12
|
+
from matelab._generated.models import ListNotebookSharesParametersQuery as WireNotebookSharesQuery
|
|
13
|
+
from matelab._generated.models import NotebookListResponse as WireNotebookListResponse
|
|
14
|
+
from matelab._generated.models import NotebookShareListResponse as WireNotebookShareListResponse
|
|
15
|
+
from matelab._generated.models import SaveNotebookRequest as WireSaveNotebookRequest
|
|
16
|
+
from matelab._generated.models import UpdateNotebookSharePermissionsRequest as WireUpdateNotebookSharePermissionsRequest
|
|
17
|
+
from matelab._transport import Encoding, Operation, SessionTransport, multipart_fields
|
|
18
|
+
from matelab.errors import MatelabError, MatelabProtocolError, MatelabUsageError, MatelabVerificationError
|
|
19
|
+
from matelab.groups import GroupRef
|
|
20
|
+
from matelab.users import UserRef
|
|
21
|
+
|
|
22
|
+
_LIST_NOTEBOOKS = Operation(
|
|
23
|
+
method="GET",
|
|
24
|
+
path="/eln_api/elns",
|
|
25
|
+
encoding=Encoding.NONE,
|
|
26
|
+
response_model=WireNotebookListResponse,
|
|
27
|
+
success_codes=frozenset({0}),
|
|
28
|
+
retry_on_access_expired=True,
|
|
29
|
+
)
|
|
30
|
+
_SAVE_NOTEBOOK = Operation(
|
|
31
|
+
method="POST",
|
|
32
|
+
path="/eln/edit",
|
|
33
|
+
encoding=Encoding.JSON,
|
|
34
|
+
response_model=WireBasicSuccessResponse,
|
|
35
|
+
success_codes=frozenset({0}),
|
|
36
|
+
)
|
|
37
|
+
_LIST_SHARES = Operation(
|
|
38
|
+
method="GET",
|
|
39
|
+
path="/eln/share_list",
|
|
40
|
+
encoding=Encoding.QUERY,
|
|
41
|
+
response_model=WireNotebookShareListResponse,
|
|
42
|
+
success_codes=frozenset({0}),
|
|
43
|
+
retry_on_access_expired=True,
|
|
44
|
+
)
|
|
45
|
+
_ADD_SHARE = Operation(
|
|
46
|
+
method="POST",
|
|
47
|
+
path="/eln/share_add",
|
|
48
|
+
encoding=Encoding.MULTIPART,
|
|
49
|
+
response_model=WireBasicSuccessResponse,
|
|
50
|
+
success_codes=frozenset({0}),
|
|
51
|
+
)
|
|
52
|
+
_UPDATE_SHARE = Operation(
|
|
53
|
+
method="POST",
|
|
54
|
+
path="/eln/share_edit",
|
|
55
|
+
encoding=Encoding.MULTIPART,
|
|
56
|
+
response_model=WireBasicSuccessResponse,
|
|
57
|
+
success_codes=frozenset({0}),
|
|
58
|
+
)
|
|
59
|
+
_DELETE_SHARE = Operation(
|
|
60
|
+
method="POST",
|
|
61
|
+
path="/eln/sharedel",
|
|
62
|
+
encoding=Encoding.MULTIPART,
|
|
63
|
+
response_model=WireBasicSuccessResponse,
|
|
64
|
+
success_codes=frozenset({0}),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass(frozen=True, slots=True)
|
|
69
|
+
class NotebookRef:
|
|
70
|
+
"""Identity and exact Provider selector for an owned or shared notebook."""
|
|
71
|
+
|
|
72
|
+
notebook_id: int
|
|
73
|
+
title: str | None
|
|
74
|
+
owner_userid: int
|
|
75
|
+
scope: Literal["owned", "shared"]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True, slots=True)
|
|
79
|
+
class PublicNotebookRef:
|
|
80
|
+
"""Identity and title selector for a public-catalog notebook."""
|
|
81
|
+
|
|
82
|
+
public_notebook_id: int
|
|
83
|
+
title: str
|
|
84
|
+
owner_userid: int
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(frozen=True, slots=True)
|
|
88
|
+
class NotebookListField:
|
|
89
|
+
uid: object
|
|
90
|
+
path: tuple[object, ...]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(frozen=True, slots=True)
|
|
94
|
+
class NotebookMetadata:
|
|
95
|
+
"""Complete metadata replacement accepted by the legacy save operation."""
|
|
96
|
+
|
|
97
|
+
title: str
|
|
98
|
+
summary: str | None = None
|
|
99
|
+
notebook_types: tuple[object, ...] = ()
|
|
100
|
+
display_fields: tuple[NotebookListField, ...] | None = None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass(frozen=True, slots=True)
|
|
104
|
+
class NotebookSummary:
|
|
105
|
+
ref: NotebookRef
|
|
106
|
+
summary: str | None
|
|
107
|
+
record_count: int
|
|
108
|
+
created_at: str | None
|
|
109
|
+
owner_name: str | None
|
|
110
|
+
group_name: str | None
|
|
111
|
+
notebook_types: tuple[object, ...]
|
|
112
|
+
display_enabled: bool
|
|
113
|
+
display_fields: tuple[NotebookListField, ...] | None
|
|
114
|
+
display_configuration_valid: bool
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass(frozen=True, slots=True)
|
|
118
|
+
class PublicNotebookSummary:
|
|
119
|
+
ref: PublicNotebookRef
|
|
120
|
+
summary: str
|
|
121
|
+
record_count: int
|
|
122
|
+
created_at: str
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass(frozen=True, slots=True)
|
|
126
|
+
class NotebookCollection:
|
|
127
|
+
owned: tuple[NotebookSummary, ...]
|
|
128
|
+
shared: tuple[NotebookSummary, ...]
|
|
129
|
+
public: tuple[PublicNotebookSummary, ...]
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def all(self) -> tuple[NotebookSummary | PublicNotebookSummary, ...]:
|
|
133
|
+
return (*self.owned, *self.shared, *self.public)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@dataclass(frozen=True, slots=True)
|
|
137
|
+
class NotebookCreateResult:
|
|
138
|
+
requested: NotebookMetadata
|
|
139
|
+
provider_acknowledged: Literal[True] = True
|
|
140
|
+
notebook: None = None
|
|
141
|
+
identity_status: Literal["provider_did_not_return_identity"] = "provider_did_not_return_identity"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@dataclass(frozen=True, slots=True)
|
|
145
|
+
class NotebookUpdateResult:
|
|
146
|
+
notebook: NotebookRef
|
|
147
|
+
requested: NotebookMetadata
|
|
148
|
+
observed: NotebookSummary | None
|
|
149
|
+
confirmation: Literal["observed_matching", "observed_mismatch", "not_observed"]
|
|
150
|
+
provider_acknowledged: Literal[True] = True
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@dataclass(frozen=True, slots=True)
|
|
154
|
+
class NotebookSharePermissionGrant:
|
|
155
|
+
"""Replacement flags; read access remains implicit and cannot be revoked here."""
|
|
156
|
+
|
|
157
|
+
write: bool = False
|
|
158
|
+
create: bool = False
|
|
159
|
+
delete: bool = False
|
|
160
|
+
sign: bool = False
|
|
161
|
+
manage_subtypes: bool = False
|
|
162
|
+
read_audit_log: bool = False
|
|
163
|
+
manage_shares: bool = False
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@dataclass(frozen=True, slots=True)
|
|
167
|
+
class NotebookShareRef:
|
|
168
|
+
notebook: NotebookRef
|
|
169
|
+
share_row_id: int
|
|
170
|
+
recipient: UserRef
|
|
171
|
+
belongs_to_caller: bool
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@dataclass(frozen=True, slots=True)
|
|
175
|
+
class NotebookShare:
|
|
176
|
+
ref: NotebookShareRef
|
|
177
|
+
recipient_name: str | None
|
|
178
|
+
recipient_email: str | None
|
|
179
|
+
stored_permission_mask: int
|
|
180
|
+
effective_read_access: Literal[True] = True
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@dataclass(frozen=True, slots=True)
|
|
184
|
+
class NotebookShares:
|
|
185
|
+
notebook: NotebookRef
|
|
186
|
+
shares: tuple[NotebookShare, ...]
|
|
187
|
+
caller_is_owner: bool
|
|
188
|
+
caller_effective_permission_mask: int
|
|
189
|
+
ordering: Literal["provider_unspecified"] = "provider_unspecified"
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@dataclass(frozen=True, slots=True)
|
|
193
|
+
class NotebookShareResult:
|
|
194
|
+
notebook: NotebookRef
|
|
195
|
+
requested: tuple[UserRef, ...]
|
|
196
|
+
visible_after_write: tuple[NotebookShare, ...]
|
|
197
|
+
not_observed_after_write: tuple[UserRef, ...]
|
|
198
|
+
observation: Literal["all_visible", "partially_visible", "none_visible"]
|
|
199
|
+
provider_acknowledged: Literal[True] = True
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@dataclass(frozen=True, slots=True)
|
|
203
|
+
class NotebookShareUpdateResult:
|
|
204
|
+
ref: NotebookShareRef
|
|
205
|
+
requested: NotebookSharePermissionGrant
|
|
206
|
+
observed: NotebookShare | None
|
|
207
|
+
observation: Literal["relation_observed", "relation_not_observed"]
|
|
208
|
+
provider_acknowledged: Literal[True] = True
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@dataclass(frozen=True, slots=True)
|
|
212
|
+
class NotebookUnshareResult:
|
|
213
|
+
ref: NotebookShareRef
|
|
214
|
+
absence_confirmed: bool | None
|
|
215
|
+
verification: Literal["confirmed_absent", "still_visible", "not_attempted_after_leaving"]
|
|
216
|
+
provider_acknowledged: Literal[True] = True
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class Notebooks:
|
|
220
|
+
"""Notebook-oriented Matelab operations."""
|
|
221
|
+
|
|
222
|
+
def __init__(self, transport: SessionTransport) -> None:
|
|
223
|
+
self._transport: SessionTransport = transport
|
|
224
|
+
|
|
225
|
+
async def list(self) -> NotebookCollection:
|
|
226
|
+
response = await self._transport.request(_LIST_NOTEBOOKS)
|
|
227
|
+
return NotebookCollection(
|
|
228
|
+
owned=tuple(self._owned_or_shared(item, scope="owned") for item in response.my),
|
|
229
|
+
shared=tuple(self._owned_or_shared(item, scope="shared") for item in response.share),
|
|
230
|
+
public=tuple(
|
|
231
|
+
PublicNotebookSummary(
|
|
232
|
+
ref=PublicNotebookRef(public_notebook_id=item.id, title=item.showtext, owner_userid=item.userid),
|
|
233
|
+
summary=item.comm,
|
|
234
|
+
record_count=item.num,
|
|
235
|
+
created_at=item.datetime_create,
|
|
236
|
+
)
|
|
237
|
+
for item in response.public
|
|
238
|
+
),
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
async def create(self, metadata: NotebookMetadata, *, group: GroupRef | None = None) -> NotebookCreateResult:
|
|
242
|
+
request = self._save_request(metadata, notebook_id=0, group_id=group.group_id if group is not None else 0)
|
|
243
|
+
_ = await self._transport.request(_SAVE_NOTEBOOK, payload=request.model_dump(mode="json", exclude_unset=True))
|
|
244
|
+
return NotebookCreateResult(requested=metadata)
|
|
245
|
+
|
|
246
|
+
async def update(self, notebook: NotebookRef, metadata: NotebookMetadata) -> NotebookUpdateResult:
|
|
247
|
+
if notebook.scope != "owned":
|
|
248
|
+
raise MatelabUsageError("Notebook metadata can only be updated through an owned NotebookRef.")
|
|
249
|
+
request = self._save_request(metadata, notebook_id=notebook.notebook_id)
|
|
250
|
+
_ = await self._transport.request(_SAVE_NOTEBOOK, payload=request.model_dump(mode="json", exclude_unset=True))
|
|
251
|
+
notebooks = await self._verified_notebook_list("notebook update")
|
|
252
|
+
observed = next((item for item in notebooks.owned if item.ref.notebook_id == notebook.notebook_id), None)
|
|
253
|
+
if observed is None:
|
|
254
|
+
confirmation: Literal["observed_matching", "observed_mismatch", "not_observed"] = "not_observed"
|
|
255
|
+
elif self._metadata_matches(observed, metadata):
|
|
256
|
+
confirmation = "observed_matching"
|
|
257
|
+
else:
|
|
258
|
+
confirmation = "observed_mismatch"
|
|
259
|
+
return NotebookUpdateResult(notebook=notebook, requested=metadata, observed=observed, confirmation=confirmation)
|
|
260
|
+
|
|
261
|
+
async def shares(self, notebook: NotebookRef) -> NotebookShares:
|
|
262
|
+
try:
|
|
263
|
+
request = WireNotebookSharesQuery(id=notebook.notebook_id)
|
|
264
|
+
except ValidationError:
|
|
265
|
+
raise MatelabUsageError("Notebook identity does not satisfy the Integration Contract.") from None
|
|
266
|
+
response = await self._transport.request(_LIST_SHARES, payload=request.model_dump(mode="json"))
|
|
267
|
+
return NotebookShares(
|
|
268
|
+
notebook=notebook,
|
|
269
|
+
shares=tuple(
|
|
270
|
+
NotebookShare(
|
|
271
|
+
ref=NotebookShareRef(
|
|
272
|
+
notebook=notebook,
|
|
273
|
+
share_row_id=item.id,
|
|
274
|
+
recipient=UserRef(userid=item.userid),
|
|
275
|
+
belongs_to_caller=item.self,
|
|
276
|
+
),
|
|
277
|
+
recipient_name=item.username,
|
|
278
|
+
recipient_email=item.email,
|
|
279
|
+
stored_permission_mask=item.power,
|
|
280
|
+
)
|
|
281
|
+
for item in response.list_
|
|
282
|
+
),
|
|
283
|
+
caller_is_owner=response.owner,
|
|
284
|
+
caller_effective_permission_mask=response.power,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
async def share(self, notebook: NotebookRef, recipients: Sequence[UserRef]) -> NotebookShareResult:
|
|
288
|
+
requested = tuple(recipients)
|
|
289
|
+
userids = tuple(recipient.userid for recipient in requested)
|
|
290
|
+
if (
|
|
291
|
+
not requested
|
|
292
|
+
or any(type(userid) is not int or userid < 1 for userid in userids)
|
|
293
|
+
or len(userids) != len(set(userids))
|
|
294
|
+
):
|
|
295
|
+
raise MatelabUsageError("Notebook share recipients must be a non-empty set of distinct users.")
|
|
296
|
+
try:
|
|
297
|
+
request = WireAddNotebookShareRequest(
|
|
298
|
+
id=notebook.notebook_id, users=json.dumps(userids, ensure_ascii=False, separators=(",", ":"))
|
|
299
|
+
)
|
|
300
|
+
except (ValidationError, TypeError, ValueError):
|
|
301
|
+
raise MatelabUsageError("Notebook share targets do not satisfy the Integration Contract.") from None
|
|
302
|
+
_ = await self._transport.request(_ADD_SHARE, files=multipart_fields(request.model_dump(mode="json")))
|
|
303
|
+
after = await self._verified_shares(notebook, "notebook share")
|
|
304
|
+
by_userid = {item.ref.recipient.userid: item for item in after.shares}
|
|
305
|
+
visible = tuple(by_userid[userid] for userid in userids if userid in by_userid)
|
|
306
|
+
not_observed = tuple(recipient for recipient in requested if recipient.userid not in by_userid)
|
|
307
|
+
observation: Literal["all_visible", "partially_visible", "none_visible"]
|
|
308
|
+
if len(visible) == len(requested):
|
|
309
|
+
observation = "all_visible"
|
|
310
|
+
elif visible:
|
|
311
|
+
observation = "partially_visible"
|
|
312
|
+
else:
|
|
313
|
+
observation = "none_visible"
|
|
314
|
+
return NotebookShareResult(
|
|
315
|
+
notebook=notebook,
|
|
316
|
+
requested=requested,
|
|
317
|
+
visible_after_write=visible,
|
|
318
|
+
not_observed_after_write=not_observed,
|
|
319
|
+
observation=observation,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
async def update_share(
|
|
323
|
+
self, ref: NotebookShareRef, permissions: NotebookSharePermissionGrant
|
|
324
|
+
) -> NotebookShareUpdateResult:
|
|
325
|
+
try:
|
|
326
|
+
request = WireUpdateNotebookSharePermissionsRequest.model_validate(
|
|
327
|
+
{
|
|
328
|
+
"id": ref.share_row_id,
|
|
329
|
+
"write": int(permissions.write),
|
|
330
|
+
"create": int(permissions.create),
|
|
331
|
+
"delete": int(permissions.delete),
|
|
332
|
+
"sign": int(permissions.sign),
|
|
333
|
+
"subtype": int(permissions.manage_subtypes),
|
|
334
|
+
"log": int(permissions.read_audit_log),
|
|
335
|
+
"user": int(permissions.manage_shares),
|
|
336
|
+
}
|
|
337
|
+
)
|
|
338
|
+
except ValidationError:
|
|
339
|
+
raise MatelabUsageError("Notebook share permissions do not satisfy the Integration Contract.") from None
|
|
340
|
+
_ = await self._transport.request(_UPDATE_SHARE, files=multipart_fields(request.model_dump(mode="json")))
|
|
341
|
+
after = await self._verified_shares(ref.notebook, "notebook share-permission update")
|
|
342
|
+
observed = next((item for item in after.shares if item.ref.share_row_id == ref.share_row_id), None)
|
|
343
|
+
if observed is not None and observed.ref.recipient != ref.recipient:
|
|
344
|
+
raise MatelabProtocolError("Matelab returned a different recipient for the notebook share row.")
|
|
345
|
+
return NotebookShareUpdateResult(
|
|
346
|
+
ref=ref,
|
|
347
|
+
requested=permissions,
|
|
348
|
+
observed=observed,
|
|
349
|
+
observation="relation_observed" if observed is not None else "relation_not_observed",
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
async def unshare(self, ref: NotebookShareRef) -> NotebookUnshareResult:
|
|
353
|
+
try:
|
|
354
|
+
request = WireDeleteNotebookShareRequest(id=ref.share_row_id)
|
|
355
|
+
except ValidationError:
|
|
356
|
+
raise MatelabUsageError("Notebook share identity does not satisfy the Integration Contract.") from None
|
|
357
|
+
_ = await self._transport.request(_DELETE_SHARE, files=multipart_fields(request.model_dump(mode="json")))
|
|
358
|
+
if ref.belongs_to_caller:
|
|
359
|
+
return NotebookUnshareResult(ref=ref, absence_confirmed=None, verification="not_attempted_after_leaving")
|
|
360
|
+
after = await self._verified_shares(ref.notebook, "notebook unshare")
|
|
361
|
+
still_visible = any(item.ref.share_row_id == ref.share_row_id for item in after.shares)
|
|
362
|
+
return NotebookUnshareResult(
|
|
363
|
+
ref=ref,
|
|
364
|
+
absence_confirmed=not still_visible,
|
|
365
|
+
verification="still_visible" if still_visible else "confirmed_absent",
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
@staticmethod
|
|
369
|
+
def _save_request(
|
|
370
|
+
metadata: NotebookMetadata, *, notebook_id: int, group_id: int | None = None
|
|
371
|
+
) -> WireSaveNotebookRequest:
|
|
372
|
+
try:
|
|
373
|
+
if any(not 1 <= len(field.path) <= 4 for field in metadata.display_fields or ()):
|
|
374
|
+
raise ValueError
|
|
375
|
+
encoded_types = json.dumps(metadata.notebook_types, ensure_ascii=False, separators=(",", ":"))
|
|
376
|
+
encoded_fields = (
|
|
377
|
+
json.dumps(
|
|
378
|
+
[{"uid": field.uid, "path": list(field.path)} for field in metadata.display_fields],
|
|
379
|
+
ensure_ascii=False,
|
|
380
|
+
separators=(",", ":"),
|
|
381
|
+
)
|
|
382
|
+
if metadata.display_fields is not None
|
|
383
|
+
else None
|
|
384
|
+
)
|
|
385
|
+
payload: dict[str, object] = {
|
|
386
|
+
"id": notebook_id,
|
|
387
|
+
"showtext": metadata.title,
|
|
388
|
+
"comm": metadata.summary,
|
|
389
|
+
"display": int(metadata.display_fields is not None),
|
|
390
|
+
"types": encoded_types,
|
|
391
|
+
"requests": encoded_fields,
|
|
392
|
+
}
|
|
393
|
+
if group_id is not None:
|
|
394
|
+
payload["group_id"] = group_id
|
|
395
|
+
return WireSaveNotebookRequest.model_validate(payload)
|
|
396
|
+
except (ValidationError, TypeError, ValueError):
|
|
397
|
+
raise MatelabUsageError("Notebook metadata does not satisfy the Integration Contract.") from None
|
|
398
|
+
|
|
399
|
+
async def _verified_notebook_list(self, action: str) -> NotebookCollection:
|
|
400
|
+
try:
|
|
401
|
+
return await self.list()
|
|
402
|
+
except MatelabError as exc:
|
|
403
|
+
raise MatelabVerificationError(
|
|
404
|
+
f"Matelab acknowledged the {action}, but readback failed; the mutation may have persisted."
|
|
405
|
+
) from exc
|
|
406
|
+
|
|
407
|
+
async def _verified_shares(self, notebook: NotebookRef, action: str) -> NotebookShares:
|
|
408
|
+
try:
|
|
409
|
+
return await self.shares(notebook)
|
|
410
|
+
except MatelabError as exc:
|
|
411
|
+
raise MatelabVerificationError(
|
|
412
|
+
f"Matelab acknowledged the {action}, but readback failed; the mutation may have persisted."
|
|
413
|
+
) from exc
|
|
414
|
+
|
|
415
|
+
@staticmethod
|
|
416
|
+
def _metadata_matches(observed: NotebookSummary, requested: NotebookMetadata) -> bool:
|
|
417
|
+
summary_matches = (observed.summary or "") == (requested.summary or "")
|
|
418
|
+
fields_match = not observed.display_enabled or (
|
|
419
|
+
observed.display_configuration_valid and observed.display_fields == (requested.display_fields or ())
|
|
420
|
+
)
|
|
421
|
+
return (
|
|
422
|
+
observed.ref.title == requested.title
|
|
423
|
+
and summary_matches
|
|
424
|
+
and observed.notebook_types == requested.notebook_types
|
|
425
|
+
and observed.display_enabled == (requested.display_fields is not None)
|
|
426
|
+
and fields_match
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
@staticmethod
|
|
430
|
+
def _owned_or_shared(item: ElnNotebook, *, scope: Literal["owned", "shared"]) -> NotebookSummary:
|
|
431
|
+
display_fields, configuration_valid = Notebooks._parse_display_fields(item.requests)
|
|
432
|
+
return NotebookSummary(
|
|
433
|
+
ref=NotebookRef(notebook_id=item.id, title=item.showtext, owner_userid=item.userid, scope=scope),
|
|
434
|
+
summary=item.comm,
|
|
435
|
+
record_count=item.num_total,
|
|
436
|
+
created_at=item.datetime_create,
|
|
437
|
+
owner_name=item.username or None,
|
|
438
|
+
group_name=item.group_name,
|
|
439
|
+
notebook_types=tuple(cast(list[object], item.types)),
|
|
440
|
+
display_enabled=bool(item.display.value),
|
|
441
|
+
display_fields=display_fields,
|
|
442
|
+
display_configuration_valid=configuration_valid,
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
@staticmethod
|
|
446
|
+
def _parse_display_fields(raw: str | None) -> tuple[tuple[NotebookListField, ...] | None, bool]:
|
|
447
|
+
if raw is None or raw == "":
|
|
448
|
+
return (), True
|
|
449
|
+
try:
|
|
450
|
+
decoded = cast(object, json.loads(raw))
|
|
451
|
+
except (TypeError, ValueError):
|
|
452
|
+
return None, False
|
|
453
|
+
if not isinstance(decoded, list):
|
|
454
|
+
return None, False
|
|
455
|
+
fields: list[NotebookListField] = []
|
|
456
|
+
for value in cast(list[object], decoded):
|
|
457
|
+
if not isinstance(value, Mapping):
|
|
458
|
+
return None, False
|
|
459
|
+
mapping = cast(Mapping[object, object], value)
|
|
460
|
+
path = mapping.get("path")
|
|
461
|
+
if "uid" not in mapping or not isinstance(path, list):
|
|
462
|
+
return None, False
|
|
463
|
+
path_values = cast(list[object], path)
|
|
464
|
+
if not 1 <= len(path_values) <= 4:
|
|
465
|
+
return None, False
|
|
466
|
+
fields.append(NotebookListField(uid=mapping["uid"], path=tuple(path_values)))
|
|
467
|
+
return tuple(fields), True
|
matelab/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|