fairscape-conversion 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.
- fairscape_conversion-0.1.0/MAPPING-SCHEMA.md +216 -0
- fairscape_conversion-0.1.0/PKG-INFO +89 -0
- fairscape_conversion-0.1.0/README.md +73 -0
- fairscape_conversion-0.1.0/__init__.py +1 -0
- fairscape_conversion-0.1.0/core/__init__.py +33 -0
- fairscape_conversion-0.1.0/core/arks.py +30 -0
- fairscape_conversion-0.1.0/core/cli.py +54 -0
- fairscape_conversion-0.1.0/core/engine.py +226 -0
- fairscape_conversion-0.1.0/core/loader.py +122 -0
- fairscape_conversion-0.1.0/core/parsers.py +54 -0
- fairscape_conversion-0.1.0/core/plugin.py +182 -0
- fairscape_conversion-0.1.0/core/records.py +99 -0
- fairscape_conversion-0.1.0/core/registry.py +55 -0
- fairscape_conversion-0.1.0/core/roundtrip.py +97 -0
- fairscape_conversion-0.1.0/core/schema.py +138 -0
- fairscape_conversion-0.1.0/fairscape_conversion.egg-info/PKG-INFO +89 -0
- fairscape_conversion-0.1.0/fairscape_conversion.egg-info/SOURCES.txt +179 -0
- fairscape_conversion-0.1.0/fairscape_conversion.egg-info/dependency_links.txt +1 -0
- fairscape_conversion-0.1.0/fairscape_conversion.egg-info/requires.txt +2 -0
- fairscape_conversion-0.1.0/fairscape_conversion.egg-info/top_level.txt +1 -0
- fairscape_conversion-0.1.0/plugins/__init__.py +1 -0
- fairscape_conversion-0.1.0/plugins/c2m2/__init__.py +29 -0
- fairscape_conversion-0.1.0/plugins/c2m2/associations.csv +43 -0
- fairscape_conversion-0.1.0/plugins/c2m2/computed.csv +5 -0
- fairscape_conversion-0.1.0/plugins/c2m2/constants.csv +39 -0
- fairscape_conversion-0.1.0/plugins/c2m2/cv_bases.csv +19 -0
- fairscape_conversion-0.1.0/plugins/c2m2/entities.csv +19 -0
- fairscape_conversion-0.1.0/plugins/c2m2/golden.json +1167 -0
- fairscape_conversion-0.1.0/plugins/c2m2/impl.py +547 -0
- fairscape_conversion-0.1.0/plugins/c2m2/input-datapackage/C2M2_datapackage.json +135 -0
- fairscape_conversion-0.1.0/plugins/c2m2/input-datapackage/biosample.tsv +2 -0
- fairscape_conversion-0.1.0/plugins/c2m2/input-datapackage/file.tsv +3 -0
- fairscape_conversion-0.1.0/plugins/c2m2/input-datapackage/file_describes_biosample.tsv +2 -0
- fairscape_conversion-0.1.0/plugins/c2m2/input-datapackage/file_format.tsv +2 -0
- fairscape_conversion-0.1.0/plugins/c2m2/input-datapackage/id_namespace.tsv +2 -0
- fairscape_conversion-0.1.0/plugins/c2m2/input-datapackage/project.tsv +2 -0
- fairscape_conversion-0.1.0/plugins/c2m2/input-datapackage/subject.tsv +1 -0
- fairscape_conversion-0.1.0/plugins/c2m2/mapper.py +386 -0
- fairscape_conversion-0.1.0/plugins/c2m2/ontology.py +140 -0
- fairscape_conversion-0.1.0/plugins/c2m2/parsers.py +371 -0
- fairscape_conversion-0.1.0/plugins/c2m2/properties.csv +122 -0
- fairscape_conversion-0.1.0/plugins/c2m2/root.json +108 -0
- fairscape_conversion-0.1.0/plugins/croissant/__init__.py +41 -0
- fairscape_conversion-0.1.0/plugins/croissant/entities.csv +5 -0
- fairscape_conversion-0.1.0/plugins/croissant/golden.json +611 -0
- fairscape_conversion-0.1.0/plugins/croissant/impl.py +95 -0
- fairscape_conversion-0.1.0/plugins/croissant/input.json +1167 -0
- fairscape_conversion-0.1.0/plugins/croissant/parsers.py +52 -0
- fairscape_conversion-0.1.0/plugins/croissant/properties.csv +43 -0
- fairscape_conversion-0.1.0/plugins/cromwell/__init__.py +206 -0
- fairscape_conversion-0.1.0/plugins/cromwell/entities.csv +8 -0
- fairscape_conversion-0.1.0/plugins/cromwell/extract.py +344 -0
- fairscape_conversion-0.1.0/plugins/cromwell/golden.json +549 -0
- fairscape_conversion-0.1.0/plugins/cromwell/input.json +174 -0
- fairscape_conversion-0.1.0/plugins/cromwell/parsers.py +259 -0
- fairscape_conversion-0.1.0/plugins/cromwell/properties.csv +56 -0
- fairscape_conversion-0.1.0/plugins/d4d/__init__.py +22 -0
- fairscape_conversion-0.1.0/plugins/d4d/entities.csv +5 -0
- fairscape_conversion-0.1.0/plugins/d4d/golden.json +520 -0
- fairscape_conversion-0.1.0/plugins/d4d/impl.py +327 -0
- fairscape_conversion-0.1.0/plugins/d4d/input.yaml +689 -0
- fairscape_conversion-0.1.0/plugins/d4d/parsers.py +128 -0
- fairscape_conversion-0.1.0/plugins/d4d/properties.csv +78 -0
- fairscape_conversion-0.1.0/plugins/example/__init__.py +45 -0
- fairscape_conversion-0.1.0/plugins/example/entities.csv +2 -0
- fairscape_conversion-0.1.0/plugins/example/golden.json +20 -0
- fairscape_conversion-0.1.0/plugins/example/input.json +16 -0
- fairscape_conversion-0.1.0/plugins/example/properties.csv +4 -0
- fairscape_conversion-0.1.0/plugins/mlflow/__init__.py +204 -0
- fairscape_conversion-0.1.0/plugins/mlflow/entities.csv +8 -0
- fairscape_conversion-0.1.0/plugins/mlflow/extract.py +554 -0
- fairscape_conversion-0.1.0/plugins/mlflow/golden.json +389 -0
- fairscape_conversion-0.1.0/plugins/mlflow/input.json +230 -0
- fairscape_conversion-0.1.0/plugins/mlflow/parsers.py +320 -0
- fairscape_conversion-0.1.0/plugins/mlflow/properties.csv +63 -0
- fairscape_conversion-0.1.0/plugins/snakemake/__init__.py +185 -0
- fairscape_conversion-0.1.0/plugins/snakemake/entities.csv +8 -0
- fairscape_conversion-0.1.0/plugins/snakemake/golden.json +407 -0
- fairscape_conversion-0.1.0/plugins/snakemake/input.json +130 -0
- fairscape_conversion-0.1.0/plugins/snakemake/parsers.py +263 -0
- fairscape_conversion-0.1.0/plugins/snakemake/properties.csv +56 -0
- fairscape_conversion-0.1.0/plugins/wrroc/__init__.py +34 -0
- fairscape_conversion-0.1.0/plugins/wrroc/entities.csv +19 -0
- fairscape_conversion-0.1.0/plugins/wrroc/golden.json +335 -0
- fairscape_conversion-0.1.0/plugins/wrroc/hooks.py +359 -0
- fairscape_conversion-0.1.0/plugins/wrroc/input.json +345 -0
- fairscape_conversion-0.1.0/plugins/wrroc/parsers.py +267 -0
- fairscape_conversion-0.1.0/plugins/wrroc/properties.csv +36 -0
- fairscape_conversion-0.1.0/pyproject.toml +49 -0
- fairscape_conversion-0.1.0/setup.cfg +4 -0
- fairscape_conversion-0.1.0/tests/test_c2m2_parity.py +63 -0
- fairscape_conversion-0.1.0/tests/test_croissant_parity.py +40 -0
- fairscape_conversion-0.1.0/tests/test_cromwell.py +124 -0
- fairscape_conversion-0.1.0/tests/test_d4d_parity.py +73 -0
- fairscape_conversion-0.1.0/tests/test_example.py +23 -0
- fairscape_conversion-0.1.0/tests/test_mlflow.py +74 -0
- fairscape_conversion-0.1.0/tests/test_plugin_examples.py +72 -0
- fairscape_conversion-0.1.0/tests/test_snakemake.py +40 -0
- fairscape_conversion-0.1.0/tests/test_wrroc_parity.py +82 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# Unified converter mapping format
|
|
2
|
+
|
|
3
|
+
One mapping format for every fairscape RO-Crate converter. A converter is a
|
|
4
|
+
**plugin**: a folder of CSVs (the mapping data) plus one subclass of
|
|
5
|
+
`core.plugin.PluginBase` holding the workflow-specific algorithms (named
|
|
6
|
+
parsers + step methods; see `docs/NEW-PLUGIN.md`). The core engine
|
|
7
|
+
(`fairscape_conversion/core/`) reads the CSVs and drives the conversion; you never re-read a
|
|
8
|
+
bespoke format to understand a converter — you read these CSVs.
|
|
9
|
+
|
|
10
|
+
Guiding rule: **if it is data it lives in a CSV; if it is an algorithm it is a
|
|
11
|
+
named parser or hook.** Blank cells are ignored. List-valued cells are
|
|
12
|
+
pipe-joined (`a|b|c`).
|
|
13
|
+
|
|
14
|
+
Every plugin has `entities.csv` + `properties.csv`. Some plugins add
|
|
15
|
+
`associations.csv` and/or `cv_bases.csv` (currently only c2m2 needs them). None
|
|
16
|
+
of these files are required to be non-empty — an export-only plugin may leave
|
|
17
|
+
half the columns blank.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## `entities.csv` — one row per source entity class
|
|
22
|
+
|
|
23
|
+
How a source node/table/record is recognized and what target entity it becomes.
|
|
24
|
+
Row order is match precedence (first matching row wins).
|
|
25
|
+
|
|
26
|
+
| column | meaning |
|
|
27
|
+
|---|---|
|
|
28
|
+
| `source_type` | the source `@type` token, C2M2 table name, or model class name to match |
|
|
29
|
+
| `discriminator` | named predicate that further gates the match (blank = match any). e.g. wrroc `instrument_is_workflow`; the datasheet/croissant `ROOT` vs `COMPONENT` position |
|
|
30
|
+
| `target_type` | the target entity/class produced (empty ⇒ node kept as-is or dropped, see `level`) |
|
|
31
|
+
| `target_type_iri` | pipe-joined `@type` array to stamp on the target node, e.g. `prov:Activity\|https://w3id.org/EVI#Computation` |
|
|
32
|
+
| `level` | role tag consumed by hooks: `workflow`, `tool`, `data`, `software`, `root`, `component`, `agent`, `drop` |
|
|
33
|
+
| `id_strategy` | how the target `@id` is set: `keep` (reuse source id), `mint`, `persistent_or_mint`, `persistent_ark_or_mint`, `ontology_term`, `column` |
|
|
34
|
+
| `id_template` | `{token}`-interpolated mint pattern, e.g. `{prefix}-biosample/{local_id}` |
|
|
35
|
+
| `id_column` | source column/field carrying the id (for `persistent_*`, `ontology_term`, `column`) |
|
|
36
|
+
| `keep_identifier` | `TRUE` ⇒ stash the original source `@id` as `identifier` so a reverse trip can restore it |
|
|
37
|
+
| `mint_prefix` | short stem for minted ARKs (wrroc), e.g. `computation-main` |
|
|
38
|
+
| `note` | free text |
|
|
39
|
+
|
|
40
|
+
`level` semantics: `agent` = kept in the output graph unchanged; `drop` = not
|
|
41
|
+
emitted as an entity (a hook may still harvest data from it); everything else is
|
|
42
|
+
a real target node. The engine mints an `@id` for every non-`agent`/non-`drop`
|
|
43
|
+
node per `id_strategy`.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## `properties.csv` — one row per source-property → target-property
|
|
48
|
+
|
|
49
|
+
One row moves one property, in one or both directions.
|
|
50
|
+
|
|
51
|
+
| column | meaning |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `source_type` | owning source entity (matches an `entities.source_type`) |
|
|
54
|
+
| `source_property` | source field/column read (blank ⇒ constant or synthesized) |
|
|
55
|
+
| `target_type` | owning target entity |
|
|
56
|
+
| `target_property` | target field written |
|
|
57
|
+
| `import_parser` | named parser, source→target direction (blank ⇒ not imported) |
|
|
58
|
+
| `export_parser` | named parser, target→source direction (blank ⇒ not exported) |
|
|
59
|
+
| `direction` | `both`, `import`, or `export` (which directions this row participates in) |
|
|
60
|
+
| `reverse_primary` | `TRUE` ⇒ for a many-to-one target, this source wins on the reverse trip |
|
|
61
|
+
| `requirement` | `required`, `recommended`, `optional`, `synthesized`, `passthrough`, `nohome` (documentation + passthrough behaviour) |
|
|
62
|
+
| `cardinality` | `scalar` or `list` (target cardinality; `list` accumulates all sources, `scalar` is first-write-wins) |
|
|
63
|
+
| `constant_value` | `{token}`-interpolated literal when `source_property` is blank |
|
|
64
|
+
| `fallback_source` | second source field tried when the first is empty |
|
|
65
|
+
| `fallback_parser` | parser applied to `fallback_source` |
|
|
66
|
+
| `source_table` | CV/lookup table this field resolves against (c2m2) |
|
|
67
|
+
| `param_name` | name for a wrapped `PropertyValue` when `target_property` is `additionalProperty` (c2m2) |
|
|
68
|
+
| `wrap` | wrap the value: `ident_ref`, `property_value`, `ruled_out_pv` (blank = none) |
|
|
69
|
+
| `note` | free text |
|
|
70
|
+
|
|
71
|
+
**`requirement` = `passthrough`** rows are copied verbatim as extra keys and
|
|
72
|
+
re-emitted on export (fairscape models allow extra keys). **`nohome`** rows are
|
|
73
|
+
documented drops (a target property with no home the other direction).
|
|
74
|
+
|
|
75
|
+
**Many-to-one merge.** When several `properties.csv` rows share one
|
|
76
|
+
`target_type` + `target_property`, they merge: a `list` target accumulates every
|
|
77
|
+
source's output, a `scalar` target keeps the first non-empty in row order (put
|
|
78
|
+
the `reverse_primary` source first so it wins). This is how d4d collapses
|
|
79
|
+
`intended_uses`, `purposes`, `tasks`, … onto `rai_data_use_cases`.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## `associations.csv` — join-table edges (c2m2 only)
|
|
84
|
+
|
|
85
|
+
C2M2 encodes relationships in link tables (`biosample_disease`, …). Each row is
|
|
86
|
+
one edge rule. The join algorithm itself (build a guid index, look each node up
|
|
87
|
+
by both its natural key and its `@id`) lives in the c2m2 `hooks.py`; this CSV is
|
|
88
|
+
only the rule data.
|
|
89
|
+
|
|
90
|
+
| column | meaning |
|
|
91
|
+
|---|---|
|
|
92
|
+
| `source_type` | entity the edge attaches to |
|
|
93
|
+
| `assoc_table` | the link/association table |
|
|
94
|
+
| `match_columns` | pipe-joined columns forming the owner's key |
|
|
95
|
+
| `match_resolver` | `entity_key` (owner's natural key) or `ontology_term` (owner is a CV node) |
|
|
96
|
+
| `value_columns` | pipe-joined columns carrying the target reference |
|
|
97
|
+
| `value_resolver` | `ontology_term`, `entity_guid`, or `raw` |
|
|
98
|
+
| `value_entity` | for `entity_guid`: the table whose node the value resolves to; for `ontology_term`: the CV source table |
|
|
99
|
+
| `target_property` | pipe-joined target properties the edge writes |
|
|
100
|
+
| `wrap` | `ident_ref`, `property_value`, `ruled_out_pv` |
|
|
101
|
+
| `filter_column` | gate the rows on this column (blank = no filter) |
|
|
102
|
+
| `filter_endswith` | keep rows whose `filter_column` ends with this (`:1` observed, `:0` ruled-out — **the CV code, never the label**) |
|
|
103
|
+
| `param_name` | name for the wrapped `PropertyValue` when `wrap` = `property_value` (the old `pv_name`) |
|
|
104
|
+
| `note` | free text |
|
|
105
|
+
|
|
106
|
+
A single association row is written twice in the CSV to express both an edge and
|
|
107
|
+
its back-reference (swap which columns are `match` vs `value`), exactly as the
|
|
108
|
+
original per-table JSON did.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## `cv_bases.csv` — controlled-vocabulary → ontology IRI bases (c2m2 only)
|
|
113
|
+
|
|
114
|
+
The prefix/table → ontology-base lookup that `ontology.resolve_term` used to
|
|
115
|
+
hold as Python dicts. Two kinds of row:
|
|
116
|
+
|
|
117
|
+
| column | meaning |
|
|
118
|
+
|---|---|
|
|
119
|
+
| `key` | the CURIE prefix (`kind=curie`, e.g. `DOID`) or the CV table (`kind=bare`, e.g. `gene`) |
|
|
120
|
+
| `kind` | `curie` (value carries a CURIE prefix) or `bare` (table's ids are bare accessions) |
|
|
121
|
+
| `iri_base` | IRI base the local id is appended to (e.g. `http://purl.obolibrary.org/obo/DOID_`) |
|
|
122
|
+
| `ontology_name` | human name of the ontology |
|
|
123
|
+
| `curie_prefix` | for `kind=bare`: the CURIE stem to reattach (e.g. `ensembl`) |
|
|
124
|
+
| `note` | free text |
|
|
125
|
+
|
|
126
|
+
A few resolutions stay in plugin code because they are not simple base lookups:
|
|
127
|
+
the `NCBI:txid…` taxonomy rule and the GlyTouCan-vs-PubChem `compound` split.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Parsers and plugin methods (the Python that a CSV references by name)
|
|
132
|
+
|
|
133
|
+
**Parsers** all share one signature:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
def parser(value, rule, ctx) -> parsed_value | None
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`value` is the source property value (`None` for synthesized fields), `rule` is
|
|
140
|
+
the compiled row (the CSV columns), and `ctx` carries conversion state (the
|
|
141
|
+
current node, the `@id`→node index, the guid map, the root node, and any
|
|
142
|
+
per-plugin extras). Returning `None` means "set nothing." Parsers are registered
|
|
143
|
+
by name in the plugin class's `import_parsers` / `export_parsers` dicts, so the
|
|
144
|
+
loader can reject a typo in a CSV at load time. Shared building blocks live in
|
|
145
|
+
`core/parsers.py`.
|
|
146
|
+
|
|
147
|
+
**Plugin methods** are the per-workflow orchestration — override them on your
|
|
148
|
+
`PluginBase` subclass (full list + when to override each: `docs/NEW-PLUGIN.md`):
|
|
149
|
+
|
|
150
|
+
| method | what it does | examples |
|
|
151
|
+
|---|---|---|
|
|
152
|
+
| `load_source` | turn the raw input into `SourceRecord`s (the common override) | example; helpers `records_from_items` / `records_from_graph` |
|
|
153
|
+
| `classify` / `discriminators` | match a record to its `entities.csv` row, gated by named predicates | wrroc workflow-vs-tool discriminator |
|
|
154
|
+
| `pre` | full-control record production: build indexes, mint ARKs, load join/CV tables | c2m2 guid-map + association index; wrroc id minting |
|
|
155
|
+
| `map_record` | **engine default** applies the property rules; override only when a record doesn't map rule-by-rule | d4d many-to-one merge; c2m2 vars pipeline |
|
|
156
|
+
| `link` | cross-node edges added after all nodes exist | wrroc `isPartOf` / `generatedBy` mirroring |
|
|
157
|
+
| `assemble` | build the root, attach children, write preservation files | c2m2 root + datapackage/sqlite copy |
|
|
158
|
+
| `export` | bespoke reverse driver or engine bypass | wrroc/d4d reverse; croissant |
|
|
159
|
+
|
|
160
|
+
A plugin implements only the steps it needs; the rest default to the engine's
|
|
161
|
+
generic behaviour. Property-rule application is the engine's own code unless a
|
|
162
|
+
plugin genuinely cannot map rule-by-rule — which is the whole point of the
|
|
163
|
+
format.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Worked rows (one per plugin)
|
|
168
|
+
|
|
169
|
+
**wrroc** `entities.csv` — a workflow-level run becomes an EVI Computation:
|
|
170
|
+
```
|
|
171
|
+
source_type,discriminator,target_type,target_type_iri,level,id_strategy,id_template,id_column,keep_identifier,mint_prefix,note
|
|
172
|
+
CreateAction,instrument_is_workflow,Computation,prov:Activity|https://w3id.org/EVI#Computation,workflow,mint,,,TRUE,computation-main,parent run
|
|
173
|
+
```
|
|
174
|
+
wrroc `properties.csv` — description synthesized to satisfy EVI's min-length:
|
|
175
|
+
```
|
|
176
|
+
source_type,source_property,target_type,target_property,import_parser,export_parser,direction,reverse_primary,requirement,cardinality,constant_value,fallback_source,fallback_parser,source_table,param_name,wrap,note
|
|
177
|
+
CreateAction,description,Computation,description,desc_min10,scalar,both,TRUE,synthesized,scalar,,,,,,,
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**d4d** `entities.csv` — a distribution becomes a DigitalObject graph node
|
|
181
|
+
(d4d reuses `target_type_iri` for the additionalType and `id_column` for the
|
|
182
|
+
url field):
|
|
183
|
+
```
|
|
184
|
+
distributions,,DigitalObject,distribution,data,keep,,download_url,FALSE,,each distribution -> DigitalObject in hasPart
|
|
185
|
+
```
|
|
186
|
+
d4d `properties.csv` — one of the many sources merging onto `rai_data_use_cases`:
|
|
187
|
+
```
|
|
188
|
+
D4D,intended_uses,Dataset,rai_data_use_cases,flatten,reverse_flatten,both,TRUE,recommended,list,,,,,,,
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**c2m2** `entities.csv` — the biosample table explodes to EVI Samples (c2m2's
|
|
192
|
+
file adds a `precedence` column before `note`; d4d/wrroc's files don't have it):
|
|
193
|
+
```
|
|
194
|
+
biosample,,Sample,,data,persistent_ark_or_mint,{prefix}-biosample/{local_id},persistent_id,FALSE,,20,
|
|
195
|
+
```
|
|
196
|
+
c2m2 `associations.csv` — observed disease edges only:
|
|
197
|
+
```
|
|
198
|
+
source_type,assoc_table,match_columns,match_resolver,value_columns,value_resolver,value_entity,target_property,wrap,filter_column,filter_endswith,param_name,note
|
|
199
|
+
biosample,biosample_disease,biosample_id_namespace|biosample_local_id,entity_key,disease,ontology_term,disease,associatedDisease,ident_ref,association_type,:1,,
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**croissant** `entities.csv` — the root crate becomes a Croissant Dataset (export-only):
|
|
203
|
+
```
|
|
204
|
+
ROCrateMetadataElem,ROOT,CroissantDataset,,root,keep,,,FALSE,,0,the crate root -> Croissant Dataset
|
|
205
|
+
```
|
|
206
|
+
croissant `properties.csv` — encodingFormat mapped to a MIME type on export:
|
|
207
|
+
```
|
|
208
|
+
Dataset,format,CroissantFileObject,encodingFormat,,map_format_to_mime_type,export,FALSE,,scalar,,,,,,,
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**example** `entities.csv` + `properties.csv` — the minimal plugin
|
|
212
|
+
(`plugins/example/`), a toy memo becoming a schema.org Dataset:
|
|
213
|
+
```
|
|
214
|
+
memo,,Dataset,https://schema.org/Dataset,data,keep,,,FALSE,,one Dataset per memo
|
|
215
|
+
memo,title,Dataset,name,scalar,,import,,required,scalar,,,,,,,
|
|
216
|
+
```
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fairscape-conversion
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: One mapping format and one engine for every fairscape RO-Crate converter (wrroc, d4d, c2m2, croissant, snakemake, cromwell, mlflow)
|
|
5
|
+
Author-email: Justin Niestroy <jniestroy@gmail.com>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Keywords: fairscape,ro-crate,provenance,evi,converter,metadata
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Topic :: Scientific/Engineering
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: fairscape-models>=1.1.5
|
|
15
|
+
Requires-Dist: pyyaml
|
|
16
|
+
|
|
17
|
+
# fairscape-conversion
|
|
18
|
+
|
|
19
|
+
Your metadata is already written down — as a datasheet, a datapackage, or a
|
|
20
|
+
workflow engine's run output. Convert it into a FAIRSCAPE/EVI RO-Crate
|
|
21
|
+
(`ro-crate-metadata.json`) instead of re-entering it. One command per format.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install -e . # from this directory; installs fairscape-conversion
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The `fairscape import` / `fairscape export` commands come with the companion
|
|
28
|
+
`fairscape` CLI (not yet published). Without it, every conversion also runs as
|
|
29
|
+
`python -m fairscape_conversion.core.cli convert <format> <import|export> IN [OUT]`.
|
|
30
|
+
|
|
31
|
+
## Import — get an RO-Crate
|
|
32
|
+
|
|
33
|
+
Pick the row that matches what you have.
|
|
34
|
+
|
|
35
|
+
| You have | You need | Run |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| A Datasheet for Datasets (D4D) | the datasheet as YAML or JSON | `fairscape import d4d datasheet.yaml -o ./crate` |
|
|
38
|
+
| A CFDE C2M2 datapackage | the directory of TSVs + `C2M2_datapackage.json` | `fairscape import c2m2 ./datapackage-dir -o ./crate` |
|
|
39
|
+
| A Workflow Run RO-Crate | its `ro-crate-metadata.json` | `fairscape import wrroc ro-crate-metadata.json -o ./crate` |
|
|
40
|
+
| A finished Cromwell/WDL run | the file from `cromwell run -m metadata.json` | `fairscape import cromwell metadata.json -o ./crate` |
|
|
41
|
+
| A finished Snakemake run | the records JSON from `snakemake --reporter fairscape` | `fairscape import snakemake records.json -o ./crate` |
|
|
42
|
+
| Finished MLflow runs | the tracking store (an `mlruns` dir or tracking URI) and `pip install mlflow` | `fairscape import mlflow ./mlruns --experiment NAME -o ./crate` |
|
|
43
|
+
|
|
44
|
+
## Export — from an RO-Crate
|
|
45
|
+
|
|
46
|
+
| You want | Run |
|
|
47
|
+
|---|---|
|
|
48
|
+
| A D4D datasheet | `fairscape export d4d ro-crate-metadata.json` |
|
|
49
|
+
| A Workflow Run RO-Crate | `fairscape export wrroc ro-crate-metadata.json` |
|
|
50
|
+
| An MLCommons Croissant document | `fairscape export croissant ro-crate-metadata.json` |
|
|
51
|
+
|
|
52
|
+
## Try it — no data needed
|
|
53
|
+
|
|
54
|
+
Every format ships a real example input and its expected output inside its
|
|
55
|
+
plugin folder, so you can run any conversion right now:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
fairscape import d4d plugins/d4d/input.yaml -o /tmp/crate
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
| format | example input | expected output |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| d4d | `plugins/d4d/input.yaml` (the AI-READI datasheet) | `plugins/d4d/golden.json` |
|
|
64
|
+
| c2m2 | `plugins/c2m2/input-datapackage/` (miniature datapackage) | `plugins/c2m2/golden.json` |
|
|
65
|
+
| wrroc | `plugins/wrroc/input.json` (CWL revsort run crate) | `plugins/wrroc/golden.json` |
|
|
66
|
+
| cromwell | `plugins/cromwell/input.json` (scatter workflow records) | `plugins/cromwell/golden.json` |
|
|
67
|
+
| snakemake | `plugins/snakemake/input.json` (3-rule chain records) | `plugins/snakemake/golden.json` |
|
|
68
|
+
| mlflow | `plugins/mlflow/input.json` (iris experiment records) | `plugins/mlflow/golden.json` |
|
|
69
|
+
| croissant | `plugins/croissant/input.json` (export this crate) | `plugins/croissant/golden.json` |
|
|
70
|
+
|
|
71
|
+
## From Python
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
import yaml
|
|
75
|
+
from fairscape_conversion.plugins import d4d
|
|
76
|
+
|
|
77
|
+
crate = d4d.convert("import", yaml.safe_load(open("datasheet.yaml")))
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Same shape for every format: `wrroc`, `c2m2`, `cromwell`, `snakemake`,
|
|
81
|
+
`mlflow` (`convert("import", ...)`), and `d4d`/`wrroc`/`croissant`
|
|
82
|
+
(`convert("export", crate)`).
|
|
83
|
+
|
|
84
|
+
## More
|
|
85
|
+
|
|
86
|
+
- Your format isn't listed → [`docs/NEW-PLUGIN.md`](docs/NEW-PLUGIN.md) —
|
|
87
|
+
a converter is a folder of two CSVs plus a small plugin class.
|
|
88
|
+
- How the engine works, what's tested → [`docs/INTERNALS.md`](docs/INTERNALS.md)
|
|
89
|
+
and [`MAPPING-SCHEMA.md`](MAPPING-SCHEMA.md).
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# fairscape-conversion
|
|
2
|
+
|
|
3
|
+
Your metadata is already written down — as a datasheet, a datapackage, or a
|
|
4
|
+
workflow engine's run output. Convert it into a FAIRSCAPE/EVI RO-Crate
|
|
5
|
+
(`ro-crate-metadata.json`) instead of re-entering it. One command per format.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install -e . # from this directory; installs fairscape-conversion
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The `fairscape import` / `fairscape export` commands come with the companion
|
|
12
|
+
`fairscape` CLI (not yet published). Without it, every conversion also runs as
|
|
13
|
+
`python -m fairscape_conversion.core.cli convert <format> <import|export> IN [OUT]`.
|
|
14
|
+
|
|
15
|
+
## Import — get an RO-Crate
|
|
16
|
+
|
|
17
|
+
Pick the row that matches what you have.
|
|
18
|
+
|
|
19
|
+
| You have | You need | Run |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| A Datasheet for Datasets (D4D) | the datasheet as YAML or JSON | `fairscape import d4d datasheet.yaml -o ./crate` |
|
|
22
|
+
| A CFDE C2M2 datapackage | the directory of TSVs + `C2M2_datapackage.json` | `fairscape import c2m2 ./datapackage-dir -o ./crate` |
|
|
23
|
+
| A Workflow Run RO-Crate | its `ro-crate-metadata.json` | `fairscape import wrroc ro-crate-metadata.json -o ./crate` |
|
|
24
|
+
| A finished Cromwell/WDL run | the file from `cromwell run -m metadata.json` | `fairscape import cromwell metadata.json -o ./crate` |
|
|
25
|
+
| A finished Snakemake run | the records JSON from `snakemake --reporter fairscape` | `fairscape import snakemake records.json -o ./crate` |
|
|
26
|
+
| Finished MLflow runs | the tracking store (an `mlruns` dir or tracking URI) and `pip install mlflow` | `fairscape import mlflow ./mlruns --experiment NAME -o ./crate` |
|
|
27
|
+
|
|
28
|
+
## Export — from an RO-Crate
|
|
29
|
+
|
|
30
|
+
| You want | Run |
|
|
31
|
+
|---|---|
|
|
32
|
+
| A D4D datasheet | `fairscape export d4d ro-crate-metadata.json` |
|
|
33
|
+
| A Workflow Run RO-Crate | `fairscape export wrroc ro-crate-metadata.json` |
|
|
34
|
+
| An MLCommons Croissant document | `fairscape export croissant ro-crate-metadata.json` |
|
|
35
|
+
|
|
36
|
+
## Try it — no data needed
|
|
37
|
+
|
|
38
|
+
Every format ships a real example input and its expected output inside its
|
|
39
|
+
plugin folder, so you can run any conversion right now:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
fairscape import d4d plugins/d4d/input.yaml -o /tmp/crate
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
| format | example input | expected output |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| d4d | `plugins/d4d/input.yaml` (the AI-READI datasheet) | `plugins/d4d/golden.json` |
|
|
48
|
+
| c2m2 | `plugins/c2m2/input-datapackage/` (miniature datapackage) | `plugins/c2m2/golden.json` |
|
|
49
|
+
| wrroc | `plugins/wrroc/input.json` (CWL revsort run crate) | `plugins/wrroc/golden.json` |
|
|
50
|
+
| cromwell | `plugins/cromwell/input.json` (scatter workflow records) | `plugins/cromwell/golden.json` |
|
|
51
|
+
| snakemake | `plugins/snakemake/input.json` (3-rule chain records) | `plugins/snakemake/golden.json` |
|
|
52
|
+
| mlflow | `plugins/mlflow/input.json` (iris experiment records) | `plugins/mlflow/golden.json` |
|
|
53
|
+
| croissant | `plugins/croissant/input.json` (export this crate) | `plugins/croissant/golden.json` |
|
|
54
|
+
|
|
55
|
+
## From Python
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
import yaml
|
|
59
|
+
from fairscape_conversion.plugins import d4d
|
|
60
|
+
|
|
61
|
+
crate = d4d.convert("import", yaml.safe_load(open("datasheet.yaml")))
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Same shape for every format: `wrroc`, `c2m2`, `cromwell`, `snakemake`,
|
|
65
|
+
`mlflow` (`convert("import", ...)`), and `d4d`/`wrroc`/`croissant`
|
|
66
|
+
(`convert("export", crate)`).
|
|
67
|
+
|
|
68
|
+
## More
|
|
69
|
+
|
|
70
|
+
- Your format isn't listed → [`docs/NEW-PLUGIN.md`](docs/NEW-PLUGIN.md) —
|
|
71
|
+
a converter is a folder of two CSVs plus a small plugin class.
|
|
72
|
+
- How the engine works, what's tested → [`docs/INTERNALS.md`](docs/INTERNALS.md)
|
|
73
|
+
and [`MAPPING-SCHEMA.md`](MAPPING-SCHEMA.md).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Unified fairscape RO-Crate converters — one CSV mapping format for all."""
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Unified converter core — one mapping format, one kernel, per-plugin code.
|
|
3
|
+
|
|
4
|
+
Public API used by the plugins:
|
|
5
|
+
|
|
6
|
+
from fairscape_conversion.core import (Plugin, Context, Record, Mapping,
|
|
7
|
+
run_pipeline, apply_import_rules, apply_export_rules,
|
|
8
|
+
run_import_parser, run_export_parser,
|
|
9
|
+
interpolate, roundtrip)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from . import parsers, roundtrip
|
|
13
|
+
from .engine import (Context, Record, apply_export_rules, apply_import_rules,
|
|
14
|
+
interpolate, run_export_parser, run_import_parser,
|
|
15
|
+
run_pipeline)
|
|
16
|
+
from .loader import load_mapping
|
|
17
|
+
from .plugin import PluginBase
|
|
18
|
+
from .records import (SourceRecord, classify_node, records_from_graph,
|
|
19
|
+
records_from_items, types_of)
|
|
20
|
+
from .registry import Plugin
|
|
21
|
+
from .schema import (AssocRule, CvBase, EntityRule, Mapping, PropertyRule,
|
|
22
|
+
split_pipe)
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"Plugin", "PluginBase", "Context", "Record", "Mapping",
|
|
26
|
+
"SourceRecord", "classify_node", "records_from_items", "records_from_graph",
|
|
27
|
+
"types_of",
|
|
28
|
+
"EntityRule", "PropertyRule", "AssocRule", "CvBase", "split_pipe",
|
|
29
|
+
"load_mapping", "run_pipeline",
|
|
30
|
+
"apply_import_rules", "apply_export_rules",
|
|
31
|
+
"run_import_parser", "run_export_parser",
|
|
32
|
+
"interpolate", "parsers", "roundtrip",
|
|
33
|
+
]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Deterministic ARK minting — the fairscape identifier scheme, in core.
|
|
2
|
+
|
|
3
|
+
``ark:{naan}/{prefix}-{slug(name)}-{sha1(source)[:7]}``: the slug is the
|
|
4
|
+
readable half, the hash names the thing (a stable source string — a file path,
|
|
5
|
+
a rule name + outputs, a session id), so re-running a conversion over the same
|
|
6
|
+
source reproduces the same identifiers. This is the scheme nf-fairscape and
|
|
7
|
+
snakemake-report-plugin-fairscape mint, and the one the wrroc plugin composes
|
|
8
|
+
(with a ``wrroc-`` tag folded into the prefix).
|
|
9
|
+
|
|
10
|
+
Plugins that need a namespace tag put it in ``prefix`` (``wrroc-computation``);
|
|
11
|
+
the minter itself adds nothing.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import hashlib
|
|
15
|
+
import re
|
|
16
|
+
|
|
17
|
+
DEFAULT_NAAN = "59853"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def slugify(text, max_len=40, fallback="entity"):
|
|
21
|
+
slug = re.sub(r"[^a-z0-9]+", "-", str(text).lower()).strip("-")
|
|
22
|
+
return slug[:max_len].strip("-") or fallback
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def short_hash(text, length=7):
|
|
26
|
+
return hashlib.sha1(str(text).encode()).hexdigest()[:length]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def mint_ark(naan, prefix, name, source, fallback="entity"):
|
|
30
|
+
return f"ark:{naan}/{prefix}-{slugify(name, fallback=fallback)}-{short_hash(source)}"
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Tiny CLI: ``fairscape_conversion convert <plugin> <import|export> <input> [output]``.
|
|
3
|
+
|
|
4
|
+
Reads JSON or YAML by extension, dispatches to the named plugin's ``convert``,
|
|
5
|
+
writes JSON (or YAML for a ``.yaml``/``.yml`` output path). The library API
|
|
6
|
+
(``fairscape_conversion.plugins.<name>.convert``) is what the tests and other code call; this
|
|
7
|
+
is only a convenience wrapper.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import importlib
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _read(path: Path):
|
|
19
|
+
text = path.read_text()
|
|
20
|
+
if path.suffix in (".yaml", ".yml"):
|
|
21
|
+
import yaml
|
|
22
|
+
return yaml.safe_load(text)
|
|
23
|
+
return json.loads(text)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _write(path: Path, data):
|
|
27
|
+
if path.suffix in (".yaml", ".yml"):
|
|
28
|
+
import yaml
|
|
29
|
+
path.write_text(yaml.dump(data, sort_keys=False, allow_unicode=True))
|
|
30
|
+
else:
|
|
31
|
+
path.write_text(json.dumps(data, indent=2, default=str))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def main(argv=None):
|
|
35
|
+
argv = argv if argv is not None else sys.argv[1:]
|
|
36
|
+
if len(argv) < 4 or argv[0] != "convert":
|
|
37
|
+
print(__doc__)
|
|
38
|
+
return 1
|
|
39
|
+
_, plugin_name, direction, in_path, *rest = argv
|
|
40
|
+
if direction not in ("import", "export"):
|
|
41
|
+
print(f"direction must be import|export, got {direction!r}")
|
|
42
|
+
return 1
|
|
43
|
+
plugin = importlib.import_module(f"fairscape_conversion.plugins.{plugin_name}")
|
|
44
|
+
result = plugin.convert(direction, _read(Path(in_path)))
|
|
45
|
+
if rest:
|
|
46
|
+
_write(Path(rest[0]), result)
|
|
47
|
+
print(f"wrote {rest[0]}")
|
|
48
|
+
else:
|
|
49
|
+
print(json.dumps(result, indent=2, default=str))
|
|
50
|
+
return 0
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if __name__ == "__main__":
|
|
54
|
+
raise SystemExit(main())
|