commonmeta-py 0.118__py3-none-any.whl → 0.120__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.
- commonmeta/__init__.py +1 -1
- commonmeta/doi_utils.py +0 -1
- commonmeta/file_utils.py +0 -10
- commonmeta/metadata.py +1 -1
- commonmeta/writers/crossref_xml_writer.py +54 -38
- commonmeta/writers/inveniordm_writer.py +10 -10
- {commonmeta_py-0.118.dist-info → commonmeta_py-0.120.dist-info}/METADATA +1 -2
- {commonmeta_py-0.118.dist-info → commonmeta_py-0.120.dist-info}/RECORD +11 -11
- {commonmeta_py-0.118.dist-info → commonmeta_py-0.120.dist-info}/WHEEL +0 -0
- {commonmeta_py-0.118.dist-info → commonmeta_py-0.120.dist-info}/entry_points.txt +0 -0
- {commonmeta_py-0.118.dist-info → commonmeta_py-0.120.dist-info}/licenses/LICENSE +0 -0
commonmeta/__init__.py
CHANGED
commonmeta/doi_utils.py
CHANGED
@@ -342,7 +342,6 @@ def is_rogue_scholar_doi(doi: str, ra: str = "crossref") -> bool:
|
|
342
342
|
|
343
343
|
def generate_wordpress_doi(prefix: str, slug: str, guid: str) -> str:
|
344
344
|
"""Generate a DOI from a WordPress GUID and slug"""
|
345
|
-
import re
|
346
345
|
|
347
346
|
if not prefix or not guid:
|
348
347
|
return ""
|
commonmeta/file_utils.py
CHANGED
@@ -44,16 +44,6 @@ def download_file(url: str) -> bytes:
|
|
44
44
|
resp = requests.get(url, stream=True)
|
45
45
|
resp.raise_for_status()
|
46
46
|
return resp.content
|
47
|
-
# # Progress bar
|
48
|
-
# total = int(resp.headers.get("content-length", 0))
|
49
|
-
|
50
|
-
# buf = io.BytesIO()
|
51
|
-
# with tqdm(total=total, unit="B", unit_scale=True, desc="downloading") as bar:
|
52
|
-
# for chunk in resp.iter_content(chunk_size=8192):
|
53
|
-
# if chunk:
|
54
|
-
# buf.write(chunk)
|
55
|
-
# bar.update(len(chunk))
|
56
|
-
# return buf.getvalue()
|
57
47
|
|
58
48
|
|
59
49
|
def write_file(filename: str, output: bytes) -> None:
|
commonmeta/metadata.py
CHANGED
@@ -484,7 +484,7 @@ class MetadataList:
|
|
484
484
|
|
485
485
|
if to == "crossref_xml":
|
486
486
|
response = push_crossref_xml_list(
|
487
|
-
self, login_id=self.login_id, login_passwd=self.login_passwd
|
487
|
+
self, login_id=self.login_id, login_passwd=self.login_passwd, legacy_key=self.legacy_key
|
488
488
|
)
|
489
489
|
return response
|
490
490
|
elif to == "datacite":
|
@@ -1,6 +1,7 @@
|
|
1
1
|
"""Crossref XML writer for commonmeta-py"""
|
2
2
|
|
3
3
|
import io
|
4
|
+
import logging
|
4
5
|
from datetime import datetime
|
5
6
|
from time import time
|
6
7
|
from typing import Optional
|
@@ -15,8 +16,11 @@ from requests_toolbelt.multipart.encoder import MultipartEncoder
|
|
15
16
|
|
16
17
|
from ..base_utils import compact, parse_xml, unparse_xml, unparse_xml_list, wrap
|
17
18
|
from ..constants import Commonmeta
|
18
|
-
from ..doi_utils import doi_from_url, validate_doi
|
19
|
+
from ..doi_utils import doi_from_url, is_rogue_scholar_doi, validate_doi
|
19
20
|
from ..utils import validate_url
|
21
|
+
from .inveniordm_writer import update_legacy_record
|
22
|
+
|
23
|
+
logger = logging.getLogger(__name__)
|
20
24
|
|
21
25
|
POSTED_CONTENT_TYPES = [
|
22
26
|
"preprint",
|
@@ -342,20 +346,25 @@ def write_crossref_xml(metadata: Commonmeta) -> Optional[str]:
|
|
342
346
|
|
343
347
|
data = convert_crossref_xml(metadata)
|
344
348
|
if data is None:
|
349
|
+
logger.error(f"Could not convert metadata to Crossref XML: {metadata.id}")
|
345
350
|
return None
|
351
|
+
|
352
|
+
# Use the marshmallow schema to dump the data
|
346
353
|
schema = CrossrefXMLSchema()
|
347
354
|
crossref_xml = schema.dump(data)
|
348
355
|
|
349
|
-
# Ensure
|
356
|
+
# Ensure consistent field ordering through the defined mapping
|
350
357
|
field_order = [MARSHMALLOW_MAP.get(k, k) for k in list(data.keys())]
|
351
358
|
crossref_xml = {k: crossref_xml[k] for k in field_order if k in crossref_xml}
|
359
|
+
|
352
360
|
# Convert to XML
|
353
361
|
return unparse_xml(crossref_xml, dialect="crossref")
|
354
362
|
|
355
363
|
|
356
|
-
def write_crossref_xml_list(metalist):
|
364
|
+
def write_crossref_xml_list(metalist) -> Optional[str]:
|
357
365
|
"""Write crossref_xml list"""
|
358
366
|
if metalist is None or not metalist.is_valid:
|
367
|
+
logger.error("Invalid metalist provided for Crossref XML generation")
|
359
368
|
return None
|
360
369
|
|
361
370
|
schema = CrossrefXMLSchema()
|
@@ -376,19 +385,21 @@ def write_crossref_xml_list(metalist):
|
|
376
385
|
return unparse_xml_list(crossref_xml_list, dialect="crossref", head=head)
|
377
386
|
|
378
387
|
|
379
|
-
def push_crossref_xml_list(metalist, login_id: str, login_passwd: str) -> bytes:
|
388
|
+
def push_crossref_xml_list(metalist, login_id: str, login_passwd: str, legacy_key:str=None) -> bytes:
|
380
389
|
"""Push crossref_xml list to Crossref API, returns the API response."""
|
381
390
|
|
382
391
|
input = write_crossref_xml_list(metalist)
|
392
|
+
if not input:
|
393
|
+
logger.error("Failed to generate XML for upload")
|
394
|
+
return "{}"
|
383
395
|
|
384
396
|
# Convert string to bytes if necessary
|
385
397
|
if isinstance(input, str):
|
386
398
|
input = input.encode("utf-8")
|
387
399
|
|
388
|
-
# The filename displayed in the Crossref admin interface
|
400
|
+
# The filename displayed in the Crossref admin interface
|
389
401
|
filename = f"{int(time())}"
|
390
402
|
|
391
|
-
# Create multipart form data
|
392
403
|
multipart_data = MultipartEncoder(
|
393
404
|
fields={
|
394
405
|
"fname": (filename, io.BytesIO(input), "application/xml"),
|
@@ -398,38 +409,42 @@ def push_crossref_xml_list(metalist, login_id: str, login_passwd: str) -> bytes:
|
|
398
409
|
}
|
399
410
|
)
|
400
411
|
|
401
|
-
# Set up the request
|
402
412
|
post_url = "https://doi.crossref.org/servlet/deposit"
|
403
413
|
headers = {"Content-Type": multipart_data.content_type}
|
414
|
+
resp = requests.post(post_url, data=multipart_data, headers=headers, timeout=10)
|
415
|
+
resp.raise_for_status()
|
416
|
+
|
417
|
+
# Parse the response
|
418
|
+
response = parse_xml(resp.content)
|
419
|
+
status = py_.get(response, "html.body.h2")
|
420
|
+
if status != "SUCCESS":
|
421
|
+
# Handle error response
|
422
|
+
message = py_.get(response, "html.body.p")
|
423
|
+
logger.error(f"Crossref API error: {message}")
|
424
|
+
return "{}"
|
425
|
+
|
426
|
+
items = []
|
427
|
+
for item in metalist.items:
|
428
|
+
record = {
|
429
|
+
"doi": item.id,
|
430
|
+
"updated": datetime.now().isoformat("T", "seconds"),
|
431
|
+
"status": "submitted",
|
432
|
+
}
|
433
|
+
|
434
|
+
# update rogue-scholar legacy record if legacy_key is provided
|
435
|
+
if is_rogue_scholar_doi(item.id, ra="crossref") and legacy_key is not None:
|
436
|
+
record = update_legacy_record(record, legacy_key=legacy_key, field="doi")
|
437
|
+
items.append(record)
|
438
|
+
|
439
|
+
# Return JSON response
|
440
|
+
return json.dumps(items, option=json.OPT_INDENT_2)
|
441
|
+
|
442
|
+
|
443
|
+
|
404
444
|
|
405
|
-
try:
|
406
|
-
# Send the request
|
407
|
-
resp = requests.post(post_url, data=multipart_data, headers=headers, timeout=10)
|
408
|
-
resp.raise_for_status()
|
409
|
-
|
410
|
-
# Parse the response
|
411
|
-
response = parse_xml(resp.content)
|
412
|
-
status = py_.get(response, "html.body.h2")
|
413
|
-
if status == "SUCCESS":
|
414
|
-
items = []
|
415
|
-
for item in metalist.items:
|
416
|
-
items.append(
|
417
|
-
{
|
418
|
-
"doi": item.id,
|
419
|
-
"updated": datetime.now().isoformat("T", "seconds"),
|
420
|
-
"status": "submitted",
|
421
|
-
}
|
422
|
-
)
|
423
445
|
|
424
|
-
# orjson has different options
|
425
|
-
return json.dumps(items, option=json.OPT_INDENT_2)
|
426
446
|
|
427
|
-
# if there is an error
|
428
|
-
message = py_.get(response, "html.body.p")
|
429
|
-
raise CrossrefError(f"Error uploading batch: {message}")
|
430
447
|
|
431
|
-
except requests.exceptions.RequestException as e:
|
432
|
-
raise CrossrefError(f"Error uploading batch: {str(e)}") from e
|
433
448
|
|
434
449
|
|
435
450
|
def get_attributes(obj, **kwargs) -> dict:
|
@@ -539,15 +554,16 @@ def get_institution(obj) -> Optional[dict]:
|
|
539
554
|
def get_titles(obj) -> Optional[dict]:
|
540
555
|
"""get titles"""
|
541
556
|
|
542
|
-
|
557
|
+
titles = {}
|
543
558
|
for t in wrap(py_.get(obj, "titles", [])):
|
544
559
|
if isinstance(t, str):
|
545
|
-
|
560
|
+
titles["title"] = t
|
546
561
|
elif isinstance(t, dict) and t.get("titleType", None) == "Subtitle":
|
547
|
-
|
548
|
-
elif isinstance(
|
549
|
-
|
550
|
-
|
562
|
+
titles["subtitle"] = t.get("title", None)
|
563
|
+
elif isinstance(t, dict):
|
564
|
+
titles["title"] = t.get("title", None)
|
565
|
+
|
566
|
+
return titles if titles else None
|
551
567
|
|
552
568
|
|
553
569
|
def get_contributors(obj) -> Optional[dict]:
|
@@ -3,7 +3,7 @@
|
|
3
3
|
import logging
|
4
4
|
import re
|
5
5
|
from time import time
|
6
|
-
from typing import
|
6
|
+
from typing import Dict, Optional
|
7
7
|
from urllib.parse import urlparse
|
8
8
|
|
9
9
|
import orjson as json
|
@@ -392,11 +392,11 @@ def push_inveniordm(
|
|
392
392
|
host: str,
|
393
393
|
token: str,
|
394
394
|
legacy_key: str
|
395
|
-
) ->
|
395
|
+
) -> Dict:
|
396
396
|
"""Push record to InvenioRDM"""
|
397
397
|
|
398
398
|
record = {}
|
399
|
-
|
399
|
+
output = write_inveniordm(metadata)
|
400
400
|
|
401
401
|
try:
|
402
402
|
# Remove IsPartOf relation with InvenioRDM community identifier after storing it
|
@@ -439,10 +439,10 @@ def push_inveniordm(
|
|
439
439
|
record = edit_published_record(record, host, token)
|
440
440
|
|
441
441
|
# Update draft record
|
442
|
-
record = update_draft_record(record, host, token,
|
442
|
+
record = update_draft_record(record, host, token, output)
|
443
443
|
else:
|
444
444
|
# Create draft record
|
445
|
-
record = create_draft_record(record, host, token,
|
445
|
+
record = create_draft_record(record, host, token, output)
|
446
446
|
|
447
447
|
# Publish draft record
|
448
448
|
record = publish_draft_record(record, host, token)
|
@@ -484,7 +484,7 @@ def push_inveniordm(
|
|
484
484
|
|
485
485
|
# optionally update rogue-scholar legacy record
|
486
486
|
if host == "rogue-scholar.org" and legacy_key is not None:
|
487
|
-
record = update_legacy_record(record, legacy_key)
|
487
|
+
record = update_legacy_record(record, legacy_key=legacy_key, field="rid")
|
488
488
|
except Exception as e:
|
489
489
|
logger.error(f"Unexpected error in push_inveniordm: {str(e)}", exc_info=True, extra={
|
490
490
|
"host": host,
|
@@ -525,7 +525,7 @@ def search_by_doi(doi, host, token) -> Optional[str]:
|
|
525
525
|
return None
|
526
526
|
|
527
527
|
|
528
|
-
def create_draft_record(record, host, token,
|
528
|
+
def create_draft_record(record, host, token, output):
|
529
529
|
"""Create a new draft record in InvenioRDM"""
|
530
530
|
headers = {
|
531
531
|
"Authorization": f"Bearer {token}",
|
@@ -533,7 +533,7 @@ def create_draft_record(record, host, token, input):
|
|
533
533
|
}
|
534
534
|
try:
|
535
535
|
response = requests.post(
|
536
|
-
f"https://{host}/api/records", headers=headers, json=
|
536
|
+
f"https://{host}/api/records", headers=headers, json=output
|
537
537
|
)
|
538
538
|
if response.status_code == 429:
|
539
539
|
record["status"] = "failed_rate_limited"
|
@@ -651,7 +651,7 @@ def add_record_to_community(record, host, token, community_id):
|
|
651
651
|
return record
|
652
652
|
|
653
653
|
|
654
|
-
def update_legacy_record(record, legacy_key: str):
|
654
|
+
def update_legacy_record(record, legacy_key: str, field:str=None) -> dict:
|
655
655
|
"""Update corresponding record in Rogue Scholar legacy database."""
|
656
656
|
|
657
657
|
legacy_host = "bosczcmeodcrajtcaddf.supabase.co"
|
@@ -662,7 +662,7 @@ def update_legacy_record(record, legacy_key: str):
|
|
662
662
|
raise ValueError("no UUID provided")
|
663
663
|
|
664
664
|
now = f"{int(time())}"
|
665
|
-
if record.get("id", None) is not None:
|
665
|
+
if field == "rid" and record.get("id", None) is not None:
|
666
666
|
output = {
|
667
667
|
"rid": record.get("id"),
|
668
668
|
"indexed_at": now,
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: commonmeta-py
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.120
|
4
4
|
Summary: Library for conversions to/from the Commonmeta scholarly metadata format
|
5
5
|
Project-URL: Homepage, https://python.commonmeta.org
|
6
6
|
Project-URL: Repository, https://github.com/front-matter/commonmeta-py
|
@@ -36,7 +36,6 @@ Requires-Dist: pyyaml>=5.4
|
|
36
36
|
Requires-Dist: requests-toolbelt>=1.0.0
|
37
37
|
Requires-Dist: requests>=2.31.0
|
38
38
|
Requires-Dist: requests>=2.32.3
|
39
|
-
Requires-Dist: setuptools>=70.3.0
|
40
39
|
Requires-Dist: simplejson~=3.18
|
41
40
|
Requires-Dist: types-beautifulsoup4<5,>=4.11
|
42
41
|
Requires-Dist: types-dateparser~=1.1
|
@@ -1,13 +1,13 @@
|
|
1
|
-
commonmeta/__init__.py,sha256=
|
1
|
+
commonmeta/__init__.py,sha256=rWmoYjzULmmEdcjXnYZxNQk0qJ2tGwxTycmYxXoH65A,2098
|
2
2
|
commonmeta/api_utils.py,sha256=y5KLfIOWOjde7LXZ36u-eneQJ-Q53yXUZg3hWpCBS2E,2685
|
3
3
|
commonmeta/author_utils.py,sha256=3lYW5s1rOUWNTKs1FP6XLfEUY3yCLOe_3L_VdJTDMp0,8585
|
4
4
|
commonmeta/base_utils.py,sha256=-MGy9q2uTiJEkPWQUYOJMdq-3tRpNnvBwlLjvllQ5g8,11164
|
5
5
|
commonmeta/cli.py,sha256=pdBpBosLNq3RS9buO-Voqawc9Ay1eSt-xP5O97iOft4,8480
|
6
6
|
commonmeta/constants.py,sha256=wSTEUiHeRdXLwjXEQD9AU2hxFyEKi5OTX2iHOKO6nF0,19844
|
7
7
|
commonmeta/date_utils.py,sha256=H2cCobX0JREIUOT_cCigGd3MG7prGiQpXk1m4ZNrFwU,6318
|
8
|
-
commonmeta/doi_utils.py,sha256=
|
9
|
-
commonmeta/file_utils.py,sha256=
|
10
|
-
commonmeta/metadata.py,sha256=
|
8
|
+
commonmeta/doi_utils.py,sha256=cOogLatKg6qea2jgMd3yLALSTfaTNUgr-IkBXIK4xZw,11498
|
9
|
+
commonmeta/file_utils.py,sha256=eFYDWyR8Gr722nvFmp542hCm-TGmO_q4ciZ85IPHpjA,2893
|
10
|
+
commonmeta/metadata.py,sha256=m9UtE95t9awrlo9w9qZtwF7Y9sRfABpA5JKUZdyz5b4,18921
|
11
11
|
commonmeta/schema_utils.py,sha256=WGpmMj9cfNMg_55hhgwY9qpO0A1HSvTLQC2equjBftI,1770
|
12
12
|
commonmeta/translators.py,sha256=CBMK4jrXRmGZiAhCh6wsJjhbDJWbcsda8UvXFXxccAw,1363
|
13
13
|
commonmeta/utils.py,sha256=pJnh3EzOU1E2nutnAZsopY_NsUX6zYmxoj5bIYqqWvE,50574
|
@@ -77,14 +77,14 @@ commonmeta/writers/__init__.py,sha256=47-snms6xBHkoEXKYV1DBtH1npAtlVtvY29Z4Zr45q
|
|
77
77
|
commonmeta/writers/bibtex_writer.py,sha256=doAdyl1NEp60mPkHPo3GMH8B-HA6MzLAdlyNsIecTzU,4972
|
78
78
|
commonmeta/writers/citation_writer.py,sha256=qs_4X3BjrSqHexmJFPvPDTp0mRIqzb0F70_Wuc7S9x0,2343
|
79
79
|
commonmeta/writers/commonmeta_writer.py,sha256=QpfyhG__7o_XpsOTCPWxGymO7YKwZi2LQh8Zic44bdc,1365
|
80
|
-
commonmeta/writers/crossref_xml_writer.py,sha256=
|
80
|
+
commonmeta/writers/crossref_xml_writer.py,sha256=Z5vhtWH-uz5WcBz1W_KOxyrvB5OQ2nTixV6EOhjOBnc,33859
|
81
81
|
commonmeta/writers/csl_writer.py,sha256=4gDYs1EzK4_L2UIRTfs25wgHmYRwdRP2zmfxF9387oU,2779
|
82
82
|
commonmeta/writers/datacite_writer.py,sha256=bcinpwhq7XnVthKHH8-sdXA34dSlvFH4ImYH768iaQU,6428
|
83
|
-
commonmeta/writers/inveniordm_writer.py,sha256=
|
83
|
+
commonmeta/writers/inveniordm_writer.py,sha256=eLllddYulFqg8qfRIScpYpnAJ82NCHXotCFJb09mHX8,25739
|
84
84
|
commonmeta/writers/ris_writer.py,sha256=3SdyEvMRaPRP1SV1MB-MXBlunE7x6og7RF1zuWtetPc,2094
|
85
85
|
commonmeta/writers/schema_org_writer.py,sha256=s18_x0ReXwAGBoEAwp2q-HCgFQ-h5qRg6JyAlqCoSFE,5871
|
86
|
-
commonmeta_py-0.
|
87
|
-
commonmeta_py-0.
|
88
|
-
commonmeta_py-0.
|
89
|
-
commonmeta_py-0.
|
90
|
-
commonmeta_py-0.
|
86
|
+
commonmeta_py-0.120.dist-info/METADATA,sha256=BxKd_hZcarnoQQUklRQ0leX_vAUu3hxYP1KGSTpH89E,7618
|
87
|
+
commonmeta_py-0.120.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
88
|
+
commonmeta_py-0.120.dist-info/entry_points.txt,sha256=U4w4BoRuS3rN5t5Y-uYSyOeU5Lh_VRVMS9OIDzIgw4w,50
|
89
|
+
commonmeta_py-0.120.dist-info/licenses/LICENSE,sha256=wsIvxF9Q9GC9vA_s79zTWP3BkXJdfUNRmALlU8GbW1s,1074
|
90
|
+
commonmeta_py-0.120.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|