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.
@@ -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
+
@@ -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
+ )