diff --git a/.maint/CONTRIBUTORS.md b/.maint/CONTRIBUTORS.md new file mode 100644 index 0000000000..69a93bfb02 --- /dev/null +++ b/.maint/CONTRIBUTORS.md @@ -0,0 +1,17 @@ +# CONTRIBUTORS + +This document lists those who have made contributions to the Project. +As per the contributor guidelines, they should be included by default in any publications derived from the Project. + +If you are new to the project, don't forget to add your name and affiliation to the list of contributors here! Our Welcome Bot will send an automated message reminding this to first-time contributors. +Before every release, unlisted contributors will be invited again to add their names to this file (just in case they missed the automated message from our Welcome Bot). + +| **Lastname** | **Name** | **Handle** | **ORCID** | **Affiliation** | +| --- | --- | --- | --- | --- | +| Adebimpe | Azeez | @a3sha2 | 0000-0001-9049-0135 | Perelman School of Medicine, University of Pennsylvania, PA, USA | +| Blair | Ross W. | @rwblair | 0000-0003-3007-1056 | Department of Psychology, Stanford University, CA, USA | +| Cieslak | Matthew | @mattcieslak | 0000-0002-1931-4734 | Perelman School of Medicine, University of Pennsylvania, PA, USA | +| MacNicol | Eilidh | @eilidhmacnicol | 0000-0003-3715-7012 | Department of Neuroimaging, King's College London | +| Naveau | Mikaël | @naveau | 0000-0001-6948-9068 | Cyceron, UMS 3408 (CNRS - UCBN), France | +| Sitek | Kevin R. | @sitek | 0000-0002-2172-5786 | Speech & Hearing Bioscience & Technology Program, Harvard University | +| Sneve | Markus H. | @markushs | 0000-0001-7644-7915 | Center for Lifespan Changes in Brain and Cognition, University of Oslo | diff --git a/.maint/FORMER.md b/.maint/FORMER.md new file mode 100644 index 0000000000..383f17bf0c --- /dev/null +++ b/.maint/FORMER.md @@ -0,0 +1,10 @@ +# FORMER MEMBERS + +This document lists former contributors or maintainers who want to disengage from the Project, and seek to be dismissed in communications or future papers. +By adding your name to this list you are giving up on all your responsibilities to the project. +Should you desire to be considered back as a contributor or maintainer, please remove your name from this list and proceed as prescribed in the governance documents. + +| **Lastname** | **Name** | **Handle** | +| --- | --- | --- | +| Berleant | Shoshana | @berleant | +| Bot | NiPreps | @nipreps-bot | diff --git a/.maint/MAINTAINERS.md b/.maint/MAINTAINERS.md new file mode 100644 index 0000000000..07843de19f --- /dev/null +++ b/.maint/MAINTAINERS.md @@ -0,0 +1,15 @@ +# Maintainers + +This document lists the Maintainers of the project. +Maintainers may be added once approved by the existing maintainers as described in the `../GOVERNANCE.md` document. +By adding your name to this list you are agreeing to abide by the governance documents and to abide by all of the Organization's polices, including the code of conduct, trademark policy, and antitrust policy. +If you are participating because of your affiliation with another organization (designated below), you represent that you have the authority to bind that organization to these policies. + + + +| **Lastname** | **Name** | **Handle** | **ORCID** | **Affiliation** | +| --- | --- | --- | --- | --- | +| Esteban | Oscar | @oesteban | 0000-0001-8435-6191 | Lausanne University Hospital and University of Lausanne, Lausanne, Switzerland | +| Goncalves | Mathias | @mgxd | 0000-0002-7252-7771 | Department of Psychology, Stanford University, CA, USA | +| Markiewicz | Christopher J. | @effigies | 0000-0002-6533-164X | Department of Psychology, Stanford University, CA, USA | + diff --git a/.maint/PIs.md b/.maint/PIs.md new file mode 100644 index 0000000000..c038f0c016 --- /dev/null +++ b/.maint/PIs.md @@ -0,0 +1,17 @@ +# PRINCIPAL INVESTIGATORS + +This documents the key personnel who oversees the development of the Project and secures funding. +The names in this file are designated by the Organization's TSC at the time of accepting the Project under the Organization. +Changes to this file must be approved by the TSC. +When a PI ceases serving as such, by default, their name will be placed in the `CONTRIBUTORS.md` file should it not be there already. + +By having your name in this list you are agreeing to abide by the governance documents and to abide by all of the Organization's polices, including the code of conduct, trademark policy, and antitrust policy. +If you are participating because of your affiliation with another organization (designated below), you represent that you have the authority to bind that organization to these policies. + +| **Lastname** | **Name** | **Handle** | **ORCID** | **Affiliation** | +| --- | --- | --- | --- | --- | --- | +| Esteban | Oscar | @oesteban | 0000-0001-8435-6191 | Department of Radiology, Lausanne University Hospital and University of Lausanne | +| Poldrack | Russell A. | @poldrack | 0000-0001-6755-0259 | Department of Psychology, Stanford University, CA, USA | +| Satterthwaite | Theodore D. | | 0000-0001-7072-9399 | Perelman School of Medicine, University of Pennsylvania, PA, USA | +| Gorgolewski | Krzysztof J. | @chrisgorgo | 0000-0003-3321-7583 | Google LLC | + diff --git a/.maint/ROADMAP.md b/.maint/ROADMAP.md new file mode 100644 index 0000000000..7e53895bec --- /dev/null +++ b/.maint/ROADMAP.md @@ -0,0 +1,7 @@ +# ROADMAP + +This document states how the roadmap is built, discussed and monitored by the Maintainers, with the engagement of Contributors and PIs. + +For example, if the GitHub Projects feature is used, this document will contain a link to the corresponding Project and document who is responsible of managing the project, contacting Contributors and Maintainers to monitor the progress of activities, organizing meetings and discussions around the activities, etc. + +This document may also indicate how the *milestones* feature of the project is used and, as for Projects, who is responsible of what coordination actions, their frequence, etc. diff --git a/.maint/contributors.json b/.maint/contributors.json deleted file mode 100644 index 2b0df140ca..0000000000 --- a/.maint/contributors.json +++ /dev/null @@ -1,37 +0,0 @@ -[ - { - "affiliation": "Perelman School of Medicine, University of Pennsylvania, PA, USA", - "name": "Adebimpe, Azeez", - "orcid": "0000-0001-9049-0135" - }, - { - "affiliation": "Perelman School of Medicine, University of Pennsylvania, PA, USA", - "name": "Cieslak, Matthew", - "orcid": "0000-0002-1931-4734" - }, - { - "affiliation": "Department of Neuroimaging, King's College London", - "name": "MacNicol, Eilidh", - "orcid": "0000-0003-3715-7012" - }, - { - "affiliation": "Cyceron, UMS 3408 (CNRS - UCBN), France", - "name": "Naveau, Mikaël", - "orcid": "0000-0001-6948-9068" - }, - { - "affiliation": "Perelman School of Medicine, University of Pennsylvania, PA, USA", - "name": "Satterthwaite, Theodore D.", - "orcid": "0000-0001-7072-9399" - }, - { - "affiliation": "Speech & Hearing Bioscience & Technology Program, Harvard University", - "name": "Sitek, Kevin R.", - "orcid": "0000-0002-2172-5786" - }, - { - "affiliation": "Center for Lifespan Changes in Brain and Cognition, University of Oslo", - "name": "Sneve, Markus H.", - "orcid": "0000-0001-7644-7915" - } -] diff --git a/.maint/developers.json b/.maint/developers.json deleted file mode 100644 index 26fc887bdb..0000000000 --- a/.maint/developers.json +++ /dev/null @@ -1,32 +0,0 @@ -[ - { - "affiliation": "Department of Psychology, Stanford University", - "name": "Blair, Ross W.", - "orcid": "0000-0003-3007-1056" - }, - { - "affiliation": "Dept. of Radiology, Lausanne University Hospital, University of Lausanne", - "name": "Esteban, Oscar", - "orcid": "0000-0001-8435-6191" - }, - { - "affiliation": "Department of Psychology, Stanford University", - "name": "Goncalves, Mathias", - "orcid": "0000-0002-7252-7771" - }, - { - "affiliation": "Department of Psychology, Stanford University", - "name": "Gorgolewski, Krzysztof J.", - "orcid": "0000-0003-3321-7583" - }, - { - "affiliation": "Department of Psychology, Stanford University", - "name": "Markiewicz, Christopher J.", - "orcid": "0000-0002-6533-164X" - }, - { - "affiliation": "Department of Psychology, Stanford University", - "name": "Poldrack, Russell A.", - "orcid": "0000-0001-6755-0259" - } -] \ No newline at end of file diff --git a/.maint/former.json b/.maint/former.json deleted file mode 100644 index 1f1b247aaa..0000000000 --- a/.maint/former.json +++ /dev/null @@ -1,5 +0,0 @@ -[ - { - "name": "Berleant, Shoshana" - } -] \ No newline at end of file diff --git a/.maint/paper_author_list.py b/.maint/paper_author_list.py deleted file mode 100644 index ccdceaae30..0000000000 --- a/.maint/paper_author_list.py +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env python3 -"""Generate an author list for a new paper or abstract.""" -import sys -from pathlib import Path -import json -from update_zenodo import get_git_lines, sort_contributors - - -# These authors should go last -AUTHORS_LAST = ["Gorgolewski, Krzysztof J.", "Satterthwaite, Theodore D.", "Poldrack, Russell A.", "Esteban, Oscar"] - - -def _aslist(inlist): - if not isinstance(inlist, list): - return [inlist] - return inlist - - -if __name__ == "__main__": - devs = json.loads(Path(".maint/developers.json").read_text()) - contribs = json.loads(Path(".maint/contributors.json").read_text()) - - author_matches, unmatched = sort_contributors( - devs + contribs, - get_git_lines(), - exclude=json.loads(Path(".maint/former.json").read_text()), - last=AUTHORS_LAST, - ) - # Remove position - affiliations = [] - for item in author_matches: - del item["position"] - for a in _aslist(item.get("affiliation", "Unaffiliated")): - if a not in affiliations: - affiliations.append(a) - - aff_indexes = [ - ", ".join( - [ - "%d" % (affiliations.index(a) + 1) - for a in _aslist(author.get("affiliation", "Unaffiliated")) - ] - ) - for author in author_matches - ] - - print( - "Some people made commits, but are missing in .maint/ " - "files: %s." % ", ".join(unmatched), - file=sys.stderr, - ) - - print("Authors (%d):" % len(author_matches)) - print( - "%s." - % "; ".join( - [ - "%s \\ :sup:`%s`\\ " % (i["name"], idx) - for i, idx in zip(author_matches, aff_indexes) - ] - ) - ) - - print( - "\n\nAffiliations:\n%s" - % "\n".join( - ["{0: >2}. {1}".format(i + 1, a) for i, a in enumerate(affiliations)] - ) - ) diff --git a/.maint/update_authors.py b/.maint/update_authors.py new file mode 100644 index 0000000000..3c7b73ac1c --- /dev/null +++ b/.maint/update_authors.py @@ -0,0 +1,317 @@ +#!/usr/bin/env python3 +"""Update and sort the creators list of the zenodo record.""" +import sys +from pathlib import Path +import json +import click +from fuzzywuzzy import fuzz, process + + +def read_md_table(md_text): + """ + Extract the first table found in a markdown document as a Python dict. + + Examples + -------- + >>> read_md_table(''' + ... # Some text + ... + ... More text + ... + ... | **Header1** | **Header2** | + ... | --- | --- | + ... | val1 | val2 | + ... | | val4 | + ... + ... | **Header3** | **Header4** | + ... | --- | --- | + ... | val1 | val2 | + ... | | val4 | + ... ''') + [{'header1': 'val1', 'header2': 'val2'}, {'header2': 'val4'}] + + """ + prev = None + keys = None + retval = [] + for line in md_text.splitlines(): + if line.strip().startswith("| --- |"): + keys = ( + k.replace("*", "").strip() + for k in prev.split("|") + ) + keys = [k.lower() for k in keys if k] + continue + elif not keys: + prev = line + continue + + if not line or not line.strip().startswith("|"): + break + + values = [v.strip() or None for v in line.split("|")][1:-1] + retval.append({k: v for k, v in zip(keys, values) if v}) + + return retval + + +def sort_contributors(entries, git_lines, exclude=None, last=None): + """Return a list of author dictionaries, ordered by contribution.""" + last = last or [] + sorted_authors = sorted(entries, key=lambda i: i["name"]) + + first_last = [ + " ".join(val["name"].split(",")[::-1]).strip() for val in sorted_authors + ] + first_last_excl = [ + " ".join(val["name"].split(",")[::-1]).strip() for val in exclude or [] + ] + + unmatched = [] + author_matches = [] + for ele in git_lines: + matches = process.extract( + ele, first_last, scorer=fuzz.token_sort_ratio, limit=2 + ) + # matches is a list [('First match', % Match), ('Second match', % Match)] + if matches[0][1] > 80: + val = sorted_authors[first_last.index(matches[0][0])] + else: + # skip unmatched names + if ele not in first_last_excl: + unmatched.append(ele) + continue + + if val not in author_matches: + author_matches.append(val) + + names = {" ".join(val["name"].split(",")[::-1]).strip() for val in author_matches} + for missing_name in first_last: + if missing_name not in names: + missing = sorted_authors[first_last.index(missing_name)] + author_matches.append(missing) + + position_matches = [] + for i, item in enumerate(author_matches): + pos = item.pop("position", None) + if pos is not None: + position_matches.append((i, int(pos))) + + for i, pos in position_matches: + if pos < 0: + pos += len(author_matches) + 1 + author_matches.insert(pos, author_matches.pop(i)) + + return author_matches, unmatched + + +def get_git_lines(fname="line-contributors.txt"): + """Run git-line-summary.""" + import shutil + import subprocess as sp + + contrib_file = Path(fname) + + lines = [] + if contrib_file.exists(): + print("WARNING: Reusing existing line-contributors.txt file.", file=sys.stderr) + lines = contrib_file.read_text().splitlines() + + git_line_summary_path = shutil.which("git-line-summary") + if not lines and git_line_summary_path: + print("Running git-line-summary on repo") + lines = sp.check_output([git_line_summary_path]).decode().splitlines() + lines = [l for l in lines if "Not Committed Yet" not in l] + contrib_file.write_text("\n".join(lines)) + + if not lines: + raise RuntimeError( + """\ +Could not find line-contributors from git repository.%s""" + % """ \ +git-line-summary not found, please install git-extras. """ + * (git_line_summary_path is None) + ) + return [" ".join(line.strip().split()[1:-1]) for line in lines if "%" in line] + + +def _namelast(inlist): + retval = [] + for i in inlist: + i["name"] = (f"{i.pop('name', '')} {i.pop('lastname', '')}").strip() + if not i["name"]: + i["name"] = i.get("handle", "") + retval.append(i) + return retval + + +@click.group() +def cli(): + """Generate authorship boilerplates.""" + pass + + +@cli.command() +@click.option("-z", "--zenodo-file", type=click.Path(exists=True), default=".zenodo.json") +@click.option("-m", "--maintainers", type=click.Path(exists=True), default=".maint/MAINTAINERS.md") +@click.option("-c", "--contributors", type=click.Path(exists=True), + default=".maint/CONTRIBUTORS.md") +@click.option("--pi", type=click.Path(exists=True), default=".maint/PIs.md") +@click.option("-f", "--former-file", type=click.Path(exists=True), default=".maint/FORMER.md") +def zenodo( + zenodo_file, + maintainers, + contributors, + pi, + former_file, +): + """Generate a new Zenodo payload file.""" + data = get_git_lines() + + zenodo = json.loads(Path(zenodo_file).read_text()) + + former = _namelast(read_md_table(Path(former_file).read_text())) + zen_creators, miss_creators = sort_contributors( + _namelast(read_md_table(Path(maintainers).read_text())), + data, + exclude=former, + ) + + zen_contributors, miss_contributors = sort_contributors( + _namelast(read_md_table(Path(contributors).read_text())), + data, + exclude=former + ) + + zen_pi = _namelast(reversed(read_md_table(Path(pi).read_text()))) + + zenodo["creators"] = zen_creators + zenodo["contributors"] = zen_contributors + [ + pi for pi in zen_pi if pi not in zen_contributors + ] + creator_names = { + c["name"] for c in zenodo["creators"] + if c["name"] != "" + } + + zenodo["contributors"] = [ + c for c in zenodo["contributors"] + if c["name"] not in creator_names + ] + + misses = set(miss_creators).intersection(miss_contributors) + if misses: + print( + "Some people made commits, but are missing in .maint/ " + f"files: {', '.join(misses)}", + file=sys.stderr, + ) + + # Remove position + for creator in zenodo["creators"]: + creator.pop("position", None) + creator.pop("handle", None) + if "affiliation" not in creator: + creator["affiliation"] = "Unknown affiliation" + elif isinstance(creator["affiliation"], list): + creator["affiliation"] = creator["affiliation"][0] + + for creator in zenodo["contributors"]: + creator.pop("handle", None) + creator["type"] = "Researcher" + creator.pop("position", None) + + if "affiliation" not in creator: + creator["affiliation"] = "Unknown affiliation" + elif isinstance(creator["affiliation"], list): + creator["affiliation"] = creator["affiliation"][0] + + Path(zenodo_file).write_text( + "%s\n" % json.dumps(zenodo, indent=2) + ) + + +@cli.command() +@click.option("-m", "--maintainers", type=click.Path(exists=True), default=".maint/MAINTAINERS.md") +@click.option("-c", "--contributors", type=click.Path(exists=True), + default=".maint/CONTRIBUTORS.md") +@click.option("--pi", type=click.Path(exists=True), default=".maint/PIs.md") +@click.option("-f", "--former-file", type=click.Path(exists=True), default=".maint/FORMER.md") +def publication( + maintainers, + contributors, + pi, + former_file, +): + """Generate the list of authors and affiliations for papers.""" + members = ( + _namelast(read_md_table(Path(maintainers).read_text())) + + _namelast(read_md_table(Path(contributors).read_text())) + ) + former_names = _namelast(read_md_table(Path(former_file).read_text())) + + hits, misses = sort_contributors( + members, + get_git_lines(), + exclude=former_names, + ) + + pi_hits = _namelast(reversed(read_md_table(Path(pi).read_text()))) + pi_names = [pi["name"] for pi in pi_hits] + hits = [ + hit for hit in hits + if hit["name"] not in pi_names + ] + pi_hits + + def _aslist(value): + if isinstance(value, (list, tuple)): + return value + return [value] + + # Remove position + affiliations = [] + for item in hits: + item.pop("position", None) + for a in _aslist(item.get("affiliation", "Unaffiliated")): + if a not in affiliations: + affiliations.append(a) + + aff_indexes = [ + ", ".join( + [ + "%d" % (affiliations.index(a) + 1) + for a in _aslist(author.get("affiliation", "Unaffiliated")) + ] + ) + for author in hits + ] + + if misses: + print( + "Some people made commits, but are missing in .maint/ " + f"files: {', '.join(misses)}", + file=sys.stderr, + ) + + print("Authors (%d):" % len(hits)) + print( + "%s." + % "; ".join( + [ + "%s \\ :sup:`%s`\\ " % (i["name"], idx) + for i, idx in zip(hits, aff_indexes) + ] + ) + ) + + print( + "\n\nAffiliations:\n%s" + % "\n".join( + ["{0: >2}. {1}".format(i + 1, a) for i, a in enumerate(affiliations)] + ) + ) + + +if __name__ == "__main__": + """ Install entry-point """ + cli() diff --git a/.maint/update_zenodo.py b/.maint/update_zenodo.py deleted file mode 100755 index 23bbe996e2..0000000000 --- a/.maint/update_zenodo.py +++ /dev/null @@ -1,137 +0,0 @@ -#!/usr/bin/env python3 -"""Update and sort the creators list of the zenodo record.""" -import sys -from pathlib import Path -import json -from fuzzywuzzy import fuzz, process - -# These ORCIDs should go last -CREATORS_LAST = ["Poldrack, Russell A.", "Esteban, Oscar"] -CONTRIBUTORS_LAST = ["Satterthwaite, Theodore D."] - - -def sort_contributors(entries, git_lines, exclude=None, last=None): - """Return a list of author dictionaries, ordered by contribution.""" - last = last or [] - sorted_authors = sorted(entries, key=lambda i: i["name"]) - - first_last = [ - " ".join(val["name"].split(",")[::-1]).strip() for val in sorted_authors - ] - first_last_excl = [ - " ".join(val["name"].split(",")[::-1]).strip() for val in exclude or [] - ] - - unmatched = [] - author_matches = [] - position = 1 - for ele in git_lines: - matches = process.extract( - ele, first_last, scorer=fuzz.token_sort_ratio, limit=2 - ) - # matches is a list [('First match', % Match), ('Second match', % Match)] - if matches[0][1] > 80: - val = sorted_authors[first_last.index(matches[0][0])] - else: - # skip unmatched names - if ele not in first_last_excl: - unmatched.append(ele) - continue - - if val not in author_matches: - val["position"] = position - author_matches.append(val) - position += 1 - - names = {" ".join(val["name"].split(",")[::-1]).strip() for val in author_matches} - for missing_name in first_last: - if missing_name not in names: - missing = sorted_authors[first_last.index(missing_name)] - missing["position"] = position - author_matches.append(missing) - position += 1 - - all_names = [val["name"] for val in author_matches] - for last_author in last: - author_matches[all_names.index(last_author)]["position"] = position - position += 1 - - author_matches = sorted(author_matches, key=lambda k: k["position"]) - - return author_matches, unmatched - - -def get_git_lines(fname="line-contributors.txt"): - """Run git-line-summary.""" - import shutil - import subprocess as sp - - contrib_file = Path(fname) - - lines = [] - if contrib_file.exists(): - print("WARNING: Reusing existing line-contributors.txt file.", file=sys.stderr) - lines = contrib_file.read_text().splitlines() - - git_line_summary_path = shutil.which("git-line-summary") - if not lines and git_line_summary_path: - print("Running git-line-summary on repo") - lines = sp.check_output([git_line_summary_path]).decode().splitlines() - lines = [l for l in lines if "Not Committed Yet" not in l] - contrib_file.write_text("\n".join(lines)) - - if not lines: - raise RuntimeError( - """\ -Could not find line-contributors from git repository.%s""" - % """ \ -git-line-summary not found, please install git-extras. """ - * (git_line_summary_path is None) - ) - return [" ".join(line.strip().split()[1:-1]) for line in lines if "%" in line] - - -if __name__ == "__main__": - data = get_git_lines() - - zenodo_file = Path(".zenodo.json") - zenodo = json.loads(zenodo_file.read_text()) - - creators = json.loads(Path(".maint/developers.json").read_text()) - zen_creators, miss_creators = sort_contributors( - creators, - data, - exclude=json.loads(Path(".maint/former.json").read_text()), - last=CREATORS_LAST, - ) - contributors = json.loads(Path(".maint/contributors.json").read_text()) - zen_contributors, miss_contributors = sort_contributors( - contributors, - data, - exclude=json.loads(Path(".maint/former.json").read_text()), - last=CONTRIBUTORS_LAST, - ) - zenodo["creators"] = zen_creators - zenodo["contributors"] = zen_contributors - - all_missing = set(miss_creators).intersection(miss_contributors) - if all_missing: - print( - "Some people made commits, but are missing in .maint/ " - f"files: {', '.join(all_missing)}.", - file=sys.stderr, - ) - - # Remove position - for creator in zenodo["creators"]: - del creator["position"] - if isinstance(creator["affiliation"], list): - creator["affiliation"] = creator["affiliation"][0] - - for creator in zenodo["contributors"]: - creator["type"] = "Researcher" - del creator["position"] - if isinstance(creator["affiliation"], list): - creator["affiliation"] = creator["affiliation"][0] - - zenodo_file.write_text("%s\n" % json.dumps(zenodo, indent=2))