ordinator 0.1.0__py3-none-any.whl
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.
- ordinator/__init__.py +10 -0
- ordinator/bibliography.py +270 -0
- ordinator/bridge.py +2424 -0
- ordinator/config.py +148 -0
- ordinator/console.py +73 -0
- ordinator/engines/__init__.py +32 -0
- ordinator/engines/descriptor.py +1156 -0
- ordinator/engines/endpoints.py +145 -0
- ordinator/engines/main.py +537 -0
- ordinator/engines/runner.py +619 -0
- ordinator/implementation.py +377 -0
- ordinator/languages/__init__.py +442 -0
- ordinator/languages/julia.py +114 -0
- ordinator/languages/r.py +113 -0
- ordinator/library_globals.py +465 -0
- ordinator/ordering.py +336 -0
- ordinator/placement.py +152 -0
- ordinator/plan.py +669 -0
- ordinator/plugins.py +161 -0
- ordinator/provenance.py +961 -0
- ordinator/reproducibility.py +268 -0
- ordinator/roots.py +39 -0
- ordinator/rsession.py +12 -0
- ordinator/settings.py +738 -0
- ordinator/step.py +285 -0
- ordinator/strictness.py +282 -0
- ordinator/trace.py +1383 -0
- ordinator/utils.py +124 -0
- ordinator/webui/__init__.py +11 -0
- ordinator/webui/index.html +3467 -0
- ordinator/webui/server.py +1642 -0
- ordinator-0.1.0.dist-info/METADATA +54 -0
- ordinator-0.1.0.dist-info/RECORD +36 -0
- ordinator-0.1.0.dist-info/WHEEL +5 -0
- ordinator-0.1.0.dist-info/licenses/LICENCE +287 -0
- ordinator-0.1.0.dist-info/top_level.txt +1 -0
ordinator/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""The machinery: a dataset passed through named, swappable, recorded steps.
|
|
2
|
+
|
|
3
|
+
A consumer declares its vocabulary as data and answers what this package asks through
|
|
4
|
+
the names it declares. Nothing here imports a consumer, and nothing here is imported on
|
|
5
|
+
import of this package: take the modules you need.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
#: The kernel's own version, apart from any consumer's. Read by this package's
|
|
9
|
+
#: pyproject.toml, which sits beside this file.
|
|
10
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""Citations, read from ``references.bib`` -- the one place they are written.
|
|
2
|
+
|
|
3
|
+
A ``references=`` entry is a *string*, and the manifest merges a run's citations
|
|
4
|
+
by exact string equality, so two spellings of one source are two sources: they
|
|
5
|
+
print twice, and a reader comparing two runs sees a difference that is only
|
|
6
|
+
typography.
|
|
7
|
+
|
|
8
|
+
So a registration cites a *key*::
|
|
9
|
+
|
|
10
|
+
references=cite('webb1980')
|
|
11
|
+
|
|
12
|
+
and the text comes from the BibTeX file the consumer declares with :func:`use`
|
|
13
|
+
(its ``references.bib``). Nothing restates a
|
|
14
|
+
citation in Python, the file is ordinary BibTeX a manuscript can use directly,
|
|
15
|
+
and the short author-year label the qaqc routines stamp on their output is
|
|
16
|
+
*derived* from the same entry rather than stored, so the two forms cannot drift
|
|
17
|
+
apart.
|
|
18
|
+
|
|
19
|
+
The parser here is deliberately small: enough BibTeX for a hand-maintained file
|
|
20
|
+
of a few dozen entries, and no dependency for a package whose job is not
|
|
21
|
+
bibliography management.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import re
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
#: The bibliography in use, declared through :func:`use`. It is the consumer's and ships
|
|
28
|
+
#: as its package data: an installed wheel has to carry it or a run's manifest could not
|
|
29
|
+
#: name its own sources.
|
|
30
|
+
BIB_PATH = None
|
|
31
|
+
|
|
32
|
+
_ENTRY = re.compile(r'@(\w+)\s*\{')
|
|
33
|
+
_FIELD = re.compile(r'[\s,]*(\w[\w-]*)\s*=\s*')
|
|
34
|
+
_BARE = re.compile(r'[^,\s]*')
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Citation(str):
|
|
38
|
+
"""A citation already formatted -- an entry of the bibliography, or what
|
|
39
|
+
:func:`credit` built -- which :func:`cite` passes through as written."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _closing(text, start, close):
|
|
43
|
+
"""The index of the ``close`` that ends what opens just before ``start``,
|
|
44
|
+
braces nested inside it counted, or ``len(text)`` where nothing does."""
|
|
45
|
+
depth, i = 0, start
|
|
46
|
+
while i < len(text) and (depth or text[i] != close):
|
|
47
|
+
depth += {'{': 1, '}': -1}.get(text[i], 0)
|
|
48
|
+
i += 1
|
|
49
|
+
return i
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _fields(body):
|
|
53
|
+
"""``{field: value}`` of one entry's body: braced, quoted or bare values."""
|
|
54
|
+
fields, i = {}, 0
|
|
55
|
+
while (m := _FIELD.match(body, i)):
|
|
56
|
+
i = m.end()
|
|
57
|
+
if i < len(body) and body[i] in '{"':
|
|
58
|
+
end = _closing(body, i + 1, '}' if body[i] == '{' else '"')
|
|
59
|
+
value, i = body[i + 1:end], end + 1
|
|
60
|
+
else:
|
|
61
|
+
value = _BARE.match(body, i).group()
|
|
62
|
+
i += len(value)
|
|
63
|
+
fields[m.group(1).lower()] = ' '.join(value.split())
|
|
64
|
+
return fields
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _parse(text):
|
|
68
|
+
"""``{key: {'type': ..., field: value}}`` for a hand-written BibTeX file.
|
|
69
|
+
|
|
70
|
+
An entry ends at the brace that balances its opening one, wherever it sits:
|
|
71
|
+
a ``}`` on the last field's line closes that entry and not the next one.
|
|
72
|
+
"""
|
|
73
|
+
entries, pos = {}, 0
|
|
74
|
+
while (m := _ENTRY.search(text, pos)):
|
|
75
|
+
end = _closing(text, m.end(), '}')
|
|
76
|
+
pos = end + 1
|
|
77
|
+
if m.group(1).lower() in ('comment', 'preamble', 'string'):
|
|
78
|
+
continue
|
|
79
|
+
key, _, body = text[m.end():end].partition(',')
|
|
80
|
+
entries[key.strip()] = {'type': m.group(1).lower(), **_fields(body)}
|
|
81
|
+
return entries
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
_ENTRIES = {}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def use(path):
|
|
88
|
+
"""Read the bibliography citations are taken from."""
|
|
89
|
+
global BIB_PATH
|
|
90
|
+
BIB_PATH = Path(path)
|
|
91
|
+
_ENTRIES.clear()
|
|
92
|
+
_ENTRIES.update(_parse(BIB_PATH.read_text(encoding='utf8')))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _authors(entry):
|
|
96
|
+
"""The author (or editor) names, ``others`` kept as the et-al marker."""
|
|
97
|
+
raw = entry.get('author') or entry.get('editor') or ''
|
|
98
|
+
return [n.strip() for n in raw.split(' and ') if n.strip()]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _surname(name):
|
|
102
|
+
"""``Webb, E. K.`` and ``B. Heinesch`` both give ``Webb`` / ``Heinesch``."""
|
|
103
|
+
return name.split(',')[0].strip() if ',' in name else name.split()[-1]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _entry(key):
|
|
107
|
+
try:
|
|
108
|
+
return _ENTRIES[key]
|
|
109
|
+
except KeyError:
|
|
110
|
+
raise KeyError(
|
|
111
|
+
f"no reference {key!r} in {getattr(BIB_PATH, 'name', 'the bibliography')}; add the entry there "
|
|
112
|
+
f"rather than writing the citation at the call site") from None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _byline(entry):
|
|
116
|
+
"""``A and B`` for two names, ``A, B, and C`` for more, ``A, et al.`` for
|
|
117
|
+
an ``and others`` list -- the house style the citations were written in."""
|
|
118
|
+
names = _authors(entry)
|
|
119
|
+
if names and names[-1] == 'others':
|
|
120
|
+
# ``Vitale, D., et al.`` but ``B. Heinesch et al.``: the comma belongs to
|
|
121
|
+
# the surname-first form, and the software credits are written the other
|
|
122
|
+
# way round.
|
|
123
|
+
sep = ', ' if ',' in names[0] else ' '
|
|
124
|
+
return f"{names[0]}{sep}et al."
|
|
125
|
+
if len(names) == 2:
|
|
126
|
+
return f"{names[0]} and {names[1]}"
|
|
127
|
+
if len(names) > 2:
|
|
128
|
+
return ", ".join(names[:-1]) + f", and {names[-1]}"
|
|
129
|
+
return names[0] if names else ''
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _format(key):
|
|
133
|
+
"""One entry as the citation string that reaches a manifest."""
|
|
134
|
+
return Citation(_formatted(_entry(key)))
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _formatted(e):
|
|
138
|
+
who = _byline(e)
|
|
139
|
+
if e['type'] == 'software':
|
|
140
|
+
# Credited by name first: the software is the thing being cited, and the
|
|
141
|
+
# authors qualify it. :func:`credit` appends the routine.
|
|
142
|
+
text = f"{e['title']} ({who})"
|
|
143
|
+
if e.get('note'):
|
|
144
|
+
text = f"{e['title']} ({who}, {e['note']})"
|
|
145
|
+
link = _link(e)
|
|
146
|
+
return f"{text}, {link}" if link else text + '.'
|
|
147
|
+
if e.get('editor') and e['type'] == 'book':
|
|
148
|
+
who += ' (eds.)'
|
|
149
|
+
|
|
150
|
+
year = e.get('year')
|
|
151
|
+
parts = [f"{who} ({year})." if who and year else who]
|
|
152
|
+
parts.append(f"{e['title']}.")
|
|
153
|
+
if e['type'] == 'article':
|
|
154
|
+
tail = e.get('journal', '')
|
|
155
|
+
if e.get('volume'):
|
|
156
|
+
tail = f"{tail} {e['volume']}".strip()
|
|
157
|
+
if e.get('pages'):
|
|
158
|
+
tail = f"{tail}, {e['pages']}".strip(', ')
|
|
159
|
+
if tail:
|
|
160
|
+
parts.append(f"{tail}.")
|
|
161
|
+
elif e['type'] == 'incollection':
|
|
162
|
+
tail = f"In: {e.get('booktitle', '')}"
|
|
163
|
+
for extra in (e.get('publisher'), e.get('pages')):
|
|
164
|
+
if extra:
|
|
165
|
+
tail += f", {extra}"
|
|
166
|
+
parts.append(f"{tail}.")
|
|
167
|
+
elif e['type'] == 'book' and e.get('publisher'):
|
|
168
|
+
parts.append(f"{e['publisher']}.")
|
|
169
|
+
if e.get('note'):
|
|
170
|
+
parts.append(f"({e['note']}.)")
|
|
171
|
+
parts.append(_link(e))
|
|
172
|
+
return " ".join(p for p in parts if p).strip()
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _link(entry):
|
|
176
|
+
"""The one resolver a citation carries: the DOI when the work has one,
|
|
177
|
+
else its URL. A DOI is written bare in the file (``10.1002/...``) and
|
|
178
|
+
printed as the ``doi.org`` address so a manifest reader can follow it."""
|
|
179
|
+
if entry.get('doi'):
|
|
180
|
+
return f"https://doi.org/{entry['doi']}"
|
|
181
|
+
return entry.get('url') or ''
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def cite(*references):
|
|
185
|
+
"""The given works as the list ``references=`` expects, without repeats.
|
|
186
|
+
|
|
187
|
+
Each argument is a key in ``references.bib``, or a :class:`Citation`
|
|
188
|
+
already formatted (what :func:`credit` and this function return). Order is
|
|
189
|
+
preserved: a registration lists its primary source first, and ``describe``
|
|
190
|
+
prints them in that order.
|
|
191
|
+
|
|
192
|
+
Raises:
|
|
193
|
+
KeyError: an argument is neither -- a misspelt key would otherwise be
|
|
194
|
+
published as its own citation in every manifest.
|
|
195
|
+
"""
|
|
196
|
+
seen, out = set(), []
|
|
197
|
+
for ref in references:
|
|
198
|
+
for one in (ref if isinstance(ref, (list, tuple)) else [ref]):
|
|
199
|
+
text = one if isinstance(one, Citation) else _format(one)
|
|
200
|
+
if text not in seen:
|
|
201
|
+
seen.add(text)
|
|
202
|
+
out.append(text)
|
|
203
|
+
return out
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def short(*references):
|
|
207
|
+
"""The works as one ``'A (yyyy); B & C (yyyy)'`` label.
|
|
208
|
+
|
|
209
|
+
For the ``reference`` attribute a qaqc routine stamps on its output, where a
|
|
210
|
+
full bibliographic string would swamp the variable's attrs. Derived from the
|
|
211
|
+
same entry the registration cites, so a routine cannot stamp one paper while
|
|
212
|
+
its registration names two.
|
|
213
|
+
|
|
214
|
+
Anything with no year (the software credits) is passed over: ``source=``
|
|
215
|
+
already names those. A key with no entry raises, as it does in :func:`cite`:
|
|
216
|
+
passed over, it would stamp one paper where the routine names two.
|
|
217
|
+
"""
|
|
218
|
+
seen, out = set(), []
|
|
219
|
+
for key in references:
|
|
220
|
+
entry = _entry(key)
|
|
221
|
+
if not entry.get('year'):
|
|
222
|
+
continue
|
|
223
|
+
raw = _authors(entry)
|
|
224
|
+
names = [_surname(n) for n in raw if n != 'others']
|
|
225
|
+
if 'others' in raw or len(names) > 2:
|
|
226
|
+
who = f"{names[0]} et al."
|
|
227
|
+
elif len(names) == 2:
|
|
228
|
+
who = f"{names[0]} & {names[1]}"
|
|
229
|
+
else:
|
|
230
|
+
who = names[0] if names else '?'
|
|
231
|
+
label = f"{who} ({entry['year']})"
|
|
232
|
+
if label not in seen:
|
|
233
|
+
seen.add(label)
|
|
234
|
+
out.append(label)
|
|
235
|
+
return '; '.join(out)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def short_of(references):
|
|
239
|
+
"""The short label for citations that are already formatted.
|
|
240
|
+
|
|
241
|
+
The inverse of what a registration does: a bridged method stamps its output
|
|
242
|
+
with the same author-year label as the entry it twins, and reading it back
|
|
243
|
+
off that entry's ``references`` is what keeps the two from drifting -- the
|
|
244
|
+
alternative being the bib keys restated in a second file.
|
|
245
|
+
|
|
246
|
+
Anything that is not a formatted entry of ``references.bib`` -- a software
|
|
247
|
+
credit, a free-text note -- has no year and is passed over, as in
|
|
248
|
+
:func:`short`.
|
|
249
|
+
"""
|
|
250
|
+
reverse = {_format(key): key for key in _ENTRIES}
|
|
251
|
+
return short(*[reverse[text] for text in (references or [])
|
|
252
|
+
if text in reverse])
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def credit(key, what, version=None):
|
|
256
|
+
"""Credit a piece of software for ``what`` -- a routine, a table, a recipe.
|
|
257
|
+
|
|
258
|
+
One phrasing for all of them, built from the entry ``key`` names. Two
|
|
259
|
+
orderings of the same words were in use -- the authors before the routine in
|
|
260
|
+
one module and after it in another -- so one source deduped as two in the
|
|
261
|
+
run manifest.
|
|
262
|
+
"""
|
|
263
|
+
e = _entry(key)
|
|
264
|
+
name = e['title'] + (f' {version}' if version else '')
|
|
265
|
+
return Citation(f"{name} ({_byline(e)}, {e['note']}), {what}.")
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def keys():
|
|
269
|
+
"""Every key ``references.bib`` defines, for tests and for the docs build."""
|
|
270
|
+
return sorted(_ENTRIES)
|