Found by the #6464 comparison; same family as the transformer strictness rejects in #6472. A good first pipeline issue: the failing code, the failing records, and the verification path are all identified below.
What is wrong
54 EBSCO records fail the Axiell/EBSCO Python transformer deterministically and therefore have no works in the 2026-07-03 pipeline. Production still has works for them because they were transformed by the previous Scala transformer, which tolerated these values. The records themselves are live in the EBSCO adapter store and re-fail identically on every run, most recently in the changeset redrive of c2dce996-824c-11f1-afb6-0a58a9feac02 (job replay-6464-ebsco-c2dce996, 2026-08-03), which is also what fired the ebsco-transformer-failures-2026-07-03 alarm that day.
Two failure classes:
- MARC 008 date type
q is unhandled. catalogue_graph/src/adapters/transformers/ebsco/parsers/field008.py (maximal_date_range) handles date types n | c u s r t d m and raises NotImplementedError for anything else. q is a valid MARC 21 code meaning "questionable date: between date 1 and date 2", so the reasonable implementation treats it like the d/m range case. The method's docstring is doctest-driven, so add a q example there.
- Partial or messy years crash the period parser.
catalogue_graph/src/adapters/transformers/ebsco/parsers/period.py builds date ranges with int(year) and raises on values that occur in this data, such as 182- (a MARC convention for the 1820s), - 1792, and 1742 - 44. The parser needs to recover what it can (for example treat 182- as the decade) or degrade to a label-only date rather than failing the whole record.
How to work on it
All the relevant code is Python under catalogue_graph/. Install uv, then from catalogue_graph/:
uv sync
uv run pytest tests/adapters/transformers/ebsco --no-cov -q
That subset covers the EBSCO transformer and passes before any changes. Keep the behaviour of valid input unchanged: the point is tolerating these shapes, not loosening date parsing generally.
Reproducing the failures without AWS
The date parsing is pure functions. The unhandled 008 date type reproduces in one line:
uv run python -c "from adapters.transformers.ebsco.parsers.field008 import Field008; Field008('||||||q17951798').maximal_date_range"
# NotImplementedError: unexpected MARC008 date type value: q
To run a whole record through the real transformation, drive the work builder directly. Save this as repro.py in catalogue_graph/ and run uv run python repro.py:
from datetime import UTC, datetime
from adapters.transformers.builders.ebsco_work_builder import EbscoWorkBuilder
from utils.marc import parse_single_marc_record
xml = """<record>
<leader>00000nam a2200000 4500</leader>
<controlfield tag='001'>ebs28825414e</controlfield>
<controlfield tag='008'>940503q17951798enk o 000 0 eng c</controlfield>
<datafield tag='245' ind1='0' ind2='0'><subfield code='a'>A title</subfield></datafield>
</record>"""
work = EbscoWorkBuilder(parse_single_marc_record(xml), datetime.now(UTC)).transform_work()
print(work)
The 008 here is the real one from ebs28825414e, so this currently raises the NotImplementedError above; after the fix it prints the transformed SourceWork. Swap in content fetched from the adapter store to reproduce the period parser failures; the next section shows how to get it.
The int() error messages in the table below show the fragment that reached int() after the period parser's preprocess and crack steps, not the raw subfield contents. ebs144516e fails with int('182-') but its actual 260$c is 1825-[19--?], and the int('1742 - 44') in ebs28836058e comes from M,DCC,XLII-M,DCC,XLIV. [1742-44]. Fetch the real record before writing fixtures rather than reverse-engineering inputs from the error messages.
Querying the real records from the adapter store
Use catalogue_graph/notebooks/adapter_source_tables.ipynb. It loads the EBSCO, Axiell and FOLIO Iceberg adapter tables, fetches a record by id, pretty-prints its MARC XML, and runs the matching transformer over it in-process.
- Log in with
aws sso login --profile platform-developer. The notebook sets AWS_PROFILE=platform-developer, which the S3 Tables REST API needs.
- From
catalogue_graph/, run uv run --with jupyter jupyter lab, or open the notebook in your editor against the project .venv; ipykernel is already a dev dependency.
- In the cell that picks an example record, uncomment the override and set
example_id = "ebs144516e". The example_table_name already defaults to the EBSCO table.
- The next cells display the raw MARC XML and transform it. The transformer catches per-record exceptions and logs them as "Error transforming record" instead of raising; that same behaviour is what turns these records into
failure_count in the pipeline.
Leave USE_REST_API_TABLE = True and ALLOW_REMOTE_TABLE_MUTATIONS = False as they are: reads are safe, and the destructive helpers further down the notebook are guarded behind that flag.
Where the tests live
- The parsers document their behaviour with doctests in
field008.py and period.py. Run them with uv run pytest --doctest-modules src/adapters/transformers/ebsco/parsers --no-cov. They are not part of the default uv run pytest run, so new behaviour needs a doctest example and a case in the regular tests too.
tests/adapters/transformers/ebsco/unit/test_production.py asserts on the 008 fall-back and date ranges, using the marc_record fixture from unit/conftest.py to build minimal pymarc records. Real 008 and 260$c values from the failing records make good cases here.
tests/adapters/transformers/ebsco/test_transformer.py runs the transformer end to end against a local Iceberg table with raw MARC XML strings as input. It is a good template for a whole-record regression test using one of the failing records.
How to verify the fix in the pipeline
After merge and deploy, re-run the transformer for the changeset that contains all 54 records (any platform engineer can run this, or ask in the channel):
AWS_PROFILE=platform-developer aws stepfunctions start-execution \
--region eu-west-1 \
--state-machine-arn arn:aws:states:eu-west-1:760097843905:stateMachine:transformer-2026-07-03 \
--name replay-ebsco-date-fix-verification \
--input '{"detail": {"transformer_type": "ebsco", "job_id": "replay-ebsco-date-fix", "changeset_ids": ["c2dce996-824c-11f1-afb6-0a58a9feac02"]}}'
The execution output reports success_count and failure_count: before the fix the run ends with 54 failures, after it it should end with 0, and the 54 records should then exist in works-source-2026-07-03 (spot-check Work[ebsco-alt-lookup/ebs144516e]).
The 54 failing records
| Record |
Error |
ebs144516e |
invalid literal for int() with base 10: '182-' |
ebs28825414e |
unexpected MARC008 date type value: q |
ebs28825932e |
unexpected MARC008 date type value: q |
ebs28830589e |
unexpected MARC008 date type value: q |
ebs28830622e |
invalid literal for int() with base 10: '- 1792' |
ebs28835937e |
invalid literal for int() with base 10: '- 1794 - -' |
ebs28836058e |
invalid literal for int() with base 10: '1742 - 44' |
ebs28836516e |
unexpected MARC008 date type value: q |
ebs28836743e |
unexpected MARC008 date type value: q |
ebs28837484e |
unexpected MARC008 date type value: q |
ebs28838441e |
unexpected MARC008 date type value: q |
ebs28840993e |
unexpected MARC008 date type value: q |
ebs28841900e |
invalid literal for int() with base 10: '- 1795 - -' |
ebs28841925e |
unexpected MARC008 date type value: q |
ebs28842707e |
unexpected MARC008 date type value: q |
ebs28844394e |
unexpected MARC008 date type value: q |
ebs28845289e |
unexpected MARC008 date type value: q |
ebs28845635e |
invalid literal for int() with base 10: '1746 - 1747' |
ebs28845878e |
unexpected MARC008 date type value: q |
ebs28846763e |
unexpected MARC008 date type value: q |
ebs28848675e |
unexpected MARC008 date type value: q |
ebs28849323e |
unexpected MARC008 date type value: q |
ebs28849684e |
unexpected MARC008 date type value: q |
ebs28851700e |
unexpected MARC008 date type value: q |
ebs28852745e |
unexpected MARC008 date type value: q |
ebs28864322e |
unexpected MARC008 date type value: q |
ebs28864387e |
unexpected MARC008 date type value: q |
ebs28864493e |
unexpected MARC008 date type value: q |
ebs375800e |
invalid literal for int() with base 10: '1788 - 1789' |
ebs557428e |
invalid literal for int() with base 10: '186-' |
ebs557433e |
invalid literal for int() with base 10: '185-' |
ebs557434e |
invalid literal for int() with base 10: '183-' |
ebs557436e |
invalid literal for int() with base 10: '185-' |
ebs557439e |
invalid literal for int() with base 10: '188-' |
ebs557444e |
invalid literal for int() with base 10: '185-' |
ebs557445e |
invalid literal for int() with base 10: '183-' |
ebs557446e |
invalid literal for int() with base 10: '183-' |
ebs557448e |
invalid literal for int() with base 10: '182-' |
ebs557451e |
invalid literal for int() with base 10: '188-' |
ebs557458e |
invalid literal for int() with base 10: '185-' |
ebs557460e |
invalid literal for int() with base 10: '187-' |
ebs557465e |
invalid literal for int() with base 10: '182-' |
ebs557468e |
invalid literal for int() with base 10: '189-' |
ebs557470e |
invalid literal for int() with base 10: '189-' |
ebs557479e |
invalid literal for int() with base 10: '178-' |
ebs557484e |
invalid literal for int() with base 10: '184-' |
ebs557487e |
invalid literal for int() with base 10: '187-' |
ebs557488e |
invalid literal for int() with base 10: '183-' |
ebs557489e |
invalid literal for int() with base 10: '185-' |
ebs557490e |
invalid literal for int() with base 10: '184-' |
ebs557491e |
invalid literal for int() with base 10: '188-' |
ebs557493e |
invalid literal for int() with base 10: '184-' |
ebs557501e |
invalid literal for int() with base 10: '182-' |
ebs557503e |
invalid literal for int() with base 10: '188-' |
Found by the #6464 comparison; same family as the transformer strictness rejects in #6472. A good first pipeline issue: the failing code, the failing records, and the verification path are all identified below.
What is wrong
54 EBSCO records fail the Axiell/EBSCO Python transformer deterministically and therefore have no works in the 2026-07-03 pipeline. Production still has works for them because they were transformed by the previous Scala transformer, which tolerated these values. The records themselves are live in the EBSCO adapter store and re-fail identically on every run, most recently in the changeset redrive of
c2dce996-824c-11f1-afb6-0a58a9feac02(jobreplay-6464-ebsco-c2dce996, 2026-08-03), which is also what fired theebsco-transformer-failures-2026-07-03alarm that day.Two failure classes:
qis unhandled.catalogue_graph/src/adapters/transformers/ebsco/parsers/field008.py(maximal_date_range) handles date typesn | c u s r t d mand raisesNotImplementedErrorfor anything else.qis a valid MARC 21 code meaning "questionable date: between date 1 and date 2", so the reasonable implementation treats it like thed/mrange case. The method's docstring is doctest-driven, so add aqexample there.catalogue_graph/src/adapters/transformers/ebsco/parsers/period.pybuilds date ranges withint(year)and raises on values that occur in this data, such as182-(a MARC convention for the 1820s),- 1792, and1742 - 44. The parser needs to recover what it can (for example treat182-as the decade) or degrade to a label-only date rather than failing the whole record.How to work on it
All the relevant code is Python under
catalogue_graph/. Install uv, then fromcatalogue_graph/:That subset covers the EBSCO transformer and passes before any changes. Keep the behaviour of valid input unchanged: the point is tolerating these shapes, not loosening date parsing generally.
Reproducing the failures without AWS
The date parsing is pure functions. The unhandled 008 date type reproduces in one line:
To run a whole record through the real transformation, drive the work builder directly. Save this as
repro.pyincatalogue_graph/and runuv run python repro.py:The 008 here is the real one from
ebs28825414e, so this currently raises theNotImplementedErrorabove; after the fix it prints the transformedSourceWork. Swap in content fetched from the adapter store to reproduce the period parser failures; the next section shows how to get it.The
int()error messages in the table below show the fragment that reachedint()after the period parser's preprocess and crack steps, not the raw subfield contents.ebs144516efails withint('182-')but its actual 260$c is1825-[19--?], and theint('1742 - 44')inebs28836058ecomes fromM,DCC,XLII-M,DCC,XLIV. [1742-44]. Fetch the real record before writing fixtures rather than reverse-engineering inputs from the error messages.Querying the real records from the adapter store
Use
catalogue_graph/notebooks/adapter_source_tables.ipynb. It loads the EBSCO, Axiell and FOLIO Iceberg adapter tables, fetches a record by id, pretty-prints its MARC XML, and runs the matching transformer over it in-process.aws sso login --profile platform-developer. The notebook setsAWS_PROFILE=platform-developer, which the S3 Tables REST API needs.catalogue_graph/, runuv run --with jupyter jupyter lab, or open the notebook in your editor against the project.venv;ipykernelis already a dev dependency.example_id = "ebs144516e". Theexample_table_namealready defaults to the EBSCO table.failure_countin the pipeline.Leave
USE_REST_API_TABLE = TrueandALLOW_REMOTE_TABLE_MUTATIONS = Falseas they are: reads are safe, and the destructive helpers further down the notebook are guarded behind that flag.Where the tests live
field008.pyandperiod.py. Run them withuv run pytest --doctest-modules src/adapters/transformers/ebsco/parsers --no-cov. They are not part of the defaultuv run pytestrun, so new behaviour needs a doctest example and a case in the regular tests too.tests/adapters/transformers/ebsco/unit/test_production.pyasserts on the 008 fall-back and date ranges, using themarc_recordfixture fromunit/conftest.pyto build minimal pymarc records. Real 008 and 260$c values from the failing records make good cases here.tests/adapters/transformers/ebsco/test_transformer.pyruns the transformer end to end against a local Iceberg table with raw MARC XML strings as input. It is a good template for a whole-record regression test using one of the failing records.How to verify the fix in the pipeline
After merge and deploy, re-run the transformer for the changeset that contains all 54 records (any platform engineer can run this, or ask in the channel):
The execution output reports
success_countandfailure_count: before the fix the run ends with 54 failures, after it it should end with 0, and the 54 records should then exist inworks-source-2026-07-03(spot-checkWork[ebsco-alt-lookup/ebs144516e]).The 54 failing records
ebs144516eebs28825414eebs28825932eebs28830589eebs28830622eebs28835937eebs28836058eebs28836516eebs28836743eebs28837484eebs28838441eebs28840993eebs28841900eebs28841925eebs28842707eebs28844394eebs28845289eebs28845635eebs28845878eebs28846763eebs28848675eebs28849323eebs28849684eebs28851700eebs28852745eebs28864322eebs28864387eebs28864493eebs375800eebs557428eebs557433eebs557434eebs557436eebs557439eebs557444eebs557445eebs557446eebs557448eebs557451eebs557458eebs557460eebs557465eebs557468eebs557470eebs557479eebs557484eebs557487eebs557488eebs557489eebs557490eebs557491eebs557493eebs557501eebs557503e