diff --git a/README.md b/README.md
index f170581..eaeec20 100644
--- a/README.md
+++ b/README.md
@@ -59,8 +59,27 @@ WORKSPACE=### Set to `dev` for local development, this will be set to `stage` an
WARNING_ONLY_LOGGERS=### Comma-seperated list of logger names to set as WARNING only, e.g. 'botocore,charset_normalizer,smart_open'
LIBGUIDES_API_TOKEN=### Libguides API token [required for libguides source]
LIBGUIDES_CLIENT_ID=### Libguides account id [required for libguides source]
+LAST_AZ_IDENTIFIERS_PATH=### S3 or local filepath of the last known public AZ item identifiers [enables synthetic delete records for researchdatabases source]
```
+#### `LAST_AZ_IDENTIFIERS_PATH`
+
+The Springshare OAI-PMH endpoint does not emit deletes for AZ (research database) items
+that have been unpublished or hidden. When `LAST_AZ_IDENTIFIERS_PATH` is set, the
+`researchdatabases` transformation queries the LibGuides API for the current set of
+public AZ items, compares it to the identifiers stored at this path, and yields synthetic
+delete records for any identifiers that have dropped out. The file is then rewritten with
+the current identifiers for the next run.
+
+Notes:
+
+- If the env var is not set, no deletes are determined and the LibGuides API is not
+queried. This makes the behavior opt-in and backwards compatible with runs that predate
+it.
+- If the env var is set but the file does not yet exist, that run cannot determine any
+deletes, but it will create the file so that subsequent runs can.
+- This also requires `LIBGUIDES_API_TOKEN` and `LIBGUIDES_CLIENT_ID` to be set.
+
## CLI commands
### `transform`
diff --git a/tests/fixtures/researchdatabases/researchdatabases-2026-08-11-full-extracted-records-to-index.xml b/tests/fixtures/researchdatabases/researchdatabases-2026-08-11-full-extracted-records-to-index.xml
new file mode 100644
index 0000000..262ebb4
--- /dev/null
+++ b/tests/fixtures/researchdatabases/researchdatabases-2026-08-11-full-extracted-records-to-index.xml
@@ -0,0 +1,80 @@
+
+
+ 2026-08-11T09:30:27Z
+
+ https://libguides.mit.edu/oai.php
+
+
+
+
+ oai:libguides.com:az/65257807
+ 2025-12-01T14:59:03Z
+ az
+
+
+
+
+
+
+
+
+
+
+ The most comprehensive index to articles in Linguistics and Language Development and use.
]]>
+ 2022-01-28 22:15:37
+ https://libguides.mit.edu/llba
+
+
+
+
+
+ oai:libguides.com:az/65257808
+ 2025-12-01T14:59:03Z
+ az
+
+
+
+
+
+
+
+
+
+ Indexes major astronomy and astrophysics journals; includes abstracts for most entries and full text scans of tens of thousands of articles.]]>
+ 2022-01-28 22:15:37
+ https://libguides.mit.edu/ads
+
+
+
+
+
+ oai:libguides.com:az/65257809
+ 2025-12-01T14:59:03Z
+ az
+
+
+
+
+
+
+
+
+ Coverage: 1990 - present
A full-text collection of newspapers, journals and magazines published by ethnic and minority presses.]]>
+ 2022-01-28 22:15:37
+ https://libguides.mit.edu/ethnic
+
+
+
+
+
diff --git a/tests/sources/json/test_libguides.py b/tests/sources/json/test_libguides.py
index fcb2e44..2fdd172 100644
--- a/tests/sources/json/test_libguides.py
+++ b/tests/sources/json/test_libguides.py
@@ -401,7 +401,7 @@ def test_libguides_api_client_fetch_guides_expands_sub_pages_into_rows():
mock_response.json.return_value = mock_api_response
with patch(
- "transmogrifier.sources.json.libguides.requests.get",
+ "transmogrifier.helpers.requests.get",
return_value=mock_response,
):
df = client.fetch_guides("fake-token")
diff --git a/tests/sources/xml/test_researchdatabases.py b/tests/sources/xml/test_researchdatabases.py
new file mode 100644
index 0000000..2d18f25
--- /dev/null
+++ b/tests/sources/xml/test_researchdatabases.py
@@ -0,0 +1,161 @@
+# ruff: noqa: PLR2004, SLF001
+
+from unittest.mock import patch
+
+import pytest
+from bs4 import Tag
+
+from transmogrifier.helpers import LibGuidesAPIClient
+from transmogrifier.sources.xml.researchdatabases import ResearchDatabases
+
+
+@pytest.fixture
+def last_az_identifiers(tmp_path) -> str:
+ path = tmp_path / "last_az_identifiers.txt"
+ with open(path, "w") as f:
+ f.write("1234")
+ return str(path)
+
+
+@pytest.fixture(autouse=True)
+def _test_env_libguides(last_az_identifiers):
+ with (
+ patch("transmogrifier.config.LIBGUIDES_CLIENT_ID", "123"),
+ patch("transmogrifier.config.LIBGUIDES_API_TOKEN", "aaabbbdddccc"),
+ patch(
+ "transmogrifier.sources.xml.researchdatabases.LAST_AZ_IDENTIFIERS_PATH",
+ last_az_identifiers,
+ ),
+ ):
+ yield
+
+
+@pytest.fixture
+def mocked_current_az_identifiers():
+ """Mock current AZ identifiers from LibGuides API, none matching previous ones."""
+ with patch.object(
+ LibGuidesAPIClient,
+ "get_current_az_identifiers",
+ return_value=["7777", "8888", "9999"],
+ ):
+ yield
+
+
+@pytest.fixture
+def researchdatabases_transformer():
+
+ return ResearchDatabases.load(
+ "researchdatabases",
+ (
+ "tests/fixtures/researchdatabases/researchdatabases-"
+ "2026-08-11-full-extracted-records-to-index.xml"
+ ),
+ )
+
+
+def test_researchdatabases_yields_oai_records_pass(researchdatabases_transformer):
+
+ oai_records = list(
+ researchdatabases_transformer._yield_oai_xml_records_for_indexing(
+ researchdatabases_transformer.source_file
+ )
+ )
+
+ assert len(oai_records) == 3
+ assert isinstance(oai_records[0], Tag)
+
+
+def test_researchdatabases_yields_deleted_records_success(
+ researchdatabases_transformer,
+ mocked_current_az_identifiers,
+):
+ deleted_records = list(
+ researchdatabases_transformer._yield_api_records_for_deleting()
+ )
+ assert len(deleted_records) == 1
+ assert isinstance(deleted_records[0], Tag)
+
+
+def test_researchdatabases_synthetic_deleted_record(
+ researchdatabases_transformer,
+ mocked_current_az_identifiers,
+):
+ """Assert that Transformer injects synthetic OAI delete records.
+
+ Example record:
+
+
+
+
+ oai:libguides.com:az/1234
+ 2026-08-11T14:13:38Z
+ az
+
+ This is a synthetic delete record created by Transmogrifier.
+
+
+ """
+ deleted_record = next(researchdatabases_transformer._yield_api_records_for_deleting())
+
+ header = deleted_record.find("record").find("header")
+
+ assert header.get("status") == "deleted"
+ assert header.find("identifier").string == "oai:libguides.com:az/1234"
+ assert (
+ deleted_record.find("note").string
+ == "This is a synthetic delete record created by Transmogrifier."
+ )
+
+
+def test_researchdatabases_last_az_identifiers_file_updated(
+ researchdatabases_transformer, mocked_current_az_identifiers, last_az_identifiers
+):
+ """Assert the last AZ identifiers list is updated."""
+ _ = list(researchdatabases_transformer._yield_api_records_for_deleting())
+ with open(last_az_identifiers) as f:
+ assert f.read() == """7777\n8888\n9999\n"""
+
+
+def test_researchdatabases_env_var_not_set_skips_deletes(
+ caplog,
+ researchdatabases_transformer,
+ mocked_current_az_identifiers,
+):
+ """Assert that deletes are opt-in via the 'LAST_AZ_IDENTIFIERS_PATH' env var."""
+ caplog.set_level("WARNING")
+ with patch(
+ "transmogrifier.sources.xml.researchdatabases.LAST_AZ_IDENTIFIERS_PATH",
+ None,
+ ):
+ assert list(researchdatabases_transformer._yield_api_records_for_deleting()) == []
+
+ assert "Env var 'LAST_AZ_IDENTIFIERS_PATH' is not set" in caplog.text
+
+
+def test_researchdatabases_identifiers_text_file_missing_creates_file(
+ caplog,
+ tmp_path,
+ researchdatabases_transformer,
+ mocked_current_az_identifiers,
+):
+ """Assert that a missing AZ identifiers list yields no deletes, but creates file."""
+ caplog.set_level("WARNING")
+ cold_start_path = tmp_path / "does-not-exist.txt"
+ with patch(
+ "transmogrifier.sources.xml.researchdatabases.LAST_AZ_IDENTIFIERS_PATH",
+ str(cold_start_path),
+ ):
+ assert list(researchdatabases_transformer._yield_api_records_for_deleting()) == []
+
+ assert "but file does not exist, this run will create it" in caplog.text
+ with open(cold_start_path) as f:
+ assert f.read() == "7777\n8888\n9999\n"
+
+
+def test_research_databases_custom_parse_source_file_yields_oai_and_synthetic_records(
+ researchdatabases_transformer,
+ mocked_current_az_identifiers,
+):
+ records = list(researchdatabases_transformer.source_records)
+
+ assert len(records) == 4 # 3 OAI + 1 synthetic delete
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 8533fbc..712eb89 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -215,9 +215,9 @@ def test_transform_no_memory_fault_for_threaded_bs4_parsing(monkeypatch, tmp_pat
"run",
"transform",
"-s",
- "researchdatabases",
+ "libguides",
"-i",
- "tests/fixtures/dataset/libguides-2025-01-09-full-extracted-records-to-index.xml",
+ "tests/fixtures/libguides/libguides-2026-02-20-full-extracted-records-to-index.jsonl",
"-o",
f"{tmp_path}/dataset",
],
diff --git a/transmogrifier/config.py b/transmogrifier/config.py
index 880cce9..208d9f9 100644
--- a/transmogrifier/config.py
+++ b/transmogrifier/config.py
@@ -127,7 +127,7 @@
"researchdatabases": {
"name": "Research Databases",
"base-url": "https://libguides.mit.edu/",
- "transform-class": "transmogrifier.sources.xml.springshare.SpringshareOaiDc",
+ "transform-class": "transmogrifier.sources.xml.researchdatabases.ResearchDatabases", # noqa: E501
},
"whoas": {
"name": "Woods Hole Open Access Server",
@@ -149,6 +149,7 @@
)
LIBGUIDES_API_TOKEN = os.getenv("LIBGUIDES_API_TOKEN")
LIBGUIDES_CLIENT_ID = os.getenv("LIBGUIDES_CLIENT_ID")
+LAST_AZ_IDENTIFIERS_PATH = os.getenv("LAST_AZ_IDENTIFIERS_PATH")
def configure_logger(
diff --git a/transmogrifier/helpers.py b/transmogrifier/helpers.py
index da3d661..f093062 100644
--- a/transmogrifier/helpers.py
+++ b/transmogrifier/helpers.py
@@ -1,7 +1,12 @@
import logging
+import re
from datetime import UTC, datetime
+import pandas as pd
+import requests
+
import transmogrifier.models as timdex
+from transmogrifier import config
from transmogrifier.config import DATE_FORMATS
logger = logging.getLogger(__name__)
@@ -131,3 +136,111 @@ def validate_date_range(
end_date,
)
return False
+
+
+class LibGuidesAPIClient:
+ """Client for LibGuides API communication and data retrieval.
+
+ This class retrieves metadata about all LibGuides via an API, retrieving data that is
+ not found in the OAI-PMH XML records or the websites themselves. This valuable data
+ is used during transformation to identify records for exclusion, occasionally
+ provide friendlier URLs, and other data augmentation.
+
+ This class is instantiated as a singleton object in this module. Once instantiated,
+ it is attached to the Libguides transformer instance. This allows class methods
+ on the transformer to access cached data from this singleton object, ultimately
+ resulting in only a single API call per multiple record transformation run.
+
+ This class relies on two environment variables:
+ - LIBGUIDES_CLIENT_ID
+ - LIBGUIDES_API_TOKEN
+ """
+
+ def __init__(self) -> None:
+ if not config.LIBGUIDES_CLIENT_ID:
+ raise RuntimeError("Required env var 'LIBGUIDES_CLIENT_ID' is not set")
+ if not config.LIBGUIDES_API_TOKEN:
+ raise RuntimeError("Required env var 'LIBGUIDES_API_TOKEN' is not set")
+
+ self.client_id = str(config.LIBGUIDES_CLIENT_ID)
+ self.client_secret = config.LIBGUIDES_API_TOKEN
+ self._api_guides_df: pd.DataFrame | None = None
+
+ @property
+ def api_guides_df(self) -> pd.DataFrame:
+ if self._api_guides_df is None:
+ self._api_guides_df = self.fetch_guides(self.get_api_token())
+ return self._api_guides_df
+
+ def get_api_token(self) -> str:
+ data = {
+ "grant_type": "client_credentials",
+ "client_id": self.client_id,
+ "client_secret": self.client_secret,
+ }
+ response = requests.post(
+ config.LIBGUIDES_TOKEN_URL, headers={}, data=data, timeout=60
+ )
+ response.raise_for_status()
+ payload = response.json()
+ return payload.get("access_token")
+
+ def fetch_guides(self, token: str) -> pd.DataFrame:
+ """Retrieve metadata for all LibGuides.
+
+ Each guide may contain a 'pages' key with a list of sub-page dicts. These
+ sub-pages are expanded into their own rows in the returned DataFrame, inheriting
+ any columns from the parent guide that the sub-page does not have.
+ """
+ logger.debug("Retrieving all guides from Libguides API.")
+ headers = {"Authorization": f"Bearer {token}"}
+ response = requests.get(config.LIBGUIDES_GUIDES_URL, headers=headers, timeout=60)
+ response.raise_for_status()
+ guides = response.json()
+
+ all_rows: list[dict] = []
+ for guide in guides:
+ pages = guide.get("pages", [])
+ all_rows.append(guide)
+ for page in pages:
+ # inherit parent columns, then overlay page-specific columns
+ page_row = {**guide, **page}
+ all_rows.append(page_row)
+
+ return pd.DataFrame(all_rows)
+
+ def get_guide_by_url(self, url: str) -> pd.Series:
+ """Get metadata for a single guide via a URL."""
+ # strip GET parameter preview=...; duplicate for base URL
+ url = re.sub(r"([&?])preview=.*", "", url)
+ url = url.removesuffix("/")
+
+ matches = self.api_guides_df[
+ (self.api_guides_df.url.str.lower() == url.lower())
+ | (self.api_guides_df.friendly_url.str.lower() == url.lower())
+ ]
+ if len(matches) == 1:
+ return matches.iloc[0]
+
+ raise ValueError(f"Found {len(matches)} guide ids for URL: {url}, expecting one.")
+
+ def fetch_az(self, token: str) -> pd.DataFrame:
+ """Retrieve AZ items from API."""
+ headers = {"Authorization": f"Bearer {token}"}
+ response = requests.get(
+ "https://lgapi-us.libapps.com/1.2/az?expand=pages",
+ headers=headers,
+ timeout=60,
+ )
+ response.raise_for_status()
+ return pd.DataFrame(response.json())
+
+ def get_current_az_identifiers(self) -> list[str]:
+ """Get list of identifiers for non-hidden / public AZ items.
+
+ When filtering to enable_hidden = 0, the count matches the OAI-PMH full harvest
+ for AZ items.
+ """
+ az_df = self.fetch_az(self.get_api_token())
+ non_hidden_az_df = az_df[az_df.enable_hidden == "0"]
+ return list(non_hidden_az_df.id)
diff --git a/transmogrifier/sources/json/libguides.py b/transmogrifier/sources/json/libguides.py
index accfb52..bfb32ad 100644
--- a/transmogrifier/sources/json/libguides.py
+++ b/transmogrifier/sources/json/libguides.py
@@ -6,18 +6,12 @@
from urllib.parse import urlparse
import pandas as pd
-import requests
from bs4 import BeautifulSoup, Tag
from dateutil.parser import parse as date_parser
import transmogrifier.models as timdex
-from transmogrifier.config import (
- LIBGUIDES_API_TOKEN,
- LIBGUIDES_CLIENT_ID,
- LIBGUIDES_GUIDES_URL,
- LIBGUIDES_TOKEN_URL,
-)
from transmogrifier.exceptions import SkippedRecordEvent
+from transmogrifier.helpers import LibGuidesAPIClient
from transmogrifier.sources.jsontransformer import JSONTransformer
from transmogrifier.sources.transformer import JSON
@@ -41,91 +35,6 @@
]
-class LibGuidesAPIClient:
- """Client for LibGuides API communication and data retrieval.
-
- This class retrieves metadata about all LibGuides via an API, retrieving data that is
- not found in the OAI-PMH XML records or the websites themselves. This valuable data
- is used during transformation to identify records for exclusion, occasionally
- provide friendlier URLs, and other data augmentation.
-
- This class is instantiated as a singleton object in this module. Once instantiated,
- it is attached to the Libguides transformer instance. This allows class methods
- on the transformer to access cached data from this singleton object, ultimately
- resulting in only a single API call per multiple record transformation run.
-
- This class relies on two environment variables:
- - LIBGUIDES_CLIENT_ID
- - LIBGUIDES_API_TOKEN
- """
-
- def __init__(self) -> None:
- if not LIBGUIDES_CLIENT_ID:
- raise RuntimeError("Required env var 'LIBGUIDES_CLIENT_ID' is not set")
- if not LIBGUIDES_API_TOKEN:
- raise RuntimeError("Required env var 'LIBGUIDES_API_TOKEN' is not set")
-
- self.client_id = str(LIBGUIDES_CLIENT_ID)
- self.client_secret = LIBGUIDES_API_TOKEN
- self._api_guides_df: pd.DataFrame | None = None
-
- @property
- def api_guides_df(self) -> pd.DataFrame:
- if self._api_guides_df is None:
- self._api_guides_df = self.fetch_guides(self.get_api_token())
- return self._api_guides_df
-
- def get_api_token(self) -> str:
- data = {
- "grant_type": "client_credentials",
- "client_id": self.client_id,
- "client_secret": self.client_secret,
- }
- response = requests.post(LIBGUIDES_TOKEN_URL, headers={}, data=data, timeout=60)
- response.raise_for_status()
- payload = response.json()
- return payload.get("access_token")
-
- def fetch_guides(self, token: str) -> pd.DataFrame:
- """Retrieve metadata for all LibGuides.
-
- Each guide may contain a 'pages' key with a list of sub-page dicts. These
- sub-pages are expanded into their own rows in the returned DataFrame, inheriting
- any columns from the parent guide that the sub-page does not have.
- """
- logger.debug("Retrieving all guides from Libguides API.")
- headers = {"Authorization": f"Bearer {token}"}
- response = requests.get(LIBGUIDES_GUIDES_URL, headers=headers, timeout=60)
- response.raise_for_status()
- guides = response.json()
-
- all_rows: list[dict] = []
- for guide in guides:
- pages = guide.get("pages", [])
- all_rows.append(guide)
- for page in pages:
- # inherit parent columns, then overlay page-specific columns
- page_row = {**guide, **page}
- all_rows.append(page_row)
-
- return pd.DataFrame(all_rows)
-
- def get_guide_by_url(self, url: str) -> pd.Series:
- """Get metadata for a single guide via a URL."""
- # strip GET parameter preview=...; duplicate for base URL
- url = re.sub(r"([&?])preview=.*", "", url)
- url = url.removesuffix("/")
-
- matches = self.api_guides_df[
- (self.api_guides_df.url.str.lower() == url.lower())
- | (self.api_guides_df.friendly_url.str.lower() == url.lower())
- ]
- if len(matches) == 1:
- return matches.iloc[0]
-
- raise ValueError(f"Found {len(matches)} guide ids for URL: {url}, expecting one.")
-
-
# instantiate a LibGuidesAPIClient singleton
libguides_api_client = LibGuidesAPIClient()
diff --git a/transmogrifier/sources/xml/researchdatabases.py b/transmogrifier/sources/xml/researchdatabases.py
new file mode 100644
index 0000000..17e4984
--- /dev/null
+++ b/transmogrifier/sources/xml/researchdatabases.py
@@ -0,0 +1,111 @@
+import logging
+from collections.abc import Iterator
+from datetime import UTC, datetime
+
+import smart_open # type: ignore[import-untyped]
+from bs4 import BeautifulSoup, Tag
+from lxml import etree
+
+from transmogrifier.config import LAST_AZ_IDENTIFIERS_PATH
+from transmogrifier.helpers import LibGuidesAPIClient
+from transmogrifier.sources.xml.springshare import SpringshareOaiDc
+
+logger = logging.getLogger(__name__)
+
+
+class ResearchDatabases(SpringshareOaiDc):
+ @classmethod
+ def parse_source_file(cls, source_file: str) -> Iterator[Tag]:
+ """Yield records from harvested OAI + API gathered records for possible delete.
+
+ Synthetic delete records are only yielded if the env var
+ 'LAST_AZ_IDENTIFIERS_PATH' is set.
+ """
+ yield from cls._yield_oai_xml_records_for_indexing(source_file)
+ yield from cls._yield_api_records_for_deleting()
+
+ @classmethod
+ def _yield_oai_xml_records_for_indexing(cls, source_file: str) -> Iterator[Tag]:
+ """Yield OAI records from extracted XML file.
+
+ This functionality is a direct port from XMLTransformer, but allows for a custom
+ self.parse_source_file in this transformer class.
+ """
+ with smart_open.open(source_file, "rb") as file:
+ for _, element in etree.iterparse(
+ file,
+ tag="{*}record",
+ encoding="utf-8",
+ recover=True,
+ ):
+ record_string = etree.tostring(element, encoding="utf-8")
+ record = cls.parse_bs4_in_isolated_thread(record_string)
+ yield record
+ element.clear()
+
+ @classmethod
+ def _yield_api_records_for_deleting(cls) -> Iterator[Tag]:
+ """Yield synthetic OAI records that will prompt deletes in TIMDEX.
+
+ This method will yield stubbed OAI records *as-if* the Springshare OAI endpoint
+ produced deletes. This is achieved by querying the Springshare API, retrieving
+ a list of public/non-hidden AZ items, and comparing to a managed list of last
+ known public/non-hidden AZ items. Any items that are no longer public should
+ be removed from TIMDEX, which these synthetic OAI XML records archive.
+
+ Determining deletes is opt-in via the env var 'LAST_AZ_IDENTIFIERS_PATH'. If
+ unset, no synthetic delete records are yielded and the Springshare API is not
+ queried. If set but the file does not yet exist, the file will be created for
+ future use.
+ """
+ if LAST_AZ_IDENTIFIERS_PATH is None:
+ logger.warning(
+ "Env var 'LAST_AZ_IDENTIFIERS_PATH' is not set, synthetic delete "
+ "records will not be determined and yielded."
+ )
+ return
+
+ client = LibGuidesAPIClient()
+
+ # retrieve current AZ identifiers from API
+ az_current_identifiers = client.get_current_az_identifiers()
+
+ # retrieve previous AZ identifiers from file
+ try:
+ with smart_open.open(LAST_AZ_IDENTIFIERS_PATH) as f:
+ az_previous_identifiers = f.read().splitlines()
+ except OSError:
+ logger.warning(
+ "Env var 'LAST_AZ_IDENTIFIERS_PATH' is set, but file does not exist, "
+ "this run will create it."
+ )
+ az_previous_identifiers = []
+
+ # isolate identifiers from previous list not in current list
+ deleted_identifiers = set(az_previous_identifiers).difference(
+ az_current_identifiers
+ )
+ logger.info(
+ f"{len(deleted_identifiers)} identifiers identified for deletion: "
+ f"{list(deleted_identifiers)}"
+ )
+
+ # yield fake OAI records that mark a record for delete
+ now_date = datetime.now(tz=UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
+ for deleted_identifier in deleted_identifiers:
+ oai_delete_record = f"""
+
+
+ oai:libguides.com:az/{deleted_identifier}
+ {now_date}
+ az
+
+ This is a synthetic delete record created by Transmogrifier.
+
+
+ """
+ yield BeautifulSoup(oai_delete_record, "xml")
+
+ # update AZ identifiers list with current AZ items
+ with smart_open.open(LAST_AZ_IDENTIFIERS_PATH, "w") as f:
+ f.writelines(f"{identifier}\n" for identifier in az_current_identifiers)
diff --git a/transmogrifier/sources/xml/springshare.py b/transmogrifier/sources/xml/springshare.py
index 3717daf..26de373 100644
--- a/transmogrifier/sources/xml/springshare.py
+++ b/transmogrifier/sources/xml/springshare.py
@@ -70,11 +70,11 @@ def get_links(self, source_record: Tag) -> list[timdex.Link] | None:
url=str(identifier.string),
)
)
-
- logger.debug(
- "Record ID %s has links that cannot be generated: missing dc:identifier",
- source_record_id,
- )
+ else:
+ logger.debug(
+ "Record ID %s has links that cannot be generated: missing dc:identifier",
+ source_record_id,
+ )
return links or None
def get_source_link(
diff --git a/transmogrifier/sources/xmltransformer.py b/transmogrifier/sources/xmltransformer.py
index 21e87dc..e97e584 100644
--- a/transmogrifier/sources/xmltransformer.py
+++ b/transmogrifier/sources/xmltransformer.py
@@ -1,7 +1,7 @@
from __future__ import annotations
import threading
-from typing import TYPE_CHECKING, final
+from typing import TYPE_CHECKING
import smart_open # type: ignore[import-untyped]
from bs4 import BeautifulSoup, Tag # type: ignore[import-untyped]
@@ -16,14 +16,11 @@
class XMLTransformer(Transformer):
"""XML transformer class."""
- @final
@classmethod
def parse_source_file(cls, source_file: str) -> Iterator[Tag]:
"""
Parse XML file and return source records as bs4 Tags via an iterator.
- May not be overridden.
-
Args:
source_file: A file containing source records to be transformed.
"""