marc-bibframe 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. marc_bibframe-0.2.1/.github/workflows/dependency-updates.yml +143 -0
  2. marc_bibframe-0.2.1/.github/workflows/publish.yml +51 -0
  3. marc_bibframe-0.2.1/.github/workflows/test.yml +44 -0
  4. marc_bibframe-0.2.1/.gitignore +10 -0
  5. marc_bibframe-0.2.1/LICENSE +21 -0
  6. marc_bibframe-0.2.1/PKG-INFO +145 -0
  7. marc_bibframe-0.2.1/README.md +125 -0
  8. marc_bibframe-0.2.1/patches/0001-bcp47-require-nonempty-script.patch +28 -0
  9. marc_bibframe-0.2.1/pyproject.toml +49 -0
  10. marc_bibframe-0.2.1/scripts/vendor.py +94 -0
  11. marc_bibframe-0.2.1/src/marc_bibframe/__init__.py +175 -0
  12. marc_bibframe-0.2.1/src/marc_bibframe/cli.py +150 -0
  13. marc_bibframe-0.2.1/src/marc_bibframe/py.typed +0 -0
  14. marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-001-007.xsl +2276 -0
  15. marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-006,008.xsl +1295 -0
  16. marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-010-048.xsl +1712 -0
  17. marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-050-088.xsl +713 -0
  18. marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-1XX,7XX,8XX-names.xsl +1141 -0
  19. marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-200-247not240-Titles.xsl +771 -0
  20. marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-240andX30-UnifTitle.xsl +535 -0
  21. marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-250-270.xsl +283 -0
  22. marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-3XX.xsl +2245 -0
  23. marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-460-468-SeriesTreat.xsl +135 -0
  24. marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-490-510-Links.xsl +145 -0
  25. marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-5XX.xsl +1448 -0
  26. marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-600-662.xsl +1417 -0
  27. marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-720+740to755.xsl +333 -0
  28. marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-758.xsl +102 -0
  29. marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-760-788-Links.xsl +771 -0
  30. marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-841-887.xsl +328 -0
  31. marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-880.xsl +119 -0
  32. marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-ControlSubfields.xsl +495 -0
  33. marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-LDR.xsl +183 -0
  34. marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-Preprocess0-Splitting.xsl +765 -0
  35. marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-Process6-Series.xsl +627 -0
  36. marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-Process8-ProvAct.xsl +1021 -0
  37. marc_bibframe-0.2.1/src/marc_bibframe/xsl/LICENSE +116 -0
  38. marc_bibframe-0.2.1/src/marc_bibframe/xsl/UPSTREAM +3 -0
  39. marc_bibframe-0.2.1/src/marc_bibframe/xsl/conf/abbreviations.xml +13 -0
  40. marc_bibframe-0.2.1/src/marc_bibframe/xsl/conf/code-to-script.xml +399 -0
  41. marc_bibframe-0.2.1/src/marc_bibframe/xsl/conf/codeMaps.xml +723 -0
  42. marc_bibframe-0.2.1/src/marc_bibframe/xsl/conf/exclusions.xml +4 -0
  43. marc_bibframe-0.2.1/src/marc_bibframe/xsl/conf/iso6392-to-1.xml +615 -0
  44. marc_bibframe-0.2.1/src/marc_bibframe/xsl/conf/languageCrosswalk.xml +1503 -0
  45. marc_bibframe-0.2.1/src/marc_bibframe/xsl/conf/map880.xml +175 -0
  46. marc_bibframe-0.2.1/src/marc_bibframe/xsl/conf/scriptCrosswalk.xml +210 -0
  47. marc_bibframe-0.2.1/src/marc_bibframe/xsl/conf/subjectThesaurus.xml +29 -0
  48. marc_bibframe-0.2.1/src/marc_bibframe/xsl/marc2bibframe2.xsl +610 -0
  49. marc_bibframe-0.2.1/src/marc_bibframe/xsl/naco-normalize.xsl +205 -0
  50. marc_bibframe-0.2.1/src/marc_bibframe/xsl/utils.xsl +964 -0
  51. marc_bibframe-0.2.1/src/marc_bibframe/xsl/variables.xsl +108 -0
  52. marc_bibframe-0.2.1/tests/__init__.py +0 -0
  53. marc_bibframe-0.2.1/tests/conftest.py +20 -0
  54. marc_bibframe-0.2.1/tests/fixtures/russian-no-script.xml +12 -0
  55. marc_bibframe-0.2.1/tests/fixtures/verne.mrc +1 -0
  56. marc_bibframe-0.2.1/tests/fixtures/verne.xml +19 -0
  57. marc_bibframe-0.2.1/tests/test_cli.py +112 -0
  58. marc_bibframe-0.2.1/tests/test_marc_bibframe.py +153 -0
  59. marc_bibframe-0.2.1/tests/test_upstream.py +56 -0
  60. marc_bibframe-0.2.1/uv.lock +300 -0
