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.
Files changed (99) hide show
  1. fairscape_conversion-0.1.0/MAPPING-SCHEMA.md +216 -0
  2. fairscape_conversion-0.1.0/PKG-INFO +89 -0
  3. fairscape_conversion-0.1.0/README.md +73 -0
  4. fairscape_conversion-0.1.0/__init__.py +1 -0
  5. fairscape_conversion-0.1.0/core/__init__.py +33 -0
  6. fairscape_conversion-0.1.0/core/arks.py +30 -0
  7. fairscape_conversion-0.1.0/core/cli.py +54 -0
  8. fairscape_conversion-0.1.0/core/engine.py +226 -0
  9. fairscape_conversion-0.1.0/core/loader.py +122 -0
  10. fairscape_conversion-0.1.0/core/parsers.py +54 -0
  11. fairscape_conversion-0.1.0/core/plugin.py +182 -0
  12. fairscape_conversion-0.1.0/core/records.py +99 -0
  13. fairscape_conversion-0.1.0/core/registry.py +55 -0
  14. fairscape_conversion-0.1.0/core/roundtrip.py +97 -0
  15. fairscape_conversion-0.1.0/core/schema.py +138 -0
  16. fairscape_conversion-0.1.0/fairscape_conversion.egg-info/PKG-INFO +89 -0
  17. fairscape_conversion-0.1.0/fairscape_conversion.egg-info/SOURCES.txt +179 -0
  18. fairscape_conversion-0.1.0/fairscape_conversion.egg-info/dependency_links.txt +1 -0
  19. fairscape_conversion-0.1.0/fairscape_conversion.egg-info/requires.txt +2 -0
  20. fairscape_conversion-0.1.0/fairscape_conversion.egg-info/top_level.txt +1 -0
  21. fairscape_conversion-0.1.0/plugins/__init__.py +1 -0
  22. fairscape_conversion-0.1.0/plugins/c2m2/__init__.py +29 -0
  23. fairscape_conversion-0.1.0/plugins/c2m2/associations.csv +43 -0
  24. fairscape_conversion-0.1.0/plugins/c2m2/computed.csv +5 -0
  25. fairscape_conversion-0.1.0/plugins/c2m2/constants.csv +39 -0
  26. fairscape_conversion-0.1.0/plugins/c2m2/cv_bases.csv +19 -0
  27. fairscape_conversion-0.1.0/plugins/c2m2/entities.csv +19 -0
  28. fairscape_conversion-0.1.0/plugins/c2m2/golden.json +1167 -0
  29. fairscape_conversion-0.1.0/plugins/c2m2/impl.py +547 -0
  30. fairscape_conversion-0.1.0/plugins/c2m2/input-datapackage/C2M2_datapackage.json +135 -0
  31. fairscape_conversion-0.1.0/plugins/c2m2/input-datapackage/biosample.tsv +2 -0
  32. fairscape_conversion-0.1.0/plugins/c2m2/input-datapackage/file.tsv +3 -0
  33. fairscape_conversion-0.1.0/plugins/c2m2/input-datapackage/file_describes_biosample.tsv +2 -0
  34. fairscape_conversion-0.1.0/plugins/c2m2/input-datapackage/file_format.tsv +2 -0
  35. fairscape_conversion-0.1.0/plugins/c2m2/input-datapackage/id_namespace.tsv +2 -0
  36. fairscape_conversion-0.1.0/plugins/c2m2/input-datapackage/project.tsv +2 -0
  37. fairscape_conversion-0.1.0/plugins/c2m2/input-datapackage/subject.tsv +1 -0
  38. fairscape_conversion-0.1.0/plugins/c2m2/mapper.py +386 -0
  39. fairscape_conversion-0.1.0/plugins/c2m2/ontology.py +140 -0
  40. fairscape_conversion-0.1.0/plugins/c2m2/parsers.py +371 -0
  41. fairscape_conversion-0.1.0/plugins/c2m2/properties.csv +122 -0
  42. fairscape_conversion-0.1.0/plugins/c2m2/root.json +108 -0
  43. fairscape_conversion-0.1.0/plugins/croissant/__init__.py +41 -0
  44. fairscape_conversion-0.1.0/plugins/croissant/entities.csv +5 -0
  45. fairscape_conversion-0.1.0/plugins/croissant/golden.json +611 -0
  46. fairscape_conversion-0.1.0/plugins/croissant/impl.py +95 -0
  47. fairscape_conversion-0.1.0/plugins/croissant/input.json +1167 -0
  48. fairscape_conversion-0.1.0/plugins/croissant/parsers.py +52 -0
  49. fairscape_conversion-0.1.0/plugins/croissant/properties.csv +43 -0
  50. fairscape_conversion-0.1.0/plugins/cromwell/__init__.py +206 -0
  51. fairscape_conversion-0.1.0/plugins/cromwell/entities.csv +8 -0
  52. fairscape_conversion-0.1.0/plugins/cromwell/extract.py +344 -0
  53. fairscape_conversion-0.1.0/plugins/cromwell/golden.json +549 -0
  54. fairscape_conversion-0.1.0/plugins/cromwell/input.json +174 -0
  55. fairscape_conversion-0.1.0/plugins/cromwell/parsers.py +259 -0
  56. fairscape_conversion-0.1.0/plugins/cromwell/properties.csv +56 -0
  57. fairscape_conversion-0.1.0/plugins/d4d/__init__.py +22 -0
  58. fairscape_conversion-0.1.0/plugins/d4d/entities.csv +5 -0
  59. fairscape_conversion-0.1.0/plugins/d4d/golden.json +520 -0
  60. fairscape_conversion-0.1.0/plugins/d4d/impl.py +327 -0
  61. fairscape_conversion-0.1.0/plugins/d4d/input.yaml +689 -0
  62. fairscape_conversion-0.1.0/plugins/d4d/parsers.py +128 -0
  63. fairscape_conversion-0.1.0/plugins/d4d/properties.csv +78 -0
  64. fairscape_conversion-0.1.0/plugins/example/__init__.py +45 -0
  65. fairscape_conversion-0.1.0/plugins/example/entities.csv +2 -0
  66. fairscape_conversion-0.1.0/plugins/example/golden.json +20 -0
  67. fairscape_conversion-0.1.0/plugins/example/input.json +16 -0
  68. fairscape_conversion-0.1.0/plugins/example/properties.csv +4 -0
  69. fairscape_conversion-0.1.0/plugins/mlflow/__init__.py +204 -0
  70. fairscape_conversion-0.1.0/plugins/mlflow/entities.csv +8 -0
  71. fairscape_conversion-0.1.0/plugins/mlflow/extract.py +554 -0
  72. fairscape_conversion-0.1.0/plugins/mlflow/golden.json +389 -0
  73. fairscape_conversion-0.1.0/plugins/mlflow/input.json +230 -0
  74. fairscape_conversion-0.1.0/plugins/mlflow/parsers.py +320 -0
  75. fairscape_conversion-0.1.0/plugins/mlflow/properties.csv +63 -0
  76. fairscape_conversion-0.1.0/plugins/snakemake/__init__.py +185 -0
  77. fairscape_conversion-0.1.0/plugins/snakemake/entities.csv +8 -0
  78. fairscape_conversion-0.1.0/plugins/snakemake/golden.json +407 -0
  79. fairscape_conversion-0.1.0/plugins/snakemake/input.json +130 -0
  80. fairscape_conversion-0.1.0/plugins/snakemake/parsers.py +263 -0
  81. fairscape_conversion-0.1.0/plugins/snakemake/properties.csv +56 -0
  82. fairscape_conversion-0.1.0/plugins/wrroc/__init__.py +34 -0
  83. fairscape_conversion-0.1.0/plugins/wrroc/entities.csv +19 -0
  84. fairscape_conversion-0.1.0/plugins/wrroc/golden.json +335 -0
  85. fairscape_conversion-0.1.0/plugins/wrroc/hooks.py +359 -0
  86. fairscape_conversion-0.1.0/plugins/wrroc/input.json +345 -0
  87. fairscape_conversion-0.1.0/plugins/wrroc/parsers.py +267 -0
  88. fairscape_conversion-0.1.0/plugins/wrroc/properties.csv +36 -0
  89. fairscape_conversion-0.1.0/pyproject.toml +49 -0
  90. fairscape_conversion-0.1.0/setup.cfg +4 -0
  91. fairscape_conversion-0.1.0/tests/test_c2m2_parity.py +63 -0
  92. fairscape_conversion-0.1.0/tests/test_croissant_parity.py +40 -0
  93. fairscape_conversion-0.1.0/tests/test_cromwell.py +124 -0
  94. fairscape_conversion-0.1.0/tests/test_d4d_parity.py +73 -0
  95. fairscape_conversion-0.1.0/tests/test_example.py +23 -0
  96. fairscape_conversion-0.1.0/tests/test_mlflow.py +74 -0
  97. fairscape_conversion-0.1.0/tests/test_plugin_examples.py +72 -0
  98. fairscape_conversion-0.1.0/tests/test_snakemake.py +40 -0
  99. 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())