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.
- marc_bibframe-0.2.1/.github/workflows/dependency-updates.yml +143 -0
- marc_bibframe-0.2.1/.github/workflows/publish.yml +51 -0
- marc_bibframe-0.2.1/.github/workflows/test.yml +44 -0
- marc_bibframe-0.2.1/.gitignore +10 -0
- marc_bibframe-0.2.1/LICENSE +21 -0
- marc_bibframe-0.2.1/PKG-INFO +145 -0
- marc_bibframe-0.2.1/README.md +125 -0
- marc_bibframe-0.2.1/patches/0001-bcp47-require-nonempty-script.patch +28 -0
- marc_bibframe-0.2.1/pyproject.toml +49 -0
- marc_bibframe-0.2.1/scripts/vendor.py +94 -0
- marc_bibframe-0.2.1/src/marc_bibframe/__init__.py +175 -0
- marc_bibframe-0.2.1/src/marc_bibframe/cli.py +150 -0
- marc_bibframe-0.2.1/src/marc_bibframe/py.typed +0 -0
- marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-001-007.xsl +2276 -0
- marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-006,008.xsl +1295 -0
- marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-010-048.xsl +1712 -0
- marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-050-088.xsl +713 -0
- marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-1XX,7XX,8XX-names.xsl +1141 -0
- marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-200-247not240-Titles.xsl +771 -0
- marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-240andX30-UnifTitle.xsl +535 -0
- marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-250-270.xsl +283 -0
- marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-3XX.xsl +2245 -0
- marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-460-468-SeriesTreat.xsl +135 -0
- marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-490-510-Links.xsl +145 -0
- marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-5XX.xsl +1448 -0
- marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-600-662.xsl +1417 -0
- marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-720+740to755.xsl +333 -0
- marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-758.xsl +102 -0
- marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-760-788-Links.xsl +771 -0
- marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-841-887.xsl +328 -0
- marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-880.xsl +119 -0
- marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-ControlSubfields.xsl +495 -0
- marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-LDR.xsl +183 -0
- marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-Preprocess0-Splitting.xsl +765 -0
- marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-Process6-Series.xsl +627 -0
- marc_bibframe-0.2.1/src/marc_bibframe/xsl/ConvSpec-Process8-ProvAct.xsl +1021 -0
- marc_bibframe-0.2.1/src/marc_bibframe/xsl/LICENSE +116 -0
- marc_bibframe-0.2.1/src/marc_bibframe/xsl/UPSTREAM +3 -0
- marc_bibframe-0.2.1/src/marc_bibframe/xsl/conf/abbreviations.xml +13 -0
- marc_bibframe-0.2.1/src/marc_bibframe/xsl/conf/code-to-script.xml +399 -0
- marc_bibframe-0.2.1/src/marc_bibframe/xsl/conf/codeMaps.xml +723 -0
- marc_bibframe-0.2.1/src/marc_bibframe/xsl/conf/exclusions.xml +4 -0
- marc_bibframe-0.2.1/src/marc_bibframe/xsl/conf/iso6392-to-1.xml +615 -0
- marc_bibframe-0.2.1/src/marc_bibframe/xsl/conf/languageCrosswalk.xml +1503 -0
- marc_bibframe-0.2.1/src/marc_bibframe/xsl/conf/map880.xml +175 -0
- marc_bibframe-0.2.1/src/marc_bibframe/xsl/conf/scriptCrosswalk.xml +210 -0
- marc_bibframe-0.2.1/src/marc_bibframe/xsl/conf/subjectThesaurus.xml +29 -0
- marc_bibframe-0.2.1/src/marc_bibframe/xsl/marc2bibframe2.xsl +610 -0
- marc_bibframe-0.2.1/src/marc_bibframe/xsl/naco-normalize.xsl +205 -0
- marc_bibframe-0.2.1/src/marc_bibframe/xsl/utils.xsl +964 -0
- marc_bibframe-0.2.1/src/marc_bibframe/xsl/variables.xsl +108 -0
- marc_bibframe-0.2.1/tests/__init__.py +0 -0
- marc_bibframe-0.2.1/tests/conftest.py +20 -0
- marc_bibframe-0.2.1/tests/fixtures/russian-no-script.xml +12 -0
- marc_bibframe-0.2.1/tests/fixtures/verne.mrc +1 -0
- marc_bibframe-0.2.1/tests/fixtures/verne.xml +19 -0
- marc_bibframe-0.2.1/tests/test_cli.py +112 -0
- marc_bibframe-0.2.1/tests/test_marc_bibframe.py +153 -0
- marc_bibframe-0.2.1/tests/test_upstream.py +56 -0
- 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,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
|
+
[](https://github.com/blue-core-lod/marc-bibframe/actions/workflows/test.yml)
|
|
24
|
+
[](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
|
+
[](https://github.com/blue-core-lod/marc-bibframe/actions/workflows/test.yml)
|
|
4
|
+
[](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"
|