labnotes 0.0.5__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.5 → labnotes-0.0.6}/PKG-INFO +1 -1
- {labnotes-0.0.5 → 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.5 → 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.5 → labnotes-0.0.6}/src/labnotes/static/labnotes.css +64 -2
- labnotes-0.0.6/src/labnotes/static/view-toggle.js +15 -0
- {labnotes-0.0.5 → labnotes-0.0.6}/src/labnotes/templates/experiment.html +22 -4
- {labnotes-0.0.5 → labnotes-0.0.6}/src/labnotes/templates/index.html +9 -2
- {labnotes-0.0.5 → labnotes-0.0.6}/uv.lock +3 -0
- labnotes-0.0.5/src/labnotes/list_runs.py +0 -12
- {labnotes-0.0.5 → labnotes-0.0.6}/.gitignore +0 -0
- {labnotes-0.0.5 → labnotes-0.0.6}/CHANGELOG.md +0 -0
- {labnotes-0.0.5 → labnotes-0.0.6}/MIGRATIONS.md +0 -0
- {labnotes-0.0.5 → labnotes-0.0.6}/README.md +0 -0
- {labnotes-0.0.5 → labnotes-0.0.6}/pyproject.toml +0 -0
- {labnotes-0.0.5 → labnotes-0.0.6}/src/labnotes/__init__.py +0 -0
- {labnotes-0.0.5 → labnotes-0.0.6}/src/labnotes/cell_progress.py +0 -0
- {labnotes-0.0.5 → labnotes-0.0.6}/src/labnotes/cli.py +0 -0
- {labnotes-0.0.5 → labnotes-0.0.6}/src/labnotes/count_runs.py +0 -0
- {labnotes-0.0.5 → labnotes-0.0.6}/src/labnotes/drain_watch.py +0 -0
- {labnotes-0.0.5 → labnotes-0.0.6}/src/labnotes/dump_experiments.py +0 -0
- {labnotes-0.0.5 → labnotes-0.0.6}/src/labnotes/format_value.py +0 -0
- {labnotes-0.0.5 → labnotes-0.0.6}/src/labnotes/list_experiments.py +0 -0
- {labnotes-0.0.5 → labnotes-0.0.6}/src/labnotes/open_experiments.py +0 -0
- {labnotes-0.0.5 → labnotes-0.0.6}/src/labnotes/open_runs.py +0 -0
- {labnotes-0.0.5 → labnotes-0.0.6}/src/labnotes/pivot_cells.py +0 -0
- {labnotes-0.0.5 → labnotes-0.0.6}/src/labnotes/read_design.py +0 -0
- {labnotes-0.0.5 → labnotes-0.0.6}/src/labnotes/read_note.py +0 -0
- {labnotes-0.0.5 → labnotes-0.0.6}/src/labnotes/render_markdown.py +0 -0
- {labnotes-0.0.5 → labnotes-0.0.6}/src/labnotes/run_columns.py +0 -0
- {labnotes-0.0.5 → labnotes-0.0.6}/src/labnotes/serve_app.py +0 -0
- {labnotes-0.0.5 → labnotes-0.0.6}/src/labnotes/sql_quote.py +0 -0
- {labnotes-0.0.5 → labnotes-0.0.6}/src/labnotes/study_progress.py +0 -0
- {labnotes-0.0.5 → labnotes-0.0.6}/src/labnotes/summarize_design.py +0 -0
- {labnotes-0.0.5 → labnotes-0.0.6}/src/labnotes/templates/base.html +0 -0
- {labnotes-0.0.5 → 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"
|
|
@@ -291,14 +291,14 @@ h3 {
|
|
|
291
291
|
|
|
292
292
|
/* Runs */
|
|
293
293
|
.runs {
|
|
294
|
-
--row:
|
|
294
|
+
--row: 2.75rem repeat(var(--columns), minmax(0, 1fr));
|
|
295
295
|
background: var(--surface);
|
|
296
296
|
border: 1px solid var(--rule);
|
|
297
297
|
border-radius: 10px;
|
|
298
298
|
overflow: hidden;
|
|
299
299
|
font-size: 0.86rem;
|
|
300
300
|
}
|
|
301
|
-
.run-head, .run summary {
|
|
301
|
+
.run-head, .run summary, .run-filters {
|
|
302
302
|
display: grid;
|
|
303
303
|
grid-template-columns: var(--row);
|
|
304
304
|
gap: 1rem;
|
|
@@ -313,6 +313,25 @@ h3 {
|
|
|
313
313
|
color: var(--ink-3);
|
|
314
314
|
border-bottom: 1px solid var(--rule);
|
|
315
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); }
|
|
316
335
|
.run { border-bottom: 1px solid var(--rule); }
|
|
317
336
|
.run:last-child { border-bottom: 0; }
|
|
318
337
|
.run summary { cursor: pointer; list-style: none; }
|
|
@@ -338,6 +357,49 @@ h3 {
|
|
|
338
357
|
.record code { background: none; padding: 0; word-break: break-word; }
|
|
339
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); }
|
|
340
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
|
+
|
|
341
403
|
@media (max-width: 52rem) {
|
|
342
404
|
main, .masthead { padding-left: 1.1rem; padding-right: 1.1rem; }
|
|
343
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
|