labnotes 0.0.34__tar.gz → 0.0.36__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.34 → labnotes-0.0.36}/PKG-INFO +1 -1
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/context.md +5 -1
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/models.py +1 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/runner/read_experiment_config.py +6 -2
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/runner/run_experiment.py +7 -3
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/tui/run_app.py +61 -6
- {labnotes-0.0.34 → labnotes-0.0.36}/.gitignore +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/CHANGELOG.md +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/MIGRATIONS.md +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/README.md +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/pyproject.toml +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/__init__.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/asset_url.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/cell_progress.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/check_counts.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/cli.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/context_guide.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/count_runs.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/create_app.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/create_experiment.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/data_html.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/default_config_text.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/default_counts.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/drain_watch.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/experiment_notes.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/experiment_progress.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/experiment_runs.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/experiments.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/format_value.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/init.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/list_experiments.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/list_runs.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/load_config.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/load_experiment.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/notebook_kernel.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/output_html.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/parse_run_view.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/pivot_cells.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/print_rows.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/python_blocks.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/read_config.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/read_path.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/render_markdown.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/runner/__init__.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/runner/describe_event.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/runner/execute_run.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/runner/hold_folder.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/runner/new_invocation_id.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/runner/new_run_id.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/runner/param_args.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/runner/param_grid.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/runner/plan_runs.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/runner/run_folder.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/runner/run_headless.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/runner/runs_parent.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/runner/shell_command.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/runner/start_experiment.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/runner/stop_then_kill.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/runner/wait_for_group.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/runner/write_run_json.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/saved_outputs.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/serve_app.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/server.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/sort_href.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/sql_quote.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/static/labnotes.css +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/static/row-link.js +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/static/run-cell.js +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/static/view-toggle.js +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/stop_idle_kernels.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/summarize_config.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/tables/__init__.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/tables/experiment_rows.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/tables/file_prefix.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/tables/iso_time.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/tables/list_experiment_runs.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/tables/open_tables.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/tables/path_glob.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/tables/path_pattern.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/tables/read_experiment.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/tables/read_file.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/tables/read_run.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/tables/summarize_experiments.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/templates/base.html +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/templates/experiment.html +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/templates/index.html +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/templates/macros.html +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/tui/__init__.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/tui/dim.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/tui/format_elapsed.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/src/labnotes/tui/run_tui.py +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/testing-conventions.toml +0 -0
- {labnotes-0.0.34 → labnotes-0.0.36}/uv.lock +0 -0
|
@@ -59,6 +59,8 @@ renaming the experiment means editing `name`.
|
|
|
59
59
|
Each combination of `[params]` runs `repeats` times, round-robin. Only runs
|
|
60
60
|
that count fill a combination, so running again fills in what failed or never
|
|
61
61
|
ran.
|
|
62
|
+
`mode = "adhoc"` skips that: every `run` runs each combination `repeats` times
|
|
63
|
+
again, whatever is already on disk. The default is `mode = "fill"`.
|
|
62
64
|
Each run gets a new run folder. `command` runs in a shell from the root, with
|
|
63
65
|
`{run_dir}` replaced by the run folder's absolute path. Pass it to the
|
|
64
66
|
script so it writes its outputs there.
|
|
@@ -84,6 +86,7 @@ A script run by labnotes should:
|
|
|
84
86
|
- take its output folder as an argument, passed in `command` as `{run_dir}`, and write everything it produces there;
|
|
85
87
|
- exit non-zero when it fails, so the run counts as failed and runs again next time;
|
|
86
88
|
- stop any background process it starts before it exits; the run ends when `command` exits.
|
|
89
|
+
- flush its output as it goes (`print(..., flush=True)`, or `PYTHONUNBUFFERED=1`), so the live view shows it as it happens.
|
|
87
90
|
|
|
88
91
|
Runs can take a long time. Start `run` in the background, then check on it with `labnotes experiments foo runs`; a run with no `ended` is still going.
|
|
89
92
|
|
|
@@ -96,7 +99,8 @@ the end of the queue. `/` filters rows by params text; Enter keeps the filter,
|
|
|
96
99
|
Escape clears it. `D` deselects every pending run the filter shows. `k` stops the
|
|
97
100
|
running run under the cursor; `k` again kills it. `+` and
|
|
98
101
|
`-` change how many run at once; lowering it lets running runs
|
|
99
|
-
finish
|
|
102
|
+
finish. Enter on a run follows its `stdout.log`; `e` switches to
|
|
103
|
+
`stderr.log` and back, and Escape returns to the table. `q` quits. Elsewhere, or with `--no-tui`, it prints a
|
|
100
104
|
line per event instead.
|
|
101
105
|
|
|
102
106
|
- `--concurrency N` runs N at a time to start with (default 1).
|
|
@@ -5,7 +5,8 @@ from pathlib import Path
|
|
|
5
5
|
from labnotes.default_counts import DEFAULT_COUNTS
|
|
6
6
|
from labnotes.models import ExperimentConfig
|
|
7
7
|
|
|
8
|
-
SETTINGS = {"name", "command", "repeats", "params", "counts"}
|
|
8
|
+
SETTINGS = {"name", "command", "repeats", "params", "counts", "mode"}
|
|
9
|
+
MODES = ("fill", "adhoc")
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
def read_experiment_config(path: Path) -> ExperimentConfig:
|
|
@@ -39,4 +40,7 @@ def read_experiment_config(path: Path) -> ExperimentConfig:
|
|
|
39
40
|
counts = values.get("counts", DEFAULT_COUNTS)
|
|
40
41
|
if not isinstance(counts, str) or not counts.strip():
|
|
41
42
|
raise ValueError(f"{path}: counts must be a SQL condition")
|
|
42
|
-
|
|
43
|
+
mode = values.get("mode", "fill")
|
|
44
|
+
if mode not in MODES:
|
|
45
|
+
raise ValueError(f"{path}: mode must be fill or adhoc")
|
|
46
|
+
return ExperimentConfig(command, repeats, params, name, counts, mode)
|
|
@@ -31,8 +31,12 @@ async def run_experiment(
|
|
|
31
31
|
report: Report,
|
|
32
32
|
listen: Listen,
|
|
33
33
|
) -> list[dict[str, object]]:
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
finished: list[dict[str, object]] = []
|
|
35
|
+
match experiment.mode:
|
|
36
|
+
case "fill":
|
|
37
|
+
tables = open_tables(config)
|
|
38
|
+
existing = await list_experiment_runs(tables, name, experiment.counts)
|
|
39
|
+
finished = [json.loads(str(r["params"])) for r in existing if r["counted"]]
|
|
36
40
|
planned: set[str] = set()
|
|
37
41
|
|
|
38
42
|
def taken(run: str) -> bool:
|
|
@@ -162,7 +166,7 @@ async def run_experiment(
|
|
|
162
166
|
async def start(run: str, params: dict[str, object]) -> None:
|
|
163
167
|
folder = run_folder(config.root, config.runs, name, run)
|
|
164
168
|
folder.mkdir(parents=True)
|
|
165
|
-
report({"event": "started", "run": run, "params": params})
|
|
169
|
+
report({"event": "started", "run": run, "params": params, "folder": folder})
|
|
166
170
|
running[run] = set()
|
|
167
171
|
result = await execute_run(
|
|
168
172
|
config.root,
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import codecs
|
|
1
2
|
import signal
|
|
2
3
|
import time
|
|
4
|
+
from pathlib import Path
|
|
3
5
|
|
|
4
6
|
from rich.text import Text
|
|
5
7
|
from textual.app import App, ComposeResult
|
|
6
8
|
from textual.binding import Binding
|
|
7
|
-
from textual.widgets import DataTable, Footer, Header, Input, Static
|
|
9
|
+
from textual.widgets import DataTable, Footer, Header, Input, Log, Static
|
|
8
10
|
|
|
9
11
|
from labnotes.models import Controls, Start
|
|
10
12
|
from labnotes.runner.describe_event import describe_event
|
|
@@ -14,6 +16,7 @@ from labnotes.tui.dim import dim
|
|
|
14
16
|
from labnotes.tui.format_elapsed import format_elapsed
|
|
15
17
|
|
|
16
18
|
COLUMNS = ("run", "params", "status", "time")
|
|
19
|
+
OTHER_STREAM = {"stdout": "stderr", "stderr": "stdout"}
|
|
17
20
|
|
|
18
21
|
|
|
19
22
|
class RunApp(App[None]):
|
|
@@ -26,7 +29,8 @@ class RunApp(App[None]):
|
|
|
26
29
|
Binding("h", "hide_failed", "Hide failed"),
|
|
27
30
|
Binding("r", "retry", "Retry"),
|
|
28
31
|
Binding("slash", "filter", "Filter", key_display="/"),
|
|
29
|
-
Binding("
|
|
32
|
+
Binding("e", "switch_output", "stdout/stderr"),
|
|
33
|
+
Binding("escape", "leave", "Back"),
|
|
30
34
|
Binding("plus,equals_sign", "concurrency(1)", "More at once", key_display="+"),
|
|
31
35
|
Binding("minus", "concurrency(-1)", "Fewer at once", key_display="-"),
|
|
32
36
|
Binding("q", "quit", "Quit"),
|
|
@@ -56,10 +60,16 @@ class RunApp(App[None]):
|
|
|
56
60
|
self.filter = ""
|
|
57
61
|
self.failed: set[str] = set()
|
|
58
62
|
self.hiding = False
|
|
63
|
+
self.folders: dict[str, Path] = {}
|
|
64
|
+
self.viewing: str | None = None
|
|
65
|
+
self.stream = "stdout"
|
|
66
|
+
self.offset: int
|
|
67
|
+
self.decoder = codecs.getincrementaldecoder("utf-8")(errors="replace")
|
|
59
68
|
|
|
60
69
|
def compose(self) -> ComposeResult:
|
|
61
70
|
yield Header()
|
|
62
71
|
yield DataTable(id="runs", cursor_type="row")
|
|
72
|
+
yield Log(id="output")
|
|
63
73
|
yield Input(id="filter", placeholder="Filter params")
|
|
64
74
|
yield Static(id="status")
|
|
65
75
|
yield Footer()
|
|
@@ -69,9 +79,11 @@ class RunApp(App[None]):
|
|
|
69
79
|
for column in COLUMNS:
|
|
70
80
|
table.add_column(column, key=column)
|
|
71
81
|
self.query_one(Input).display = False
|
|
82
|
+
self.query_one(Log).display = False
|
|
72
83
|
|
|
73
84
|
def on_ready(self) -> None:
|
|
74
85
|
self.set_interval(1, self.tick)
|
|
86
|
+
self.set_interval(0.25, self.follow)
|
|
75
87
|
self.run_worker(self.drive())
|
|
76
88
|
|
|
77
89
|
async def drive(self) -> None:
|
|
@@ -99,6 +111,7 @@ class RunApp(App[None]):
|
|
|
99
111
|
self.idle = False
|
|
100
112
|
self.pending.discard(run)
|
|
101
113
|
self.started[run] = time.monotonic()
|
|
114
|
+
self.folders[run] = Path(str(event["folder"]))
|
|
102
115
|
if run == self.next_run:
|
|
103
116
|
self.show("")
|
|
104
117
|
args = " ".join(param_args(event["params"])) # type: ignore[arg-type]
|
|
@@ -139,9 +152,7 @@ class RunApp(App[None]):
|
|
|
139
152
|
self.show(describe_event(event))
|
|
140
153
|
case "idle":
|
|
141
154
|
self.idle = True
|
|
142
|
-
|
|
143
|
-
passed = len(self.ended) - failed
|
|
144
|
-
self.show(f"{passed} passed, {failed} failed. q to quit.")
|
|
155
|
+
self.show_summary()
|
|
145
156
|
case "concurrency":
|
|
146
157
|
self.show_concurrency(event)
|
|
147
158
|
self.show(describe_event(event))
|
|
@@ -152,6 +163,11 @@ class RunApp(App[None]):
|
|
|
152
163
|
case _:
|
|
153
164
|
self.show(describe_event(event))
|
|
154
165
|
|
|
166
|
+
def show_summary(self) -> None:
|
|
167
|
+
failed = len(self.failed)
|
|
168
|
+
passed = len(self.ended) - failed
|
|
169
|
+
self.show(f"{passed} passed, {failed} failed. q to quit.")
|
|
170
|
+
|
|
155
171
|
def reorder(self, event: dict[str, object], row: int) -> None:
|
|
156
172
|
waiting: list[str] = [*event["pending"], *event["held"]] # type: ignore[misc]
|
|
157
173
|
for run in waiting:
|
|
@@ -239,7 +255,46 @@ class RunApp(App[None]):
|
|
|
239
255
|
field.display = True
|
|
240
256
|
field.focus()
|
|
241
257
|
|
|
242
|
-
def
|
|
258
|
+
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
|
|
259
|
+
self.view(str(event.row_key.value), "stdout")
|
|
260
|
+
|
|
261
|
+
def view(self, run: str, stream: str) -> None:
|
|
262
|
+
self.viewing = run
|
|
263
|
+
self.stream = stream
|
|
264
|
+
self.offset = 0
|
|
265
|
+
self.decoder.reset()
|
|
266
|
+
self.query_one(Log).clear()
|
|
267
|
+
self.query_one(DataTable).display = False
|
|
268
|
+
self.query_one(Log).display = True
|
|
269
|
+
self.show(f"{run} {stream}. e switches, Escape goes back.")
|
|
270
|
+
self.follow()
|
|
271
|
+
|
|
272
|
+
def follow(self) -> None:
|
|
273
|
+
if self.viewing not in self.folders:
|
|
274
|
+
return
|
|
275
|
+
path = self.folders[self.viewing] / f"{self.stream}.log"
|
|
276
|
+
if not path.exists():
|
|
277
|
+
return
|
|
278
|
+
with path.open("rb") as log:
|
|
279
|
+
log.seek(self.offset)
|
|
280
|
+
data = log.read()
|
|
281
|
+
self.offset += len(data)
|
|
282
|
+
self.query_one(Log).write(self.decoder.decode(data))
|
|
283
|
+
|
|
284
|
+
def action_switch_output(self) -> None:
|
|
285
|
+
if self.viewing is not None:
|
|
286
|
+
self.view(self.viewing, OTHER_STREAM[self.stream])
|
|
287
|
+
|
|
288
|
+
def action_leave(self) -> None:
|
|
289
|
+
if self.viewing is not None:
|
|
290
|
+
self.viewing = None
|
|
291
|
+
self.query_one(Log).display = False
|
|
292
|
+
self.query_one(DataTable).display = True
|
|
293
|
+
if self.idle:
|
|
294
|
+
self.show_summary()
|
|
295
|
+
else:
|
|
296
|
+
self.show("")
|
|
297
|
+
return
|
|
243
298
|
field = self.query_one(Input)
|
|
244
299
|
field.value = ""
|
|
245
300
|
field.display = False
|
|
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
|
|
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
|
|
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
|
|
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
|