diff --git a/cds_ils/config.py b/cds_ils/config.py index 99acfaa17..607b40f11 100644 --- a/cds_ils/config.py +++ b/cds_ils/config.py @@ -778,6 +778,7 @@ def query_params_modifier(extra_params): }, "ebl": {"priority": 4, "agency_code": "MiAaPQ"}, "safari": {"priority": 4, "agency_code": "OCoLC"}, + "gobi": {"priority": 4, "agency_code": "YBPUID"}, } CDS_ILS_IMPORTER_UPLOADS_PATH = "/tmp" diff --git a/cds_ils/importer/providers/gobi/__init__.py b/cds_ils/importer/providers/gobi/__init__.py new file mode 100644 index 000000000..2c27aa678 --- /dev/null +++ b/cds_ils/importer/providers/gobi/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2026 CERN. +# +# CDS-ILS is free software; you can redistribute it and/or modify it under +# the terms of the MIT License; see LICENSE file for more details. + +"""CDS-ILS Gobi Importer module.""" diff --git a/cds_ils/importer/providers/gobi/gobi.py b/cds_ils/importer/providers/gobi/gobi.py new file mode 100644 index 000000000..8d383db66 --- /dev/null +++ b/cds_ils/importer/providers/gobi/gobi.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2026 CERN. +# +# CDS-ILS is free software; you can redistribute it and/or modify it under +# the terms of the MIT License; see LICENSE file for more details. +"""CDS-ILS GOBI model.""" +from copy import deepcopy + +from cds_ils.importer.base_model import Base +from cds_ils.importer.base_model import model as model_base +from cds_ils.importer.providers.gobi.ignore_fields import GOBI_IGNORE_FIELDS + + +class GOBIDocument(Base): + """GOBI model for XML mapping.""" + + __query__ = "003:YBPUID" + + __ignore_keys__ = GOBI_IGNORE_FIELDS + + _default_fields = {"document_type": "BOOK", "languages": ["ENG"]} + + def do( + self, + blob, + ignore_missing=True, + exception_handlers=None, + init_fields=None, + ): + """Overwrite the do method.""" + init_fields = deepcopy(self._default_fields) + return super().do(blob, ignore_missing, exception_handlers, init_fields) + + +model = GOBIDocument(bases=(model_base,), entry_point_group="cds_ils.importer.document") diff --git a/cds_ils/importer/providers/gobi/ignore_fields.py b/cds_ils/importer/providers/gobi/ignore_fields.py new file mode 100644 index 000000000..20d12f4c4 --- /dev/null +++ b/cds_ils/importer/providers/gobi/ignore_fields.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2026 CERN. +# +# CDS-ILS is free software; you can redistribute it and/or modify it under +# the terms of the MIT License; see LICENSE file for more details. +"""CDS-ILS Gobi Importer ignore fields.""" +GOBI_IGNORE_FIELDS = { + "005", + "006", + "007", + "008", + "020__q", + "020__z", + "040__a", + "040__c", + "043__a", + "050_4b", + "082042", + "1001_d", + "24510c", + "264_4c", + "336__2", + "336__a", + "336__b", + "337__2", + "337__a", + "337__b", + "338__2", + "338__a", + "338__b", + "500__a", + "504__a", + "533__a", + "533__b", + "533__n", + "588__a", + "5880_a", + "650_0x", + "650_0z", + "7102_a", + "77608c", + "77608w", +} diff --git a/cds_ils/importer/providers/gobi/importer.py b/cds_ils/importer/providers/gobi/importer.py new file mode 100644 index 000000000..2e4e21085 --- /dev/null +++ b/cds_ils/importer/providers/gobi/importer.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2026 CERN. +# +# CDS-ILS is free software; you can redistribute it and/or modify it under +# the terms of the MIT License; see LICENSE file for more details. + +"""CDS-ILS Gobi Importer module.""" + +from cds_ils.importer.importer import Importer + + +class GOBIImporter(Importer): + """Importer class for GOBI.""" + + EITEM_OPEN_ACCESS = False + EITEM_URLS_LOGIN_REQUIRED = True diff --git a/cds_ils/importer/providers/gobi/rules/__init__.py b/cds_ils/importer/providers/gobi/rules/__init__.py new file mode 100644 index 000000000..986d85c5e --- /dev/null +++ b/cds_ils/importer/providers/gobi/rules/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2026 CERN. +# +# CDS-ILS is free software; you can redistribute it and/or modify it under +# the terms of the MIT License; see LICENSE file for more details. + +"""CDS-ILS GOBI Importer rules.""" diff --git a/cds_ils/importer/providers/gobi/rules/document.py b/cds_ils/importer/providers/gobi/rules/document.py new file mode 100644 index 000000000..5ffc9a13b --- /dev/null +++ b/cds_ils/importer/providers/gobi/rules/document.py @@ -0,0 +1,229 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2020 CERN. +# +# CDS-ILS is free software; you can redistribute it and/or modify it under +# the terms of the MIT License; see LICENSE file for more details. + +"""CDS-ILS GOBI Importer rules.""" + +import re + +from dojson.utils import for_each_value, force_list + +from cds_ils.importer.errors import UnexpectedValue +from cds_ils.importer.providers.cds.helpers.decorators import ( + filter_list_values, + out_strip, +) +from cds_ils.importer.providers.cds.helpers.parsers import clean_val +from cds_ils.importer.providers.gobi.gobi import model +from cds_ils.importer.providers.utils import _get_correct_ils_contributor_role, rreplace + + +# REQUIRED FIELDS + +@model.over("alternative_identifiers", "^001") +@filter_list_values +def recid(self, key, value): + """Record Identifier.""" + self["provider_recid"] = value + return [{"scheme": "GOBI", "value": value}] + + +@model.over("agency_code", "^003") +def agency_code(self, key, value): + """Control number identifier.""" + return value + + +@model.over("title", "^245") +@out_strip +def title(self, key, value): + """Translates title.""" + if "title" in self: + raise UnexpectedValue(message="Ambiguous title") + + if "b" in value: + _alternative_titles = self.get("alternative_titles", []) + subtitle = clean_val("b", value, str).rstrip("/") + _alternative_titles.append({"value": subtitle, "type": "SUBTITLE"}) + self["alternative_titles"] = _alternative_titles + + title = clean_val("a", value, str, req=True).rstrip(":").rstrip("/") + title = " ".join(title.split()) + + return title + + +@model.over("authors", "(^100)|(^700)") +@filter_list_values +def authors(self, key, value): + """Translates authors.""" + _authors = self.get("authors", []) + author = { + "full_name": clean_val("a", value, str, req=True).rstrip(",").rstrip("."), + "roles": [_get_correct_ils_contributor_role("e", clean_val("e", value, str))], + "type": "PERSON", + } + _authors.append(author) + return _authors + + +@model.over("imprint", "^264_1") +@out_strip +def imprint(self, key, value): + """Translate imprint field.""" + _publication_year = self.get("publication_year") + if _publication_year: + raise UnexpectedValue(subfield="c", message="doubled publication year") + + year = clean_val("c", value, str) + if year: + numbers = re.findall(r"\d{4}", year) + self["publication_year"] = numbers[0] if numbers else year.strip("[]") + + return { + "place": clean_val("a", value, str).rstrip(":"), + "publisher": clean_val("b", value, str).rstrip(","), + } + + +# EITEM fields + +@model.over("_eitem", "^85640") +@out_strip +def eitem(self, key, value): + """Translate included eitems.""" + _eitem = self.get("_eitem", {}) + urls = [] + for v in force_list(value): + urls.append( + { + "description": "e-book", + "value": clean_val("u", v, str), + } + ) + _eitem.update({"urls": urls}) + return _eitem + + +# OPTIONAL FIELDS + +@model.over("identifiers", "^020__") +@filter_list_values +def identifiers(self, key, value): + """Translate identifiers. + + GOBI records sometimes put the qualifier inline in $a + (e.g. "9781483392646 (electronic bk.)") instead of a + separate $q, so it needs stripping with a regex rather + than relying on clean_val("q", ...). + """ + _isbns = self.get("identifiers", []) + for v in force_list(value): + sub_a = clean_val("a", v, str) + if sub_a: + isbn_value = re.sub(r"\s*\(.*\)\s*$", "", sub_a).strip() + isbn = {"value": isbn_value, "scheme": "ISBN", "material": "DIGITAL"} + if isbn not in _isbns: + _isbns.append(isbn) + return _isbns + + +@model.over("subjects", "^050_4") +def subjects_loc(self, key, value): + """Translates subject classification.""" + _subjects = self.get("subjects", []) + subject = {"scheme": "LOC", "value": clean_val("a", value, str)} + if subject not in _subjects: + _subjects.append(subject) + return _subjects + + +@model.over("subjects", "(^082_4)|(^08204)") +def subjects_dewey(self, key, value): + """Translates subject classification.""" + _subjects = self.get("subjects", []) + subject = {"scheme": "DEWEY", "value": clean_val("a", value, str)} + if subject not in _subjects: + _subjects.append(subject) + return _subjects + + +@model.over("number_of_pages", "^300__") +@out_strip +def number_of_pages(self, key, value): + """Translate number of pages.""" + pages = clean_val("a", value, str) + if pages: + numbers = re.findall(r"\d+", pages) + return numbers[0] if numbers else None + + +@model.over("_serial", "(^490)|(^830)") +@filter_list_values +@for_each_value +def serial(self, key, value): + """Translate serial.""" + volume = clean_val("v", value, str) + if volume: + volume = re.findall(r"\d+", volume) + + serial_title = clean_val("a", value, str, req=True).rstrip(",").rstrip(";").strip() + serial_title = " ".join(serial_title.split()) + + return { + "title": serial_title, + "volume": volume[0] if volume else None, + } + + +@model.over("table_of_content", "^505") +@out_strip +def table_of_content(self, key, value): + """Translate table of content. + + GOBI records vary: some use repeated $t subfields + (chapter-by-chapter), others a single $a with items + separated by " -- ". + """ + toc = [] + for v in force_list(value): + parts = clean_val("t", v, str, multiple_values=True) or [] + if not any(parts): + a_val = clean_val("a", v, str) + parts = a_val.split("--") if a_val else [] + toc.extend(p.strip(" -") for p in parts if p and p.strip(" -")) + return toc + + +@model.over("keywords", "^650_0") +@filter_list_values +def keywords(self, key, value): + """Translate keywords.""" + _keywords = self.get("keywords", []) + keyword = { + "source": "GOBI", + "value": clean_val("a", value, str, req=True).rstrip("."), + } + if keyword not in _keywords: + _keywords.append(keyword) + return _keywords + + +@model.over("identifiers", "^776") +@filter_list_values +def id_isbns(self, key, value): + """Translate print-version identifiers.""" + _identifiers = self.get("identifiers", []) + isbn_values = clean_val("z", value, str, multiple_values=True) or [] + for isbn_value in isbn_values: + isbn = { + "scheme": "ISBN", + "value": isbn_value, + "material": "PRINT_VERSION", + } + if isbn not in _identifiers: + _identifiers.append(isbn) + return _identifiers \ No newline at end of file diff --git a/cds_ils/vocabularies/data/alternative_identifier_schemes.json b/cds_ils/vocabularies/data/alternative_identifier_schemes.json index adf809d90..c5a776753 100644 --- a/cds_ils/vocabularies/data/alternative_identifier_schemes.json +++ b/cds_ils/vocabularies/data/alternative_identifier_schemes.json @@ -47,5 +47,10 @@ "data": { "url_path": "https://repository.cern/records/{identifier}" } + }, + { + "type": "alternative_identifier_scheme", + "key": "GOBI", + "text": "GOBI" } ] diff --git a/setup.cfg b/setup.cfg index 5687174a2..5c9a0c3ed 100644 --- a/setup.cfg +++ b/setup.cfg @@ -80,6 +80,7 @@ cds_ils.importers = springer = cds_ils.importer.providers.springer.importer:SpringerImporter ebl = cds_ils.importer.providers.ebl.importer:EBLImporter safari = cds_ils.importer.providers.safari.importer:SafariImporter + gobi = cds_ils.importer.providers.gobi.importer:GOBIImporter cds_ils.document_importers = cds = cds_ils.importer.providers.cds.document_importer:CDSDocumentImporter cds_ils.importer.base = @@ -91,6 +92,7 @@ cds_ils.importer.models = springer_document = cds_ils.importer.providers.springer.springer:model ebl_document = cds_ils.importer.providers.ebl.ebl:model safari_document = cds_ils.importer.providers.safari.safari:model + gobi_document = cds_ils.importer.providers.gobi.gobi:model cds_ils.migrator.models = cds_book = cds_ils.importer.providers.cds.models.document:model cds_standard = cds_ils.importer.providers.cds.models.standard:model @@ -111,6 +113,7 @@ cds_ils.importer.document = springer = cds_ils.importer.providers.springer.rules.document ebl = cds_ils.importer.providers.ebl.rules.document safari = cds_ils.importer.providers.safari.rules.document + gobi = cds_ils.importer.providers.gobi.rules.document cds_ils.importer.series = serial = cds_ils.importer.providers.cds.rules.serial multipart = cds_ils.importer.providers.cds.rules.multipart diff --git a/ui/src/config.js b/ui/src/config.js index 5ce193cc8..8af5b1318 100644 --- a/ui/src/config.js +++ b/ui/src/config.js @@ -263,6 +263,7 @@ export const config = { { key: "ebl", text: "EbookCentral (EBL)", value: "ebl" }, { key: "safari", text: "O'reilly (Safari)", value: "safari" }, { key: "snv", text: "SNV Standards", value: "snv" }, + { key: "gobi", text: "GOBI", value: "gobi" }, ], modes: [ { key: "create", text: "Import", value: "IMPORT" },