Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cds_ils/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
8 changes: 8 additions & 0 deletions cds_ils/importer/providers/gobi/__init__.py
Original file line number Diff line number Diff line change
@@ -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."""
36 changes: 36 additions & 0 deletions cds_ils/importer/providers/gobi/gobi.py
Original file line number Diff line number Diff line change
@@ -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")
44 changes: 44 additions & 0 deletions cds_ils/importer/providers/gobi/ignore_fields.py
Original file line number Diff line number Diff line change
@@ -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",
}
17 changes: 17 additions & 0 deletions cds_ils/importer/providers/gobi/importer.py
Original file line number Diff line number Diff line change
@@ -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
8 changes: 8 additions & 0 deletions cds_ils/importer/providers/gobi/rules/__init__.py
Original file line number Diff line number Diff line change
@@ -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."""
229 changes: 229 additions & 0 deletions cds_ils/importer/providers/gobi/rules/document.py
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions cds_ils/vocabularies/data/alternative_identifier_schemes.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,10 @@
"data": {
"url_path": "https://repository.cern/records/{identifier}"
}
},
{
"type": "alternative_identifier_scheme",
"key": "GOBI",
"text": "GOBI"
}
]
3 changes: 3 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions ui/src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down
Loading