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/literature.py
ADDED
|
@@ -0,0 +1,1243 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import math
|
|
3
|
+
import re
|
|
4
|
+
from collections.abc import Mapping, Sequence
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
from types import MappingProxyType
|
|
8
|
+
from typing import Literal, TypeAlias, cast
|
|
9
|
+
|
|
10
|
+
from pydantic import ValidationError
|
|
11
|
+
|
|
12
|
+
from matelab._generated.models import BasicSuccessResponse as WireBasicSuccessResponse
|
|
13
|
+
from matelab._generated.models import DeleteLiteratureCommentRequest as WireDeleteLiteratureCommentRequest
|
|
14
|
+
from matelab._generated.models import DeleteLiteraturePdfRequest as WireDeleteLiteraturePdfRequest
|
|
15
|
+
from matelab._generated.models import DeletePersonalLiteratureItemRequest as WireDeletePersonalLiteratureItemRequest
|
|
16
|
+
from matelab._generated.models import DownloadLiteraturePdfParametersQuery as WireDownloadLiteraturePdfQuery
|
|
17
|
+
from matelab._generated.models import ExtractedLiteratureAuthor as WireExtractedLiteratureAuthor
|
|
18
|
+
from matelab._generated.models import ExtractLiteratureMetadataRequest as WireExtractLiteratureMetadataRequest
|
|
19
|
+
from matelab._generated.models import ListLiteratureItemsParametersQuery as WireListLiteratureItemsQuery
|
|
20
|
+
from matelab._generated.models import LiteratureContent1 as WireInvalidLiteratureContent
|
|
21
|
+
from matelab._generated.models import LiteratureContentObject as WireLiteratureContentObject
|
|
22
|
+
from matelab._generated.models import LiteratureDetailResponse as WireLiteratureDetailResponse
|
|
23
|
+
from matelab._generated.models import LiteratureFieldDefinition as WireLiteratureFieldDefinition
|
|
24
|
+
from matelab._generated.models import LiteratureLibrariesResponse as WireLiteratureLibrariesResponse
|
|
25
|
+
from matelab._generated.models import LiteratureLibrary as WireLiteratureLibrary
|
|
26
|
+
from matelab._generated.models import LiteratureListItem as WireLiteratureListItem
|
|
27
|
+
from matelab._generated.models import LiteratureListResponse as WireLiteratureListResponse
|
|
28
|
+
from matelab._generated.models import LiteratureMetadataExtractionResponse as WireLiteratureMetadataExtractionResponse
|
|
29
|
+
from matelab._generated.models import LiteratureTemplateResponse as WireLiteratureTemplateResponse
|
|
30
|
+
from matelab._generated.models import ReadLiteratureDetailParametersQuery as WireReadLiteratureDetailQuery
|
|
31
|
+
from matelab._generated.models import ReplaceLiteraturePdfRequest as WireReplaceLiteraturePdfRequest
|
|
32
|
+
from matelab._generated.models import SaveLiteratureCommentRequest as WireSaveLiteratureCommentRequest
|
|
33
|
+
from matelab._generated.models import SaveLiteratureContent as WireSaveLiteratureContent
|
|
34
|
+
from matelab._generated.models import SaveLiteratureItemRequest as WireSaveLiteratureItemRequest
|
|
35
|
+
from matelab._generated.models import ShareLiteratureItemsRequest as WireShareLiteratureItemsRequest
|
|
36
|
+
from matelab._transport import Encoding, Operation, SessionTransport, StreamOperation, multipart_fields
|
|
37
|
+
from matelab.errors import MatelabError, MatelabProtocolError, MatelabUsageError, MatelabVerificationError
|
|
38
|
+
from matelab.streaming import ByteRange, DownloadStream
|
|
39
|
+
from matelab.uploads import StagedFile, UploadBindingRef
|
|
40
|
+
from matelab.users import UserRef, UserSummary
|
|
41
|
+
|
|
42
|
+
_LIST_LIBRARIES = Operation(
|
|
43
|
+
method="GET",
|
|
44
|
+
path="/recommend_fav/stores",
|
|
45
|
+
encoding=Encoding.NONE,
|
|
46
|
+
response_model=WireLiteratureLibrariesResponse,
|
|
47
|
+
success_codes=frozenset({0}),
|
|
48
|
+
retry_on_access_expired=True,
|
|
49
|
+
)
|
|
50
|
+
_LIST_ITEMS = Operation(
|
|
51
|
+
method="GET",
|
|
52
|
+
path="/recommend_fav/list",
|
|
53
|
+
encoding=Encoding.QUERY,
|
|
54
|
+
response_model=WireLiteratureListResponse,
|
|
55
|
+
success_codes=frozenset({0}),
|
|
56
|
+
retry_on_access_expired=True,
|
|
57
|
+
)
|
|
58
|
+
_READ_ITEM = Operation(
|
|
59
|
+
method="GET",
|
|
60
|
+
path="/recommend_fav/detail",
|
|
61
|
+
encoding=Encoding.QUERY,
|
|
62
|
+
response_model=WireLiteratureDetailResponse,
|
|
63
|
+
success_codes=frozenset({0}),
|
|
64
|
+
retry_on_access_expired=True,
|
|
65
|
+
)
|
|
66
|
+
_SAVE_COMMENT = Operation(
|
|
67
|
+
method="POST",
|
|
68
|
+
path="/recommend_fav/comment",
|
|
69
|
+
encoding=Encoding.MULTIPART,
|
|
70
|
+
response_model=WireBasicSuccessResponse,
|
|
71
|
+
success_codes=frozenset({0}),
|
|
72
|
+
)
|
|
73
|
+
_DELETE_COMMENT = Operation(
|
|
74
|
+
method="POST",
|
|
75
|
+
path="/recommend_fav/comment_del",
|
|
76
|
+
encoding=Encoding.MULTIPART,
|
|
77
|
+
response_model=WireBasicSuccessResponse,
|
|
78
|
+
success_codes=frozenset({0}),
|
|
79
|
+
)
|
|
80
|
+
_SHARE_ITEMS = Operation(
|
|
81
|
+
method="POST",
|
|
82
|
+
path="/recommend_fav/share",
|
|
83
|
+
encoding=Encoding.MULTIPART,
|
|
84
|
+
response_model=WireBasicSuccessResponse,
|
|
85
|
+
success_codes=frozenset({0}),
|
|
86
|
+
)
|
|
87
|
+
_DELETE_PERSONAL_ITEM = Operation(
|
|
88
|
+
method="POST",
|
|
89
|
+
path="/recommend_fav/del",
|
|
90
|
+
encoding=Encoding.MULTIPART,
|
|
91
|
+
response_model=WireBasicSuccessResponse,
|
|
92
|
+
success_codes=frozenset({0}),
|
|
93
|
+
)
|
|
94
|
+
_READ_CREATION_SCHEMA = Operation(
|
|
95
|
+
method="GET",
|
|
96
|
+
path="/recommend_fav/template",
|
|
97
|
+
encoding=Encoding.NONE,
|
|
98
|
+
response_model=WireLiteratureTemplateResponse,
|
|
99
|
+
success_codes=frozenset({0}),
|
|
100
|
+
retry_on_access_expired=True,
|
|
101
|
+
)
|
|
102
|
+
_EXTRACT_METADATA = Operation(
|
|
103
|
+
method="POST",
|
|
104
|
+
path="/actions/pdf_doi",
|
|
105
|
+
encoding=Encoding.MULTIPART,
|
|
106
|
+
response_model=WireLiteratureMetadataExtractionResponse,
|
|
107
|
+
success_codes=frozenset({0}),
|
|
108
|
+
retry_on_access_expired=True,
|
|
109
|
+
)
|
|
110
|
+
_SAVE_ITEM = Operation(
|
|
111
|
+
method="POST",
|
|
112
|
+
path="/recommend_fav/edit",
|
|
113
|
+
encoding=Encoding.MULTIPART,
|
|
114
|
+
response_model=WireBasicSuccessResponse,
|
|
115
|
+
success_codes=frozenset({0}),
|
|
116
|
+
)
|
|
117
|
+
_REPLACE_PDF = Operation(
|
|
118
|
+
method="POST",
|
|
119
|
+
path="/recommend_fav/pdf_add",
|
|
120
|
+
encoding=Encoding.MULTIPART,
|
|
121
|
+
response_model=WireBasicSuccessResponse,
|
|
122
|
+
success_codes=frozenset({0}),
|
|
123
|
+
)
|
|
124
|
+
_DELETE_PDF = Operation(
|
|
125
|
+
method="POST",
|
|
126
|
+
path="/recommend_fav/pdf_del",
|
|
127
|
+
encoding=Encoding.MULTIPART,
|
|
128
|
+
response_model=WireBasicSuccessResponse,
|
|
129
|
+
success_codes=frozenset({0}),
|
|
130
|
+
)
|
|
131
|
+
_DOWNLOAD_PDF = StreamOperation(
|
|
132
|
+
method="GET",
|
|
133
|
+
path="/url_eln/hash",
|
|
134
|
+
retry_on_access_expired=True,
|
|
135
|
+
media_types=frozenset({"application/pdf", "application/octet-stream"}),
|
|
136
|
+
)
|
|
137
|
+
_SHA256_PATTERN = re.compile(r"^[0-9a-f]{64}$")
|
|
138
|
+
_COMMENT_ATTACHMENT_MARKER = "matelab-staged-file"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _empty_mapping() -> Mapping[str, object]:
|
|
142
|
+
return MappingProxyType({})
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class LiteratureSource(StrEnum):
|
|
146
|
+
SUBSCRIBED = "subscribed"
|
|
147
|
+
USER_CREATED = "user_created"
|
|
148
|
+
SHARED_COPY = "shared_copy"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@dataclass(frozen=True, slots=True)
|
|
152
|
+
class PersonalLiteratureLibraryRef:
|
|
153
|
+
"""The caller's synthetic private library (`store_id=0`)."""
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@dataclass(frozen=True, slots=True)
|
|
157
|
+
class SharedLiteratureLibraryRef:
|
|
158
|
+
literature_library_id: int
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
LiteratureLibraryRef: TypeAlias = PersonalLiteratureLibraryRef | SharedLiteratureLibraryRef
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@dataclass(frozen=True, slots=True)
|
|
165
|
+
class PendingLiteratureShareRef:
|
|
166
|
+
"""A pending copy identity, not a readable library-item identity."""
|
|
167
|
+
|
|
168
|
+
pending_item_id: int
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@dataclass(frozen=True, slots=True)
|
|
172
|
+
class PersonalLiteratureItemRef:
|
|
173
|
+
literature_item_id: int
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@dataclass(frozen=True, slots=True)
|
|
177
|
+
class SharedLiteratureItemRef:
|
|
178
|
+
literature_item_id: int
|
|
179
|
+
library: SharedLiteratureLibraryRef
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
LiteratureItemRef: TypeAlias = PersonalLiteratureItemRef | SharedLiteratureItemRef
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@dataclass(frozen=True, slots=True)
|
|
186
|
+
class LiteratureLibrarySummary:
|
|
187
|
+
ref: LiteratureLibraryRef | None
|
|
188
|
+
kind: Literal["personal", "shared", "dangling_membership"]
|
|
189
|
+
name: str | None
|
|
190
|
+
permission_mask: int
|
|
191
|
+
item_count: int
|
|
192
|
+
keywords: tuple[str, ...]
|
|
193
|
+
introduction: str | None
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@dataclass(frozen=True, slots=True)
|
|
197
|
+
class PendingLiteratureShareComment:
|
|
198
|
+
comment_id: int
|
|
199
|
+
share: PendingLiteratureShareRef
|
|
200
|
+
content: str
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@dataclass(frozen=True, slots=True)
|
|
204
|
+
class PendingLiteratureShare:
|
|
205
|
+
ref: PendingLiteratureShareRef
|
|
206
|
+
title: str | None
|
|
207
|
+
sender_name: str
|
|
208
|
+
introduction: str | None
|
|
209
|
+
keywords: tuple[str, ...]
|
|
210
|
+
comments: tuple[PendingLiteratureShareComment, ...]
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@dataclass(frozen=True, slots=True)
|
|
214
|
+
class LiteratureLibraries:
|
|
215
|
+
personal: LiteratureLibrarySummary
|
|
216
|
+
shared: tuple[LiteratureLibrarySummary, ...]
|
|
217
|
+
dangling_memberships: tuple[LiteratureLibrarySummary, ...]
|
|
218
|
+
pending_shares: tuple[PendingLiteratureShare, ...]
|
|
219
|
+
unmatched_pending_comments: tuple[PendingLiteratureShareComment, ...]
|
|
220
|
+
ordering: Literal["provider_unspecified"] = "provider_unspecified"
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@dataclass(frozen=True, slots=True)
|
|
224
|
+
class LiteratureCitation:
|
|
225
|
+
year: float | None = None
|
|
226
|
+
volume: str | None = None
|
|
227
|
+
issue: str | None = None
|
|
228
|
+
page: str | None = None
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@dataclass(frozen=True, slots=True)
|
|
232
|
+
class LiteratureMetadata:
|
|
233
|
+
"""Complete canonical metadata replacement accepted by the Provider template."""
|
|
234
|
+
|
|
235
|
+
title: str
|
|
236
|
+
journal: str | None = None
|
|
237
|
+
citation: LiteratureCitation | None = None
|
|
238
|
+
author: str | None = None
|
|
239
|
+
keywords: tuple[str, ...] | None = ()
|
|
240
|
+
abstract: str | None = ""
|
|
241
|
+
doi: str | None = None
|
|
242
|
+
url: str | None = None
|
|
243
|
+
references: tuple[str, ...] | None = ()
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@dataclass(frozen=True, slots=True)
|
|
247
|
+
class LiteratureStoredMetadata:
|
|
248
|
+
"""Decoded stored metadata, including source fields a canonical update would drop."""
|
|
249
|
+
|
|
250
|
+
valid_object: bool
|
|
251
|
+
title: str | None
|
|
252
|
+
journal: str | None
|
|
253
|
+
citation: LiteratureCitation | None
|
|
254
|
+
author: str | None
|
|
255
|
+
authors: tuple[str, ...] | None
|
|
256
|
+
keywords: tuple[str, ...] | None
|
|
257
|
+
abstract: str | None
|
|
258
|
+
reference_information: str | None
|
|
259
|
+
doi: str | None
|
|
260
|
+
url: str | None
|
|
261
|
+
references: tuple[str, ...] | None
|
|
262
|
+
extension_fields: Mapping[str, object] = field(default_factory=_empty_mapping)
|
|
263
|
+
fields_dropped_by_canonical_update: tuple[str, ...] = ()
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@dataclass(frozen=True, slots=True)
|
|
267
|
+
class LiteraturePdf:
|
|
268
|
+
item: LiteratureItemRef
|
|
269
|
+
sha256: str | None = field(repr=False)
|
|
270
|
+
filename: str | None
|
|
271
|
+
size: int | None
|
|
272
|
+
|
|
273
|
+
@property
|
|
274
|
+
def is_attached(self) -> bool:
|
|
275
|
+
return self.sha256 is not None or self.filename is not None or self.size is not None
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
@dataclass(frozen=True, slots=True)
|
|
279
|
+
class LiteratureItemSummary:
|
|
280
|
+
ref: LiteratureItemRef
|
|
281
|
+
source: LiteratureSource
|
|
282
|
+
star: int
|
|
283
|
+
title: str
|
|
284
|
+
author: str
|
|
285
|
+
citation_html: str
|
|
286
|
+
abstract: str
|
|
287
|
+
journal: str
|
|
288
|
+
keywords: tuple[str, ...]
|
|
289
|
+
publication_date: str
|
|
290
|
+
matching_author_fragments: tuple[str, ...]
|
|
291
|
+
pdf: LiteraturePdf
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@dataclass(frozen=True, slots=True)
|
|
295
|
+
class LiteraturePage:
|
|
296
|
+
library: LiteratureLibraryRef
|
|
297
|
+
items: tuple[LiteratureItemSummary, ...]
|
|
298
|
+
library_keywords: tuple[str, ...]
|
|
299
|
+
library_name: str
|
|
300
|
+
permission_mask: int
|
|
301
|
+
page: int
|
|
302
|
+
requested_page_size: int
|
|
303
|
+
effective_page_size: int
|
|
304
|
+
total_count: int
|
|
305
|
+
has_more: bool
|
|
306
|
+
ordering: Literal["descending_item_id"] = "descending_item_id"
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@dataclass(frozen=True, slots=True)
|
|
310
|
+
class LiteratureNestedField:
|
|
311
|
+
field_id: str
|
|
312
|
+
field_type: str
|
|
313
|
+
label: str | tuple[str, str]
|
|
314
|
+
width: int | None
|
|
315
|
+
attributes: Mapping[str, object] = field(default_factory=_empty_mapping)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@dataclass(frozen=True, slots=True)
|
|
319
|
+
class LiteratureField:
|
|
320
|
+
field_id: str
|
|
321
|
+
field_type: str
|
|
322
|
+
label: str | tuple[str, str]
|
|
323
|
+
required: bool | None
|
|
324
|
+
group: str | None
|
|
325
|
+
help_text: str | tuple[str, str] | None
|
|
326
|
+
print_template: str | None
|
|
327
|
+
nested: tuple[LiteratureNestedField, ...]
|
|
328
|
+
visible: bool | None
|
|
329
|
+
width: int | None
|
|
330
|
+
condition: Mapping[str, object] | None
|
|
331
|
+
attributes: Mapping[str, object] = field(default_factory=_empty_mapping)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
@dataclass(frozen=True, slots=True)
|
|
335
|
+
class LiteratureCreationSchema:
|
|
336
|
+
fields: tuple[LiteratureField, ...]
|
|
337
|
+
metadata_extraction_available: bool
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
@dataclass(frozen=True, slots=True)
|
|
341
|
+
class LiteratureCommentRef:
|
|
342
|
+
item: LiteratureItemRef
|
|
343
|
+
comment_id: int
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
@dataclass(frozen=True, slots=True)
|
|
347
|
+
class LiteratureComment:
|
|
348
|
+
ref: LiteratureCommentRef
|
|
349
|
+
content: str
|
|
350
|
+
author: UserRef
|
|
351
|
+
author_name: str | None
|
|
352
|
+
created_at: str
|
|
353
|
+
owned_by_caller: bool
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
@dataclass(frozen=True, slots=True)
|
|
357
|
+
class LiteratureDetail:
|
|
358
|
+
ref: LiteratureItemRef
|
|
359
|
+
metadata: LiteratureStoredMetadata
|
|
360
|
+
schema_fields: tuple[LiteratureField, ...]
|
|
361
|
+
library_keywords: tuple[str, ...]
|
|
362
|
+
item_keywords: tuple[str, ...]
|
|
363
|
+
star: int
|
|
364
|
+
comments: tuple[LiteratureComment, ...]
|
|
365
|
+
pdf: LiteraturePdf
|
|
366
|
+
source: LiteratureSource
|
|
367
|
+
permission_mask: int
|
|
368
|
+
library_name: str
|
|
369
|
+
authorization_scope: Literal["provider_authorized_response_pvd_011_not_security_proof"] = (
|
|
370
|
+
"provider_authorized_response_pvd_011_not_security_proof"
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
@dataclass(frozen=True, slots=True)
|
|
375
|
+
class DoiMetadataSource:
|
|
376
|
+
doi: str
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
@dataclass(frozen=True, slots=True)
|
|
380
|
+
class StagedPdfMetadataSource:
|
|
381
|
+
staged_pdf: StagedFile
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
LiteratureMetadataSource: TypeAlias = DoiMetadataSource | StagedPdfMetadataSource
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
@dataclass(frozen=True, slots=True)
|
|
388
|
+
class ExtractedLiteratureAuthor:
|
|
389
|
+
name: str
|
|
390
|
+
affiliations: tuple[str, ...] | None
|
|
391
|
+
email: str | None
|
|
392
|
+
corresponding: bool | None
|
|
393
|
+
orcid: str | None
|
|
394
|
+
extension_fields: Mapping[str, object] = field(default_factory=_empty_mapping)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
@dataclass(frozen=True, slots=True)
|
|
398
|
+
class ExtractedLiteratureMetadata:
|
|
399
|
+
doi: str | None
|
|
400
|
+
url: str | None
|
|
401
|
+
publication_date: str | None
|
|
402
|
+
publisher: str | None
|
|
403
|
+
journal: str | None
|
|
404
|
+
issue: str | None
|
|
405
|
+
volume: str | None
|
|
406
|
+
page: str | None
|
|
407
|
+
title: str | None
|
|
408
|
+
abstract: str | None
|
|
409
|
+
authors: tuple[ExtractedLiteratureAuthor, ...] | None
|
|
410
|
+
keywords: tuple[str, ...] | None
|
|
411
|
+
references: tuple[str, ...] | None
|
|
412
|
+
fundings: tuple[object, ...] | None
|
|
413
|
+
language: str | None
|
|
414
|
+
citation_count: int | None
|
|
415
|
+
extension_fields: Mapping[str, object] = field(default_factory=_empty_mapping)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
@dataclass(frozen=True, slots=True)
|
|
419
|
+
class LiteratureCreateResult:
|
|
420
|
+
requested: LiteratureMetadata
|
|
421
|
+
staged_pdf: StagedFile | None
|
|
422
|
+
created_item: None = None
|
|
423
|
+
identity_status: Literal["provider_did_not_return_identity"] = "provider_did_not_return_identity"
|
|
424
|
+
persistence: Literal["provider_acknowledged_not_readback_identifiable"] = (
|
|
425
|
+
"provider_acknowledged_not_readback_identifiable"
|
|
426
|
+
)
|
|
427
|
+
metadata_pdf_atomicity: Literal["not_guaranteed"] = "not_guaranteed"
|
|
428
|
+
automatic_retry: Literal[False] = False
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
@dataclass(frozen=True, slots=True)
|
|
432
|
+
class LiteratureUpdateResult:
|
|
433
|
+
item: LiteratureItemRef
|
|
434
|
+
requested: LiteratureMetadata
|
|
435
|
+
observed: LiteratureDetail
|
|
436
|
+
confirmation: Literal["observed_matching", "observed_mismatch"]
|
|
437
|
+
source_metadata_loss_authorized: bool
|
|
438
|
+
provider_cas: Literal[False] = False
|
|
439
|
+
automatic_retry: Literal[False] = False
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
@dataclass(frozen=True, slots=True)
|
|
443
|
+
class LiteraturePdfReplaceResult:
|
|
444
|
+
item: LiteratureItemRef
|
|
445
|
+
staged_pdf: StagedFile
|
|
446
|
+
observed: LiteraturePdf
|
|
447
|
+
confirmation: Literal["observed_matching", "observed_mismatch"]
|
|
448
|
+
metadata_pdf_atomicity: Literal["separate_mutation_not_atomic"] = "separate_mutation_not_atomic"
|
|
449
|
+
automatic_retry: Literal[False] = False
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
@dataclass(frozen=True, slots=True)
|
|
453
|
+
class LiteraturePdfDeleteResult:
|
|
454
|
+
item: LiteratureItemRef
|
|
455
|
+
observed: LiteraturePdf
|
|
456
|
+
absence_confirmed: bool
|
|
457
|
+
metadata_pdf_atomicity: Literal["separate_mutation_not_atomic"] = "separate_mutation_not_atomic"
|
|
458
|
+
automatic_retry: Literal[False] = False
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
@dataclass(frozen=True, slots=True)
|
|
462
|
+
class LiteraturePermanentDeleteResult:
|
|
463
|
+
item: PersonalLiteratureItemRef
|
|
464
|
+
absence_confirmed: bool
|
|
465
|
+
verification: Literal["personal_library_snapshot"]
|
|
466
|
+
recoverable: Literal[False] = False
|
|
467
|
+
automatic_retry: Literal[False] = False
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
@dataclass(frozen=True, slots=True)
|
|
471
|
+
class LiteratureShareResult:
|
|
472
|
+
items: tuple[LiteratureItemRef, ...]
|
|
473
|
+
recipients: tuple[UserRef, ...]
|
|
474
|
+
copied_owner_comment: bool
|
|
475
|
+
per_recipient_results: None = None
|
|
476
|
+
outcome: Literal["provider_acknowledged_unverified_batch"] = "provider_acknowledged_unverified_batch"
|
|
477
|
+
atomicity: Literal["provider_does_not_report_or_guarantee_batch_atomicity"] = (
|
|
478
|
+
"provider_does_not_report_or_guarantee_batch_atomicity"
|
|
479
|
+
)
|
|
480
|
+
automatic_retry: Literal[False] = False
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
@dataclass(frozen=True, slots=True)
|
|
484
|
+
class LiteratureCommentDraft:
|
|
485
|
+
"""Rich text; use one `matelab-staged-file` marker when attaching a staged file."""
|
|
486
|
+
|
|
487
|
+
html: str
|
|
488
|
+
attachment: StagedFile | None = None
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
@dataclass(frozen=True, slots=True)
|
|
492
|
+
class LiteratureCommentSaveResult:
|
|
493
|
+
item: LiteratureItemRef
|
|
494
|
+
action: Literal["created", "updated"]
|
|
495
|
+
upload_binding: UploadBindingRef
|
|
496
|
+
owner_comments_after: tuple[LiteratureComment, ...]
|
|
497
|
+
observed: LiteratureComment | None
|
|
498
|
+
confirmation: Literal["one_owner_comment_observed", "no_owner_comment_observed", "multiple_owner_comments_observed"]
|
|
499
|
+
binding_authorization: Literal["provider_does_not_verify_uploader"] = "provider_does_not_verify_uploader"
|
|
500
|
+
automatic_retry: Literal[False] = False
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
@dataclass(frozen=True, slots=True)
|
|
504
|
+
class LiteratureCommentDeleteResult:
|
|
505
|
+
comment: LiteratureCommentRef
|
|
506
|
+
absence_confirmed: bool
|
|
507
|
+
automatic_retry: Literal[False] = False
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
class Literature:
|
|
511
|
+
"""Literature discovery, canonical metadata, sharing, comments, and PDF workflows."""
|
|
512
|
+
|
|
513
|
+
def __init__(self, transport: SessionTransport) -> None:
|
|
514
|
+
self._transport: SessionTransport = transport
|
|
515
|
+
|
|
516
|
+
async def libraries(self) -> LiteratureLibraries:
|
|
517
|
+
response = await self._transport.request(_LIST_LIBRARIES)
|
|
518
|
+
personal_items = [item for item in response.stores if item.id == 0]
|
|
519
|
+
if len(personal_items) != 1:
|
|
520
|
+
raise MatelabProtocolError("Matelab did not return exactly one synthetic personal literature library.")
|
|
521
|
+
personal_wire = personal_items[0]
|
|
522
|
+
personal = self._library(personal_wire, kind="personal")
|
|
523
|
+
shared = tuple(self._library(item, kind="shared") for item in response.stores if item.id not in {0, None})
|
|
524
|
+
dangling = tuple(self._library(item, kind="dangling_membership") for item in response.stores if item.id is None)
|
|
525
|
+
comments = tuple(
|
|
526
|
+
PendingLiteratureShareComment(
|
|
527
|
+
comment_id=item.id, share=PendingLiteratureShareRef(pending_item_id=item.fav_id), content=item.content
|
|
528
|
+
)
|
|
529
|
+
for item in response.share_comments
|
|
530
|
+
)
|
|
531
|
+
comment_by_share: dict[int, list[PendingLiteratureShareComment]] = {}
|
|
532
|
+
for comment in comments:
|
|
533
|
+
comment_by_share.setdefault(comment.share.pending_item_id, []).append(comment)
|
|
534
|
+
shares = tuple(
|
|
535
|
+
PendingLiteratureShare(
|
|
536
|
+
ref=PendingLiteratureShareRef(pending_item_id=item.id),
|
|
537
|
+
title=item.title,
|
|
538
|
+
sender_name=item.username,
|
|
539
|
+
introduction=item.intro,
|
|
540
|
+
keywords=tuple(item.keywords),
|
|
541
|
+
comments=tuple(comment_by_share.get(item.id, ())),
|
|
542
|
+
)
|
|
543
|
+
for item in response.share
|
|
544
|
+
)
|
|
545
|
+
known_share_ids = {item.ref.pending_item_id for item in shares}
|
|
546
|
+
return LiteratureLibraries(
|
|
547
|
+
personal=personal,
|
|
548
|
+
shared=shared,
|
|
549
|
+
dangling_memberships=dangling,
|
|
550
|
+
pending_shares=shares,
|
|
551
|
+
unmatched_pending_comments=tuple(
|
|
552
|
+
comment for comment in comments if comment.share.pending_item_id not in known_share_ids
|
|
553
|
+
),
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
async def list(
|
|
557
|
+
self,
|
|
558
|
+
library: LiteratureLibraryRef | None = None,
|
|
559
|
+
*,
|
|
560
|
+
page: int = 1,
|
|
561
|
+
page_size: int = 10,
|
|
562
|
+
sources: Sequence[LiteratureSource] = (),
|
|
563
|
+
stars: Sequence[int] = (),
|
|
564
|
+
keywords: Sequence[str] = (),
|
|
565
|
+
search: str = "",
|
|
566
|
+
search_author: bool = False,
|
|
567
|
+
) -> LiteraturePage:
|
|
568
|
+
selected_library: LiteratureLibraryRef = library or PersonalLiteratureLibraryRef()
|
|
569
|
+
store_id = self._library_id(selected_library)
|
|
570
|
+
source_values = tuple(self._source_wire_value(source) for source in sources)
|
|
571
|
+
star_values = tuple(stars)
|
|
572
|
+
keyword_values = tuple(keywords)
|
|
573
|
+
if (
|
|
574
|
+
len(source_values) != len(set(source_values))
|
|
575
|
+
or any(type(star) is not int or not 1 <= star <= 5 for star in star_values)
|
|
576
|
+
or len(star_values) != len(set(star_values))
|
|
577
|
+
or any(not isinstance(keyword, str) for keyword in cast(tuple[object, ...], keyword_values))
|
|
578
|
+
or len(keyword_values) != len(set(keyword_values))
|
|
579
|
+
or type(search_author) is not bool
|
|
580
|
+
):
|
|
581
|
+
raise MatelabUsageError("Literature list filters contain invalid or duplicate values.")
|
|
582
|
+
try:
|
|
583
|
+
request = WireListLiteratureItemsQuery.model_validate(
|
|
584
|
+
{
|
|
585
|
+
"page": page,
|
|
586
|
+
"page_size": page_size,
|
|
587
|
+
"store_id": store_id,
|
|
588
|
+
"types": json.dumps(source_values, separators=(",", ":")) if source_values else "",
|
|
589
|
+
"stars": json.dumps(star_values, separators=(",", ":")) if star_values else "",
|
|
590
|
+
"keywords": (
|
|
591
|
+
json.dumps(keyword_values, ensure_ascii=False, separators=(",", ":")) if keyword_values else ""
|
|
592
|
+
),
|
|
593
|
+
"search": search,
|
|
594
|
+
"search_type": int(search_author),
|
|
595
|
+
}
|
|
596
|
+
)
|
|
597
|
+
except (ValidationError, TypeError, ValueError):
|
|
598
|
+
raise MatelabUsageError("Literature list filters do not satisfy the Integration Contract.") from None
|
|
599
|
+
response = await self._transport.request(_LIST_ITEMS, payload=request.model_dump(mode="json"))
|
|
600
|
+
normalized_page = max(1, request.page)
|
|
601
|
+
effective_page_size = max(request.page_size, 10)
|
|
602
|
+
return LiteraturePage(
|
|
603
|
+
library=selected_library,
|
|
604
|
+
items=tuple(self._summary(item, selected_library) for item in response.items),
|
|
605
|
+
library_keywords=tuple(response.keywords),
|
|
606
|
+
library_name=response.store_name,
|
|
607
|
+
permission_mask=response.power,
|
|
608
|
+
page=normalized_page,
|
|
609
|
+
requested_page_size=request.page_size,
|
|
610
|
+
effective_page_size=effective_page_size,
|
|
611
|
+
total_count=response.count,
|
|
612
|
+
has_more=normalized_page * effective_page_size < response.count,
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
async def read(self, item: LiteratureItemRef) -> LiteratureDetail:
|
|
616
|
+
item_id = self._item_id(item)
|
|
617
|
+
try:
|
|
618
|
+
request = WireReadLiteratureDetailQuery(id=item_id)
|
|
619
|
+
except ValidationError:
|
|
620
|
+
raise MatelabUsageError("Literature item identity does not satisfy the Integration Contract.") from None
|
|
621
|
+
response = await self._transport.request(_READ_ITEM, payload=request.model_dump(mode="json"))
|
|
622
|
+
return self._detail(item, response)
|
|
623
|
+
|
|
624
|
+
async def creation_schema(self) -> LiteratureCreationSchema:
|
|
625
|
+
response = await self._transport.request(_READ_CREATION_SCHEMA)
|
|
626
|
+
return LiteratureCreationSchema(
|
|
627
|
+
fields=tuple(self._field(item) for item in response.template), metadata_extraction_available=response.doipdf
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
async def extract_metadata(self, source: LiteratureMetadataSource) -> ExtractedLiteratureMetadata:
|
|
631
|
+
raw_source = cast(object, source)
|
|
632
|
+
if isinstance(raw_source, DoiMetadataSource):
|
|
633
|
+
payload: dict[str, object] = {"id": 0, "doi": raw_source.doi}
|
|
634
|
+
elif isinstance(raw_source, StagedPdfMetadataSource):
|
|
635
|
+
self._validate_staged_file(raw_source.staged_pdf)
|
|
636
|
+
payload = {"id": raw_source.staged_pdf.temporary_file_id, "doi": ""}
|
|
637
|
+
else:
|
|
638
|
+
raise MatelabUsageError("Literature metadata extraction needs a DOI or a completed staged PDF.")
|
|
639
|
+
try:
|
|
640
|
+
request = WireExtractLiteratureMetadataRequest.model_validate(payload)
|
|
641
|
+
except ValidationError:
|
|
642
|
+
raise MatelabUsageError("Literature metadata source does not satisfy the Integration Contract.") from None
|
|
643
|
+
request_payload = cast(dict[str, object], request.model_dump(mode="json"))
|
|
644
|
+
response = await self._transport.request(_EXTRACT_METADATA, files=multipart_fields(request_payload))
|
|
645
|
+
item = response.doi
|
|
646
|
+
return ExtractedLiteratureMetadata(
|
|
647
|
+
doi=item.DOI,
|
|
648
|
+
url=item.url,
|
|
649
|
+
publication_date=item.date,
|
|
650
|
+
publisher=item.publisher,
|
|
651
|
+
journal=item.publication,
|
|
652
|
+
issue=item.issue,
|
|
653
|
+
volume=item.volume,
|
|
654
|
+
page=item.page,
|
|
655
|
+
title=item.title,
|
|
656
|
+
abstract=item.abstract,
|
|
657
|
+
authors=(
|
|
658
|
+
tuple(self._extracted_author(author) for author in item.authors) if item.authors is not None else None
|
|
659
|
+
),
|
|
660
|
+
keywords=tuple(item.keywords) if item.keywords is not None else None,
|
|
661
|
+
references=tuple(item.refs) if item.refs is not None else None,
|
|
662
|
+
fundings=tuple(cast(list[object], item.fundings)) if item.fundings is not None else None,
|
|
663
|
+
language=item.language,
|
|
664
|
+
citation_count=item.citation_count,
|
|
665
|
+
extension_fields=MappingProxyType(cast(dict[str, object], dict(item.model_extra or {}))),
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
async def download_pdf(
|
|
669
|
+
self, pdf: LiteraturePdf, *, byte_range: ByteRange | None = None, preview: bool = True
|
|
670
|
+
) -> DownloadStream:
|
|
671
|
+
if (
|
|
672
|
+
pdf.sha256 is None
|
|
673
|
+
or _SHA256_PATTERN.fullmatch(pdf.sha256) is None
|
|
674
|
+
or type(preview) is not bool
|
|
675
|
+
or self._item_id(pdf.item) < 1
|
|
676
|
+
):
|
|
677
|
+
raise MatelabUsageError("Literature PDF identity does not satisfy the Integration Contract.")
|
|
678
|
+
try:
|
|
679
|
+
request = WireDownloadLiteraturePdfQuery.model_validate(
|
|
680
|
+
{
|
|
681
|
+
"t": "recommend_fav_pdf",
|
|
682
|
+
"id": self._item_id(pdf.item),
|
|
683
|
+
"h": pdf.sha256,
|
|
684
|
+
"f": pdf.filename or "",
|
|
685
|
+
"c": int(preview),
|
|
686
|
+
"thumb": 0,
|
|
687
|
+
}
|
|
688
|
+
)
|
|
689
|
+
except ValidationError:
|
|
690
|
+
raise MatelabUsageError("Literature PDF identity does not satisfy the Integration Contract.") from None
|
|
691
|
+
return await self._transport.stream(
|
|
692
|
+
_DOWNLOAD_PDF,
|
|
693
|
+
payload=request.model_dump(mode="json"),
|
|
694
|
+
range_header=byte_range.to_header() if byte_range is not None else None,
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
async def create(
|
|
698
|
+
self, metadata: LiteratureMetadata, *, staged_pdf: StagedFile | None = None
|
|
699
|
+
) -> LiteratureCreateResult:
|
|
700
|
+
if staged_pdf is not None:
|
|
701
|
+
self._validate_staged_file(staged_pdf)
|
|
702
|
+
request = self._save_request(metadata, item=None, staged_pdf=staged_pdf)
|
|
703
|
+
request_payload = cast(dict[str, object], request.model_dump(mode="json"))
|
|
704
|
+
_ = await self._transport.request(_SAVE_ITEM, files=multipart_fields(request_payload))
|
|
705
|
+
return LiteratureCreateResult(requested=metadata, staged_pdf=staged_pdf)
|
|
706
|
+
|
|
707
|
+
async def update(
|
|
708
|
+
self, item: LiteratureItemRef, metadata: LiteratureMetadata, *, allow_source_metadata_loss: bool = False
|
|
709
|
+
) -> LiteratureUpdateResult:
|
|
710
|
+
if type(cast(object, allow_source_metadata_loss)) is not bool:
|
|
711
|
+
raise MatelabUsageError("Source-metadata-loss acknowledgement must be a boolean.")
|
|
712
|
+
before = await self.read(item)
|
|
713
|
+
if before.metadata.fields_dropped_by_canonical_update and not allow_source_metadata_loss:
|
|
714
|
+
fields = ", ".join(before.metadata.fields_dropped_by_canonical_update)
|
|
715
|
+
message = (
|
|
716
|
+
f"Canonical replacement would drop source metadata ({fields}); "
|
|
717
|
+
+ "set allow_source_metadata_loss=True only when that loss is intentional."
|
|
718
|
+
)
|
|
719
|
+
raise MatelabUsageError(message)
|
|
720
|
+
request = self._save_request(metadata, item=item, staged_pdf=None)
|
|
721
|
+
request_payload = cast(dict[str, object], request.model_dump(mode="json"))
|
|
722
|
+
_ = await self._transport.request(_SAVE_ITEM, files=multipart_fields(request_payload))
|
|
723
|
+
observed = await self._verified_read(item, "literature metadata replacement")
|
|
724
|
+
return LiteratureUpdateResult(
|
|
725
|
+
item=item,
|
|
726
|
+
requested=metadata,
|
|
727
|
+
observed=observed,
|
|
728
|
+
confirmation="observed_matching"
|
|
729
|
+
if self._metadata_matches(observed.metadata, metadata)
|
|
730
|
+
else "observed_mismatch",
|
|
731
|
+
source_metadata_loss_authorized=allow_source_metadata_loss,
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
async def replace_pdf(self, item: LiteratureItemRef, staged_pdf: StagedFile) -> LiteraturePdfReplaceResult:
|
|
735
|
+
self._validate_staged_file(staged_pdf)
|
|
736
|
+
try:
|
|
737
|
+
request = WireReplaceLiteraturePdfRequest.model_validate(
|
|
738
|
+
{"id": self._item_id(item), "uid": staged_pdf.binding.uid}
|
|
739
|
+
)
|
|
740
|
+
except ValidationError:
|
|
741
|
+
raise MatelabUsageError("Literature PDF replacement does not satisfy the Integration Contract.") from None
|
|
742
|
+
_ = await self._transport.request(_REPLACE_PDF, files=multipart_fields(request.model_dump(mode="json")))
|
|
743
|
+
observed = (await self._verified_read(item, "literature PDF replacement")).pdf
|
|
744
|
+
matches = (
|
|
745
|
+
observed.sha256 == staged_pdf.sha256
|
|
746
|
+
and observed.filename == staged_pdf.filename
|
|
747
|
+
and observed.size == staged_pdf.size
|
|
748
|
+
)
|
|
749
|
+
return LiteraturePdfReplaceResult(
|
|
750
|
+
item=item,
|
|
751
|
+
staged_pdf=staged_pdf,
|
|
752
|
+
observed=observed,
|
|
753
|
+
confirmation="observed_matching" if matches else "observed_mismatch",
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
async def delete_pdf(self, item: LiteratureItemRef) -> LiteraturePdfDeleteResult:
|
|
757
|
+
try:
|
|
758
|
+
request = WireDeleteLiteraturePdfRequest(id=self._item_id(item))
|
|
759
|
+
except ValidationError:
|
|
760
|
+
raise MatelabUsageError("Literature PDF identity does not satisfy the Integration Contract.") from None
|
|
761
|
+
_ = await self._transport.request(_DELETE_PDF, files=multipart_fields(request.model_dump(mode="json")))
|
|
762
|
+
observed = (await self._verified_read(item, "literature PDF deletion")).pdf
|
|
763
|
+
return LiteraturePdfDeleteResult(item=item, observed=observed, absence_confirmed=not observed.is_attached)
|
|
764
|
+
|
|
765
|
+
async def permanently_delete(self, item: PersonalLiteratureItemRef) -> LiteraturePermanentDeleteResult:
|
|
766
|
+
raw_item = cast(object, item)
|
|
767
|
+
if not isinstance(raw_item, PersonalLiteratureItemRef):
|
|
768
|
+
raise MatelabUsageError(
|
|
769
|
+
"Permanent literature deletion only accepts an item observed in the personal library."
|
|
770
|
+
)
|
|
771
|
+
try:
|
|
772
|
+
request = WireDeletePersonalLiteratureItemRequest(id=self._item_id(item))
|
|
773
|
+
except ValidationError:
|
|
774
|
+
raise MatelabUsageError("Personal literature identity does not satisfy the Integration Contract.") from None
|
|
775
|
+
_ = await self._transport.request(
|
|
776
|
+
_DELETE_PERSONAL_ITEM, files=multipart_fields(request.model_dump(mode="json"))
|
|
777
|
+
)
|
|
778
|
+
absent = await self._verified_personal_absence(item)
|
|
779
|
+
return LiteraturePermanentDeleteResult(
|
|
780
|
+
item=item, absence_confirmed=absent, verification="personal_library_snapshot"
|
|
781
|
+
)
|
|
782
|
+
|
|
783
|
+
async def share(
|
|
784
|
+
self,
|
|
785
|
+
items: Sequence[LiteratureItemSummary],
|
|
786
|
+
recipients: Sequence[UserSummary],
|
|
787
|
+
*,
|
|
788
|
+
introduction: str = "",
|
|
789
|
+
copy_owner_comment: bool = False,
|
|
790
|
+
) -> LiteratureShareResult:
|
|
791
|
+
selected = tuple(items)
|
|
792
|
+
targets = tuple(recipients)
|
|
793
|
+
item_ids = tuple(self._item_id(item.ref) for item in selected)
|
|
794
|
+
userids = tuple(target.ref.userid for target in targets)
|
|
795
|
+
identity = self._transport.require_session().identity
|
|
796
|
+
if (
|
|
797
|
+
not selected
|
|
798
|
+
or len(item_ids) != len(set(item_ids))
|
|
799
|
+
or not targets
|
|
800
|
+
or any(type(userid) is not int or userid < 1 for userid in userids)
|
|
801
|
+
or len(userids) != len(set(userids))
|
|
802
|
+
or identity is None
|
|
803
|
+
or identity.userid in userids
|
|
804
|
+
or type(copy_owner_comment) is not bool
|
|
805
|
+
):
|
|
806
|
+
message = (
|
|
807
|
+
"Literature sharing needs distinct list-observed items, distinct search-observed non-self users, "
|
|
808
|
+
+ "and a resolved caller identity."
|
|
809
|
+
)
|
|
810
|
+
raise MatelabUsageError(message)
|
|
811
|
+
if copy_owner_comment:
|
|
812
|
+
for selected_item in selected:
|
|
813
|
+
detail = await self.read(selected_item.ref)
|
|
814
|
+
if sum(comment.owned_by_caller for comment in detail.comments) > 1:
|
|
815
|
+
raise MatelabUsageError(
|
|
816
|
+
"The source has multiple caller-owned comments, so comment copying is ambiguous (PVD-035)."
|
|
817
|
+
)
|
|
818
|
+
try:
|
|
819
|
+
request = WireShareLiteratureItemsRequest.model_validate(
|
|
820
|
+
{
|
|
821
|
+
"ids": json.dumps(item_ids, separators=(",", ":")),
|
|
822
|
+
"users": json.dumps(userids, separators=(",", ":")),
|
|
823
|
+
"comm": int(copy_owner_comment),
|
|
824
|
+
"intro": introduction,
|
|
825
|
+
}
|
|
826
|
+
)
|
|
827
|
+
except (ValidationError, TypeError, ValueError):
|
|
828
|
+
raise MatelabUsageError("Literature share input does not satisfy the Integration Contract.") from None
|
|
829
|
+
share_payload = cast(dict[str, object], request.model_dump(mode="json"))
|
|
830
|
+
_ = await self._transport.request(_SHARE_ITEMS, files=multipart_fields(share_payload))
|
|
831
|
+
return LiteratureShareResult(
|
|
832
|
+
items=tuple(item.ref for item in selected),
|
|
833
|
+
recipients=tuple(target.ref for target in targets),
|
|
834
|
+
copied_owner_comment=copy_owner_comment,
|
|
835
|
+
)
|
|
836
|
+
|
|
837
|
+
async def save_comment(self, item: LiteratureItemRef, draft: LiteratureCommentDraft) -> LiteratureCommentSaveResult:
|
|
838
|
+
before = await self.read(item)
|
|
839
|
+
owned_before = tuple(comment for comment in before.comments if comment.owned_by_caller)
|
|
840
|
+
if len(owned_before) > 1:
|
|
841
|
+
raise MatelabUsageError(
|
|
842
|
+
"The caller already owns multiple comments on this item, so create/edit selection is ambiguous (PVD-035)."
|
|
843
|
+
)
|
|
844
|
+
rendered, binding = self._render_comment(draft)
|
|
845
|
+
if owned_before:
|
|
846
|
+
payload: dict[str, object] = {
|
|
847
|
+
"id": owned_before[0].ref.comment_id,
|
|
848
|
+
"fav_id": 0,
|
|
849
|
+
"comment": rendered,
|
|
850
|
+
"uid": binding.uid,
|
|
851
|
+
}
|
|
852
|
+
action: Literal["created", "updated"] = "updated"
|
|
853
|
+
else:
|
|
854
|
+
payload = {"id": 0, "fav_id": self._item_id(item), "comment": rendered, "uid": binding.uid}
|
|
855
|
+
action = "created"
|
|
856
|
+
try:
|
|
857
|
+
request = WireSaveLiteratureCommentRequest.model_validate(payload)
|
|
858
|
+
except ValidationError:
|
|
859
|
+
raise MatelabUsageError("Literature comment does not satisfy the Integration Contract.") from None
|
|
860
|
+
request_payload = cast(dict[str, object], request.model_dump(mode="json"))
|
|
861
|
+
_ = await self._transport.request(_SAVE_COMMENT, files=multipart_fields(request_payload))
|
|
862
|
+
after = await self._verified_read(item, "literature comment save")
|
|
863
|
+
owned_after = tuple(comment for comment in after.comments if comment.owned_by_caller)
|
|
864
|
+
if len(owned_after) == 1:
|
|
865
|
+
confirmation: Literal[
|
|
866
|
+
"one_owner_comment_observed", "no_owner_comment_observed", "multiple_owner_comments_observed"
|
|
867
|
+
] = "one_owner_comment_observed"
|
|
868
|
+
observed = owned_after[0]
|
|
869
|
+
elif not owned_after:
|
|
870
|
+
confirmation = "no_owner_comment_observed"
|
|
871
|
+
observed = None
|
|
872
|
+
else:
|
|
873
|
+
confirmation = "multiple_owner_comments_observed"
|
|
874
|
+
observed = None
|
|
875
|
+
return LiteratureCommentSaveResult(
|
|
876
|
+
item=item,
|
|
877
|
+
action=action,
|
|
878
|
+
upload_binding=binding,
|
|
879
|
+
owner_comments_after=owned_after,
|
|
880
|
+
observed=observed,
|
|
881
|
+
confirmation=confirmation,
|
|
882
|
+
)
|
|
883
|
+
|
|
884
|
+
async def delete_comment(self, comment: LiteratureComment) -> LiteratureCommentDeleteResult:
|
|
885
|
+
if not comment.owned_by_caller:
|
|
886
|
+
raise MatelabUsageError("Only a caller-owned, detail-observed literature comment can be deleted.")
|
|
887
|
+
try:
|
|
888
|
+
request = WireDeleteLiteratureCommentRequest(id=comment.ref.comment_id)
|
|
889
|
+
except ValidationError:
|
|
890
|
+
raise MatelabUsageError("Literature comment identity does not satisfy the Integration Contract.") from None
|
|
891
|
+
_ = await self._transport.request(_DELETE_COMMENT, files=multipart_fields(request.model_dump(mode="json")))
|
|
892
|
+
after = await self._verified_read(comment.ref.item, "literature comment deletion")
|
|
893
|
+
return LiteratureCommentDeleteResult(
|
|
894
|
+
comment=comment.ref,
|
|
895
|
+
absence_confirmed=all(item.ref.comment_id != comment.ref.comment_id for item in after.comments),
|
|
896
|
+
)
|
|
897
|
+
|
|
898
|
+
async def _verified_read(self, item: LiteratureItemRef, action: str) -> LiteratureDetail:
|
|
899
|
+
try:
|
|
900
|
+
return await self.read(item)
|
|
901
|
+
except MatelabError as exc:
|
|
902
|
+
raise MatelabVerificationError(
|
|
903
|
+
f"Matelab acknowledged the {action}, but readback failed; the mutation may have persisted."
|
|
904
|
+
) from exc
|
|
905
|
+
|
|
906
|
+
async def _verified_personal_absence(self, item: PersonalLiteratureItemRef) -> bool:
|
|
907
|
+
try:
|
|
908
|
+
first = await self.list(PersonalLiteratureLibraryRef(), page=1, page_size=256)
|
|
909
|
+
if any(summary.ref == item for summary in first.items):
|
|
910
|
+
return False
|
|
911
|
+
for page_number in range(2, math.ceil(first.total_count / first.effective_page_size) + 1):
|
|
912
|
+
page = await self.list(
|
|
913
|
+
PersonalLiteratureLibraryRef(), page=page_number, page_size=first.effective_page_size
|
|
914
|
+
)
|
|
915
|
+
if any(summary.ref == item for summary in page.items):
|
|
916
|
+
return False
|
|
917
|
+
return True
|
|
918
|
+
except MatelabError as exc:
|
|
919
|
+
message = (
|
|
920
|
+
"Matelab acknowledged permanent literature deletion, but personal-library readback failed; "
|
|
921
|
+
+ "the deletion may have persisted."
|
|
922
|
+
)
|
|
923
|
+
raise MatelabVerificationError(message) from exc
|
|
924
|
+
|
|
925
|
+
def _save_request(
|
|
926
|
+
self, metadata: LiteratureMetadata, *, item: LiteratureItemRef | None, staged_pdf: StagedFile | None
|
|
927
|
+
) -> WireSaveLiteratureItemRequest:
|
|
928
|
+
wire_metadata = self._wire_metadata(metadata)
|
|
929
|
+
payload = {
|
|
930
|
+
"id": self._item_id(item) if item is not None else 0,
|
|
931
|
+
"store_id": 0,
|
|
932
|
+
"uid": staged_pdf.binding.uid if staged_pdf is not None else "",
|
|
933
|
+
"info": json.dumps(wire_metadata.model_dump(mode="json"), ensure_ascii=False, separators=(",", ":")),
|
|
934
|
+
}
|
|
935
|
+
try:
|
|
936
|
+
return WireSaveLiteratureItemRequest.model_validate(payload)
|
|
937
|
+
except (ValidationError, TypeError, ValueError):
|
|
938
|
+
raise MatelabUsageError("Literature save input does not satisfy the Integration Contract.") from None
|
|
939
|
+
|
|
940
|
+
@staticmethod
|
|
941
|
+
def _wire_metadata(metadata: LiteratureMetadata) -> WireSaveLiteratureContent:
|
|
942
|
+
raw_metadata = cast(object, metadata)
|
|
943
|
+
if (
|
|
944
|
+
not isinstance(raw_metadata, LiteratureMetadata)
|
|
945
|
+
or any(not isinstance(value, str) for value in cast(tuple[object, ...], raw_metadata.keywords or ()))
|
|
946
|
+
or any(not isinstance(value, str) for value in cast(tuple[object, ...], raw_metadata.references or ()))
|
|
947
|
+
or (
|
|
948
|
+
raw_metadata.citation is not None
|
|
949
|
+
and (
|
|
950
|
+
not isinstance(cast(object, raw_metadata.citation), LiteratureCitation)
|
|
951
|
+
or isinstance(raw_metadata.citation.year, bool)
|
|
952
|
+
)
|
|
953
|
+
)
|
|
954
|
+
):
|
|
955
|
+
raise MatelabUsageError("Literature metadata contains invalid canonical fields.")
|
|
956
|
+
try:
|
|
957
|
+
return WireSaveLiteratureContent.model_validate(
|
|
958
|
+
{
|
|
959
|
+
"title": metadata.title,
|
|
960
|
+
"journal": metadata.journal,
|
|
961
|
+
"info": (
|
|
962
|
+
{
|
|
963
|
+
"year": metadata.citation.year,
|
|
964
|
+
"volume": metadata.citation.volume,
|
|
965
|
+
"issue": metadata.citation.issue,
|
|
966
|
+
"page": metadata.citation.page,
|
|
967
|
+
}
|
|
968
|
+
if metadata.citation is not None
|
|
969
|
+
else None
|
|
970
|
+
),
|
|
971
|
+
"author": metadata.author,
|
|
972
|
+
"keywords": list(metadata.keywords) if metadata.keywords is not None else None,
|
|
973
|
+
"abstract": metadata.abstract,
|
|
974
|
+
"doi": metadata.doi,
|
|
975
|
+
"url": metadata.url,
|
|
976
|
+
"reference": list(metadata.references) if metadata.references is not None else None,
|
|
977
|
+
}
|
|
978
|
+
)
|
|
979
|
+
except ValidationError:
|
|
980
|
+
raise MatelabUsageError("Literature metadata does not satisfy the Integration Contract.") from None
|
|
981
|
+
|
|
982
|
+
def _detail(self, item: LiteratureItemRef, response: WireLiteratureDetailResponse) -> LiteratureDetail:
|
|
983
|
+
return LiteratureDetail(
|
|
984
|
+
ref=item,
|
|
985
|
+
metadata=self._stored_metadata(response.content.root),
|
|
986
|
+
schema_fields=tuple(self._field(field) for field in response.template),
|
|
987
|
+
library_keywords=tuple(response.keywords),
|
|
988
|
+
item_keywords=tuple(response.keywords_item),
|
|
989
|
+
star=response.star,
|
|
990
|
+
comments=tuple(
|
|
991
|
+
LiteratureComment(
|
|
992
|
+
ref=LiteratureCommentRef(item=item, comment_id=comment.id),
|
|
993
|
+
content=comment.content,
|
|
994
|
+
author=UserRef(userid=comment.userid),
|
|
995
|
+
author_name=comment.user,
|
|
996
|
+
created_at=comment.dtime,
|
|
997
|
+
owned_by_caller=comment.owner,
|
|
998
|
+
)
|
|
999
|
+
for comment in response.comments
|
|
1000
|
+
),
|
|
1001
|
+
pdf=LiteraturePdf(item=item, sha256=response.hash, filename=response.filename, size=response.size),
|
|
1002
|
+
source=self._source(response.source),
|
|
1003
|
+
permission_mask=response.power,
|
|
1004
|
+
library_name=response.store_name,
|
|
1005
|
+
)
|
|
1006
|
+
|
|
1007
|
+
@staticmethod
|
|
1008
|
+
def _stored_metadata(
|
|
1009
|
+
content: WireLiteratureContentObject | WireInvalidLiteratureContent,
|
|
1010
|
+
) -> LiteratureStoredMetadata:
|
|
1011
|
+
if isinstance(content, WireInvalidLiteratureContent):
|
|
1012
|
+
return LiteratureStoredMetadata(
|
|
1013
|
+
valid_object=False,
|
|
1014
|
+
title=None,
|
|
1015
|
+
journal=None,
|
|
1016
|
+
citation=None,
|
|
1017
|
+
author=None,
|
|
1018
|
+
authors=None,
|
|
1019
|
+
keywords=None,
|
|
1020
|
+
abstract=None,
|
|
1021
|
+
reference_information=None,
|
|
1022
|
+
doi=None,
|
|
1023
|
+
url=None,
|
|
1024
|
+
references=None,
|
|
1025
|
+
)
|
|
1026
|
+
extension_fields = cast(dict[str, object], dict(content.model_extra or {}))
|
|
1027
|
+
dropped = set(extension_fields)
|
|
1028
|
+
if content.authors is not None:
|
|
1029
|
+
dropped.add("authors")
|
|
1030
|
+
if content.reference_information is not None:
|
|
1031
|
+
dropped.add("reference_information")
|
|
1032
|
+
return LiteratureStoredMetadata(
|
|
1033
|
+
valid_object=True,
|
|
1034
|
+
title=content.title,
|
|
1035
|
+
journal=content.journal,
|
|
1036
|
+
citation=(
|
|
1037
|
+
LiteratureCitation(
|
|
1038
|
+
year=content.info.year, volume=content.info.volume, issue=content.info.issue, page=content.info.page
|
|
1039
|
+
)
|
|
1040
|
+
if content.info is not None
|
|
1041
|
+
else None
|
|
1042
|
+
),
|
|
1043
|
+
author=content.author,
|
|
1044
|
+
authors=tuple(content.authors) if content.authors is not None else None,
|
|
1045
|
+
keywords=tuple(content.keywords) if content.keywords is not None else None,
|
|
1046
|
+
abstract=content.abstract,
|
|
1047
|
+
reference_information=content.reference_information,
|
|
1048
|
+
doi=content.doi,
|
|
1049
|
+
url=content.url,
|
|
1050
|
+
references=tuple(content.reference) if content.reference is not None else None,
|
|
1051
|
+
extension_fields=MappingProxyType(extension_fields),
|
|
1052
|
+
fields_dropped_by_canonical_update=tuple(sorted(dropped)),
|
|
1053
|
+
)
|
|
1054
|
+
|
|
1055
|
+
@staticmethod
|
|
1056
|
+
def _metadata_matches(observed: LiteratureStoredMetadata, requested: LiteratureMetadata) -> bool:
|
|
1057
|
+
return (
|
|
1058
|
+
observed.valid_object
|
|
1059
|
+
and observed.title == requested.title
|
|
1060
|
+
and observed.journal == requested.journal
|
|
1061
|
+
and observed.citation == requested.citation
|
|
1062
|
+
and observed.author == requested.author
|
|
1063
|
+
and (observed.keywords or ()) == (requested.keywords or ())
|
|
1064
|
+
and (observed.abstract or "") == (requested.abstract or "")
|
|
1065
|
+
and observed.doi == requested.doi
|
|
1066
|
+
and observed.url == requested.url
|
|
1067
|
+
and (observed.references or ()) == (requested.references or ())
|
|
1068
|
+
)
|
|
1069
|
+
|
|
1070
|
+
def _render_comment(self, draft: LiteratureCommentDraft) -> tuple[str, UploadBindingRef]:
|
|
1071
|
+
raw_draft = cast(object, draft)
|
|
1072
|
+
if not isinstance(raw_draft, LiteratureCommentDraft) or not isinstance(cast(object, raw_draft.html), str):
|
|
1073
|
+
raise MatelabUsageError("Literature comment draft is invalid.")
|
|
1074
|
+
if raw_draft.attachment is None:
|
|
1075
|
+
if _COMMENT_ATTACHMENT_MARKER in raw_draft.html or "url_eln/temp" in raw_draft.html:
|
|
1076
|
+
raise MatelabUsageError("Temporary literature-comment URLs require a completed staged-file object.")
|
|
1077
|
+
return raw_draft.html, UploadBindingRef.new()
|
|
1078
|
+
self._validate_staged_file(raw_draft.attachment)
|
|
1079
|
+
if raw_draft.html.count(_COMMENT_ATTACHMENT_MARKER) != 1 or "url_eln/temp" in raw_draft.html:
|
|
1080
|
+
raise MatelabUsageError(
|
|
1081
|
+
"A literature comment attachment requires exactly one matelab-staged-file marker and no raw temp URL."
|
|
1082
|
+
)
|
|
1083
|
+
return (
|
|
1084
|
+
raw_draft.html.replace(_COMMENT_ATTACHMENT_MARKER, raw_draft.attachment.temporary_url),
|
|
1085
|
+
raw_draft.attachment.binding,
|
|
1086
|
+
)
|
|
1087
|
+
|
|
1088
|
+
@staticmethod
|
|
1089
|
+
def _validate_staged_file(staged: StagedFile) -> None:
|
|
1090
|
+
raw_staged = cast(object, staged)
|
|
1091
|
+
if (
|
|
1092
|
+
not isinstance(raw_staged, StagedFile)
|
|
1093
|
+
or raw_staged.temporary_file_id < 1
|
|
1094
|
+
or not raw_staged.binding.uid
|
|
1095
|
+
or len(raw_staged.binding.uid) > 45
|
|
1096
|
+
or not raw_staged.session.upload_session_id
|
|
1097
|
+
or len(raw_staged.session.upload_session_id) > 45
|
|
1098
|
+
or raw_staged.session.next_offset != raw_staged.size
|
|
1099
|
+
or not raw_staged.filename
|
|
1100
|
+
or "\r" in raw_staged.filename
|
|
1101
|
+
or "\n" in raw_staged.filename
|
|
1102
|
+
or raw_staged.size < 0
|
|
1103
|
+
or _SHA256_PATTERN.fullmatch(raw_staged.sha256) is None
|
|
1104
|
+
or not raw_staged.temporary_url
|
|
1105
|
+
or raw_staged.completion != "provider_acknowledged_complete"
|
|
1106
|
+
or raw_staged.binding_state != "staged_not_bound"
|
|
1107
|
+
):
|
|
1108
|
+
raise MatelabUsageError("Completed staged-file metadata is invalid.")
|
|
1109
|
+
|
|
1110
|
+
@staticmethod
|
|
1111
|
+
def _library(
|
|
1112
|
+
item: WireLiteratureLibrary, *, kind: Literal["personal", "shared", "dangling_membership"]
|
|
1113
|
+
) -> LiteratureLibrarySummary:
|
|
1114
|
+
if kind == "personal":
|
|
1115
|
+
ref: LiteratureLibraryRef | None = PersonalLiteratureLibraryRef()
|
|
1116
|
+
elif kind == "shared" and item.id is not None:
|
|
1117
|
+
ref = SharedLiteratureLibraryRef(literature_library_id=item.id)
|
|
1118
|
+
else:
|
|
1119
|
+
ref = None
|
|
1120
|
+
return LiteratureLibrarySummary(
|
|
1121
|
+
ref=ref,
|
|
1122
|
+
kind=kind,
|
|
1123
|
+
name=item.showtext,
|
|
1124
|
+
permission_mask=item.power,
|
|
1125
|
+
item_count=item.num,
|
|
1126
|
+
keywords=tuple(item.keywords),
|
|
1127
|
+
introduction=item.intro,
|
|
1128
|
+
)
|
|
1129
|
+
|
|
1130
|
+
@staticmethod
|
|
1131
|
+
def _summary(item: WireLiteratureListItem, library: LiteratureLibraryRef) -> LiteratureItemSummary:
|
|
1132
|
+
if isinstance(library, PersonalLiteratureLibraryRef):
|
|
1133
|
+
ref: LiteratureItemRef = PersonalLiteratureItemRef(literature_item_id=item.id)
|
|
1134
|
+
else:
|
|
1135
|
+
ref = SharedLiteratureItemRef(literature_item_id=item.id, library=library)
|
|
1136
|
+
pdf = LiteraturePdf(item=ref, sha256=item.hash or None, filename=item.filename or None, size=None)
|
|
1137
|
+
return LiteratureItemSummary(
|
|
1138
|
+
ref=ref,
|
|
1139
|
+
source=Literature._source(item.source),
|
|
1140
|
+
star=item.star,
|
|
1141
|
+
title=item.title,
|
|
1142
|
+
author=item.author,
|
|
1143
|
+
citation_html=item.info,
|
|
1144
|
+
abstract=item.abstract,
|
|
1145
|
+
journal=item.journal,
|
|
1146
|
+
keywords=tuple(item.keywords),
|
|
1147
|
+
publication_date=item.dtime,
|
|
1148
|
+
matching_author_fragments=tuple(item.authors_bold),
|
|
1149
|
+
pdf=pdf,
|
|
1150
|
+
)
|
|
1151
|
+
|
|
1152
|
+
@staticmethod
|
|
1153
|
+
def _field(item: WireLiteratureFieldDefinition) -> LiteratureField:
|
|
1154
|
+
label: str | tuple[str, str]
|
|
1155
|
+
if isinstance(item.name, str):
|
|
1156
|
+
label = item.name
|
|
1157
|
+
else:
|
|
1158
|
+
label = cast(tuple[str, str], tuple(item.name.root))
|
|
1159
|
+
if isinstance(item.comm, str) or item.comm is None:
|
|
1160
|
+
help_text: str | tuple[str, str] | None = item.comm
|
|
1161
|
+
else:
|
|
1162
|
+
help_text = cast(tuple[str, str], tuple(item.comm.root))
|
|
1163
|
+
nested = tuple(
|
|
1164
|
+
LiteratureNestedField(
|
|
1165
|
+
field_id=child.id,
|
|
1166
|
+
field_type=child.type.value,
|
|
1167
|
+
label=(child.name if isinstance(child.name, str) else cast(tuple[str, str], tuple(child.name.root))),
|
|
1168
|
+
width=child.width,
|
|
1169
|
+
attributes=MappingProxyType(cast(dict[str, object], dict(child.model_extra or {}))),
|
|
1170
|
+
)
|
|
1171
|
+
for child in item.template or ()
|
|
1172
|
+
)
|
|
1173
|
+
return LiteratureField(
|
|
1174
|
+
field_id=item.id,
|
|
1175
|
+
field_type=item.type.value,
|
|
1176
|
+
label=label,
|
|
1177
|
+
required=item.required,
|
|
1178
|
+
group=item.group,
|
|
1179
|
+
help_text=help_text,
|
|
1180
|
+
print_template=item.print,
|
|
1181
|
+
nested=nested,
|
|
1182
|
+
visible=item.show,
|
|
1183
|
+
width=item.width,
|
|
1184
|
+
condition=(
|
|
1185
|
+
MappingProxyType(cast(dict[str, object], item.condition.model_dump(mode="json")))
|
|
1186
|
+
if item.condition is not None
|
|
1187
|
+
else None
|
|
1188
|
+
),
|
|
1189
|
+
attributes=MappingProxyType(cast(dict[str, object], dict(item.model_extra or {}))),
|
|
1190
|
+
)
|
|
1191
|
+
|
|
1192
|
+
@staticmethod
|
|
1193
|
+
def _extracted_author(item: WireExtractedLiteratureAuthor) -> ExtractedLiteratureAuthor:
|
|
1194
|
+
return ExtractedLiteratureAuthor(
|
|
1195
|
+
name=item.name,
|
|
1196
|
+
affiliations=tuple(item.affiliation) if item.affiliation is not None else None,
|
|
1197
|
+
email=item.email,
|
|
1198
|
+
corresponding=item.is_corresponding,
|
|
1199
|
+
orcid=item.orcid,
|
|
1200
|
+
extension_fields=MappingProxyType(cast(dict[str, object], dict(item.model_extra or {}))),
|
|
1201
|
+
)
|
|
1202
|
+
|
|
1203
|
+
@staticmethod
|
|
1204
|
+
def _source(value: object) -> LiteratureSource:
|
|
1205
|
+
source = int(cast(int, value))
|
|
1206
|
+
mapping = {1: LiteratureSource.SUBSCRIBED, 2: LiteratureSource.USER_CREATED, 3: LiteratureSource.SHARED_COPY}
|
|
1207
|
+
try:
|
|
1208
|
+
return mapping[source]
|
|
1209
|
+
except KeyError:
|
|
1210
|
+
raise MatelabProtocolError("Matelab returned an unknown literature source.") from None
|
|
1211
|
+
|
|
1212
|
+
@staticmethod
|
|
1213
|
+
def _source_wire_value(value: LiteratureSource) -> int:
|
|
1214
|
+
try:
|
|
1215
|
+
source = LiteratureSource(value)
|
|
1216
|
+
except (TypeError, ValueError):
|
|
1217
|
+
raise MatelabUsageError("Literature source filter is invalid.") from None
|
|
1218
|
+
return {LiteratureSource.SUBSCRIBED: 1, LiteratureSource.USER_CREATED: 2, LiteratureSource.SHARED_COPY: 3}[
|
|
1219
|
+
source
|
|
1220
|
+
]
|
|
1221
|
+
|
|
1222
|
+
@staticmethod
|
|
1223
|
+
def _library_id(library: LiteratureLibraryRef) -> int:
|
|
1224
|
+
raw_library = cast(object, library)
|
|
1225
|
+
if isinstance(raw_library, PersonalLiteratureLibraryRef):
|
|
1226
|
+
return 0
|
|
1227
|
+
if (
|
|
1228
|
+
isinstance(raw_library, SharedLiteratureLibraryRef)
|
|
1229
|
+
and type(raw_library.literature_library_id) is int
|
|
1230
|
+
and raw_library.literature_library_id >= 1
|
|
1231
|
+
):
|
|
1232
|
+
return raw_library.literature_library_id
|
|
1233
|
+
raise MatelabUsageError("Literature library identity is invalid.")
|
|
1234
|
+
|
|
1235
|
+
@staticmethod
|
|
1236
|
+
def _item_id(item: LiteratureItemRef | None) -> int:
|
|
1237
|
+
if isinstance(item, (PersonalLiteratureItemRef, SharedLiteratureItemRef)):
|
|
1238
|
+
value = item.literature_item_id
|
|
1239
|
+
if type(value) is int and value >= 1:
|
|
1240
|
+
if isinstance(item, SharedLiteratureItemRef):
|
|
1241
|
+
_ = Literature._library_id(item.library)
|
|
1242
|
+
return value
|
|
1243
|
+
raise MatelabUsageError("Literature item identity is invalid.")
|