opencost 0.1.0__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.
- opencost-0.1.0/PKG-INFO +186 -0
- opencost-0.1.0/README.md +166 -0
- opencost-0.1.0/pyproject.toml +77 -0
- opencost-0.1.0/pyproject.toml.orig +54 -0
- opencost-0.1.0/src/opencost/__init__.py +100 -0
- opencost-0.1.0/src/opencost/_common.py +9 -0
- opencost-0.1.0/src/opencost/_contract.py +103 -0
- opencost-0.1.0/src/opencost/_institution.py +59 -0
- opencost-0.1.0/src/opencost/_invoice.py +218 -0
- opencost-0.1.0/src/opencost/_publication.py +274 -0
- opencost-0.1.0/src/opencost/_types.py +85 -0
- opencost-0.1.0/src/opencost/_validators.py +37 -0
- opencost-0.1.0/src/opencost/xml/__init__.py +19 -0
- opencost-0.1.0/src/opencost/xml/_common.py +3 -0
- opencost-0.1.0/src/opencost/xml/_deserialize.py +139 -0
- opencost-0.1.0/src/opencost/xml/_serialize.py +104 -0
opencost-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: opencost
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pydantic models for the openCost publication-cost metadata schema, with XML de-/serialization
|
|
5
|
+
Keywords: openCost,scholarly publishing,APC,metadata,pydantic,XML
|
|
6
|
+
Author: Sven Marcus, Sergej Wildemann
|
|
7
|
+
Author-email: Sven Marcus <s.marcus@tu-braunschweig.de>, Sergej Wildemann <s.wildemann@tu-braunschweig.de>
|
|
8
|
+
License-Expression: GPL-3.0-or-later
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Dist: pydantic>=2.10
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# opencost
|
|
22
|
+
|
|
23
|
+
Pydantic models for the [openCost](https://github.com/opencost-de/opencost)
|
|
24
|
+
metadata schema — a schema for the **financial** metadata of scholarly
|
|
25
|
+
publications — plus a serializer that emits schema-valid openCost XML.
|
|
26
|
+
|
|
27
|
+
openCost describes two top-level entities, both collectable in one
|
|
28
|
+
`opencost:data` document:
|
|
29
|
+
|
|
30
|
+
- `publication` — a single article (or other publication) and its costs:
|
|
31
|
+
identifiers, paying institution, COAR publication type, and `cost_data`
|
|
32
|
+
(invoices with itemized amounts, or a link to a contract).
|
|
33
|
+
- `contract` — payment models such as transformative agreements,
|
|
34
|
+
memberships, or subscriptions, with grouped invoices per accounting period.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
uv add opencost # or: pip install opencost
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
### Building documents
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from decimal import Decimal
|
|
48
|
+
|
|
49
|
+
import opencost
|
|
50
|
+
|
|
51
|
+
publication = opencost.PublicationType(
|
|
52
|
+
primary_identifier=opencost.PublicationPrimaryIdentifier(doi="10.1234/abcd"),
|
|
53
|
+
institution=opencost.InstitutionType(
|
|
54
|
+
id=[opencost.InstitutionId(type="ror", value="010zzcb52")]
|
|
55
|
+
),
|
|
56
|
+
publication_type=opencost.CoarPublicationType.journal_article,
|
|
57
|
+
cost_data=opencost.PublicationCostDataType(
|
|
58
|
+
invoice=[
|
|
59
|
+
opencost.PublicationInvoiceType(
|
|
60
|
+
invoice_number="INV-4711",
|
|
61
|
+
creditor="Publisher GmbH",
|
|
62
|
+
dates=opencost.Dates(invoice="2026-05-01", paid="2026-05-20"),
|
|
63
|
+
amount_invoice=opencost.AmountInvoice(
|
|
64
|
+
amount=Decimal("1980.00"), currency="EUR"
|
|
65
|
+
),
|
|
66
|
+
amounts_paid=opencost.PublicationAmountsPaid(
|
|
67
|
+
amount_paid=[
|
|
68
|
+
opencost.PublicationAmountPaidType(
|
|
69
|
+
amount=Decimal("1650.00"),
|
|
70
|
+
currency="EUR",
|
|
71
|
+
cost_type="gold-oa",
|
|
72
|
+
vat=Decimal("342.00"),
|
|
73
|
+
)
|
|
74
|
+
]
|
|
75
|
+
),
|
|
76
|
+
)
|
|
77
|
+
]
|
|
78
|
+
),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
xml = opencost.to_xml(opencost.Data(publication=[publication]))
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
which produces
|
|
85
|
+
|
|
86
|
+
```xml
|
|
87
|
+
<data xmlns="https://opencost.de">
|
|
88
|
+
<publication>
|
|
89
|
+
<primary_identifier>
|
|
90
|
+
<doi>10.1234/abcd</doi>
|
|
91
|
+
</primary_identifier>
|
|
92
|
+
...
|
|
93
|
+
</publication>
|
|
94
|
+
</data>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Parsing documents
|
|
98
|
+
|
|
99
|
+
`from_xml` is the exact inverse — children are matched to fields by
|
|
100
|
+
element name, and every value is validated through the same models:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from pathlib import Path
|
|
104
|
+
|
|
105
|
+
data = opencost.from_xml(Path("report.xml").read_text())
|
|
106
|
+
|
|
107
|
+
for publication in data.publication or []:
|
|
108
|
+
doi = publication.primary_identifier.doi
|
|
109
|
+
for invoice in publication.cost_data.invoice or []:
|
|
110
|
+
for amount in invoice.amounts_paid.amount_paid:
|
|
111
|
+
print(doi, amount.amount, amount.currency, amount.cost_type.value)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Parsed values are fully typed (`Decimal` amounts, enum members, `bool`s),
|
|
115
|
+
unknown elements are rejected, and the round-trip is lossless:
|
|
116
|
+
`from_xml(to_xml(d)) == d`.
|
|
117
|
+
|
|
118
|
+
## Validation
|
|
119
|
+
|
|
120
|
+
The models enforce the schema rules that can be expressed as type
|
|
121
|
+
constraints:
|
|
122
|
+
|
|
123
|
+
- required lists (`Annotated[list[T], Field(min_length=1)]`) reject empty
|
|
124
|
+
lists — e.g. `PublicationSecondaryIdentifiers(id=[])` fails;
|
|
125
|
+
- either/or rules (`EitherFieldMixin`) — e.g. `Dates` needs `invoice` or
|
|
126
|
+
`paid`, `Data` needs `publication` or `contract`;
|
|
127
|
+
- exactly-one rules — `PublicationPrimaryIdentifier` takes a `doi` **or** a
|
|
128
|
+
`bibliographic_information` block, never both or neither;
|
|
129
|
+
- patterns — `Currency` is a three-letter ISO 4217 code, `DateFormat` is
|
|
130
|
+
`YYYY`, `YYYY-MM` or `YYYY-MM-DD`;
|
|
131
|
+
- strict model config — unknown/misspelled fields are rejected
|
|
132
|
+
(`extra="forbid"`), and aliased fields accept both the Python name and
|
|
133
|
+
the wire alias (`from_` or `from`).
|
|
134
|
+
|
|
135
|
+
### Validating generated documents
|
|
136
|
+
|
|
137
|
+
The XSD is owned by the upstream [opencost repository](https://github.com/opencost-de/opencost)
|
|
138
|
+
(`doc/opencost.xsd`). This package deliberately does **not** ship a copy —
|
|
139
|
+
validate generated documents against the upstream schema, e.g. with
|
|
140
|
+
[xmllint](https://xmlsoft.org/xmllint.html):
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
xmllint --noout --schema path/to/opencost/doc/opencost.xsd data.xml
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
In this repository the upstream schema is pinned as a git submodule
|
|
147
|
+
(`vendor/opencost`) and used by the test suite — it is a development-only
|
|
148
|
+
dependency, never a runtime one.
|
|
149
|
+
|
|
150
|
+
## XML (de)serialization
|
|
151
|
+
|
|
152
|
+
`to_xml` derives the XML shape from the models themselves:
|
|
153
|
+
|
|
154
|
+
- field declaration order → child order (deterministic; the XSD uses
|
|
155
|
+
`xs:all`/symmetric choices, so order is not semantically constrained);
|
|
156
|
+
- `None` → element omitted (`minOccurs=0`);
|
|
157
|
+
- `list` → repeated sibling elements (`maxOccurs="unbounded"`);
|
|
158
|
+
- field `alias` → element name (e.g. `from_` → `<from>`);
|
|
159
|
+
- enums → their XSD wire values (`journal article`, `gold-oa`);
|
|
160
|
+
- `bool` → `true`/`false`; `Decimal` → two decimal places.
|
|
161
|
+
|
|
162
|
+
`from_xml` is the exact inverse — the models drive parsing too: children
|
|
163
|
+
are matched to fields by element name, type coercion (Decimal, booleans,
|
|
164
|
+
enum-by-value, patterns) is pydantic's job, and unknown elements are
|
|
165
|
+
rejected via `extra="forbid"`. Documents with the default namespace or an
|
|
166
|
+
`opencost:` prefix parse identically. Round-trip stable:
|
|
167
|
+
`from_xml(to_xml(d)) == d`.
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
data = opencost.from_xml(Path("report.xml").read_text())
|
|
171
|
+
for pub in data.publication or []:
|
|
172
|
+
...
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Development
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
git submodule update --init # pins upstream schema for the validation tests
|
|
179
|
+
uv sync
|
|
180
|
+
uv run pytest
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## License
|
|
184
|
+
|
|
185
|
+
GPL-3.0-or-later.
|
|
186
|
+
|
opencost-0.1.0/README.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# opencost
|
|
2
|
+
|
|
3
|
+
Pydantic models for the [openCost](https://github.com/opencost-de/opencost)
|
|
4
|
+
metadata schema — a schema for the **financial** metadata of scholarly
|
|
5
|
+
publications — plus a serializer that emits schema-valid openCost XML.
|
|
6
|
+
|
|
7
|
+
openCost describes two top-level entities, both collectable in one
|
|
8
|
+
`opencost:data` document:
|
|
9
|
+
|
|
10
|
+
- `publication` — a single article (or other publication) and its costs:
|
|
11
|
+
identifiers, paying institution, COAR publication type, and `cost_data`
|
|
12
|
+
(invoices with itemized amounts, or a link to a contract).
|
|
13
|
+
- `contract` — payment models such as transformative agreements,
|
|
14
|
+
memberships, or subscriptions, with grouped invoices per accounting period.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
uv add opencost # or: pip install opencost
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
### Building documents
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from decimal import Decimal
|
|
28
|
+
|
|
29
|
+
import opencost
|
|
30
|
+
|
|
31
|
+
publication = opencost.PublicationType(
|
|
32
|
+
primary_identifier=opencost.PublicationPrimaryIdentifier(doi="10.1234/abcd"),
|
|
33
|
+
institution=opencost.InstitutionType(
|
|
34
|
+
id=[opencost.InstitutionId(type="ror", value="010zzcb52")]
|
|
35
|
+
),
|
|
36
|
+
publication_type=opencost.CoarPublicationType.journal_article,
|
|
37
|
+
cost_data=opencost.PublicationCostDataType(
|
|
38
|
+
invoice=[
|
|
39
|
+
opencost.PublicationInvoiceType(
|
|
40
|
+
invoice_number="INV-4711",
|
|
41
|
+
creditor="Publisher GmbH",
|
|
42
|
+
dates=opencost.Dates(invoice="2026-05-01", paid="2026-05-20"),
|
|
43
|
+
amount_invoice=opencost.AmountInvoice(
|
|
44
|
+
amount=Decimal("1980.00"), currency="EUR"
|
|
45
|
+
),
|
|
46
|
+
amounts_paid=opencost.PublicationAmountsPaid(
|
|
47
|
+
amount_paid=[
|
|
48
|
+
opencost.PublicationAmountPaidType(
|
|
49
|
+
amount=Decimal("1650.00"),
|
|
50
|
+
currency="EUR",
|
|
51
|
+
cost_type="gold-oa",
|
|
52
|
+
vat=Decimal("342.00"),
|
|
53
|
+
)
|
|
54
|
+
]
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
]
|
|
58
|
+
),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
xml = opencost.to_xml(opencost.Data(publication=[publication]))
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
which produces
|
|
65
|
+
|
|
66
|
+
```xml
|
|
67
|
+
<data xmlns="https://opencost.de">
|
|
68
|
+
<publication>
|
|
69
|
+
<primary_identifier>
|
|
70
|
+
<doi>10.1234/abcd</doi>
|
|
71
|
+
</primary_identifier>
|
|
72
|
+
...
|
|
73
|
+
</publication>
|
|
74
|
+
</data>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Parsing documents
|
|
78
|
+
|
|
79
|
+
`from_xml` is the exact inverse — children are matched to fields by
|
|
80
|
+
element name, and every value is validated through the same models:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from pathlib import Path
|
|
84
|
+
|
|
85
|
+
data = opencost.from_xml(Path("report.xml").read_text())
|
|
86
|
+
|
|
87
|
+
for publication in data.publication or []:
|
|
88
|
+
doi = publication.primary_identifier.doi
|
|
89
|
+
for invoice in publication.cost_data.invoice or []:
|
|
90
|
+
for amount in invoice.amounts_paid.amount_paid:
|
|
91
|
+
print(doi, amount.amount, amount.currency, amount.cost_type.value)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Parsed values are fully typed (`Decimal` amounts, enum members, `bool`s),
|
|
95
|
+
unknown elements are rejected, and the round-trip is lossless:
|
|
96
|
+
`from_xml(to_xml(d)) == d`.
|
|
97
|
+
|
|
98
|
+
## Validation
|
|
99
|
+
|
|
100
|
+
The models enforce the schema rules that can be expressed as type
|
|
101
|
+
constraints:
|
|
102
|
+
|
|
103
|
+
- required lists (`Annotated[list[T], Field(min_length=1)]`) reject empty
|
|
104
|
+
lists — e.g. `PublicationSecondaryIdentifiers(id=[])` fails;
|
|
105
|
+
- either/or rules (`EitherFieldMixin`) — e.g. `Dates` needs `invoice` or
|
|
106
|
+
`paid`, `Data` needs `publication` or `contract`;
|
|
107
|
+
- exactly-one rules — `PublicationPrimaryIdentifier` takes a `doi` **or** a
|
|
108
|
+
`bibliographic_information` block, never both or neither;
|
|
109
|
+
- patterns — `Currency` is a three-letter ISO 4217 code, `DateFormat` is
|
|
110
|
+
`YYYY`, `YYYY-MM` or `YYYY-MM-DD`;
|
|
111
|
+
- strict model config — unknown/misspelled fields are rejected
|
|
112
|
+
(`extra="forbid"`), and aliased fields accept both the Python name and
|
|
113
|
+
the wire alias (`from_` or `from`).
|
|
114
|
+
|
|
115
|
+
### Validating generated documents
|
|
116
|
+
|
|
117
|
+
The XSD is owned by the upstream [opencost repository](https://github.com/opencost-de/opencost)
|
|
118
|
+
(`doc/opencost.xsd`). This package deliberately does **not** ship a copy —
|
|
119
|
+
validate generated documents against the upstream schema, e.g. with
|
|
120
|
+
[xmllint](https://xmlsoft.org/xmllint.html):
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
xmllint --noout --schema path/to/opencost/doc/opencost.xsd data.xml
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
In this repository the upstream schema is pinned as a git submodule
|
|
127
|
+
(`vendor/opencost`) and used by the test suite — it is a development-only
|
|
128
|
+
dependency, never a runtime one.
|
|
129
|
+
|
|
130
|
+
## XML (de)serialization
|
|
131
|
+
|
|
132
|
+
`to_xml` derives the XML shape from the models themselves:
|
|
133
|
+
|
|
134
|
+
- field declaration order → child order (deterministic; the XSD uses
|
|
135
|
+
`xs:all`/symmetric choices, so order is not semantically constrained);
|
|
136
|
+
- `None` → element omitted (`minOccurs=0`);
|
|
137
|
+
- `list` → repeated sibling elements (`maxOccurs="unbounded"`);
|
|
138
|
+
- field `alias` → element name (e.g. `from_` → `<from>`);
|
|
139
|
+
- enums → their XSD wire values (`journal article`, `gold-oa`);
|
|
140
|
+
- `bool` → `true`/`false`; `Decimal` → two decimal places.
|
|
141
|
+
|
|
142
|
+
`from_xml` is the exact inverse — the models drive parsing too: children
|
|
143
|
+
are matched to fields by element name, type coercion (Decimal, booleans,
|
|
144
|
+
enum-by-value, patterns) is pydantic's job, and unknown elements are
|
|
145
|
+
rejected via `extra="forbid"`. Documents with the default namespace or an
|
|
146
|
+
`opencost:` prefix parse identically. Round-trip stable:
|
|
147
|
+
`from_xml(to_xml(d)) == d`.
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
data = opencost.from_xml(Path("report.xml").read_text())
|
|
151
|
+
for pub in data.publication or []:
|
|
152
|
+
...
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Development
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
git submodule update --init # pins upstream schema for the validation tests
|
|
159
|
+
uv sync
|
|
160
|
+
uv run pytest
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## License
|
|
164
|
+
|
|
165
|
+
GPL-3.0-or-later.
|
|
166
|
+
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "opencost"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Pydantic models for the openCost publication-cost metadata schema, with XML de-/serialization"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = "GPL-3.0-or-later"
|
|
8
|
+
keywords = [
|
|
9
|
+
"openCost",
|
|
10
|
+
"scholarly publishing",
|
|
11
|
+
"APC",
|
|
12
|
+
"metadata",
|
|
13
|
+
"pydantic",
|
|
14
|
+
"XML",
|
|
15
|
+
]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Intended Audience :: Science/Research",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Topic :: Scientific/Engineering",
|
|
24
|
+
"Typing :: Typed",
|
|
25
|
+
]
|
|
26
|
+
dependencies = ["pydantic>=2.10"]
|
|
27
|
+
|
|
28
|
+
[[project.authors]]
|
|
29
|
+
name = "Sven Marcus"
|
|
30
|
+
email = "s.marcus@tu-braunschweig.de"
|
|
31
|
+
|
|
32
|
+
[[project.authors]]
|
|
33
|
+
name = "Sergej Wildemann"
|
|
34
|
+
email = "s.wildemann@tu-braunschweig.de"
|
|
35
|
+
|
|
36
|
+
[dependency-groups]
|
|
37
|
+
dev = [
|
|
38
|
+
"lxml>=5.0",
|
|
39
|
+
"mypy>=1.14",
|
|
40
|
+
"pytest>=8.0",
|
|
41
|
+
"ruff>=0.9",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[build-system]
|
|
45
|
+
requires = ["uv_build>=0.12.3,<0.13.0"]
|
|
46
|
+
build-backend = "uv_build"
|
|
47
|
+
|
|
48
|
+
[tool.pytest.ini_options]
|
|
49
|
+
testpaths = ["tests"]
|
|
50
|
+
|
|
51
|
+
[tool.ruff]
|
|
52
|
+
line-length = 100
|
|
53
|
+
src = [
|
|
54
|
+
"src",
|
|
55
|
+
"tests",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
[tool.ruff.lint]
|
|
59
|
+
select = [
|
|
60
|
+
"E",
|
|
61
|
+
"F",
|
|
62
|
+
"I",
|
|
63
|
+
"UP",
|
|
64
|
+
"B",
|
|
65
|
+
"RUF",
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
[tool.ruff.lint.per-file-ignores]
|
|
69
|
+
"src/opencost/__init__.py" = [
|
|
70
|
+
"I001",
|
|
71
|
+
"RUF022",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
[tool.mypy]
|
|
75
|
+
strict = true
|
|
76
|
+
mypy_path = "src"
|
|
77
|
+
packages = ["opencost"]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "opencost"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Pydantic models for the openCost publication-cost metadata schema, with XML de-/serialization"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = "GPL-3.0-or-later"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Sven Marcus", email = "s.marcus@tu-braunschweig.de" },
|
|
10
|
+
{ name = "Sergej Wildemann", email = "s.wildemann@tu-braunschweig.de" }
|
|
11
|
+
]
|
|
12
|
+
keywords = ["openCost", "scholarly publishing", "APC", "metadata", "pydantic", "XML"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"Intended Audience :: Science/Research",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Topic :: Scientific/Engineering",
|
|
21
|
+
"Typing :: Typed",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
dependencies = ["pydantic>=2.10"]
|
|
25
|
+
|
|
26
|
+
[dependency-groups]
|
|
27
|
+
dev = [
|
|
28
|
+
"lxml>=5.0",
|
|
29
|
+
"mypy>=1.14",
|
|
30
|
+
"pytest>=8.0",
|
|
31
|
+
"ruff>=0.9",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[build-system]
|
|
35
|
+
requires = ["uv_build>=0.12.3,<0.13.0"]
|
|
36
|
+
build-backend = "uv_build"
|
|
37
|
+
|
|
38
|
+
[tool.pytest.ini_options]
|
|
39
|
+
testpaths = ["tests"]
|
|
40
|
+
|
|
41
|
+
[tool.ruff]
|
|
42
|
+
line-length = 100
|
|
43
|
+
src = ["src", "tests"]
|
|
44
|
+
|
|
45
|
+
[tool.ruff.lint]
|
|
46
|
+
select = ["E", "F", "I", "UP", "B", "RUF"]
|
|
47
|
+
|
|
48
|
+
[tool.ruff.lint.per-file-ignores]
|
|
49
|
+
"src/opencost/__init__.py" = ["I001", "RUF022"]
|
|
50
|
+
|
|
51
|
+
[tool.mypy]
|
|
52
|
+
strict = true
|
|
53
|
+
mypy_path = "src"
|
|
54
|
+
packages = ["opencost"]
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Pydantic models for the openCost metadata schema, with XML (de)serialization.
|
|
2
|
+
|
|
3
|
+
openCost (https://github.com/opencost-de/opencost) is a metadata schema for
|
|
4
|
+
the financial side of scholarly publishing: article-level cost data
|
|
5
|
+
(``publication``) and contracts / transformative agreements (``contract``).
|
|
6
|
+
|
|
7
|
+
>>> from opencost import Data, PublicationType, from_xml, to_xml
|
|
8
|
+
>>> xml = to_xml(Data(publication=[...])) # models -> openCost XML
|
|
9
|
+
>>> data = from_xml('<data xmlns="https://opencost.de">…</data>') # XML -> models
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from ._types import NonEmptyString as NonEmptyString
|
|
13
|
+
from ._types import Currency as Currency
|
|
14
|
+
from ._types import DateFormat as DateFormat
|
|
15
|
+
from ._types import ContractCostType as ContractCostType
|
|
16
|
+
from ._types import PublicationCostType as PublicationCostType
|
|
17
|
+
from ._common import Data as Data
|
|
18
|
+
from ._contract import ContractType as ContractType
|
|
19
|
+
from ._contract import ContractPrimaryIdentifier as ContractPrimaryIdentifier
|
|
20
|
+
from ._contract import ContractPrimaryIdentifierType as ContractPrimaryIdentifierType
|
|
21
|
+
from ._contract import ContractSecondaryIdType as ContractSecondaryIdType
|
|
22
|
+
from ._contract import ContractSecondaryIdTypeEnum as ContractSecondaryIdTypeEnum
|
|
23
|
+
from ._contract import ContractSecondaryIdentifiersType as ContractSecondaryIdentifiersType
|
|
24
|
+
from ._contract import ParticipationType as ParticipationType
|
|
25
|
+
from ._invoice import PublicationInvoiceType as PublicationInvoiceType
|
|
26
|
+
from ._invoice import PublicationAmountPaidType as PublicationAmountPaidType
|
|
27
|
+
from ._invoice import PublicationAmountsPaid as PublicationAmountsPaid
|
|
28
|
+
from ._invoice import AmountInvoice as AmountInvoice
|
|
29
|
+
from ._invoice import Dates as Dates
|
|
30
|
+
from ._invoice import ContractCostDataType as ContractCostDataType
|
|
31
|
+
from ._invoice import ContractAmountPaidType as ContractAmountPaidType
|
|
32
|
+
from ._invoice import ContractAmountsPaid as ContractAmountsPaid
|
|
33
|
+
from ._invoice import ContractInvoiceType as ContractInvoiceType
|
|
34
|
+
from ._invoice import ContractInvoicePeriodType as ContractInvoicePeriodType
|
|
35
|
+
from ._invoice import ContractInvoiceGroupType as ContractInvoiceGroupType
|
|
36
|
+
from ._institution import InstitutionType as InstitutionType
|
|
37
|
+
from ._institution import InstitutionId as InstitutionId
|
|
38
|
+
from ._institution import InstitutionIdType as InstitutionIdType
|
|
39
|
+
from ._institution import InstitutionName as InstitutionName
|
|
40
|
+
from ._institution import InstitutionNameType as InstitutionNameType
|
|
41
|
+
from ._publication import PublicationType as PublicationType
|
|
42
|
+
from ._publication import PublicationPrimaryIdentifier as PublicationPrimaryIdentifier
|
|
43
|
+
from ._publication import PublicationSecondaryIdType as PublicationSecondaryIdType
|
|
44
|
+
from ._publication import PublicationSecondaryIdTypeEnum as PublicationSecondaryIdTypeEnum
|
|
45
|
+
from ._publication import PublicationSecondaryIdentifiers as PublicationSecondaryIdentifiers
|
|
46
|
+
from ._publication import BibliographicInformation as BibliographicInformation
|
|
47
|
+
from ._publication import CoarPublicationType as CoarPublicationType
|
|
48
|
+
from ._publication import PublicationCostDataType as PublicationCostDataType
|
|
49
|
+
from ._publication import PartOfContractType as PartOfContractType
|
|
50
|
+
from ._validators import EitherFieldMixin as EitherFieldMixin
|
|
51
|
+
from ._validators import OpenCostModel as OpenCostModel
|
|
52
|
+
from .xml import NAMESPACE as NAMESPACE
|
|
53
|
+
from .xml import from_xml as from_xml
|
|
54
|
+
from .xml import to_xml as to_xml
|
|
55
|
+
|
|
56
|
+
__all__ = [
|
|
57
|
+
"NonEmptyString",
|
|
58
|
+
"Currency",
|
|
59
|
+
"DateFormat",
|
|
60
|
+
"ContractCostType",
|
|
61
|
+
"PublicationCostType",
|
|
62
|
+
"Data",
|
|
63
|
+
"ContractType",
|
|
64
|
+
"ContractPrimaryIdentifier",
|
|
65
|
+
"ContractPrimaryIdentifierType",
|
|
66
|
+
"ContractSecondaryIdType",
|
|
67
|
+
"ContractSecondaryIdTypeEnum",
|
|
68
|
+
"ContractSecondaryIdentifiersType",
|
|
69
|
+
"ParticipationType",
|
|
70
|
+
"PublicationInvoiceType",
|
|
71
|
+
"PublicationAmountPaidType",
|
|
72
|
+
"PublicationAmountsPaid",
|
|
73
|
+
"AmountInvoice",
|
|
74
|
+
"Dates",
|
|
75
|
+
"ContractCostDataType",
|
|
76
|
+
"ContractAmountPaidType",
|
|
77
|
+
"ContractAmountsPaid",
|
|
78
|
+
"ContractInvoiceType",
|
|
79
|
+
"ContractInvoicePeriodType",
|
|
80
|
+
"ContractInvoiceGroupType",
|
|
81
|
+
"InstitutionType",
|
|
82
|
+
"InstitutionId",
|
|
83
|
+
"InstitutionIdType",
|
|
84
|
+
"InstitutionName",
|
|
85
|
+
"InstitutionNameType",
|
|
86
|
+
"PublicationType",
|
|
87
|
+
"PublicationPrimaryIdentifier",
|
|
88
|
+
"PublicationSecondaryIdType",
|
|
89
|
+
"PublicationSecondaryIdTypeEnum",
|
|
90
|
+
"PublicationSecondaryIdentifiers",
|
|
91
|
+
"BibliographicInformation",
|
|
92
|
+
"CoarPublicationType",
|
|
93
|
+
"PartOfContractType",
|
|
94
|
+
"PublicationCostDataType",
|
|
95
|
+
"EitherFieldMixin",
|
|
96
|
+
"OpenCostModel",
|
|
97
|
+
"NAMESPACE",
|
|
98
|
+
"from_xml",
|
|
99
|
+
"to_xml",
|
|
100
|
+
]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from ._contract import ContractType
|
|
2
|
+
from ._publication import PublicationType
|
|
3
|
+
from ._validators import EitherFieldMixin
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Data(EitherFieldMixin):
|
|
7
|
+
either_fields = ("publication", "contract")
|
|
8
|
+
publication: list[PublicationType] | None = None
|
|
9
|
+
contract: list[ContractType] | None = None
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Field descriptions and notes are taken from the openCost documentation
|
|
2
|
+
# (doc/README.md of https://github.com/opencost-de/opencost), GPL-3.0-or-later,
|
|
3
|
+
# vendored as submodule vendor/opencost @ af6d257.
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
from pydantic import Field
|
|
8
|
+
|
|
9
|
+
from ._institution import InstitutionType
|
|
10
|
+
from ._invoice import ContractCostDataType
|
|
11
|
+
from ._types import DateFormat, NonEmptyString
|
|
12
|
+
from ._validators import OpenCostModel
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ContractPrimaryIdentifierType(Enum):
|
|
16
|
+
"""Contract primary identifier scheme (§5.1).
|
|
17
|
+
|
|
18
|
+
Currently only ESAC is accepted.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
ESAC = "ESAC"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ContractPrimaryIdentifier(OpenCostModel):
|
|
25
|
+
"""Persistent, global identifier for the contract (§5).
|
|
26
|
+
|
|
27
|
+
Currently only an ESAC ID is accepted
|
|
28
|
+
(https://esac-initiative.org/about/transformative-agreements/agreement-registry/).
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
value: NonEmptyString = Field(description="ESAC agreement id of the contract.")
|
|
32
|
+
type: ContractPrimaryIdentifierType = Field(
|
|
33
|
+
description="Identifier scheme; currently only `ESAC`."
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ContractSecondaryIdTypeEnum(Enum):
|
|
38
|
+
"""Contract secondary identifier scheme (§6.1.1)."""
|
|
39
|
+
|
|
40
|
+
oai = "oai"
|
|
41
|
+
ezb = "ezb"
|
|
42
|
+
local = "local"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ContractSecondaryIdType(OpenCostModel):
|
|
46
|
+
"""Secondary identifier for the contract (§6.1)."""
|
|
47
|
+
|
|
48
|
+
value: NonEmptyString = Field(description="Identifier value.")
|
|
49
|
+
type: ContractSecondaryIdTypeEnum = Field(description="Identifier scheme of `value`.")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ContractSecondaryIdentifiersType(OpenCostModel):
|
|
53
|
+
"""Contains additional, optional identifiers for the contract (§6)."""
|
|
54
|
+
|
|
55
|
+
id: Annotated[
|
|
56
|
+
list[ContractSecondaryIdType],
|
|
57
|
+
Field(min_length=1, description="Additional (persistent) identifiers."),
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ParticipationType(OpenCostModel):
|
|
62
|
+
"""Contains information on the dates an institution joined and left a
|
|
63
|
+
contract (§4)."""
|
|
64
|
+
|
|
65
|
+
to: DateFormat = Field(
|
|
66
|
+
description="The date when the institution left the contract. Not to "
|
|
67
|
+
"be confused with the end date of the agreement itself, which may be "
|
|
68
|
+
"later."
|
|
69
|
+
)
|
|
70
|
+
from_: DateFormat = Field(
|
|
71
|
+
...,
|
|
72
|
+
alias="from",
|
|
73
|
+
description="The date when the institution joined the contract. Not "
|
|
74
|
+
"to be confused with the start date of the agreement itself, which "
|
|
75
|
+
"may be earlier.",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class ContractType(OpenCostModel):
|
|
80
|
+
"""Top-level element, corresponds to a contract for which costs are to be
|
|
81
|
+
recorded (§1).
|
|
82
|
+
|
|
83
|
+
Examples of such contracts are transformative agreements and memberships.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
contract_name: NonEmptyString = Field(description="A human-readable label for the contract.")
|
|
87
|
+
institution: InstitutionType = Field(
|
|
88
|
+
description="Contains information to identify the institution taking part in the contract."
|
|
89
|
+
)
|
|
90
|
+
participation: ParticipationType = Field(
|
|
91
|
+
description="The dates the institution joined and left the contract."
|
|
92
|
+
)
|
|
93
|
+
primary_identifier: ContractPrimaryIdentifier = Field(
|
|
94
|
+
description="Persistent, global identifier for the contract; "
|
|
95
|
+
"currently only an ESAC ID is accepted."
|
|
96
|
+
)
|
|
97
|
+
secondary_identifiers: ContractSecondaryIdentifiersType | None = Field(
|
|
98
|
+
default=None,
|
|
99
|
+
description="Additional, optional identifiers for the contract.",
|
|
100
|
+
)
|
|
101
|
+
cost_data: ContractCostDataType = Field(
|
|
102
|
+
description="Aggregates payments related to this contract."
|
|
103
|
+
)
|