Source code for scippneutron.metadata._orcid
# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2025 Scipp contributors (https://github.com/scipp)
from __future__ import annotations
import itertools
from typing import Any
from pydantic import GetCoreSchemaHandler
from pydantic_core import core_schema
[docs]
class ORCIDiD:
"""An ORCID iD.
Ensures that the id is valid during initialization.
See https://support.orcid.org/hc/en-us/articles/360006897674-Structure-of-the-ORCID-Identifier
This class can be used with Pydantic models.
Examples
--------
>>> from scippneutron.metadata import ORCIDiD
>>> orcid_id = ORCIDiD('0000-0000-0000-0001')
>>> str(orcid_id)
'https://orcid.org/0000-0000-0000-0001'
Or equivalently with an explicit resolver:
>>> orcid_id = ORCIDiD('https://orcid.org/0000-0000-0000-0001')
>>> str(orcid_id)
'https://orcid.org/0000-0000-0000-0001'
"""
__slots__ = ('_orcid_id',)
[docs]
def __init__(self, orcid_id: str | ORCIDiD) -> None:
if isinstance(orcid_id, ORCIDiD):
self._orcid_id: str = orcid_id._orcid_id
else:
self._orcid_id = _parse_id(orcid_id)
def __str__(self) -> str:
return self._orcid_id
def __repr__(self) -> str:
return f'ORCIDiD({self!s})'
def __eq__(self, other: object) -> bool:
if isinstance(other, ORCIDiD):
return self._orcid_id == other._orcid_id
if isinstance(other, str):
try:
return self._orcid_id == _parse_id(other)
except ValueError: # other is not a valid ORCID iD
return False
return NotImplemented
def __ne__(self, other: object) -> bool:
if (b := self.__eq__(other)) is NotImplemented:
return NotImplemented
return not b
def __hash__(self) -> int:
return hash(self._orcid_id)
@classmethod
def __get_pydantic_core_schema__(
cls, _source_type: Any, _handler: GetCoreSchemaHandler
) -> core_schema.CoreSchema:
return core_schema.no_info_after_validator_function(
_parse_pydantic,
core_schema.union_schema(
[core_schema.is_instance_schema(ORCIDiD), core_schema.str_schema()]
),
serialization=core_schema.plain_serializer_function_ser_schema(
cls.__str__, info_arg=False, return_schema=core_schema.str_schema()
),
)
def _parse_pydantic(value: str | ORCIDiD) -> ORCIDiD:
return ORCIDiD(value)
_ORCID_RESOLVER: str = 'https://orcid.org'
def _parse_id(value: str) -> str:
parts = value.rsplit('/', 1)
if len(parts) == 2:
resolver, orcid_id = parts
if resolver != _ORCID_RESOLVER:
# Must be the correct ORCID URL.
raise ValueError(
f"Invalid ORCID URL: '{resolver}'. Must be '{_ORCID_RESOLVER}'"
)
else:
value = f'{_ORCID_RESOLVER}/{value}'
(orcid_id,) = parts
_check_id(orcid_id)
return value
def _check_id(orcid_id: str) -> None:
segments = orcid_id.split('-')
if len(segments) != 4 or not all(len(s) == 4 for s in segments):
# Must have 4 blocks of 4 digits each.
raise ValueError(f"Invalid ORCID iD: '{orcid_id}'. Incorrect structure.")
if _orcid_id_checksum(segments) != orcid_id[-1]:
# Checksum must match the last digit.
raise ValueError(f"Invalid ORCID iD: '{orcid_id}'. Checksum does not match.")
def _orcid_id_checksum(segments: list[str]) -> str:
total = 0
for d in map(int, itertools.islice(itertools.chain(*segments), 4 * 4 - 1)):
total = (total + d) * 2
result = (12 - total % 11) % 11
return 'X' if result == 10 else str(result)