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 CHANGED
@@ -10,7 +10,7 @@ commonmeta-py is a Python library to convert scholarly metadata
10
10
  """
11
11
 
12
12
  __title__ = "commonmeta-py"
13
- __version__ = "0.118"
13
+ __version__ = "0.120"
14
14
  __author__ = "Martin Fenner"
15
15
  __license__ = "MIT"
16
16
 
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 the order of fields in the XML matches the expected order
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, using the current UNIX timestamp
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
- title = {}
557
+ titles = {}
543
558
  for t in wrap(py_.get(obj, "titles", [])):
544
559
  if isinstance(t, str):
545
- title["title"] = t
560
+ titles["title"] = t
546
561
  elif isinstance(t, dict) and t.get("titleType", None) == "Subtitle":
547
- title["subtitle"] = t.get("title", None)
548
- elif isinstance(title, dict):
549
- title["title"] = t.get("title", None)
550
- return title
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 Any, Dict, Optional, Tuple
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
- ) -> Tuple[Dict[str, Any], Optional[Exception]]:
395
+ ) -> Dict:
396
396
  """Push record to InvenioRDM"""
397
397
 
398
398
  record = {}
399
- input = write_inveniordm(metadata)
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, input)
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, input)
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, input):
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=input
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.118
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=RbBIG26IxRjdvGzHH77sogy-rtAEHC51It7TQ6wPjS0,2098
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=kS9wBoZQHvV-fqFoW9j-_aN_7Kj1I6sQdqnqK3Nno0M,11512
9
- commonmeta/file_utils.py,sha256=tGvXxScjh-PPo5YvLDyk4sqwY5Q50N0zAmBHVaUOLeU,3268
10
- commonmeta/metadata.py,sha256=U9uYOkKAdgqLiQwQKXMXjrAFvO6avMWp9heJG4cNAAY,18893
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=d-Rb2Vd_g3UW8GM4APIT7fivSQ5GMssZ6Ubi3OykHaw,33479
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=el4Eapa9HHg1ceddHb9rd4d1MAsM1ZBZ1F4jQUV8mbI,25717
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.118.dist-info/METADATA,sha256=BD1a2ouuh4hYizCty5tBGQuQM3m6LWVf1zfmTQwnFPU,7652
87
- commonmeta_py-0.118.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
88
- commonmeta_py-0.118.dist-info/entry_points.txt,sha256=U4w4BoRuS3rN5t5Y-uYSyOeU5Lh_VRVMS9OIDzIgw4w,50
89
- commonmeta_py-0.118.dist-info/licenses/LICENSE,sha256=wsIvxF9Q9GC9vA_s79zTWP3BkXJdfUNRmALlU8GbW1s,1074
90
- commonmeta_py-0.118.dist-info/RECORD,,
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,,