labnotes 0.0.4__tar.gz → 0.0.6__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.
- {labnotes-0.0.4 → labnotes-0.0.6}/PKG-INFO +1 -1
- {labnotes-0.0.4 → labnotes-0.0.6}/src/labnotes/create_app.py +4 -1
- labnotes-0.0.6/src/labnotes/list_runs.py +29 -0
- {labnotes-0.0.4 → labnotes-0.0.6}/src/labnotes/load_experiment.py +12 -3
- labnotes-0.0.6/src/labnotes/models.py +9 -0
- labnotes-0.0.6/src/labnotes/parse_run_view.py +20 -0
- labnotes-0.0.6/src/labnotes/sort_href.py +13 -0
- {labnotes-0.0.4 → labnotes-0.0.6}/src/labnotes/static/labnotes.css +66 -2
- labnotes-0.0.6/src/labnotes/static/view-toggle.js +15 -0
- {labnotes-0.0.4 → labnotes-0.0.6}/src/labnotes/templates/experiment.html +22 -4
- {labnotes-0.0.4 → labnotes-0.0.6}/src/labnotes/templates/index.html +9 -2
- {labnotes-0.0.4 → labnotes-0.0.6}/uv.lock +3 -0
- labnotes-0.0.4/src/labnotes/list_runs.py +0 -12
- {labnotes-0.0.4 → labnotes-0.0.6}/.gitignore +0 -0
- {labnotes-0.0.4 → labnotes-0.0.6}/CHANGELOG.md +0 -0
- {labnotes-0.0.4 → labnotes-0.0.6}/MIGRATIONS.md +0 -0
- {labnotes-0.0.4 → labnotes-0.0.6}/README.md +0 -0
- {labnotes-0.0.4 → labnotes-0.0.6}/pyproject.toml +0 -0
- {labnotes-0.0.4 → labnotes-0.0.6}/src/labnotes/__init__.py +0 -0
- {labnotes-0.0.4 → labnotes-0.0.6}/src/labnotes/cell_progress.py +0 -0
- {labnotes-0.0.4 → labnotes-0.0.6}/src/labnotes/cli.py +0 -0
- {labnotes-0.0.4 → labnotes-0.0.6}/src/labnotes/count_runs.py +0 -0
- {labnotes-0.0.4 → labnotes-0.0.6}/src/labnotes/drain_watch.py +0 -0
- {labnotes-0.0.4 → labnotes-0.0.6}/src/labnotes/dump_experiments.py +0 -0
- {labnotes-0.0.4 → labnotes-0.0.6}/src/labnotes/format_value.py +0 -0
- {labnotes-0.0.4 → labnotes-0.0.6}/src/labnotes/list_experiments.py +0 -0
- {labnotes-0.0.4 → labnotes-0.0.6}/src/labnotes/open_experiments.py +0 -0
- {labnotes-0.0.4 → labnotes-0.0.6}/src/labnotes/open_runs.py +0 -0
- {labnotes-0.0.4 → labnotes-0.0.6}/src/labnotes/pivot_cells.py +0 -0
- {labnotes-0.0.4 → labnotes-0.0.6}/src/labnotes/read_design.py +0 -0
- {labnotes-0.0.4 → labnotes-0.0.6}/src/labnotes/read_note.py +0 -0
- {labnotes-0.0.4 → labnotes-0.0.6}/src/labnotes/render_markdown.py +0 -0
- {labnotes-0.0.4 → labnotes-0.0.6}/src/labnotes/run_columns.py +0 -0
- {labnotes-0.0.4 → labnotes-0.0.6}/src/labnotes/serve_app.py +0 -0
- {labnotes-0.0.4 → labnotes-0.0.6}/src/labnotes/sql_quote.py +0 -0
- {labnotes-0.0.4 → labnotes-0.0.6}/src/labnotes/study_progress.py +0 -0
- {labnotes-0.0.4 → labnotes-0.0.6}/src/labnotes/summarize_design.py +0 -0
- {labnotes-0.0.4 → labnotes-0.0.6}/src/labnotes/templates/base.html +0 -0
- {labnotes-0.0.4 → labnotes-0.0.6}/src/labnotes/templates/macros.html +0 -0
|
@@ -52,7 +52,10 @@ def create_app(experiments_folder: Path, data_folder: Path) -> Starlette:
|
|
|
52
52
|
async def experiment(request: Request) -> Response:
|
|
53
53
|
state = request.app.state
|
|
54
54
|
loaded = await load_experiment(
|
|
55
|
-
state.experiments,
|
|
55
|
+
state.experiments,
|
|
56
|
+
state.runs,
|
|
57
|
+
request.path_params["name"],
|
|
58
|
+
request.query_params,
|
|
56
59
|
)
|
|
57
60
|
if loaded is None:
|
|
58
61
|
raise HTTPException(404, "No such experiment")
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from dirsql import DirSQL
|
|
2
|
+
|
|
3
|
+
from labnotes.models import RunView
|
|
4
|
+
from labnotes.sql_quote import quote_identifier, quote_string
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
async def list_runs(
|
|
8
|
+
runs: DirSQL, study: str, completed_run: str, view: RunView
|
|
9
|
+
) -> list[dict[str, object]]:
|
|
10
|
+
where = [f"study = {quote_string(study)}"]
|
|
11
|
+
if view.status:
|
|
12
|
+
negate = {"complete": "", "incomplete": "NOT "}[view.status]
|
|
13
|
+
where.append(f"{negate}({completed_run})")
|
|
14
|
+
where += [
|
|
15
|
+
f"instr(lower(CAST({quote_identifier(column)} AS TEXT)),"
|
|
16
|
+
f" lower({quote_string(value)})) > 0"
|
|
17
|
+
for column, value in view.filters.items()
|
|
18
|
+
]
|
|
19
|
+
direction = "DESC" if view.descending else "ASC"
|
|
20
|
+
if view.sort == "status":
|
|
21
|
+
order = f"labnotes_completed {direction}"
|
|
22
|
+
elif view.sort:
|
|
23
|
+
order = f"{quote_identifier(view.sort)} {direction}"
|
|
24
|
+
else:
|
|
25
|
+
order = "2 DESC"
|
|
26
|
+
return await runs.query(
|
|
27
|
+
f"SELECT ({completed_run}) AS labnotes_completed, * FROM runs"
|
|
28
|
+
f" WHERE {' AND '.join(where)} ORDER BY {order}"
|
|
29
|
+
)
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
import json
|
|
2
|
+
from collections.abc import Mapping
|
|
2
3
|
from typing import cast
|
|
3
4
|
|
|
4
5
|
from dirsql import DirSQL
|
|
5
6
|
|
|
6
7
|
from labnotes.list_runs import list_runs
|
|
8
|
+
from labnotes.parse_run_view import parse_run_view
|
|
7
9
|
from labnotes.render_markdown import render_markdown
|
|
8
10
|
from labnotes.run_columns import run_columns
|
|
11
|
+
from labnotes.sort_href import sort_href
|
|
9
12
|
from labnotes.sql_quote import quote_string
|
|
10
13
|
from labnotes.study_progress import study_progress
|
|
11
14
|
from labnotes.summarize_design import summarize_design
|
|
12
15
|
|
|
13
16
|
|
|
14
17
|
async def load_experiment(
|
|
15
|
-
experiments: DirSQL, runs: DirSQL, name: str
|
|
18
|
+
experiments: DirSQL, runs: DirSQL, name: str, params: Mapping[str, str]
|
|
16
19
|
) -> dict[str, object] | None:
|
|
17
20
|
found = await experiments.query(
|
|
18
21
|
f"SELECT name, study, design FROM experiments WHERE name = {quote_string(name)}"
|
|
@@ -29,10 +32,12 @@ async def load_experiment(
|
|
|
29
32
|
study = None if row["study"] is None else str(row["study"])
|
|
30
33
|
columns = await run_columns(runs)
|
|
31
34
|
progress = await study_progress(runs, study, summary, columns)
|
|
35
|
+
shown = columns[:1] + [key for key in varies if key in columns[1:]]
|
|
36
|
+
view = parse_run_view(params, shown)
|
|
32
37
|
listed = (
|
|
33
38
|
[]
|
|
34
39
|
if study is None
|
|
35
|
-
else await list_runs(runs, study, str(summary["completed_run"]))
|
|
40
|
+
else await list_runs(runs, study, str(summary["completed_run"]), view)
|
|
36
41
|
)
|
|
37
42
|
return {
|
|
38
43
|
"name": row["name"],
|
|
@@ -44,5 +49,9 @@ async def load_experiment(
|
|
|
44
49
|
for note in notes
|
|
45
50
|
],
|
|
46
51
|
"runs_list": listed,
|
|
47
|
-
"run_columns":
|
|
52
|
+
"run_columns": shown,
|
|
53
|
+
"run_view": view,
|
|
54
|
+
"sort_hrefs": {
|
|
55
|
+
column: sort_href(view, column) for column in ["status", *shown]
|
|
56
|
+
},
|
|
48
57
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
|
|
3
|
+
from labnotes.models import RunView
|
|
4
|
+
|
|
5
|
+
STATUSES = {"complete", "incomplete"}
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def parse_run_view(params: Mapping[str, str], columns: list[str]) -> RunView:
|
|
9
|
+
sort = params.get("sort")
|
|
10
|
+
status = params.get("status")
|
|
11
|
+
return RunView(
|
|
12
|
+
sort=sort if sort in columns or sort == "status" else None,
|
|
13
|
+
descending=params.get("order") == "desc",
|
|
14
|
+
status=status if status in STATUSES else None,
|
|
15
|
+
filters={
|
|
16
|
+
column: value
|
|
17
|
+
for column in columns
|
|
18
|
+
if (value := params.get(f"f.{column}", "").strip())
|
|
19
|
+
},
|
|
20
|
+
)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from urllib.parse import urlencode
|
|
2
|
+
|
|
3
|
+
from labnotes.models import RunView
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def sort_href(view: RunView, column: str) -> str:
|
|
7
|
+
params = {"sort": column}
|
|
8
|
+
if view.sort == column and not view.descending:
|
|
9
|
+
params["order"] = "desc"
|
|
10
|
+
if view.status:
|
|
11
|
+
params["status"] = view.status
|
|
12
|
+
params.update({f"f.{name}": value for name, value in view.filters.items()})
|
|
13
|
+
return f"?{urlencode(params)}#runs"
|
|
@@ -163,7 +163,9 @@ h3 {
|
|
|
163
163
|
}
|
|
164
164
|
.ledger td { padding: 1.1rem 1.25rem; border-bottom: 1px solid var(--rule); vertical-align: middle; }
|
|
165
165
|
.ledger tbody tr:last-child td { border-bottom: 0; }
|
|
166
|
+
.ledger tbody tr { position: relative; cursor: pointer; }
|
|
166
167
|
.ledger tbody tr:hover { background: color-mix(in srgb, var(--accent) 4%, transparent); }
|
|
168
|
+
.ledger .title-link::after { content: ""; position: absolute; inset: 0; }
|
|
167
169
|
.ledger .num { text-align: right; }
|
|
168
170
|
.ledger th.num { text-align: right; }
|
|
169
171
|
|
|
@@ -289,14 +291,14 @@ h3 {
|
|
|
289
291
|
|
|
290
292
|
/* Runs */
|
|
291
293
|
.runs {
|
|
292
|
-
--row:
|
|
294
|
+
--row: 2.75rem repeat(var(--columns), minmax(0, 1fr));
|
|
293
295
|
background: var(--surface);
|
|
294
296
|
border: 1px solid var(--rule);
|
|
295
297
|
border-radius: 10px;
|
|
296
298
|
overflow: hidden;
|
|
297
299
|
font-size: 0.86rem;
|
|
298
300
|
}
|
|
299
|
-
.run-head, .run summary {
|
|
301
|
+
.run-head, .run summary, .run-filters {
|
|
300
302
|
display: grid;
|
|
301
303
|
grid-template-columns: var(--row);
|
|
302
304
|
gap: 1rem;
|
|
@@ -311,6 +313,25 @@ h3 {
|
|
|
311
313
|
color: var(--ink-3);
|
|
312
314
|
border-bottom: 1px solid var(--rule);
|
|
313
315
|
}
|
|
316
|
+
.run-head .sort { color: inherit; text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
317
|
+
.run-head .sort:hover, .run-head .sorted { color: var(--ink); }
|
|
318
|
+
.run-head .arrow { margin-left: 0.3rem; color: var(--accent); }
|
|
319
|
+
.run-filters { padding-top: 0.5rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--rule); background: var(--paper); }
|
|
320
|
+
.run-filters input, .run-filters select {
|
|
321
|
+
width: 100%;
|
|
322
|
+
min-width: 0;
|
|
323
|
+
font: inherit;
|
|
324
|
+
font-size: 0.82rem;
|
|
325
|
+
padding: 0.2rem 0.45rem;
|
|
326
|
+
color: var(--ink);
|
|
327
|
+
background: var(--surface);
|
|
328
|
+
border: 1px solid var(--rule);
|
|
329
|
+
border-radius: 5px;
|
|
330
|
+
}
|
|
331
|
+
.run-filters select { padding: 0.2rem 0; }
|
|
332
|
+
.run-filters input:focus, .run-filters select:focus { outline: none; border-color: var(--accent); }
|
|
333
|
+
.run-empty { margin: 0; padding: 1rem 1.25rem; }
|
|
334
|
+
h2 .clear { font-family: var(--sans); font-size: 0.82rem; margin-left: 0.8rem; color: var(--ink-3); }
|
|
314
335
|
.run { border-bottom: 1px solid var(--rule); }
|
|
315
336
|
.run:last-child { border-bottom: 0; }
|
|
316
337
|
.run summary { cursor: pointer; list-style: none; }
|
|
@@ -336,6 +357,49 @@ h3 {
|
|
|
336
357
|
.record code { background: none; padding: 0; word-break: break-word; }
|
|
337
358
|
.record pre { margin: 0; white-space: pre-wrap; word-break: break-word; max-height: 22rem; overflow: auto; font-size: 0.78rem; color: var(--ink-2); }
|
|
338
359
|
|
|
360
|
+
/* Index view toggle */
|
|
361
|
+
.view-toggle {
|
|
362
|
+
float: right;
|
|
363
|
+
display: inline-flex;
|
|
364
|
+
margin-top: 0.6rem;
|
|
365
|
+
border: 1px solid var(--rule-strong);
|
|
366
|
+
border-radius: 7px;
|
|
367
|
+
overflow: hidden;
|
|
368
|
+
}
|
|
369
|
+
.view-toggle button {
|
|
370
|
+
font: inherit;
|
|
371
|
+
font-size: 0.82rem;
|
|
372
|
+
padding: 0.3rem 0.8rem;
|
|
373
|
+
border: 0;
|
|
374
|
+
background: var(--surface);
|
|
375
|
+
color: var(--ink-2);
|
|
376
|
+
cursor: pointer;
|
|
377
|
+
}
|
|
378
|
+
.view-toggle button + button { border-left: 1px solid var(--rule-strong); }
|
|
379
|
+
.view-toggle button[aria-pressed="true"] { background: var(--accent-soft); color: var(--ink); }
|
|
380
|
+
|
|
381
|
+
/* Index as cards: the same table, laid out as a grid. */
|
|
382
|
+
.ledger[data-view="cards"] { display: block; background: none; border: 0; border-radius: 0; overflow: visible; }
|
|
383
|
+
.ledger[data-view="cards"] thead { display: none; }
|
|
384
|
+
.ledger[data-view="cards"] tbody {
|
|
385
|
+
display: grid;
|
|
386
|
+
grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr));
|
|
387
|
+
gap: 1rem;
|
|
388
|
+
}
|
|
389
|
+
.ledger[data-view="cards"] tbody tr {
|
|
390
|
+
display: flex;
|
|
391
|
+
flex-direction: column;
|
|
392
|
+
gap: 0.7rem;
|
|
393
|
+
padding: 1.1rem 1.25rem;
|
|
394
|
+
background: var(--surface);
|
|
395
|
+
border: 1px solid var(--rule);
|
|
396
|
+
border-radius: 10px;
|
|
397
|
+
}
|
|
398
|
+
.ledger[data-view="cards"] tbody tr:hover { background: var(--surface); border-color: var(--accent); }
|
|
399
|
+
.ledger[data-view="cards"] td { display: block; padding: 0; border: 0; text-align: left; }
|
|
400
|
+
.ledger[data-view="cards"] .progress-col { width: auto; }
|
|
401
|
+
.ledger[data-view="cards"] td[data-label]::before { content: attr(data-label) " "; color: var(--ink-3); font-size: 0.82rem; }
|
|
402
|
+
|
|
339
403
|
@media (max-width: 52rem) {
|
|
340
404
|
main, .masthead { padding-left: 1.1rem; padding-right: 1.1rem; }
|
|
341
405
|
h1 { font-size: 2rem; }
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const ledger = document.querySelector(".ledger");
|
|
2
|
+
const buttons = document.querySelectorAll(".view-toggle button");
|
|
3
|
+
|
|
4
|
+
function show(view) {
|
|
5
|
+
ledger.dataset.view = view;
|
|
6
|
+
for (const button of buttons) {
|
|
7
|
+
button.setAttribute("aria-pressed", String(button.dataset.view === view));
|
|
8
|
+
}
|
|
9
|
+
localStorage.setItem("labnotes.view", view);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
for (const button of buttons) {
|
|
13
|
+
button.addEventListener("click", () => show(button.dataset.view));
|
|
14
|
+
}
|
|
15
|
+
show(localStorage.getItem("labnotes.view") ?? "table");
|
|
@@ -121,13 +121,29 @@
|
|
|
121
121
|
|
|
122
122
|
{% if e.study %}
|
|
123
123
|
<section id="runs" class="block">
|
|
124
|
-
|
|
125
|
-
{%
|
|
124
|
+
{% set view = e.run_view %}
|
|
125
|
+
{% set narrowed = view.status or view.filters %}
|
|
126
|
+
<h2>Runs <span class="count num">{{ e.runs_list | length }}{% if narrowed %} of {{ e.runs }}{% endif %}</span>
|
|
127
|
+
{% if narrowed %}<a class="clear" href="{{ '?sort=' ~ (view.sort | urlencode) ~ ('&order=desc' if view.descending else '') if view.sort else '?' }}#runs">Clear filters</a>{% endif %}</h2>
|
|
128
|
+
{% if e.runs %}
|
|
126
129
|
<div class="runs" style="--columns: {{ e.run_columns | length }}">
|
|
127
130
|
<div class="run-head">
|
|
128
|
-
|
|
129
|
-
{%
|
|
131
|
+
{% for column in ['status'] + e.run_columns %}
|
|
132
|
+
{% set sorted = view.sort == column %}
|
|
133
|
+
<a class="sort{{ ' sorted' if sorted }}" href="{{ e.sort_hrefs[column] }}"{% if sorted %} aria-sort="{{ 'descending' if view.descending else 'ascending' }}"{% endif %} title="Sort by {{ column }}">{{ '' if column == 'status' else column }}{% if sorted %}<span class="arrow">{{ '↓' if view.descending else '↑' }}</span>{% endif %}</a>
|
|
134
|
+
{% endfor %}
|
|
130
135
|
</div>
|
|
136
|
+
<form class="run-filters" method="get" action="#runs">
|
|
137
|
+
{% if view.sort %}<input type="hidden" name="sort" value="{{ view.sort }}">{% endif %}
|
|
138
|
+
{% if view.descending %}<input type="hidden" name="order" value="desc">{% endif %}
|
|
139
|
+
<select name="status" aria-label="Filter by status" onchange="this.form.requestSubmit()">
|
|
140
|
+
<option value=""{{ ' selected' if not view.status }}>all</option>
|
|
141
|
+
<option value="complete"{{ ' selected' if view.status == 'complete' }}>✓</option>
|
|
142
|
+
<option value="incomplete"{{ ' selected' if view.status == 'incomplete' }}>✕</option>
|
|
143
|
+
</select>
|
|
144
|
+
{% for column in e.run_columns %}<input type="search" name="f.{{ column }}" value="{{ view.filters.get(column, '') }}" placeholder="filter" aria-label="Filter {{ column }}">{% endfor %}
|
|
145
|
+
<button type="submit" hidden>Filter</button>
|
|
146
|
+
</form>
|
|
131
147
|
{% for run in e.runs_list %}
|
|
132
148
|
<details class="run">
|
|
133
149
|
<summary>
|
|
@@ -142,6 +158,8 @@
|
|
|
142
158
|
{% endfor %}
|
|
143
159
|
</dl>
|
|
144
160
|
</details>
|
|
161
|
+
{% else %}
|
|
162
|
+
<p class="empty run-empty">No runs match these filters.</p>
|
|
145
163
|
{% endfor %}
|
|
146
164
|
</div>
|
|
147
165
|
{% else %}
|
|
@@ -3,13 +3,19 @@
|
|
|
3
3
|
{% block crumbs %}<span class="crumb">{{ folder }}</span>{% endblock %}
|
|
4
4
|
{% block main %}
|
|
5
5
|
<section class="intro">
|
|
6
|
+
{% if experiments %}
|
|
7
|
+
<div class="view-toggle" role="group" aria-label="View">
|
|
8
|
+
<button type="button" data-view="table" aria-pressed="true">Table</button>
|
|
9
|
+
<button type="button" data-view="cards" aria-pressed="false">Cards</button>
|
|
10
|
+
</div>
|
|
11
|
+
{% endif %}
|
|
6
12
|
<h1>Experiments</h1>
|
|
7
13
|
<p class="lede">{{ experiments | length }} experiment{{ '' if experiments | length == 1 else 's' }},
|
|
8
14
|
{{ experiments | sum(attribute='runs') }} runs recorded.</p>
|
|
9
15
|
</section>
|
|
10
16
|
|
|
11
17
|
{% if experiments %}
|
|
12
|
-
<table class="ledger">
|
|
18
|
+
<table class="ledger" data-view="table">
|
|
13
19
|
<thead>
|
|
14
20
|
<tr>
|
|
15
21
|
<th scope="col">Experiment</th>
|
|
@@ -29,7 +35,7 @@
|
|
|
29
35
|
{% for key, values in experiment.varies.items() %}<span class="factor">{{ key }} <b>{{ values | length }}</b>{% if not loop.last or experiment.runs_per_cell %}<span class="times">×</span>{% endif %}</span> {% endfor %}
|
|
30
36
|
{% if experiment.runs_per_cell %}<span class="factor">runs <b>{{ experiment.runs_per_cell }}</b></span>{% endif %}
|
|
31
37
|
</td>
|
|
32
|
-
<td class="num">{{ experiment.runs }}</td>
|
|
38
|
+
<td class="num" data-label="Runs">{{ experiment.runs }}</td>
|
|
33
39
|
<td class="progress-col">
|
|
34
40
|
{% if experiment.filled is not none and experiment.target %}
|
|
35
41
|
<div class="progress-line">
|
|
@@ -43,6 +49,7 @@
|
|
|
43
49
|
{% endfor %}
|
|
44
50
|
</tbody>
|
|
45
51
|
</table>
|
|
52
|
+
<script src="{{ url_for('static', path='view-toggle.js') }}" defer></script>
|
|
46
53
|
{% else %}
|
|
47
54
|
<p class="empty">No experiments found. Each experiment is a folder holding a <code>design.yaml</code> and its markdown notes.</p>
|
|
48
55
|
{% endif %}
|
|
@@ -10,6 +10,9 @@ resolution-markers = [
|
|
|
10
10
|
exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values.
|
|
11
11
|
exclude-newer-span = "P3D"
|
|
12
12
|
|
|
13
|
+
[options.exclude-newer-package]
|
|
14
|
+
labnotes = { timestamp = "0001-01-01T00:00:00Z", span = "PT0S" }
|
|
15
|
+
|
|
13
16
|
[[package]]
|
|
14
17
|
name = "anyio"
|
|
15
18
|
version = "4.15.1"
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
from dirsql import DirSQL
|
|
2
|
-
|
|
3
|
-
from labnotes.sql_quote import quote_string
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
async def list_runs(
|
|
7
|
-
runs: DirSQL, study: str, completed_run: str
|
|
8
|
-
) -> list[dict[str, object]]:
|
|
9
|
-
return await runs.query(
|
|
10
|
-
f"SELECT ({completed_run}) AS labnotes_completed, * FROM runs"
|
|
11
|
-
f" WHERE study = {quote_string(study)} ORDER BY 2 DESC"
|
|
12
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|