dv_schema_models 0.9.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.
- dv_schema_models-0.9.0/PKG-INFO +216 -0
- dv_schema_models-0.9.0/README.md +198 -0
- dv_schema_models-0.9.0/pyproject.toml +158 -0
- dv_schema_models-0.9.0/src/dv_schema_models/__init__.py +1 -0
- dv_schema_models-0.9.0/src/dv_schema_models/dataset_instance.py +305 -0
- dv_schema_models-0.9.0/src/dv_schema_models/dataverse_schema.py +109 -0
- dv_schema_models-0.9.0/src/dv_schema_models/file_instance.py +124 -0
- dv_schema_models-0.9.0/src/dv_schema_models/role_assignments.py +89 -0
- dv_schema_models-0.9.0/src/dv_schema_models/schema_driven_records.py +99 -0
- dv_schema_models-0.9.0/src/dv_schema_models/schema_spreadsheet.py +134 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dv_schema_models
|
|
3
|
+
Version: 0.9.0
|
|
4
|
+
Summary: Turns Harvard Dataverse Project metadatablocks schema and dataset JSON into Pydantic models.
|
|
5
|
+
Author: Ken Lui
|
|
6
|
+
Author-email: Ken Lui <kenlh.lui@utoronto.ca>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Requires-Dist: pydantic>=2.0.0
|
|
9
|
+
Requires-Dist: pydantic-settings>=2.0.0
|
|
10
|
+
Requires-Python: >=3.10, <4.0
|
|
11
|
+
Project-URL: homepage, https://github.com/kenlhlui/dv_schema_models
|
|
12
|
+
Project-URL: source, https://github.com/kenlhlui/dv_schema_models
|
|
13
|
+
Project-URL: changelog, https://github.com/kenlhlui/dv_schema_models/blob/main/CHANGELOG.md
|
|
14
|
+
Project-URL: releasenotes, https://github.com/kenlhlui/dv_schema_models/releases
|
|
15
|
+
Project-URL: documentation, https://kenlhlui.github.io/dv_schema_models
|
|
16
|
+
Project-URL: issues, https://github.com/kenlhlui/dv_schema_models/issues
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# dv_schema_models
|
|
20
|
+
|
|
21
|
+
Pydantic models for Dataverse metadata — parse the schema, load dataset
|
|
22
|
+
exports, and validate field values against the schema.
|
|
23
|
+
|
|
24
|
+
> [!CAUTION]
|
|
25
|
+
> This library is under active development and the API is not yet stable. Breaking changes may occur between releases. Please pin to a specific version in your `pyproject.toml` or `requirements.txt` if you want to avoid surprises.
|
|
26
|
+
|
|
27
|
+
## Pre-requisites
|
|
28
|
+
1. Python 3.10+
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
1. With `uv` (recommended):
|
|
33
|
+
```bash
|
|
34
|
+
uv add dv_schema_models
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
2. With `pip`:
|
|
38
|
+
```bash
|
|
39
|
+
pip install dv_schema_models
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
To export schemas to Excel (see [usage #5](#5-export-the-schema-to-a-spreadsheet)), install with the `spreadsheet` extra:
|
|
43
|
+
```bash
|
|
44
|
+
uv add "dv_schema_models[spreadsheet]" # or: pip install "dv_schema_models[spreadsheet]"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Concepts
|
|
48
|
+
|
|
49
|
+
| Thing | What it is |
|
|
50
|
+
|---|---|
|
|
51
|
+
| **Schema** | `/api/metadatablocks` response — defines what fields *can* exist, their types, and rules |
|
|
52
|
+
| **Dataset instance** | `GET /api/datasets/:id` response — the actual metadata values for one dataset |
|
|
53
|
+
| **Record model** | A Pydantic model *generated from* the schema, used to validate instance values |
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
### 1. Load and query the schema
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
import json
|
|
61
|
+
from dv_schema_models.dataverse_schema import load_schema
|
|
62
|
+
|
|
63
|
+
schema = load_schema(json.load(open("dv_schema.json")))
|
|
64
|
+
|
|
65
|
+
schema.block_names() # ['citation', 'geospatial', ...]
|
|
66
|
+
block = schema.get_block("citation")
|
|
67
|
+
block.fields.keys() # top-level field names
|
|
68
|
+
block.required_fields() # leaf fields where isRequired=True
|
|
69
|
+
block.all_leaf_fields() # flattened, including nested compound fields
|
|
70
|
+
|
|
71
|
+
field = block.get_field("keyword")
|
|
72
|
+
field.is_compound() # True — has childFields
|
|
73
|
+
field.iter_leaf_fields() # [keywordValue, keywordVocabulary, ...]
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 2. Load a dataset and read values
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
import json
|
|
80
|
+
from dv_schema_models.dataset_instance import IsPartOf, load_dataset
|
|
81
|
+
|
|
82
|
+
dataset = load_dataset(json.load(open("ds_metadata.json")))
|
|
83
|
+
|
|
84
|
+
# Load the possible typeNames for a given block
|
|
85
|
+
dataset.field_names("citation") # ['title', 'author', 'keyword', ...]
|
|
86
|
+
dataset.data.latestVersion.metadataBlocks.get("citation").field_names() # same
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# Shortcut from the top level
|
|
90
|
+
dataset.get_value("citation", "title") # plain string
|
|
91
|
+
|
|
92
|
+
# Or drill down
|
|
93
|
+
block = dataset.data.latestVersion.metadataBlocks.get("citation")
|
|
94
|
+
block.get_value("keyword") # unwrapped Python value (str / list / dict)
|
|
95
|
+
block.get_field("author").simple_value() # [{'authorName': 'Author1', 'authorAffiliation': 'Author1Aff'...} ... {'authorName': 'Author2', 'authorAffiliation': 'Author2Aff'...}]
|
|
96
|
+
|
|
97
|
+
# Pull one subfield out of a compound field
|
|
98
|
+
block.get_subfield_values("author", "authorName") # ['Author1', 'Author2']
|
|
99
|
+
|
|
100
|
+
# Walk the isPartOf chain (dataset -> collection -> parent collection -> ...), possibly None
|
|
101
|
+
IsPartOf.get_field_list(dataset.data.isPartOf, "identifier") # ['sub-collection', 'top-collection']
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 3. Work with files
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from dv_schema_models.dataset_instance import load_dataset
|
|
108
|
+
from dv_schema_models.file_instance import FileInstance
|
|
109
|
+
|
|
110
|
+
dataset = load_dataset(json.load(open("ds_metadata.json")))
|
|
111
|
+
|
|
112
|
+
files = dataset.data.latestVersion.files or []
|
|
113
|
+
FileInstance.sum_field(files, "filesize") # sum a DataFile field across files, e.g. total filesize
|
|
114
|
+
FileInstance.list_field(files, "dataFile.filename") # list a field's values across files, dotted path for nested fields
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`sum_field` skips files with no `dataFile` or a `None` value for the field. Returns `None` (and logs a warning) if any present value isn't numeric.
|
|
118
|
+
|
|
119
|
+
`list_field` takes a dotted path (e.g. `"restricted"` for a top-level field, `"dataFile.checksum.type"` for a nested one) and skips entries where the path is missing or `None`.
|
|
120
|
+
|
|
121
|
+
### 4. Work with role assignments
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
import json
|
|
125
|
+
from dv_schema_models.role_assignments import load_role_assignments
|
|
126
|
+
|
|
127
|
+
role_assignments = load_role_assignments(json.load(open("ds_role_assignments.json")))
|
|
128
|
+
|
|
129
|
+
role_assignments.count_field("assignee") # number of assignments with an "assignee" field
|
|
130
|
+
role_assignments.count_field("roleName", "Curator") # number of assignments where roleName == "Curator"
|
|
131
|
+
role_assignments.get_value("assignee") # ['@personA', '@personB', ...]
|
|
132
|
+
|
|
133
|
+
# Fields not on the schema (e.g. the `_roleAlias` Dataverse sends) are still reachable
|
|
134
|
+
role_assignments.data[0].get_raw("_roleAlias") # 'curator'
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`load_role_assignments` also accepts the error envelope Dataverse returns when the request isn't permitted (`{"status": "ERROR", "message": "..."}`) — `data` is `None`, and `message` is reachable via `role_assignments.model_extra`.
|
|
138
|
+
|
|
139
|
+
### 5. Validate instance values against the schema
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
import json
|
|
143
|
+
from dv_schema_models.dataverse_schema import load_schema
|
|
144
|
+
from dv_schema_models.dataset_instance import load_dataset
|
|
145
|
+
from dv_schema_models.schema_driven_records import build_record_model, flatten_instance
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
schema = load_schema(json.load(open("dv_schema.json")))
|
|
149
|
+
dataset = load_dataset(json.load(open("ds_metadata.json")))
|
|
150
|
+
|
|
151
|
+
citation_schema = schema.get_block("citation")
|
|
152
|
+
CitationRecord = build_record_model(citation_schema) # dynamic Pydantic model
|
|
153
|
+
|
|
154
|
+
block = dataset.data.latestVersion.metadataBlocks.get("citation")
|
|
155
|
+
raw = flatten_instance(block) # {typeName: value, ...}
|
|
156
|
+
record = CitationRecord.model_validate(raw)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The generated model enforces field names, required/optional status, list wrapping for `multiple=True` fields, and `int`/`float` types where declared by the schema.
|
|
160
|
+
|
|
161
|
+
### 6. Discover available fields
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
# Fields actually present in this dataset instance
|
|
165
|
+
block = dataset.data.latestVersion.metadataBlocks.get("citation")
|
|
166
|
+
block.field_names() # e.g. ['title', 'author', 'keyword', ...]
|
|
167
|
+
|
|
168
|
+
# All fields the schema defines (including absent/optional ones)
|
|
169
|
+
schema.get_block("citation").all_leaf_fields().keys()
|
|
170
|
+
|
|
171
|
+
# After validation, access as typed attributes
|
|
172
|
+
record = CitationRecord.model_validate(flatten_instance(block))
|
|
173
|
+
record.title # str
|
|
174
|
+
record.author # list[...] for multiple=True compound fields
|
|
175
|
+
record.keyword # None if not present in this dataset (optional fields default to None)
|
|
176
|
+
# Note: field names with dots become underscores — e.g. 'resolution.Spatial' → record.resolution_Spatial
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### 7. Export the schema to a spreadsheet
|
|
180
|
+
|
|
181
|
+
Requires the `spreadsheet` extra (see [Installation](#installation)).
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
import json
|
|
185
|
+
from dv_schema_models.dataverse_schema import load_schema
|
|
186
|
+
from dv_schema_models.schema_spreadsheet import SchemaSpreadsheet
|
|
187
|
+
|
|
188
|
+
schema = load_schema(json.load(open("dv_schema.json")))
|
|
189
|
+
SchemaSpreadsheet(schema).write("dv_schema.xlsx")
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Writes an `.xlsx` workbook with one formatted worksheet per metadata block plus a combined **All** sheet. See [docs/schema_spreadsheet/README.md](docs/schema_spreadsheet/README.md) for the output layout, column mapping, and architecture.
|
|
193
|
+
|
|
194
|
+
## Input file shapes
|
|
195
|
+
|
|
196
|
+
**Schema** — output of Dataverse `/api/metadatablocks`:
|
|
197
|
+
```json
|
|
198
|
+
{"status": "OK", "data": [{"id": 10, "name": "citation", "fields": {...}}]}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**Dataset** — output of Dataverse `GET /api/datasets/:id`:
|
|
202
|
+
```json
|
|
203
|
+
{"status": "OK", "data": {"latestVersion": {"metadataBlocks": {"citation": {"fields": [...]}}}}}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**Role assignments** — output of Dataverse `GET /api/datasets/:id/assignments`:
|
|
207
|
+
```json
|
|
208
|
+
{"status": "OK", "data": [{"id": 1, "assignee": "@user", "roleId": 7, "roleName": "Curator", "definitionPointId": 34847}]}
|
|
209
|
+
```
|
|
210
|
+
Error responses (e.g. `{"status": "ERROR", "message": "..."}`, no `data` key) are also accepted — see [usage #4](#4-work-with-role-assignments).
|
|
211
|
+
|
|
212
|
+
## Citation
|
|
213
|
+
If you use this library in your work, please cite according to [CITATION](CITATION.cff)
|
|
214
|
+
|
|
215
|
+
## License
|
|
216
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# dv_schema_models
|
|
2
|
+
|
|
3
|
+
Pydantic models for Dataverse metadata — parse the schema, load dataset
|
|
4
|
+
exports, and validate field values against the schema.
|
|
5
|
+
|
|
6
|
+
> [!CAUTION]
|
|
7
|
+
> This library is under active development and the API is not yet stable. Breaking changes may occur between releases. Please pin to a specific version in your `pyproject.toml` or `requirements.txt` if you want to avoid surprises.
|
|
8
|
+
|
|
9
|
+
## Pre-requisites
|
|
10
|
+
1. Python 3.10+
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
1. With `uv` (recommended):
|
|
15
|
+
```bash
|
|
16
|
+
uv add dv_schema_models
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
2. With `pip`:
|
|
20
|
+
```bash
|
|
21
|
+
pip install dv_schema_models
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
To export schemas to Excel (see [usage #5](#5-export-the-schema-to-a-spreadsheet)), install with the `spreadsheet` extra:
|
|
25
|
+
```bash
|
|
26
|
+
uv add "dv_schema_models[spreadsheet]" # or: pip install "dv_schema_models[spreadsheet]"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Concepts
|
|
30
|
+
|
|
31
|
+
| Thing | What it is |
|
|
32
|
+
|---|---|
|
|
33
|
+
| **Schema** | `/api/metadatablocks` response — defines what fields *can* exist, their types, and rules |
|
|
34
|
+
| **Dataset instance** | `GET /api/datasets/:id` response — the actual metadata values for one dataset |
|
|
35
|
+
| **Record model** | A Pydantic model *generated from* the schema, used to validate instance values |
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
### 1. Load and query the schema
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
import json
|
|
43
|
+
from dv_schema_models.dataverse_schema import load_schema
|
|
44
|
+
|
|
45
|
+
schema = load_schema(json.load(open("dv_schema.json")))
|
|
46
|
+
|
|
47
|
+
schema.block_names() # ['citation', 'geospatial', ...]
|
|
48
|
+
block = schema.get_block("citation")
|
|
49
|
+
block.fields.keys() # top-level field names
|
|
50
|
+
block.required_fields() # leaf fields where isRequired=True
|
|
51
|
+
block.all_leaf_fields() # flattened, including nested compound fields
|
|
52
|
+
|
|
53
|
+
field = block.get_field("keyword")
|
|
54
|
+
field.is_compound() # True — has childFields
|
|
55
|
+
field.iter_leaf_fields() # [keywordValue, keywordVocabulary, ...]
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 2. Load a dataset and read values
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
import json
|
|
62
|
+
from dv_schema_models.dataset_instance import IsPartOf, load_dataset
|
|
63
|
+
|
|
64
|
+
dataset = load_dataset(json.load(open("ds_metadata.json")))
|
|
65
|
+
|
|
66
|
+
# Load the possible typeNames for a given block
|
|
67
|
+
dataset.field_names("citation") # ['title', 'author', 'keyword', ...]
|
|
68
|
+
dataset.data.latestVersion.metadataBlocks.get("citation").field_names() # same
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# Shortcut from the top level
|
|
72
|
+
dataset.get_value("citation", "title") # plain string
|
|
73
|
+
|
|
74
|
+
# Or drill down
|
|
75
|
+
block = dataset.data.latestVersion.metadataBlocks.get("citation")
|
|
76
|
+
block.get_value("keyword") # unwrapped Python value (str / list / dict)
|
|
77
|
+
block.get_field("author").simple_value() # [{'authorName': 'Author1', 'authorAffiliation': 'Author1Aff'...} ... {'authorName': 'Author2', 'authorAffiliation': 'Author2Aff'...}]
|
|
78
|
+
|
|
79
|
+
# Pull one subfield out of a compound field
|
|
80
|
+
block.get_subfield_values("author", "authorName") # ['Author1', 'Author2']
|
|
81
|
+
|
|
82
|
+
# Walk the isPartOf chain (dataset -> collection -> parent collection -> ...), possibly None
|
|
83
|
+
IsPartOf.get_field_list(dataset.data.isPartOf, "identifier") # ['sub-collection', 'top-collection']
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 3. Work with files
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from dv_schema_models.dataset_instance import load_dataset
|
|
90
|
+
from dv_schema_models.file_instance import FileInstance
|
|
91
|
+
|
|
92
|
+
dataset = load_dataset(json.load(open("ds_metadata.json")))
|
|
93
|
+
|
|
94
|
+
files = dataset.data.latestVersion.files or []
|
|
95
|
+
FileInstance.sum_field(files, "filesize") # sum a DataFile field across files, e.g. total filesize
|
|
96
|
+
FileInstance.list_field(files, "dataFile.filename") # list a field's values across files, dotted path for nested fields
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`sum_field` skips files with no `dataFile` or a `None` value for the field. Returns `None` (and logs a warning) if any present value isn't numeric.
|
|
100
|
+
|
|
101
|
+
`list_field` takes a dotted path (e.g. `"restricted"` for a top-level field, `"dataFile.checksum.type"` for a nested one) and skips entries where the path is missing or `None`.
|
|
102
|
+
|
|
103
|
+
### 4. Work with role assignments
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
import json
|
|
107
|
+
from dv_schema_models.role_assignments import load_role_assignments
|
|
108
|
+
|
|
109
|
+
role_assignments = load_role_assignments(json.load(open("ds_role_assignments.json")))
|
|
110
|
+
|
|
111
|
+
role_assignments.count_field("assignee") # number of assignments with an "assignee" field
|
|
112
|
+
role_assignments.count_field("roleName", "Curator") # number of assignments where roleName == "Curator"
|
|
113
|
+
role_assignments.get_value("assignee") # ['@personA', '@personB', ...]
|
|
114
|
+
|
|
115
|
+
# Fields not on the schema (e.g. the `_roleAlias` Dataverse sends) are still reachable
|
|
116
|
+
role_assignments.data[0].get_raw("_roleAlias") # 'curator'
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`load_role_assignments` also accepts the error envelope Dataverse returns when the request isn't permitted (`{"status": "ERROR", "message": "..."}`) — `data` is `None`, and `message` is reachable via `role_assignments.model_extra`.
|
|
120
|
+
|
|
121
|
+
### 5. Validate instance values against the schema
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
import json
|
|
125
|
+
from dv_schema_models.dataverse_schema import load_schema
|
|
126
|
+
from dv_schema_models.dataset_instance import load_dataset
|
|
127
|
+
from dv_schema_models.schema_driven_records import build_record_model, flatten_instance
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
schema = load_schema(json.load(open("dv_schema.json")))
|
|
131
|
+
dataset = load_dataset(json.load(open("ds_metadata.json")))
|
|
132
|
+
|
|
133
|
+
citation_schema = schema.get_block("citation")
|
|
134
|
+
CitationRecord = build_record_model(citation_schema) # dynamic Pydantic model
|
|
135
|
+
|
|
136
|
+
block = dataset.data.latestVersion.metadataBlocks.get("citation")
|
|
137
|
+
raw = flatten_instance(block) # {typeName: value, ...}
|
|
138
|
+
record = CitationRecord.model_validate(raw)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The generated model enforces field names, required/optional status, list wrapping for `multiple=True` fields, and `int`/`float` types where declared by the schema.
|
|
142
|
+
|
|
143
|
+
### 6. Discover available fields
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
# Fields actually present in this dataset instance
|
|
147
|
+
block = dataset.data.latestVersion.metadataBlocks.get("citation")
|
|
148
|
+
block.field_names() # e.g. ['title', 'author', 'keyword', ...]
|
|
149
|
+
|
|
150
|
+
# All fields the schema defines (including absent/optional ones)
|
|
151
|
+
schema.get_block("citation").all_leaf_fields().keys()
|
|
152
|
+
|
|
153
|
+
# After validation, access as typed attributes
|
|
154
|
+
record = CitationRecord.model_validate(flatten_instance(block))
|
|
155
|
+
record.title # str
|
|
156
|
+
record.author # list[...] for multiple=True compound fields
|
|
157
|
+
record.keyword # None if not present in this dataset (optional fields default to None)
|
|
158
|
+
# Note: field names with dots become underscores — e.g. 'resolution.Spatial' → record.resolution_Spatial
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### 7. Export the schema to a spreadsheet
|
|
162
|
+
|
|
163
|
+
Requires the `spreadsheet` extra (see [Installation](#installation)).
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
import json
|
|
167
|
+
from dv_schema_models.dataverse_schema import load_schema
|
|
168
|
+
from dv_schema_models.schema_spreadsheet import SchemaSpreadsheet
|
|
169
|
+
|
|
170
|
+
schema = load_schema(json.load(open("dv_schema.json")))
|
|
171
|
+
SchemaSpreadsheet(schema).write("dv_schema.xlsx")
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Writes an `.xlsx` workbook with one formatted worksheet per metadata block plus a combined **All** sheet. See [docs/schema_spreadsheet/README.md](docs/schema_spreadsheet/README.md) for the output layout, column mapping, and architecture.
|
|
175
|
+
|
|
176
|
+
## Input file shapes
|
|
177
|
+
|
|
178
|
+
**Schema** — output of Dataverse `/api/metadatablocks`:
|
|
179
|
+
```json
|
|
180
|
+
{"status": "OK", "data": [{"id": 10, "name": "citation", "fields": {...}}]}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**Dataset** — output of Dataverse `GET /api/datasets/:id`:
|
|
184
|
+
```json
|
|
185
|
+
{"status": "OK", "data": {"latestVersion": {"metadataBlocks": {"citation": {"fields": [...]}}}}}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Role assignments** — output of Dataverse `GET /api/datasets/:id/assignments`:
|
|
189
|
+
```json
|
|
190
|
+
{"status": "OK", "data": [{"id": 1, "assignee": "@user", "roleId": 7, "roleName": "Curator", "definitionPointId": 34847}]}
|
|
191
|
+
```
|
|
192
|
+
Error responses (e.g. `{"status": "ERROR", "message": "..."}`, no `data` key) are also accepted — see [usage #4](#4-work-with-role-assignments).
|
|
193
|
+
|
|
194
|
+
## Citation
|
|
195
|
+
If you use this library in your work, please cite according to [CITATION](CITATION.cff)
|
|
196
|
+
|
|
197
|
+
## License
|
|
198
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
[build-system] # https://docs.astral.sh/uv/concepts/build-backend/
|
|
2
|
+
requires = ["uv_build>=0.11.31,<0.12"]
|
|
3
|
+
build-backend = "uv_build"
|
|
4
|
+
|
|
5
|
+
[project] # https://packaging.python.org/en/latest/specifications/pyproject-toml/
|
|
6
|
+
name = "dv_schema_models"
|
|
7
|
+
version = "0.9.0"
|
|
8
|
+
description = "Turns Harvard Dataverse Project metadatablocks schema and dataset JSON into Pydantic models."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Ken Lui", email = "kenlh.lui@utoronto.ca" },
|
|
13
|
+
]
|
|
14
|
+
requires-python = ">=3.10,<4.0"
|
|
15
|
+
dependencies = [
|
|
16
|
+
"pydantic (>=2.0.0)",
|
|
17
|
+
"pydantic-settings (>=2.0.0)",
|
|
18
|
+
]
|
|
19
|
+
license = "MIT"
|
|
20
|
+
|
|
21
|
+
[project.urls] # https://packaging.python.org/en/latest/specifications/well-known-project-urls/#well-known-labels
|
|
22
|
+
homepage = "https://github.com/kenlhlui/dv_schema_models"
|
|
23
|
+
source = "https://github.com/kenlhlui/dv_schema_models"
|
|
24
|
+
changelog = "https://github.com/kenlhlui/dv_schema_models/blob/main/CHANGELOG.md"
|
|
25
|
+
releasenotes = "https://github.com/kenlhlui/dv_schema_models/releases"
|
|
26
|
+
documentation = "https://kenlhlui.github.io/dv_schema_models"
|
|
27
|
+
issues = "https://github.com/kenlhlui/dv_schema_models/issues"
|
|
28
|
+
|
|
29
|
+
[dependency-groups] # https://docs.astral.sh/uv/concepts/projects/dependencies/#development-dependencies
|
|
30
|
+
dev = [
|
|
31
|
+
"codespell (>=2.4.1)",
|
|
32
|
+
"commitizen (>=4.3.0)",
|
|
33
|
+
"coverage[toml] (>=7.6.10)",
|
|
34
|
+
"ipykernel (>=6.29.4)",
|
|
35
|
+
"ipython (>=8.18.0)",
|
|
36
|
+
"ipywidgets (>=8.1.2)",
|
|
37
|
+
"zensical (>=0.0.51)",
|
|
38
|
+
"mkdocstrings[python] (>=0.26.2)",
|
|
39
|
+
"poethepoet (>=0.32.1)",
|
|
40
|
+
"prek",
|
|
41
|
+
"pytest (>=8.3.4)",
|
|
42
|
+
"pytest-mock (>=3.14.0)",
|
|
43
|
+
"pytest-xdist (>=3.6.1)",
|
|
44
|
+
"ruff (>=0.9.2)",
|
|
45
|
+
"ty (>=0.0.6)",
|
|
46
|
+
"mkdocstrings-python (>=2.0.5)",
|
|
47
|
+
"httpx2>=2.5.0",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[tool.codespell] # https://github.com/codespell-project/codespell
|
|
51
|
+
builtin = "en-GB_to_en-US,clear,code,rare"
|
|
52
|
+
check-filenames = true
|
|
53
|
+
skip = "./*_cache/,./.venv,./reports"
|
|
54
|
+
|
|
55
|
+
[tool.commitizen] # https://commitizen-tools.github.io/commitizen/config/
|
|
56
|
+
bump_message = "bump: v$current_version → v$new_version"
|
|
57
|
+
tag_format = "v$version"
|
|
58
|
+
update_changelog_on_bump = true
|
|
59
|
+
version_provider = "uv"
|
|
60
|
+
version_scheme = "pep440"
|
|
61
|
+
|
|
62
|
+
[tool.coverage.report] # https://coverage.readthedocs.io/en/latest/config.html#report
|
|
63
|
+
precision = 1
|
|
64
|
+
show_missing = true
|
|
65
|
+
skip_covered = true
|
|
66
|
+
|
|
67
|
+
[tool.coverage.run] # https://coverage.readthedocs.io/en/latest/config.html#run
|
|
68
|
+
branch = true
|
|
69
|
+
command_line = "--module pytest"
|
|
70
|
+
data_file = "reports/.coverage"
|
|
71
|
+
source = ["src"]
|
|
72
|
+
|
|
73
|
+
[tool.coverage.xml] # https://coverage.readthedocs.io/en/latest/config.html#xml
|
|
74
|
+
output = "reports/coverage.xml"
|
|
75
|
+
|
|
76
|
+
[tool.pytest.ini_options] # https://docs.pytest.org/en/latest/reference/reference.html#ini-options-ref
|
|
77
|
+
addopts = "--color=yes --doctest-modules --exitfirst --failed-first --verbosity=2 --junitxml=reports/pytest.xml"
|
|
78
|
+
testpaths = ["src", "tests"]
|
|
79
|
+
xfail_strict = true
|
|
80
|
+
|
|
81
|
+
[tool.ruff] # https://docs.astral.sh/ruff/settings/
|
|
82
|
+
fix = true
|
|
83
|
+
line-length = 100
|
|
84
|
+
src = ["src", "tests"]
|
|
85
|
+
target-version = "py310"
|
|
86
|
+
|
|
87
|
+
[tool.ruff.format]
|
|
88
|
+
docstring-code-format = true
|
|
89
|
+
skip-magic-trailing-comma = true
|
|
90
|
+
|
|
91
|
+
[tool.ruff.lint]
|
|
92
|
+
select = ["ALL"]
|
|
93
|
+
ignore = ["CPY", "FIX", "T20", "ARG001", "COM812", "D203", "D213", "E501", "PD008", "PD009", "PGH003", "RET504", "S101", "TD003"]
|
|
94
|
+
unfixable = ["ERA001", "F401", "F841", "T201", "T203"]
|
|
95
|
+
|
|
96
|
+
[tool.ruff.lint.flake8-annotations]
|
|
97
|
+
allow-star-arg-any = true
|
|
98
|
+
ignore-fully-untyped = true
|
|
99
|
+
|
|
100
|
+
[tool.ruff.lint.flake8-tidy-imports]
|
|
101
|
+
ban-relative-imports = "all"
|
|
102
|
+
|
|
103
|
+
[tool.ruff.lint.isort]
|
|
104
|
+
split-on-trailing-comma = false
|
|
105
|
+
|
|
106
|
+
[tool.ruff.lint.pycodestyle]
|
|
107
|
+
max-doc-length = 100
|
|
108
|
+
|
|
109
|
+
[tool.ruff.lint.pydocstyle]
|
|
110
|
+
convention = "numpy"
|
|
111
|
+
|
|
112
|
+
[tool.ty.environment]
|
|
113
|
+
python-platform = "all"
|
|
114
|
+
|
|
115
|
+
[tool.ty.rules]
|
|
116
|
+
unused-ignore-comment = "warn"
|
|
117
|
+
|
|
118
|
+
[tool.poe.executor] # https://github.com/nat-n/poethepoet
|
|
119
|
+
type = "simple"
|
|
120
|
+
|
|
121
|
+
[tool.poe.tasks]
|
|
122
|
+
|
|
123
|
+
[tool.poe.tasks.docs]
|
|
124
|
+
help = "Build or serve the documentation"
|
|
125
|
+
shell = """
|
|
126
|
+
if [ $serve ]
|
|
127
|
+
then {
|
|
128
|
+
zensical serve
|
|
129
|
+
} else {
|
|
130
|
+
zensical build
|
|
131
|
+
} fi
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
[[tool.poe.tasks.docs.args]]
|
|
135
|
+
help = "Serve the documentation locally with live reload"
|
|
136
|
+
type = "boolean"
|
|
137
|
+
name = "serve"
|
|
138
|
+
options = ["--serve"]
|
|
139
|
+
|
|
140
|
+
[tool.poe.tasks.lint]
|
|
141
|
+
help = "Lint this package"
|
|
142
|
+
cmd = """
|
|
143
|
+
prek run
|
|
144
|
+
--all-files
|
|
145
|
+
--color always
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
[tool.poe.tasks.test]
|
|
149
|
+
help = "Test this package"
|
|
150
|
+
|
|
151
|
+
[[tool.poe.tasks.test.sequence]]
|
|
152
|
+
cmd = "coverage run"
|
|
153
|
+
|
|
154
|
+
[[tool.poe.tasks.test.sequence]]
|
|
155
|
+
cmd = "coverage report"
|
|
156
|
+
|
|
157
|
+
[[tool.poe.tasks.test.sequence]]
|
|
158
|
+
cmd = "coverage xml"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""dv_schema_models."""
|