openrunner-sdk 2.17.4__tar.gz → 2.18.0__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.
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/PKG-INFO +1 -1
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/__init__.py +1 -1
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/cli.py +5 -4
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/install_commands.py +316 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/pyproject.toml +1 -1
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/tests/conftest.py +34 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/tests/test_cli.py +7 -7
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/tests/test_integration_fastai.py +3 -4
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/tests/test_integration_huggingface.py +3 -2
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/tests/test_integration_keras.py +4 -6
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/tests/test_integration_langchain.py +4 -6
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/tests/test_integration_lightning.py +4 -4
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/tests/test_integration_pytorch.py +3 -4
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/tests/test_integration_sklearn.py +3 -3
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/tests/test_integration_xgboost.py +3 -3
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/.gitignore +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/=6.0 +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/=8.1 +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/README.md +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/CLAUDE.md +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/api_client.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/artifact.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/buffer.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/cache.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/config.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/cost.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/dataset.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/environment.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/evaluation.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/feedback.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/git_info.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/guardrails.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/integration/__init__.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/integration/accelerate.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/integration/anthropic_tracer.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/integration/catboost.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/integration/diffusers.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/integration/fastai.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/integration/forced_alignment.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/integration/gladia.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/integration/gymnasium.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/integration/huggingface.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/integration/hydra.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/integration/ignite.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/integration/jax.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/integration/keras.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/integration/langchain.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/integration/lightgbm.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/integration/lightning.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/integration/llamaindex.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/integration/openai_finetune.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/integration/openai_tracer.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/integration/optuna.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/integration/pytorch.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/integration/sb3.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/integration/sklearn.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/integration/tensorflow.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/integration/trl.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/integration/tts.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/integration/ultralytics.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/integration/voice_agent.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/integration/whisper.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/integration/xgboost.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/launch.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/mcp_server.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/media.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/migrate.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/model.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/offline.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/pii.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/plot.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/prompt.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/query_api.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/redact.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/run.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/scorers.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/sender.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/session.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/settings.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/summary.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/sweep.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/system_metrics.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/tensorboard.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/trace.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/transcript_formatter.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/wal.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/wandb_compat/__init__.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/wandb_compat/_shim.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/openrunner/wer.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/tests/__init__.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/tests/test_alert.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/tests/test_aliases.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/tests/test_api_client.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/tests/test_artifact.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/tests/test_buffer.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/tests/test_cache.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/tests/test_class_scorers.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/tests/test_config.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/tests/test_evaluation.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/tests/test_finish.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/tests/test_git_info.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/tests/test_init.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/tests/test_launch.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/tests/test_log.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/tests/test_log_code.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/tests/test_media.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/tests/test_migrate.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/tests/test_offline.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/tests/test_offline_sync.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/tests/test_pii.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/tests/test_plot.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/tests/test_query_api.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/tests/test_resume.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/tests/test_sdk_features.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/tests/test_sender.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/tests/test_summary.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/tests/test_sweep.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/tests/test_system_metrics.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/tests/test_trace.py +0 -0
- {openrunner_sdk-2.17.4 → openrunner_sdk-2.18.0}/tests/test_wandb_compat.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openrunner-sdk
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.18.0
|
|
4
4
|
Summary: OpenRunner SDK - W&B-compatible ML experiment tracking client
|
|
5
5
|
Project-URL: Homepage, https://github.com/jqueguiner/openrunner
|
|
6
6
|
Project-URL: Repository, https://github.com/jqueguiner/openrunner
|
|
@@ -120,7 +120,7 @@ launch.from_run = _launch_from_run # type: ignore[attr-defined]
|
|
|
120
120
|
# openrunner.trace.patch_openai() syntax
|
|
121
121
|
trace.patch_openai = _patch_openai # type: ignore[attr-defined]
|
|
122
122
|
|
|
123
|
-
__version__ = "2.
|
|
123
|
+
__version__ = "2.18.0"
|
|
124
124
|
|
|
125
125
|
logger = logging.getLogger("openrunner")
|
|
126
126
|
|
|
@@ -124,11 +124,12 @@ def login(host: str | None, expiry: int, manual: bool) -> None:
|
|
|
124
124
|
base_url = host
|
|
125
125
|
elif settings.get("base_url"):
|
|
126
126
|
base_url = settings["base_url"]
|
|
127
|
-
elif sys.stdin.isatty():
|
|
128
|
-
base_url = click.prompt("Server URL")
|
|
129
127
|
else:
|
|
130
|
-
|
|
131
|
-
|
|
128
|
+
try:
|
|
129
|
+
base_url = click.prompt("Server URL", default="http://localhost:8000")
|
|
130
|
+
except (EOFError, click.Abort):
|
|
131
|
+
click.echo("Error: No server URL configured. Run with --host or set base_url in ~/.openrunner/settings.json")
|
|
132
|
+
return
|
|
132
133
|
|
|
133
134
|
if manual:
|
|
134
135
|
# Legacy manual flow
|
|
@@ -468,6 +468,316 @@ Report version and installed commands to user. Remind them to enable the MCP plu
|
|
|
468
468
|
"""
|
|
469
469
|
|
|
470
470
|
|
|
471
|
+
PLOT_SESSION_RUN_CMD = """---
|
|
472
|
+
name: {prefix}plot-session-run
|
|
473
|
+
description: Plot training metrics from runs in current project (ASCII chart + table, PNG on desktop)
|
|
474
|
+
argument-hint: "[run_id] [--metric key] [--all] [--watch [secs]]"
|
|
475
|
+
allowed-tools:
|
|
476
|
+
- Bash
|
|
477
|
+
- Read
|
|
478
|
+
---
|
|
479
|
+
|
|
480
|
+
Plot training run metrics from the current OpenRunner project as ASCII sparklines and summary table.
|
|
481
|
+
If on a desktop with display, also generates a PNG chart.
|
|
482
|
+
|
|
483
|
+
Use `--watch` (or `-w`) for live refresh every 2 minutes (override: `--watch 30` for 30s).
|
|
484
|
+
Auto-stops when no runs are in progress.
|
|
485
|
+
|
|
486
|
+
## Process
|
|
487
|
+
|
|
488
|
+
### Step 1: Fetch runs and metrics
|
|
489
|
+
|
|
490
|
+
```bash
|
|
491
|
+
python3 -c "
|
|
492
|
+
import sys, os, json
|
|
493
|
+
from pathlib import Path
|
|
494
|
+
for p in ([os.path.join(d, 'site-packages') for d in __import__('glob').glob(os.path.expanduser('~/.local/lib/python3.*/')) + __import__('glob').glob(os.path.expanduser('~/Library/Python/3.*/lib/python/'))]):
|
|
495
|
+
if os.path.isdir(p): sys.path.insert(0, p)
|
|
496
|
+
|
|
497
|
+
from openrunner.session import get_session_config
|
|
498
|
+
from openrunner.api_client import APIClient
|
|
499
|
+
|
|
500
|
+
config = get_session_config()
|
|
501
|
+
if not config.get('api_key'):
|
|
502
|
+
print('NOT_CONFIGURED')
|
|
503
|
+
sys.exit(1)
|
|
504
|
+
|
|
505
|
+
client = APIClient(base_url=config['base_url'], api_key=config['api_key'])
|
|
506
|
+
|
|
507
|
+
# Resolve project
|
|
508
|
+
project_name = config.get('project', '')
|
|
509
|
+
project_id = None
|
|
510
|
+
for p in client.list_projects():
|
|
511
|
+
if f'{{p.get(\"org_name\",\"\")}}/{{p.get(\"name\",\"\")}}' == project_name or p.get('name') == project_name:
|
|
512
|
+
project_id = p.get('id')
|
|
513
|
+
break
|
|
514
|
+
|
|
515
|
+
if not project_id:
|
|
516
|
+
print(f'PROJECT_NOT_FOUND={{project_name}}')
|
|
517
|
+
client.close()
|
|
518
|
+
sys.exit(1)
|
|
519
|
+
|
|
520
|
+
args = '$ARGUMENTS'.strip()
|
|
521
|
+
run_id = None
|
|
522
|
+
metric_keys = ['train/loss', 'learning_rate']
|
|
523
|
+
show_all = False
|
|
524
|
+
watch_mode = False
|
|
525
|
+
watch_interval = 120
|
|
526
|
+
|
|
527
|
+
# Parse args
|
|
528
|
+
parts = args.split() if args else []
|
|
529
|
+
i = 0
|
|
530
|
+
while i < len(parts):
|
|
531
|
+
if parts[i] == '--metric' and i+1 < len(parts):
|
|
532
|
+
metric_keys = [parts[i+1]]; i += 2
|
|
533
|
+
elif parts[i] == '--all':
|
|
534
|
+
show_all = True; i += 1
|
|
535
|
+
elif parts[i] == '--watch' or parts[i] == '-w':
|
|
536
|
+
watch_mode = True
|
|
537
|
+
if i+1 < len(parts) and parts[i+1].isdigit():
|
|
538
|
+
watch_interval = int(parts[i+1]); i += 2
|
|
539
|
+
else:
|
|
540
|
+
i += 1
|
|
541
|
+
elif not parts[i].startswith('--'):
|
|
542
|
+
run_id = parts[i]; i += 1
|
|
543
|
+
else:
|
|
544
|
+
i += 1
|
|
545
|
+
|
|
546
|
+
import time as _time
|
|
547
|
+
|
|
548
|
+
iteration = 0
|
|
549
|
+
while True:
|
|
550
|
+
iteration += 1
|
|
551
|
+
|
|
552
|
+
# Clear screen on refresh (not first run)
|
|
553
|
+
if iteration > 1:
|
|
554
|
+
print('\\033[2J\\033[H', end='')
|
|
555
|
+
|
|
556
|
+
# Get runs
|
|
557
|
+
if run_id:
|
|
558
|
+
runs = [{{'id': run_id}}]
|
|
559
|
+
else:
|
|
560
|
+
resp = client._request('get', f'/projects/{{project_id}}/runs', params={{'limit': 10, 'order': 'desc'}})
|
|
561
|
+
runs_data = resp.json()
|
|
562
|
+
runs = runs_data.get('runs', runs_data) if isinstance(runs_data, dict) else runs_data
|
|
563
|
+
if not runs:
|
|
564
|
+
print('NO_RUNS')
|
|
565
|
+
client.close()
|
|
566
|
+
sys.exit(1)
|
|
567
|
+
|
|
568
|
+
# Collect metrics
|
|
569
|
+
bars = '▁▂▃▄▅▆▇█'
|
|
570
|
+
results = []
|
|
571
|
+
|
|
572
|
+
for run in runs[:10]:
|
|
573
|
+
rid = run.get('id', run) if isinstance(run, dict) else run
|
|
574
|
+
try:
|
|
575
|
+
keys_param = ','.join(metric_keys)
|
|
576
|
+
resp = client._request('get', f'/runs/{{rid}}/metrics', params={{'keys': keys_param}})
|
|
577
|
+
data = resp.json()
|
|
578
|
+
metrics = data.get('metrics', {{}})
|
|
579
|
+
if not metrics and not show_all:
|
|
580
|
+
continue
|
|
581
|
+
|
|
582
|
+
# If none of the requested metrics found, use whatever is available
|
|
583
|
+
found_any = any(k in metrics for k in metric_keys)
|
|
584
|
+
if not found_any and metrics and not show_all:
|
|
585
|
+
metric_keys = list(metrics.keys())[:2]
|
|
586
|
+
|
|
587
|
+
for key, points in metrics.items():
|
|
588
|
+
if not show_all and key not in metric_keys:
|
|
589
|
+
continue
|
|
590
|
+
if not points:
|
|
591
|
+
continue
|
|
592
|
+
|
|
593
|
+
values = [pt.get('value', pt) if isinstance(pt, dict) else pt for pt in points]
|
|
594
|
+
steps = [pt.get('step', i) if isinstance(pt, dict) else i for i, pt in enumerate(points)]
|
|
595
|
+
values = [v for v in values if isinstance(v, (int, float))]
|
|
596
|
+
if not values:
|
|
597
|
+
continue
|
|
598
|
+
|
|
599
|
+
min_v = min(values)
|
|
600
|
+
max_v = max(values)
|
|
601
|
+
range_v = max_v - min_v or 1
|
|
602
|
+
width = 60
|
|
603
|
+
|
|
604
|
+
# Sample if too many
|
|
605
|
+
if len(values) > width:
|
|
606
|
+
step_size = len(values) / width
|
|
607
|
+
sampled = [values[int(j * step_size)] for j in range(width)]
|
|
608
|
+
else:
|
|
609
|
+
sampled = values
|
|
610
|
+
|
|
611
|
+
sparkline = ''
|
|
612
|
+
for v in sampled:
|
|
613
|
+
idx = int((v - min_v) / range_v * (len(bars) - 1))
|
|
614
|
+
sparkline += bars[idx]
|
|
615
|
+
|
|
616
|
+
run_name = run.get('display_name') or run.get('name') or rid[:8] if isinstance(run, dict) else rid[:8]
|
|
617
|
+
run_state = run.get('state', 'unknown') if isinstance(run, dict) else 'unknown'
|
|
618
|
+
direction = 'rising' if values[-1] > values[0] else 'falling'
|
|
619
|
+
pct = ((values[-1] - values[0]) / (values[0] or 1)) * 100
|
|
620
|
+
|
|
621
|
+
results.append({{
|
|
622
|
+
'run_id': rid,
|
|
623
|
+
'run_name': run_name,
|
|
624
|
+
'metric': key,
|
|
625
|
+
'sparkline': sparkline,
|
|
626
|
+
'min': min_v,
|
|
627
|
+
'max': max_v,
|
|
628
|
+
'latest': values[-1],
|
|
629
|
+
'steps': len(values),
|
|
630
|
+
'step_range': f'{{steps[0]}}->{{steps[-1]}}',
|
|
631
|
+
'direction': direction,
|
|
632
|
+
'pct': pct,
|
|
633
|
+
'state': run_state,
|
|
634
|
+
}})
|
|
635
|
+
except Exception as e:
|
|
636
|
+
results.append({{'run_id': rid, 'error': str(e)}})
|
|
637
|
+
|
|
638
|
+
if not results:
|
|
639
|
+
print('NO_METRICS')
|
|
640
|
+
client.close()
|
|
641
|
+
sys.exit(1)
|
|
642
|
+
|
|
643
|
+
# ASCII output
|
|
644
|
+
print('=' * 72)
|
|
645
|
+
print(f' OPENRUNNER METRICS — {{project_name}}')
|
|
646
|
+
print('=' * 72)
|
|
647
|
+
|
|
648
|
+
for r in results:
|
|
649
|
+
if 'error' in r:
|
|
650
|
+
print(f' {{r[\"run_id\"][:8]}} — error: {{r[\"error\"]}}')
|
|
651
|
+
continue
|
|
652
|
+
print()
|
|
653
|
+
print(f' Run: {{r[\"run_name\"]}} ({{r[\"run_id\"][:8]}})')
|
|
654
|
+
print(f' Metric: {{r[\"metric\"]}} | Steps: {{r[\"step_range\"]}} ({{r[\"steps\"]}} pts)')
|
|
655
|
+
print(' Range: %.6f -> %.6f | Latest: %.6f' % (r['min'], r['max'], r['latest']))
|
|
656
|
+
print(' ' + r['sparkline'])
|
|
657
|
+
arrow = '↗' if r['pct'] > 0 else '↘'
|
|
658
|
+
print(' %s %s (%+.1f%%)' % (arrow, r['direction'], r['pct']))
|
|
659
|
+
|
|
660
|
+
# Summary table
|
|
661
|
+
print()
|
|
662
|
+
print('-' * 72)
|
|
663
|
+
print(' %-20s %-15s %10s %10s %10s %8s' % ('Run', 'Metric', 'Latest', 'Min', 'Max', 'Trend'))
|
|
664
|
+
print('-' * 72)
|
|
665
|
+
for r in results:
|
|
666
|
+
if 'error' in r:
|
|
667
|
+
continue
|
|
668
|
+
name = (r['run_name'] or '')[:20]
|
|
669
|
+
met = r['metric'][:15]
|
|
670
|
+
arrow = '↗' if r['pct'] > 0 else '↘'
|
|
671
|
+
trend = '%s%+.0f%%' % (arrow, r['pct'])
|
|
672
|
+
print(' %-20s %-15s %10.4f %10.4f %10.4f %8s' % (name, met, r['latest'], r['min'], r['max'], trend))
|
|
673
|
+
print('-' * 72)
|
|
674
|
+
|
|
675
|
+
# Live runs footer — show link + loss indicator for in-progress runs
|
|
676
|
+
base_url = config.get('base_url', '').rstrip('/')
|
|
677
|
+
live_runs = [r for r in results if r.get('state') in ('running', 'in_progress') and 'error' not in r]
|
|
678
|
+
if live_runs:
|
|
679
|
+
print()
|
|
680
|
+
print('=' * 72)
|
|
681
|
+
print(' LIVE RUNS')
|
|
682
|
+
print('=' * 72)
|
|
683
|
+
seen_ids = set()
|
|
684
|
+
for r in live_runs:
|
|
685
|
+
if r['run_id'] in seen_ids:
|
|
686
|
+
continue
|
|
687
|
+
seen_ids.add(r['run_id'])
|
|
688
|
+
# Loss indicator
|
|
689
|
+
loss_metrics = [x for x in live_runs if x['run_id'] == r['run_id'] and 'loss' in x['metric'].lower()]
|
|
690
|
+
lr_metrics = [x for x in live_runs if x['run_id'] == r['run_id'] and 'lr' in x['metric'].lower() or 'learning' in x['metric'].lower()]
|
|
691
|
+
indicator = ''
|
|
692
|
+
if loss_metrics:
|
|
693
|
+
lm = loss_metrics[0]
|
|
694
|
+
arrow = '↘' if lm['pct'] < 0 else '↗'
|
|
695
|
+
indicator += ' loss: %.4f %s' % (lm['latest'], arrow)
|
|
696
|
+
if lr_metrics:
|
|
697
|
+
lrm = lr_metrics[0]
|
|
698
|
+
indicator += ' | lr: %.2e' % lrm['latest']
|
|
699
|
+
run_url = '%s/%s/runs/%s' % (base_url, project_name, r['run_id'])
|
|
700
|
+
print()
|
|
701
|
+
print(' %s %s' % (r['run_name'], indicator))
|
|
702
|
+
# Compact sparkline for loss
|
|
703
|
+
if loss_metrics:
|
|
704
|
+
print(' %s' % loss_metrics[0]['sparkline'])
|
|
705
|
+
print(' -> %s' % run_url)
|
|
706
|
+
print()
|
|
707
|
+
print('=' * 72)
|
|
708
|
+
|
|
709
|
+
# Watch mode: loop or exit
|
|
710
|
+
if not watch_mode:
|
|
711
|
+
break
|
|
712
|
+
if not live_runs:
|
|
713
|
+
print('\\nAll runs finished. Exiting watch mode.')
|
|
714
|
+
break
|
|
715
|
+
print('\\n Refreshing in %ds... (Ctrl+C to stop)' % watch_interval)
|
|
716
|
+
try:
|
|
717
|
+
_time.sleep(watch_interval)
|
|
718
|
+
except KeyboardInterrupt:
|
|
719
|
+
print('\\nWatch stopped.')
|
|
720
|
+
break
|
|
721
|
+
|
|
722
|
+
# PNG generation if display available
|
|
723
|
+
has_display = os.environ.get('DISPLAY') or os.environ.get('WAYLAND_DISPLAY') or sys.platform == 'darwin'
|
|
724
|
+
if has_display:
|
|
725
|
+
try:
|
|
726
|
+
import matplotlib
|
|
727
|
+
matplotlib.use('Agg')
|
|
728
|
+
import matplotlib.pyplot as plt
|
|
729
|
+
import tempfile
|
|
730
|
+
|
|
731
|
+
fig, axes = plt.subplots(len([r for r in results if 'error' not in r]), 1,
|
|
732
|
+
figsize=(12, 3 * len([r for r in results if 'error' not in r])),
|
|
733
|
+
squeeze=False)
|
|
734
|
+
fig.suptitle(f'OpenRunner Metrics — {{project_name}}', fontsize=14, fontweight='bold')
|
|
735
|
+
|
|
736
|
+
plot_idx = 0
|
|
737
|
+
for r in results:
|
|
738
|
+
if 'error' in r:
|
|
739
|
+
continue
|
|
740
|
+
# Re-fetch values for plot
|
|
741
|
+
resp2 = client._request('get', f'/runs/{{r[\"run_id\"]}}/metrics', params={{'keys': r[\"metric\"]}})
|
|
742
|
+
pts = resp2.json().get('metrics', {{}}).get(r['metric'], [])
|
|
743
|
+
vals = [pt.get('value', pt) if isinstance(pt, dict) else pt for pt in pts]
|
|
744
|
+
stp = [pt.get('step', i) if isinstance(pt, dict) else i for i, pt in enumerate(pts)]
|
|
745
|
+
vals = [v for v in vals if isinstance(v, (int, float))]
|
|
746
|
+
stp = stp[:len(vals)]
|
|
747
|
+
|
|
748
|
+
ax = axes[plot_idx][0]
|
|
749
|
+
ax.plot(stp, vals, linewidth=1.5, color='#2196F3')
|
|
750
|
+
ax.fill_between(stp, vals, alpha=0.1, color='#2196F3')
|
|
751
|
+
ax.set_title(f'{{r[\"run_name\"]}} — {{r[\"metric\"]}}', fontsize=11)
|
|
752
|
+
ax.set_xlabel('Step')
|
|
753
|
+
ax.set_ylabel(r['metric'])
|
|
754
|
+
ax.grid(True, alpha=0.3)
|
|
755
|
+
plot_idx += 1
|
|
756
|
+
|
|
757
|
+
plt.tight_layout()
|
|
758
|
+
png_path = Path(tempfile.gettempdir()) / 'openrunner_metrics.png'
|
|
759
|
+
fig.savefig(png_path, dpi=150, bbox_inches='tight')
|
|
760
|
+
plt.close(fig)
|
|
761
|
+
print(f'\\nPNG_SAVED={{png_path}}')
|
|
762
|
+
except ImportError:
|
|
763
|
+
print('\\nPNG_SKIP=matplotlib not installed (pip install matplotlib)')
|
|
764
|
+
except Exception as e:
|
|
765
|
+
print(f'\\nPNG_ERROR={{e}}')
|
|
766
|
+
|
|
767
|
+
client.close()
|
|
768
|
+
print('\\nDONE')
|
|
769
|
+
"
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
### Step 2: Present results
|
|
773
|
+
|
|
774
|
+
Show the ASCII chart and table to the user. If a PNG was generated, mention its path so they can open it.
|
|
775
|
+
|
|
776
|
+
If NOT_CONFIGURED: tell user to run `/openrunner:setup` first.
|
|
777
|
+
If NO_RUNS: tell user no training runs found in project.
|
|
778
|
+
If NO_METRICS: tell user runs exist but no metrics logged yet.
|
|
779
|
+
"""
|
|
780
|
+
|
|
471
781
|
MCP_CMD = """---
|
|
472
782
|
name: {prefix}mcp
|
|
473
783
|
description: Register OpenRunner MCP server for native tool access
|
|
@@ -535,6 +845,7 @@ def install_claude_code() -> list[str]:
|
|
|
535
845
|
"code-upload.md": CODE_UPLOAD_CMD,
|
|
536
846
|
"update.md": UPDATE_CMD,
|
|
537
847
|
"mcp.md": MCP_CMD,
|
|
848
|
+
"plot-session-run.md": PLOT_SESSION_RUN_CMD,
|
|
538
849
|
}
|
|
539
850
|
|
|
540
851
|
files = []
|
|
@@ -555,6 +866,10 @@ When the user asks about training metrics, loss charts, run data, or experiment
|
|
|
555
866
|
|
|
556
867
|
Do NOT parse log files or grep for metrics when OpenRunner MCP tools are available.
|
|
557
868
|
|
|
869
|
+
When the user asks to plot or visualize run metrics in this session:
|
|
870
|
+
- Use `/openrunner:plot-session-run` for full ASCII charts + table + PNG
|
|
871
|
+
- Supports: `/openrunner:plot-session-run [run_id] [--metric key] [--all]`
|
|
872
|
+
|
|
558
873
|
When the user asks about papers, research, or sessions:
|
|
559
874
|
- Use `mcp__openrunner__openrunner_search_papers` for paper content
|
|
560
875
|
- Use `mcp__openrunner__openrunner_search_research` for research plans
|
|
@@ -597,6 +912,7 @@ def _install_all_commands_to_dir(cmd_dir: Path, prefix: str = "openrunner:") ->
|
|
|
597
912
|
"code-upload.md": CODE_UPLOAD_CMD,
|
|
598
913
|
"update.md": UPDATE_CMD,
|
|
599
914
|
"mcp.md": MCP_CMD,
|
|
915
|
+
"plot-session-run.md": PLOT_SESSION_RUN_CMD,
|
|
600
916
|
}
|
|
601
917
|
|
|
602
918
|
files = []
|
|
@@ -68,6 +68,40 @@ def temp_git_repo(tmp_path: Path) -> Generator[Path, None, None]:
|
|
|
68
68
|
yield repo_dir
|
|
69
69
|
|
|
70
70
|
|
|
71
|
+
@pytest.fixture
|
|
72
|
+
def block_import(monkeypatch: pytest.MonkeyPatch):
|
|
73
|
+
"""Return a helper that blocks a module from being imported.
|
|
74
|
+
|
|
75
|
+
Usage:
|
|
76
|
+
def test_foo(block_import):
|
|
77
|
+
block_import("torch")
|
|
78
|
+
with pytest.raises(ImportError):
|
|
79
|
+
import openrunner.integration.pytorch
|
|
80
|
+
"""
|
|
81
|
+
import builtins
|
|
82
|
+
import sys
|
|
83
|
+
|
|
84
|
+
real_import = builtins.__import__
|
|
85
|
+
blocked: set[str] = set()
|
|
86
|
+
|
|
87
|
+
def mock_import(name, *args, **kwargs):
|
|
88
|
+
for pkg in blocked:
|
|
89
|
+
if name == pkg or name.startswith(pkg + "."):
|
|
90
|
+
raise ImportError(f"No module named '{name}'")
|
|
91
|
+
return real_import(name, *args, **kwargs)
|
|
92
|
+
|
|
93
|
+
monkeypatch.setattr(builtins, "__import__", mock_import)
|
|
94
|
+
|
|
95
|
+
def _block(module_name: str):
|
|
96
|
+
blocked.add(module_name)
|
|
97
|
+
# Remove from sys.modules
|
|
98
|
+
to_remove = [k for k in sys.modules if k == module_name or k.startswith(module_name + ".")]
|
|
99
|
+
for k in to_remove:
|
|
100
|
+
monkeypatch.delitem(sys.modules, k, raising=False)
|
|
101
|
+
|
|
102
|
+
return _block
|
|
103
|
+
|
|
104
|
+
|
|
71
105
|
@pytest.fixture
|
|
72
106
|
def mock_http_transport() -> MagicMock:
|
|
73
107
|
"""Create a mock HTTP transport for testing API client without real network calls."""
|
|
@@ -39,8 +39,8 @@ class TestLogin:
|
|
|
39
39
|
def test_login_stores_settings(self, runner, settings_dir):
|
|
40
40
|
result = runner.invoke(
|
|
41
41
|
main,
|
|
42
|
-
["login"],
|
|
43
|
-
input="
|
|
42
|
+
["login", "--manual"],
|
|
43
|
+
input="http://myserver:8000\nmy-api-key\n",
|
|
44
44
|
)
|
|
45
45
|
assert result.exit_code == 0
|
|
46
46
|
assert "Login successful" in result.output
|
|
@@ -54,8 +54,8 @@ class TestLogin:
|
|
|
54
54
|
def test_login_uses_default_url(self, runner, settings_dir):
|
|
55
55
|
result = runner.invoke(
|
|
56
56
|
main,
|
|
57
|
-
["login"],
|
|
58
|
-
input="
|
|
57
|
+
["login", "--manual"],
|
|
58
|
+
input="\nmy-api-key\n", # empty input = default URL
|
|
59
59
|
)
|
|
60
60
|
assert result.exit_code == 0
|
|
61
61
|
settings = json.loads(
|
|
@@ -70,15 +70,15 @@ class TestLogin:
|
|
|
70
70
|
)
|
|
71
71
|
result = runner.invoke(
|
|
72
72
|
main,
|
|
73
|
-
["login"],
|
|
74
|
-
input="new-key\
|
|
73
|
+
["login", "--manual"],
|
|
74
|
+
input="new-key\n",
|
|
75
75
|
)
|
|
76
76
|
assert result.exit_code == 0
|
|
77
77
|
settings = json.loads(
|
|
78
78
|
(settings_dir / "settings.json").read_text()
|
|
79
79
|
)
|
|
80
80
|
assert settings["api_key"] == "new-key"
|
|
81
|
-
assert settings["base_url"] == "http://
|
|
81
|
+
assert settings["base_url"] == "http://old:8000"
|
|
82
82
|
|
|
83
83
|
|
|
84
84
|
class TestSyncAll:
|
|
@@ -163,10 +163,9 @@ class TestOpenRunnerFastAICallback:
|
|
|
163
163
|
class TestFastAIImportError:
|
|
164
164
|
"""ImportError when fastai is missing."""
|
|
165
165
|
|
|
166
|
-
def test_import_error_when_fastai_missing(self, monkeypatch):
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
monkeypatch.delitem(sys.modules, "fastai.callback.core", raising=False)
|
|
166
|
+
def test_import_error_when_fastai_missing(self, monkeypatch, block_import):
|
|
167
|
+
block_import("fastai")
|
|
168
|
+
import sys
|
|
170
169
|
sys.modules.pop("openrunner.integration.fastai", None)
|
|
171
170
|
|
|
172
171
|
with pytest.raises(ImportError, match="pip install openrunner"):
|
|
@@ -161,8 +161,9 @@ class TestOpenRunnerCallback:
|
|
|
161
161
|
class TestHuggingFaceImportError:
|
|
162
162
|
"""Test 10: clear ImportError when transformers is missing."""
|
|
163
163
|
|
|
164
|
-
def test_import_error_when_transformers_missing(self, monkeypatch):
|
|
165
|
-
|
|
164
|
+
def test_import_error_when_transformers_missing(self, monkeypatch, block_import):
|
|
165
|
+
block_import("transformers")
|
|
166
|
+
import sys
|
|
166
167
|
sys.modules.pop("openrunner.integration.huggingface", None)
|
|
167
168
|
|
|
168
169
|
with pytest.raises(ImportError, match="pip install openrunner"):
|
|
@@ -147,12 +147,10 @@ class TestOpenRunnerKerasCallback:
|
|
|
147
147
|
class TestKerasImportError:
|
|
148
148
|
"""ImportError when keras is missing."""
|
|
149
149
|
|
|
150
|
-
def test_import_error_when_keras_missing(self, monkeypatch):
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
monkeypatch.delitem(sys.modules, "tensorflow.keras", raising=False)
|
|
155
|
-
monkeypatch.delitem(sys.modules, "tensorflow.keras.callbacks", raising=False)
|
|
150
|
+
def test_import_error_when_keras_missing(self, monkeypatch, block_import):
|
|
151
|
+
block_import("keras")
|
|
152
|
+
block_import("tensorflow")
|
|
153
|
+
import sys
|
|
156
154
|
sys.modules.pop("openrunner.integration.keras", None)
|
|
157
155
|
|
|
158
156
|
with pytest.raises(ImportError, match="pip install openrunner"):
|
|
@@ -200,12 +200,10 @@ class TestOpenRunnerCallbackHandler:
|
|
|
200
200
|
class TestLangChainImportError:
|
|
201
201
|
"""ImportError when langchain is missing."""
|
|
202
202
|
|
|
203
|
-
def test_import_error_when_langchain_missing(self, monkeypatch):
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
monkeypatch.delitem(sys.modules, "langchain.callbacks", raising=False)
|
|
208
|
-
monkeypatch.delitem(sys.modules, "langchain.callbacks.base", raising=False)
|
|
203
|
+
def test_import_error_when_langchain_missing(self, monkeypatch, block_import):
|
|
204
|
+
block_import("langchain_core")
|
|
205
|
+
block_import("langchain")
|
|
206
|
+
import sys
|
|
209
207
|
sys.modules.pop("openrunner.integration.langchain", None)
|
|
210
208
|
|
|
211
209
|
with pytest.raises(ImportError, match="pip install openrunner"):
|
|
@@ -175,10 +175,10 @@ class TestOpenRunnerLogger:
|
|
|
175
175
|
class TestLightningImportError:
|
|
176
176
|
"""Test 8: clear ImportError when lightning is missing."""
|
|
177
177
|
|
|
178
|
-
def test_import_error_when_lightning_missing(self, monkeypatch):
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
178
|
+
def test_import_error_when_lightning_missing(self, monkeypatch, block_import):
|
|
179
|
+
block_import("lightning")
|
|
180
|
+
block_import("pytorch_lightning")
|
|
181
|
+
import sys
|
|
182
182
|
sys.modules.pop("openrunner.integration.lightning", None)
|
|
183
183
|
|
|
184
184
|
with pytest.raises(ImportError, match="pip install openrunner"):
|
|
@@ -113,10 +113,9 @@ class TestLogGradients:
|
|
|
113
113
|
class TestPyTorchImportError:
|
|
114
114
|
"""Test 4: clear ImportError when torch is missing."""
|
|
115
115
|
|
|
116
|
-
def test_import_error_when_torch_missing(self, monkeypatch):
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
monkeypatch.delitem(sys.modules, "torch.nn", raising=False)
|
|
116
|
+
def test_import_error_when_torch_missing(self, monkeypatch, block_import):
|
|
117
|
+
block_import("torch")
|
|
118
|
+
import sys
|
|
120
119
|
sys.modules.pop("openrunner.integration.pytorch", None)
|
|
121
120
|
|
|
122
121
|
with pytest.raises(ImportError, match="pip install openrunner"):
|
|
@@ -250,9 +250,9 @@ class TestLogFeatureImportance:
|
|
|
250
250
|
class TestSklearnImportError:
|
|
251
251
|
"""ImportError when sklearn is missing."""
|
|
252
252
|
|
|
253
|
-
def test_import_error_when_sklearn_missing(self, monkeypatch):
|
|
254
|
-
|
|
255
|
-
|
|
253
|
+
def test_import_error_when_sklearn_missing(self, monkeypatch, block_import):
|
|
254
|
+
block_import("sklearn")
|
|
255
|
+
import sys
|
|
256
256
|
sys.modules.pop("openrunner.integration.sklearn", None)
|
|
257
257
|
|
|
258
258
|
with pytest.raises(ImportError, match="pip install openrunner"):
|
|
@@ -167,9 +167,9 @@ class TestOpenRunnerXGBoostCallback:
|
|
|
167
167
|
class TestXGBoostImportError:
|
|
168
168
|
"""ImportError when xgboost is missing."""
|
|
169
169
|
|
|
170
|
-
def test_import_error_when_xgboost_missing(self, monkeypatch):
|
|
171
|
-
|
|
172
|
-
|
|
170
|
+
def test_import_error_when_xgboost_missing(self, monkeypatch, block_import):
|
|
171
|
+
block_import("xgboost")
|
|
172
|
+
import sys
|
|
173
173
|
sys.modules.pop("openrunner.integration.xgboost", None)
|
|
174
174
|
|
|
175
175
|
with pytest.raises(ImportError, match="pip install openrunner"):
|
|
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
|
|
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
|