272 lines
7.4 KiB
Python
272 lines
7.4 KiB
Python
# /// script
|
|
# requires-python = ">=3.12"
|
|
# dependencies = [
|
|
# "dump-things-pyclient @ https://hub.psychoinformatics.de/datalink/dump-things-pyclient.git",
|
|
# "things-enrichment-tools @ https://hub.psychoinformatics.de/orinoco/things-enrichment-tools.git",
|
|
# "click",
|
|
# ]
|
|
# ///
|
|
|
|
|
|
import csv
|
|
import re
|
|
import sys
|
|
from os import environ
|
|
|
|
import click
|
|
from dump_things_pyclient.communicate import (
|
|
collection_read_record_with_pid,
|
|
collection_write_record,
|
|
get_session,
|
|
)
|
|
|
|
import things_enrichment_tools as tet
|
|
|
|
# PID anchor is the things_enrichment_tools repo/dataset UUID
|
|
self_id = 'datalad:10e82c66-d2ca-4f8f-884e-f9a5728eed1d/instruments/trr379-q01-subject-importer'
|
|
|
|
|
|
def read_tsv(path):
|
|
with open(path, newline='', encoding='utf-8') as f:
|
|
reader = csv.DictReader(f, delimiter='\t')
|
|
for row in reader:
|
|
yield dict(row)
|
|
|
|
|
|
def proc_sub(
|
|
sub_id: str,
|
|
group_id: str | None,
|
|
*,
|
|
study_pid: str,
|
|
table_pid: str,
|
|
pool_api_url: str,
|
|
pool_collection: str,
|
|
sub_pid_prefix: str,
|
|
token: str,
|
|
session,
|
|
) -> None:
|
|
""" """
|
|
modified = set()
|
|
owner_props = {'source_id': table_pid, 'owner_id': self_id}
|
|
pid = f'{sub_pid_prefix}{sub_id}'
|
|
rec = collection_read_record_with_pid(
|
|
pool_api_url,
|
|
pool_collection,
|
|
pid,
|
|
format='json',
|
|
token=token,
|
|
session=session,
|
|
) or {'pid': pid}
|
|
modified.add(
|
|
tet.update_data_property(
|
|
rec,
|
|
collection_slot='characterized_by',
|
|
predicate='dcterms:type',
|
|
value_key='object',
|
|
topical_slot='kind',
|
|
value='obo:NCBITaxon_9606',
|
|
**owner_props,
|
|
)
|
|
)
|
|
modified.add(
|
|
tet.update_data_property(
|
|
rec,
|
|
collection_slot='characterized_by',
|
|
predicate='dlthings:study',
|
|
value_key='object',
|
|
topical_slot='study',
|
|
value=study_pid,
|
|
**owner_props,
|
|
)
|
|
)
|
|
|
|
modified.add(
|
|
tet.update_object_property(
|
|
rec,
|
|
slot='generated_by',
|
|
value={'object': study_pid},
|
|
**owner_props,
|
|
)
|
|
)
|
|
if group_id:
|
|
modified.add(
|
|
tet.update_multivalued_object_property(
|
|
rec,
|
|
slot='influenced_by',
|
|
values=[{'object': group_id, 'roles': ['obo:OBI_0000174']}],
|
|
**owner_props,
|
|
)
|
|
)
|
|
modified.add(
|
|
tet.update_multivalued_object_property(
|
|
rec,
|
|
slot='identifiers',
|
|
values=[{'creator': 'trr379root:projects/q01', 'notation': sub_id}],
|
|
**owner_props,
|
|
)
|
|
)
|
|
|
|
if True not in modified:
|
|
return False
|
|
|
|
collection_write_record(
|
|
pool_api_url,
|
|
pool_collection,
|
|
class_name='TRR379Subject',
|
|
record=rec,
|
|
format='json',
|
|
token=token,
|
|
session=session,
|
|
)
|
|
return True
|
|
|
|
|
|
@click.command()
|
|
@click.option(
|
|
"--subid-colname",
|
|
default='trr-sub',
|
|
show_default=True,
|
|
help="name of the TSV column holding the subject identifiers",
|
|
)
|
|
@click.option(
|
|
"--grpid-colname",
|
|
default='recr-group',
|
|
show_default=True,
|
|
help="name of the TSV column holding the recruiting group identifiers",
|
|
)
|
|
@click.option(
|
|
"--recr-group", 'recr_groups',
|
|
type=(str, str),
|
|
multiple=True,
|
|
help="code/id pair; may be given multiple times.",
|
|
)
|
|
@click.option(
|
|
"--subid-expr",
|
|
default=r'^[A-Z]\d\d\d\d$',
|
|
show_default=True,
|
|
help="Regular expression any subject identifier must match.",
|
|
)
|
|
@click.option(
|
|
"--subpid-prefix",
|
|
default='trr379:subjects/',
|
|
show_default=True,
|
|
help='prefix to prepend to the subject identifier to form the record PID',
|
|
)
|
|
@click.option(
|
|
'--pool-api-url',
|
|
default='https://pool.v0.trr379.de/api',
|
|
help='Knowledge pool API URL to receive the subject record submission.',
|
|
show_default=True,
|
|
)
|
|
@click.option(
|
|
'--pool-collection',
|
|
default='protected',
|
|
help='Target knowledge pool record collection',
|
|
show_default=True,
|
|
)
|
|
@click.argument(
|
|
'study_pid',
|
|
)
|
|
@click.argument(
|
|
'table_pid',
|
|
)
|
|
@click.argument(
|
|
'infile',
|
|
)
|
|
def main(
|
|
infile: str,
|
|
study_pid: str,
|
|
table_pid: str,
|
|
pool_api_url: str,
|
|
pool_collection: str,
|
|
recr_groups: tuple[tuple[str, str]],
|
|
subid_expr: str,
|
|
subid_colname: str,
|
|
grpid_colname: str,
|
|
subpid_prefix: str,
|
|
) -> None:
|
|
"""Read participant information and submit (updated) subject records
|
|
|
|
Three arguments are required (in order):
|
|
|
|
\b
|
|
1. study PID
|
|
2. table PID
|
|
3. input file path
|
|
|
|
The input file must be in tab-separated value (TSV). Two items of
|
|
information are read from each row in this table (one row per participant):
|
|
|
|
\b
|
|
- pseudonymized subject identifier (`trr-sub` column)
|
|
- subject recruiting group code (`recr-group` column)
|
|
|
|
Name and conventions for this information matches the agreement for ID
|
|
broker table formatting in TRR379.
|
|
|
|
The study PID identifies the the study (part) from which the imported
|
|
information originate. It should match an existing knowledge pool `Study`
|
|
record. For example, the value `trr379:studies/q01-fp1-aachen` would expand
|
|
to `https://trr379.de/ns/studies/q01-fp1-aachen`.
|
|
|
|
The table PID identifiers the specific version of the source table. For
|
|
files tracked in Git, this can be the content blob SHA (prefixed with
|
|
`dldi:`). Alternatively, an ISO date string can be given.
|
|
"""
|
|
errored = False
|
|
session = get_session()
|
|
sub_id_re = re.compile(subid_expr)
|
|
recr_group_map = dict(recr_groups)
|
|
token = environ['DTC_TOKEN']
|
|
for row in read_tsv(infile):
|
|
sub_id = row[subid_colname]
|
|
# take off BIDS prefix
|
|
if sub_id.startswith('sub-'):
|
|
sub_id = sub_id[4:]
|
|
# minimum sanity-checking to avoid spamming the knowledge pool
|
|
if not sub_id_re.match(sub_id):
|
|
print( # noqa: T201
|
|
f'ERROR: {sub_id!r} is not a valid subject identifier',
|
|
file=sys.stderr,
|
|
)
|
|
errored = True
|
|
continue
|
|
group_id = row[grpid_colname]
|
|
# map to standard terminology
|
|
if group_id:
|
|
try:
|
|
group_id = recr_group_map[group_id]
|
|
except KeyError:
|
|
print( # noqa: T201
|
|
f'IGNORE: {sub_id!r} {grpid_colname} {group_id!r} unrecognized',
|
|
file=sys.stderr,
|
|
)
|
|
group_id = None
|
|
errored = True
|
|
else:
|
|
group_id = None
|
|
|
|
if proc_sub(
|
|
sub_id,
|
|
group_id,
|
|
study_pid=study_pid,
|
|
table_pid=table_pid,
|
|
pool_api_url=pool_api_url,
|
|
pool_collection=pool_collection,
|
|
sub_pid_prefix=subpid_prefix,
|
|
token=token,
|
|
session=session,
|
|
):
|
|
print(f'OK: {sub_id!r} submitted', file=sys.stderr) # noqa: T201
|
|
else:
|
|
# we do not issue those, because they are the majority and drown
|
|
# everything else in them, but the others are the key messages.
|
|
#print(f'SKIP: {sub_id!r} exists and is unmodified', file=sys.stderr) # noqa: T201
|
|
pass
|
|
if errored:
|
|
print('ERROR: see report(s) above', file=sys.stderr) # noqa: T201
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|