@@ -0,0 +1,143 @@
1
+ name: Weekly Dependency Updates
2
+
3
+ on:
4
+ schedule:
5
+ - cron: '0 9 * * 1' # Every Monday at 9 AM UTC
6
+ workflow_dispatch: # Enables the "Run workflow" button in GitHub
7
+
8
+ jobs:
9
+ update-dependencies:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ # The lowest supported version, which is the one most likely to catch
14
+ # an incompatibility. The pull request this opens runs the full matrix.
15
+ python-version: [3.12]
16
+ permissions:
17
+ contents: write
18
+ pull-requests: write
19
+ steps:
20
+ - name: Checkout
21
+ uses: actions/checkout@v4
22
+
23
+ - name: Set up Python ${{ matrix.python-version }}
24
+ uses: actions/setup-python@v5
25
+ with:
26
+ python-version: ${{ matrix.python-version }}
27
+
28
+ - name: Install uv
29
+ uses: astral-sh/setup-uv@v5
30
+
31
+ - name: Update dependencies
32
+ run: uv lock --upgrade
33
+
34
+ - name: Check for dependency changes
35
+ id: check_changes
36
+ run: |
37
+ if git diff --quiet uv.lock; then
38
+ echo "changed=false" >> $GITHUB_OUTPUT
39
+ else
40
+ echo "changed=true" >> $GITHUB_OUTPUT
41
+ fi
42
+
43
+ - name: Install dependencies
44
+ if: steps.check_changes.outputs.changed == 'true'
45
+ run: uv sync
46
+
47
+ # The same checks CI runs, in the same order. A dependency bump can break
48
+ # formatting or typing as easily as it breaks a test -- ruff and ty are
49
+ # themselves dependencies here.
50
+ - name: Format
51
+ if: steps.check_changes.outputs.changed == 'true'
52
+ run: uv run ruff format --diff
53
+
54
+ - name: Lint
55
+ if: steps.check_changes.outputs.changed == 'true'
56
+ run: uv run ruff check
57
+
58
+ - name: Types
59
+ if: steps.check_changes.outputs.changed == 'true'
60
+ run: uv run ty check
61
+
62
+ - name: Run tests
63
+ if: steps.check_changes.outputs.changed == 'true'
64
+ env:
65
+ # The upstream-version test calls the GitHub API. Without a token the
66
+ # anonymous limit is 60/hour per IP, which shared runners share, and
67
+ # the test skips instead of checking anything.
68
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
69
+ run: uv run pytest
70
+
71
+ - name: Bump patch version
72
+ id: bump_version
73
+ if: steps.check_changes.outputs.changed == 'true'
74
+ run: |
75
+ NEW_VERSION=$(python3 -c "
76
+ import re
77
+ with open('pyproject.toml') as f:
78
+ content = f.read()
79
+ m = re.search(r'version = \"(\d+)\.(\d+)\.(\d+)\"', content)
80
+ major, minor, patch = m.groups()
81
+ print(f'{major}.{minor}.{int(patch) + 1}')
82
+ ")
83
+ sed -i "s/^version = \"[0-9]*\.[0-9]*\.[0-9]*\"/version = \"$NEW_VERSION\"/" pyproject.toml
84
+ echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
85
+
86
+ - name: Sync lockfile with bumped version
87
+ if: steps.check_changes.outputs.changed == 'true'
88
+ run: uv lock
89
+
90
+ - name: Create Pull Request
91
+ id: create_pr
92
+ if: steps.check_changes.outputs.changed == 'true' && success()
93
+ uses: peter-evans/create-pull-request@v7
94
+ with:
95
+ token: ${{ secrets.GITHUB_TOKEN }}
96
+ branch: dependency-updates
97
+ base: main
98
+ title: 'Weekly dependency updates (${{ steps.bump_version.outputs.new_version }})'
99
+ body: |
100
+ Weekly automated dependency updates.
101
+
102
+ - Updated `uv.lock` via `uv lock --upgrade`
103
+ - Bumped patch version to `${{ steps.bump_version.outputs.new_version }}`
104
+
105
+ Publishing happens on release, so merging this prepares the next
106
+ version rather than releasing it.
107
+
108
+ Note this updates the Python dependencies only. The vendored LC
109
+ stylesheets track upstream separately, via `./scripts/vendor.py`.
110
+ commit-message: 'Update dependencies and bump patch version to ${{ steps.bump_version.outputs.new_version }}'
111
+ labels: dependencies
112
+
113
+ # Skipped rather than failed when no webhook is configured.
114
+ - name: Notify Slack
115
+ if: steps.check_changes.outputs.changed == 'true' && success()
116
+ env:
117
+ SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
118
+ PR_URL: ${{ steps.create_pr.outputs.pull-request-url }}
119
+ NEW_VERSION: ${{ steps.bump_version.outputs.new_version }}
120
+ REPO_NAME: ${{ github.event.repository.name }}
121
+ run: |
122
+ if [ -z "$SLACK_WEBHOOK_URL" ]; then
123
+ echo "No SLACK_WEBHOOK_URL configured, skipping."
124
+ exit 0
125
+ fi
126
+ curl -X POST -H 'Content-type: application/json' \
127
+ --data "{\"text\": \"$REPO_NAME dependency update PR opened: <$PR_URL|v$NEW_VERSION>\"}" \
128
+ "$SLACK_WEBHOOK_URL"
129
+
130
+ - name: Notify Slack on failure
131
+ if: steps.check_changes.outputs.changed == 'true' && failure()
132
+ env:
133
+ SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
134
+ REPO_NAME: ${{ github.event.repository.name }}
135
+ RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
136
+ run: |
137
+ if [ -z "$SLACK_WEBHOOK_URL" ]; then
138
+ echo "No SLACK_WEBHOOK_URL configured, skipping."
139
+ exit 0
140
+ fi
141
+ curl -X POST -H 'Content-type: application/json' \
142
+ --data "{\"text\": \"$REPO_NAME dependency update failed: <$RUN_URL|view run>\"}" \
143
+ "$SLACK_WEBHOOK_URL"
@@ -0,0 +1,51 @@
1
+ name: Publish to PyPI
2
+
3
+ # Requires this repo to be registered as a trusted publisher on PyPI, so no
4
+ # API token is needed: https://docs.pypi.org/trusted-publishers/
5
+
6
+ on:
7
+ release:
8
+ types:
9
+ published
10
+
11
+ jobs:
12
+ publish:
13
+ runs-on: ubuntu-latest
14
+
15
+ environment:
16
+ name: pypi
17
+ url: https://pypi.org/p/marc-bibframe
18
+
19
+ permissions:
20
+ id-token: write
21
+
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+
25
+ - name: Install uv
26
+ uses: astral-sh/setup-uv@v5
27
+
28
+ # Don't publish something that doesn't pass its own tests.
29
+ - name: Check
30
+ env:
31
+ # The upstream-version test calls the GitHub API. Without a token the
32
+ # anonymous limit is 60/hour per IP, which shared runners share, and
33
+ # the test skips instead of checking anything.
34
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
35
+ run: |
36
+ uv run ruff format --diff
37
+ uv run ruff check
38
+ uv run ty check
39
+ uv run pytest
40
+
41
+ # The vendored stylesheets are the whole point of the package, so make
42
+ # sure a release ships upstream-plus-patches and not a hand-edited tree.
43
+ - name: Vendored stylesheets match upstream plus patches
44
+ run: |
45
+ uv run ./scripts/vendor.py "$(grep '^tag:' src/marc_bibframe/xsl/UPSTREAM | cut -d' ' -f2)"
46
+ git diff --exit-code -- src/marc_bibframe/xsl
47
+
48
+ - name: Publish
49
+ run: |
50
+ uv build
51
+ uv publish
@@ -0,0 +1,44 @@
1
+ name: Test
2
+ on:
3
+ - push
4
+ - pull_request
5
+ jobs:
6
+ build:
7
+ runs-on: ubuntu-latest
8
+ strategy:
9
+ matrix:
10
+ python-version: ["3.12", "3.13"]
11
+ steps:
12
+
13
+ - name: checkout
14
+ uses: actions/checkout@v4
15
+
16
+ - name: Set up Python ${{ matrix.python-version }}
17
+ uses: actions/setup-python@v5
18
+ with:
19
+ python-version: ${{ matrix.python-version }}
20
+
21
+ - name: Install uv
22
+ uses: astral-sh/setup-uv@v5
23
+
24
+ - name: Formatting
25
+ run: uv run ruff format --diff
26
+
27
+ - name: Linting
28
+ run: uv run ruff check
29
+
30
+ - name: Type Checking
31
+ run: uv run ty check
32
+
33
+ - name: Tests
34
+ env:
35
+ # The upstream-version test calls the GitHub API. Without a token the
36
+ # anonymous limit is 60/hour per IP, which shared runners share, and
37
+ # the test skips instead of checking anything.
38
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
39
+ run: uv run pytest
40
+
41
+ - name: Vendored stylesheets match upstream plus patches
42
+ run: |
43
+ uv run ./scripts/vendor.py "$(grep '^tag:' src/marc_bibframe/xsl/UPSTREAM | cut -d' ' -f2)"
44
+ git diff --exit-code -- src/marc_bibframe/xsl
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ .mypy_cache/
10
+ .coverage
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Blue Core
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,145 @@
1
+ Metadata-Version: 2.5
2
+ Name: marc-bibframe
3
+ Version: 0.2.1
4
+ Summary: Convert MARC records to BIBFRAME RDF using the Library of Congress marc2bibframe2 XSLT
5
+ Project-URL: Homepage, https://github.com/blue-core-lod/marc-bibframe
6
+ Project-URL: Issues, https://github.com/blue-core-lod/marc-bibframe/issues
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Keywords: bibframe,libraries,marc,marc2bibframe2,marcxml,rdf
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Topic :: Text Processing :: Markup :: XML
15
+ Requires-Python: >=3.12
16
+ Requires-Dist: lxml>=6.0.2
17
+ Requires-Dist: pymarc>=5.2.0
18
+ Requires-Dist: rdflib>=7.1.3
19
+ Description-Content-Type: text/markdown
20
+
21
+ # marc-bibframe
22
+
23
+ [![Test](https://github.com/blue-core-lod/marc-bibframe/actions/workflows/test.yml/badge.svg)](https://github.com/blue-core-lod/marc-bibframe/actions/workflows/test.yml)
24
+ [![PyPI](https://img.shields.io/pypi/v/marc-bibframe)](https://pypi.org/project/marc-bibframe/)
25
+
26
+ Convert MARC records to BIBFRAME RDF in Python, using the Library of Congress
27
+ [marc2bibframe2](https://github.com/lcnetdev/marc2bibframe2) XSLT.
28
+
29
+ The stylesheet is vendored in this package, so there is nothing to install
30
+ alongside it and no Java or Saxon involved, just `lxml`, `pymarc` and
31
+ `rdflib`.
32
+
33
+ ## Install
34
+
35
+ ```
36
+ pip install marc-bibframe
37
+ ```
38
+
39
+ ## Command line
40
+
41
+ You can use it from the command line:
42
+
43
+ ```
44
+ $ marc-bibframe record.mrc
45
+ $ marc-bibframe record.xml --format json-ld --baseuri https://example.edu/catalog/
46
+ $ yaz-marcdump -o marcxml big.mrc | marc-bibframe -f nt -o big.nt
47
+ ```
48
+
49
+ Binary MARC21 and MARCXML are both accepted and told apart by their content,
50
+ so there is no flag for it. With no filename it reads standard input.
51
+
52
+ `--format` takes `turtle` (the default), `json-ld`, `ntriples`, `xml`, or
53
+ `rdfxml`. The last is the stylesheet's own output passed through without a
54
+ parse into rdflib, which is faster and keeps the transform's exact shape.
55
+
56
+ `marc-bibframe --help` lists the stylesheet parameters, which are also
57
+ available as flags.
58
+
59
+ ## Use as a library
60
+
61
+ Or you can use it as a function in your own Python programs:
62
+
63
+ ```python
64
+ from marc_bibframe import marc_to_graph
65
+
66
+ with open("record.mrc", "rb") as fh:
67
+ graph = marc_to_graph(fh, baseuri="https://example.edu/catalog/")
68
+
69
+ print(graph.serialize(format="turtle"))
70
+ ```
71
+
72
+ MARCXML is accepted directly, and RDF/XML is available if you would rather not
73
+ pay for a parse into an rdflib.Graph:
74
+
75
+ ```python
76
+ from marc_bibframe import marc_to_marcxml, marcxml_to_graph, marcxml_to_rdfxml
77
+
78
+ marcxml = marc_to_marcxml(open("record.mrc", "rb").read())
79
+ graph = marcxml_to_graph(marcxml)
80
+ rdfxml = marcxml_to_rdfxml(marcxml)
81
+ ```
82
+
83
+ All four functions accept the stylesheet's parameters as keyword arguments —
84
+ `baseuri`, `idfield`, `idsource`, `localfields`, `bcp47_inference` and
85
+ `generation_datestamp`. See the docstring on `marcxml_to_rdfxml` for what each
86
+ one does.
87
+
88
+ ## A note on the URIs it mints
89
+
90
+ MARC does not identify most of what it describes, so the transform invents
91
+ URIs for them, built from `baseuri` and the record's own id:
92
+
93
+ ```
94
+ https://example.edu/catalog/99123456#Work
95
+ https://example.edu/catalog/99123456#Instance
96
+ https://example.edu/catalog/99123456#Agent100-3
97
+ https://example.edu/catalog/99123456#Topic650-5
98
+ ```
99
+
100
+ These are **not** authority URIs. They are scoped to the record they came
101
+ from, so two records describing the same person produce two different agent
102
+ URIs. If you need them reconciled against an authority (LC, Wikidata, your
103
+ own store) that happens after this library hands you the graph.
104
+
105
+ For the same reason the default `baseuri` is `http://example.org/`, matching
106
+ the stylesheet's own. Pick something under your control, and prefer a
107
+ namespace that cannot be mistaken for an authority's: minting
108
+ `http://id.loc.gov/resources/99123456#Agent100-3` produces URIs that look like
109
+ LC's but are not.
110
+
111
+ ## Keeping up with upstream
112
+
113
+ `src/marc_bibframe/xsl/` is a copy of the LC stylesheets — see
114
+ [`UPSTREAM`](src/marc_bibframe/xsl/UPSTREAM) for the tag and commit. Local
115
+ changes are not edited in place; they live as patches in
116
+ [`patches/`](patches/) and are reapplied on every re-vendor:
117
+
118
+ ```
119
+ ./scripts/vendor.py v3.2.0
120
+ ```
121
+
122
+ If a patch stops applying the script says so and stops, which is usually the
123
+ signal that it was fixed upstream and can be deleted. The aim is to keep
124
+ `patches/` empty; anything in there should also be reported to LC.
125
+
126
+ The test suite checks the vendored tag against LC's latest release and fails
127
+ when there is a newer one, with the command to run. It skips rather than fails
128
+ if the GitHub API is unreachable.
129
+
130
+ The stylesheets are [CC0](src/marc_bibframe/xsl/LICENSE). This wrapper is
131
+ [MIT](LICENSE).
132
+
133
+ ## Reproducible output
134
+
135
+ The transform stamps the current time into the work's admin metadata.
136
+ `generation_datestamp` (`--datestamp`) overrides it, which makes the RDF/XML
137
+ byte-for-byte reproducible. The other serializations still vary between runs:
138
+ rdflib mints fresh blank node labels every time it serializes.
139
+
140
+ ## Development
141
+
142
+ ```
143
+ uv sync
144
+ uv run pytest
145
+ ```
@@ -0,0 +1,125 @@
1
+ # marc-bibframe
2
+
3
+ [![Test](https://github.com/blue-core-lod/marc-bibframe/actions/workflows/test.yml/badge.svg)](https://github.com/blue-core-lod/marc-bibframe/actions/workflows/test.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/marc-bibframe)](https://pypi.org/project/marc-bibframe/)
5
+
6
+ Convert MARC records to BIBFRAME RDF in Python, using the Library of Congress
7
+ [marc2bibframe2](https://github.com/lcnetdev/marc2bibframe2) XSLT.
8
+
9
+ The stylesheet is vendored in this package, so there is nothing to install
10
+ alongside it and no Java or Saxon involved, just `lxml`, `pymarc` and
11
+ `rdflib`.
12
+
13
+ ## Install
14
+
15
+ ```
16
+ pip install marc-bibframe
17
+ ```
18
+
19
+ ## Command line
20
+
21
+ You can use it from the command line:
22
+
23
+ ```
24
+ $ marc-bibframe record.mrc
25
+ $ marc-bibframe record.xml --format json-ld --baseuri https://example.edu/catalog/
26
+ $ yaz-marcdump -o marcxml big.mrc | marc-bibframe -f nt -o big.nt
27
+ ```
28
+
29
+ Binary MARC21 and MARCXML are both accepted and told apart by their content,
30
+ so there is no flag for it. With no filename it reads standard input.
31
+
32
+ `--format` takes `turtle` (the default), `json-ld`, `ntriples`, `xml`, or
33
+ `rdfxml`. The last is the stylesheet's own output passed through without a
34
+ parse into rdflib, which is faster and keeps the transform's exact shape.
35
+
36
+ `marc-bibframe --help` lists the stylesheet parameters, which are also
37
+ available as flags.
38
+
39
+ ## Use as a library
40
+
41
+ Or you can use it as a function in your own Python programs:
42
+
43
+ ```python
44
+ from marc_bibframe import marc_to_graph
45
+
46
+ with open("record.mrc", "rb") as fh:
47
+ graph = marc_to_graph(fh, baseuri="https://example.edu/catalog/")
48
+
49
+ print(graph.serialize(format="turtle"))
50
+ ```
51
+
52
+ MARCXML is accepted directly, and RDF/XML is available if you would rather not
53
+ pay for a parse into an rdflib.Graph:
54
+
55
+ ```python
56
+ from marc_bibframe import marc_to_marcxml, marcxml_to_graph, marcxml_to_rdfxml
57
+
58
+ marcxml = marc_to_marcxml(open("record.mrc", "rb").read())
59
+ graph = marcxml_to_graph(marcxml)
60
+ rdfxml = marcxml_to_rdfxml(marcxml)
61
+ ```
62
+
63
+ All four functions accept the stylesheet's parameters as keyword arguments —
64
+ `baseuri`, `idfield`, `idsource`, `localfields`, `bcp47_inference` and
65
+ `generation_datestamp`. See the docstring on `marcxml_to_rdfxml` for what each
66
+ one does.
67
+
68
+ ## A note on the URIs it mints
69
+
70
+ MARC does not identify most of what it describes, so the transform invents
71
+ URIs for them, built from `baseuri` and the record's own id:
72
+
73
+ ```
74
+ https://example.edu/catalog/99123456#Work
75
+ https://example.edu/catalog/99123456#Instance
76
+ https://example.edu/catalog/99123456#Agent100-3
77
+ https://example.edu/catalog/99123456#Topic650-5
78
+ ```
79
+
80
+ These are **not** authority URIs. They are scoped to the record they came
81
+ from, so two records describing the same person produce two different agent
82
+ URIs. If you need them reconciled against an authority (LC, Wikidata, your
83
+ own store) that happens after this library hands you the graph.
84
+
85
+ For the same reason the default `baseuri` is `http://example.org/`, matching
86
+ the stylesheet's own. Pick something under your control, and prefer a
87
+ namespace that cannot be mistaken for an authority's: minting
88
+ `http://id.loc.gov/resources/99123456#Agent100-3` produces URIs that look like
89
+ LC's but are not.
90
+
91
+ ## Keeping up with upstream
92
+
93
+ `src/marc_bibframe/xsl/` is a copy of the LC stylesheets — see
94
+ [`UPSTREAM`](src/marc_bibframe/xsl/UPSTREAM) for the tag and commit. Local
95
+ changes are not edited in place; they live as patches in
96
+ [`patches/`](patches/) and are reapplied on every re-vendor:
97
+
98
+ ```
99
+ ./scripts/vendor.py v3.2.0
100
+ ```
101
+
102
+ If a patch stops applying the script says so and stops, which is usually the
103
+ signal that it was fixed upstream and can be deleted. The aim is to keep
104
+ `patches/` empty; anything in there should also be reported to LC.
105
+
106
+ The test suite checks the vendored tag against LC's latest release and fails
107
+ when there is a newer one, with the command to run. It skips rather than fails
108
+ if the GitHub API is unreachable.
109
+
110
+ The stylesheets are [CC0](src/marc_bibframe/xsl/LICENSE). This wrapper is
111
+ [MIT](LICENSE).
112
+
113
+ ## Reproducible output
114
+
115
+ The transform stamps the current time into the work's admin metadata.
116
+ `generation_datestamp` (`--datestamp`) overrides it, which makes the RDF/XML
117
+ byte-for-byte reproducible. The other serializations still vary between runs:
118
+ rdflib mints fresh blank node labels every time it serializes.
119
+
120
+ ## Development
121
+
122
+ ```
123
+ uv sync
124
+ uv run pytest
125
+ ```
@@ -0,0 +1,28 @@
1
+ Subject: Don't infer a BCP-47 script subtag when no script was detected
2
+
3
+ In utils.xsl's language-tag handling, the $bcp47inferrence branch compares the
4
+ language's expected script against $vScript without first checking that
5
+ $vScript is non-empty. When a record yields no script, the comparison
6
+ "expected != ''" succeeds and the transform emits a bogus tag such as "ru-"
7
+ instead of plain "ru".
8
+
9
+ Requiring $vScript != '' makes the branch fall through to the plain language
10
+ code, which is the correct output.
11
+
12
+ Upstream status: not yet reported to lcnetdev/marc2bibframe2.
13
+ Origin: blue-core-lod/bluecore_api "Fix language tag bug" (2026-08-28).
14
+
15
+ diff --git a/src/marc_bibframe/xsl/utils.xsl b/src/marc_bibframe/xsl/utils.xsl
16
+ index 13b8f95..9f136e3 100644
17
+ --- a/src/marc_bibframe/xsl/utils.xsl
18
+ +++ b/src/marc_bibframe/xsl/utils.xsl
19
+ @@ -93,7 +93,8 @@
20
+ </xsl:variable>
21
+ <xsl:choose>
22
+ <xsl:when test="$vLang = 'en' and $vScript != '' and $vScript != 'latn'"><xsl:value-of select="concat('und-',$vScript)"/></xsl:when>
23
+ - <xsl:when test="$bcp47inferrence and
24
+ + <xsl:when test="$bcp47inferrence and
25
+ + $vScript != '' and
26
+ $code-to-script-map/code-to-script-map/entry[@key=$vLang] and
27
+ $code-to-script-map/code-to-script-map/entry[@key=$vLang]/value != $vScript">
28
+ <xsl:value-of select="concat($vLang,'-',$vScript)"/>
@@ -0,0 +1,49 @@
1
+ [project]
2
+ name = "marc-bibframe"
3
+ version = "0.2.1"
4
+ description = "Convert MARC records to BIBFRAME RDF using the Library of Congress marc2bibframe2 XSLT"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ license = "MIT"
8
+ keywords = ["marc", "bibframe", "marcxml", "rdf", "marc2bibframe2", "libraries"]
9
+ classifiers = [
10
+ "Development Status :: 3 - Alpha",
11
+ "Intended Audience :: Developers",
12
+ "Programming Language :: Python :: 3.12",
13
+ "Programming Language :: Python :: 3.13",
14
+ "Topic :: Text Processing :: Markup :: XML",
15
+ ]
16
+ dependencies = [
17
+ "lxml>=6.0.2",
18
+ "pymarc>=5.2.0",
19
+ "rdflib>=7.1.3",
20
+ ]
21
+
22
+ [project.scripts]
23
+ marc-bibframe = "marc_bibframe.cli:main"
24
+
25
+ [project.urls]
26
+ Homepage = "https://github.com/blue-core-lod/marc-bibframe"
27
+ Issues = "https://github.com/blue-core-lod/marc-bibframe/issues"
28
+
29
+ [dependency-groups]
30
+ dev = [
31
+ "pytest>=8.3.4",
32
+ "ruff>=0.9.6",
33
+ # ty resolves lxml through these; without them lxml.etree is unresolved.
34
+ "lxml-stubs>=0.5.1",
35
+ # Declared so CI resolves the same checker as a local `uv run ty check`,
36
+ # rather than whatever happens to be on PATH.
37
+ "ty>=0.0.1a1",
38
+ ]
39
+
40
+ [build-system]
41
+ requires = ["hatchling"]
42
+ build-backend = "hatchling.build"
43
+
44
+ [tool.hatch.build.targets.wheel]
45
+ packages = ["src/marc_bibframe"]
46
+
47
+ [tool.pytest.ini_options]
48
+ pythonpath = ["src"]
49
+ addopts = "-v"