expr-tracker 0.3.1__tar.gz → 0.3.2__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.
Files changed (113) hide show
  1. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/PKG-INFO +2 -1
  2. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/docs/guide/cli.md +14 -0
  3. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/pyproject.toml +1 -0
  4. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/src/expr_tracker/cli.py +17 -2
  5. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_cli.py +79 -1
  6. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/uv.lock +2 -0
  7. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/.github/workflows/docs.yaml +0 -0
  8. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/.github/workflows/release.yaml +0 -0
  9. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/.gitignore +0 -0
  10. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/LICENSE +0 -0
  11. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/README.md +0 -0
  12. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/docs/architecture.md +0 -0
  13. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/docs/design.md +0 -0
  14. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/docs/examples.md +0 -0
  15. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/docs/getting-started.md +0 -0
  16. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/docs/guide/alerts.md +0 -0
  17. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/docs/guide/artifacts.md +0 -0
  18. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/docs/guide/backends.md +0 -0
  19. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/docs/guide/distributed.md +0 -0
  20. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/docs/guide/history.md +0 -0
  21. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/docs/guide/logging.md +0 -0
  22. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/docs/guide/spans.md +0 -0
  23. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/docs/guide/streams.md +0 -0
  24. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/docs/index.md +0 -0
  25. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/docs/reference/api.md +0 -0
  26. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/docs/reference/configuration.md +0 -0
  27. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/docs/reference/expressions.md +0 -0
  28. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/examples/README.md +0 -0
  29. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/examples/alert_rules.py +0 -0
  30. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/examples/checkpoints.py +0 -0
  31. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/examples/early_stopping.py +0 -0
  32. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/examples/multiprocess_pipeline.py +0 -0
  33. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/examples/profile_step.py +0 -0
  34. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/examples/quickstart.py +0 -0
  35. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/mkdocs.yml +0 -0
  36. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/src/expr_tracker/__init__.py +0 -0
  37. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/src/expr_tracker/_compat.py +0 -0
  38. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/src/expr_tracker/alerts/__init__.py +0 -0
  39. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/src/expr_tracker/alerts/backends/__init__.py +0 -0
  40. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/src/expr_tracker/alerts/backends/base.py +0 -0
  41. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/src/expr_tracker/alerts/backends/cards.py +0 -0
  42. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/src/expr_tracker/alerts/dispatch.py +0 -0
  43. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/src/expr_tracker/alerts/engine.py +0 -0
  44. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/src/expr_tracker/alerts/expr/__init__.py +0 -0
  45. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/src/expr_tracker/alerts/expr/eval.py +0 -0
  46. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/src/expr_tracker/alerts/expr/functions.py +0 -0
  47. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/src/expr_tracker/alerts/expr/lexer.py +0 -0
  48. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/src/expr_tracker/alerts/expr/nodes.py +0 -0
  49. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/src/expr_tracker/alerts/expr/parser.py +0 -0
  50. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/src/expr_tracker/alerts/expr/rule.py +0 -0
  51. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/src/expr_tracker/alerts/models.py +0 -0
  52. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/src/expr_tracker/artifacts.py +0 -0
  53. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/src/expr_tracker/encoders.py +0 -0
  54. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/src/expr_tracker/history/__init__.py +0 -0
  55. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/src/expr_tracker/history/codec.py +0 -0
  56. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/src/expr_tracker/history/frame.py +0 -0
  57. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/src/expr_tracker/history/naming.py +0 -0
  58. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/src/expr_tracker/history/reader.py +0 -0
  59. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/src/expr_tracker/history/series.py +0 -0
  60. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/src/expr_tracker/history/store.py +0 -0
  61. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/src/expr_tracker/history/writer.py +0 -0
  62. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/src/expr_tracker/plugins.py +0 -0
  63. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/src/expr_tracker/py.typed +0 -0
  64. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/src/expr_tracker/run.py +0 -0
  65. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/src/expr_tracker/spans.py +0 -0
  66. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/src/expr_tracker/summary.py +0 -0
  67. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/src/expr_tracker/trace.py +0 -0
  68. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/src/expr_tracker/tracker.py +0 -0
  69. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/src/expr_tracker/types.py +0 -0
  70. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/conftest.py +0 -0
  71. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_alert_backends.py +0 -0
  72. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_alert_delivery.py +0 -0
  73. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_alert_dispatch.py +0 -0
  74. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_alert_engine.py +0 -0
  75. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_alert_models.py +0 -0
  76. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_alert_routing.py +0 -0
  77. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_artifacts.py +0 -0
  78. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_benchmark.py +0 -0
  79. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_cache.py +0 -0
  80. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_correctness.py +0 -0
  81. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_distributed.py +0 -0
  82. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_e2e.py +0 -0
  83. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_examples.py +0 -0
  84. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_expr_builder.py +0 -0
  85. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_expr_eval.py +0 -0
  86. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_expr_functions.py +0 -0
  87. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_expr_parser.py +0 -0
  88. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_expr_properties.py +0 -0
  89. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_failure_modes.py +0 -0
  90. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_features.py +0 -0
  91. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_frame_codec_summary.py +0 -0
  92. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_history.py +0 -0
  93. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_hot_paths.py +0 -0
  94. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_integration.py +0 -0
  95. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_lark.py +0 -0
  96. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_perf.py +0 -0
  97. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_public_surfaces.py +0 -0
  98. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_review_regressions.py +0 -0
  99. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_rule_lifecycle.py +0 -0
  100. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_run_backends.py +0 -0
  101. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_run_dir.py +0 -0
  102. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_scenarios.py +0 -0
  103. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_span_plugins.py +0 -0
  104. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_spans.py +0 -0
  105. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_step_commit.py +0 -0
  106. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_streams.py +0 -0
  107. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_stress.py +0 -0
  108. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_trace.py +0 -0
  109. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_trackio.py +0 -0
  110. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_value_encoding.py +0 -0
  111. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_wandb.py +0 -0
  112. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_writer_buffer.py +0 -0
  113. {expr_tracker-0.3.1 → expr_tracker-0.3.2}/tests/test_writer_durability.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: expr_tracker
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Local-first experiment tracking with queryable history and expression-based alerts on your training metrics
5
5
  Project-URL: Homepage, https://hspk.github.io/expr_tracker/
6
6
  Project-URL: Documentation, https://hspk.github.io/expr_tracker/
@@ -27,6 +27,7 @@ Requires-Python: >=3.10
27
27
  Requires-Dist: click>=8.1.0
28
28
  Requires-Dist: loguru>=0.7.3
29
29
  Requires-Dist: pydantic>=2.0
30
+ Requires-Dist: python-dotenv>=1.0
30
31
  Provides-Extra: all
31
32
  Requires-Dist: nvidia-ml-py>=12.0; extra == 'all'
32
33
  Requires-Dist: pandas>=1.5; extra == 'all'
@@ -89,3 +89,17 @@ et alert "node 3 is on fire" --level critical --channel oncall
89
89
 
90
90
  Channels come from the same configuration the library uses: `ET_ALERT_CONFIG` or the
91
91
  `ET_LARK_WEBHOOK_URL` environment variable. See [Alerts](alerts.md).
92
+
93
+ `et alert` automatically loads `.env` from the directory where you invoke it,
94
+ before resolving alert configuration. Existing environment variables take
95
+ precedence, including empty values. It does not search parent directories; a
96
+ missing `.env` is fine. Use `--no-dotenv` to disable loading.
97
+
98
+ ```dotenv
99
+ # .env
100
+ ET_LARK_WEBHOOK_URL=https://example.com/your-webhook
101
+ ```
102
+
103
+ Quoted values, comments, and `${VAR}` expansion are supported; shell commands are
104
+ not executed. Keep secrets out of version control. This loading applies only to
105
+ `et alert`, not other CLI commands or the Python API.
@@ -34,6 +34,7 @@ dependencies = [
34
34
  "click>=8.1.0",
35
35
  "loguru>=0.7.3",
36
36
  "pydantic>=2.0",
37
+ "python-dotenv>=1.0",
37
38
  ]
38
39
 
39
40
  dynamic = ["version"]
@@ -4,8 +4,10 @@ from __future__ import annotations
4
4
 
5
5
  import json
6
6
  import time
7
+ from pathlib import Path
7
8
 
8
9
  import click
10
+ from dotenv import load_dotenv
9
11
 
10
12
  from .alerts import alert as send_alert
11
13
  from .alerts.dispatch import Dispatcher
@@ -26,8 +28,21 @@ def main():
26
28
  @click.option("--title", default="Alert", help="Title of the alert")
27
29
  @click.option("--level", default="info", help="info | warning | error | critical")
28
30
  @click.option("--channel", "channels", multiple=True, help="Restrict to these channels")
29
- def alert(msg: str, title: str, level: str, channels: tuple[str, ...]):
30
- """Send a manual alert."""
31
+ @click.option(
32
+ "--no-dotenv", is_flag=True, help="Skip loading .env from the current directory"
33
+ )
34
+ def alert(
35
+ msg: str, title: str, level: str, channels: tuple[str, ...], no_dotenv: bool
36
+ ):
37
+ """Send a manual alert, loading the current directory's .env by default."""
38
+ if not no_dotenv:
39
+ try:
40
+ with Path(".env").open(encoding="utf-8") as stream:
41
+ load_dotenv(stream=stream, override=False)
42
+ except FileNotFoundError:
43
+ pass
44
+ except (OSError, UnicodeError) as exc:
45
+ raise click.ClickException(f"Could not read .env: {exc}") from exc
31
46
  send_alert(title=title, text=msg, level=level, channels=list(channels) or None)
32
47
 
33
48
 
@@ -1,6 +1,7 @@
1
1
  """The full CLI surface: every command, every format, and their error paths."""
2
2
 
3
3
  import json
4
+ import os
4
5
 
5
6
  import pytest
6
7
  from click.testing import CliRunner
@@ -10,7 +11,9 @@ from expr_tracker.history import HistoryStore
10
11
 
11
12
 
12
13
  @pytest.fixture
13
- def runner():
14
+ def runner(monkeypatch, tmp_path):
15
+ monkeypatch.chdir(tmp_path)
16
+ monkeypatch.setattr(os, "environ", os.environ.copy())
14
17
  return CliRunner()
15
18
 
16
19
 
@@ -237,6 +240,81 @@ def test_alert_without_a_message_fails(runner, channel):
237
240
  assert runner.invoke(cli.main, ["alert"]).exit_code != 0
238
241
 
239
242
 
243
+ def test_alert_loads_dotenv_before_resolving_channels(runner, monkeypatch, tmp_path):
244
+ from expr_tracker.alerts import load_config
245
+
246
+ monkeypatch.delenv("ET_LARK_WEBHOOK_URL", raising=False)
247
+ monkeypatch.delenv("ET_ALERT_CONFIG", raising=False)
248
+ (tmp_path / ".env").write_text(
249
+ '# Alert settings\nexport ET_LARK_WEBHOOK_URL="https://example.test/hook"\n',
250
+ encoding="utf-8",
251
+ )
252
+ configs = []
253
+ monkeypatch.setattr(cli, "send_alert", lambda **kwargs: configs.append(load_config()))
254
+
255
+ invoke(runner, "alert", "hello")
256
+
257
+ assert any(
258
+ c.type == "lark" and c.resolve_url() == "https://example.test/hook"
259
+ for c in configs[0].channels
260
+ )
261
+
262
+
263
+ @pytest.mark.parametrize("existing", ["from-shell", ""])
264
+ def test_alert_dotenv_preserves_existing_environment(
265
+ runner, channel, monkeypatch, tmp_path, existing
266
+ ):
267
+ monkeypatch.setenv("ET_TEST_DOTENV", existing)
268
+ (tmp_path / ".env").write_text("ET_TEST_DOTENV=from-file\n", encoding="utf-8")
269
+
270
+ invoke(runner, "alert", "hello")
271
+
272
+ assert os.environ["ET_TEST_DOTENV"] == existing
273
+ assert len(channel) == 1
274
+
275
+
276
+ def test_alert_does_not_search_parent_dotenv(runner, channel, monkeypatch, tmp_path):
277
+ monkeypatch.delenv("ET_TEST_DOTENV", raising=False)
278
+ (tmp_path / ".env").write_text("ET_TEST_DOTENV=parent\n", encoding="utf-8")
279
+ child = tmp_path / "child"
280
+ child.mkdir()
281
+ monkeypatch.chdir(child)
282
+
283
+ invoke(runner, "alert", "hello")
284
+
285
+ assert "ET_TEST_DOTENV" not in os.environ
286
+ assert len(channel) == 1
287
+
288
+
289
+ def test_alert_can_skip_dotenv(runner, channel, monkeypatch, tmp_path):
290
+ monkeypatch.delenv("ET_TEST_DOTENV", raising=False)
291
+ (tmp_path / ".env").write_text("ET_TEST_DOTENV=from-file\n", encoding="utf-8")
292
+
293
+ invoke(runner, "alert", "hello", "--no-dotenv")
294
+
295
+ assert "ET_TEST_DOTENV" not in os.environ
296
+ assert len(channel) == 1
297
+
298
+
299
+ def test_alert_reports_unreadable_dotenv(runner, channel, tmp_path):
300
+ (tmp_path / ".env").mkdir()
301
+
302
+ result = runner.invoke(cli.main, ["alert", "hello"])
303
+
304
+ assert result.exit_code != 0
305
+ assert "Could not read .env" in result.output
306
+ assert not channel
307
+
308
+
309
+ def test_other_commands_do_not_load_dotenv(runner, monkeypatch, tmp_path):
310
+ monkeypatch.delenv("ET_TEST_DOTENV", raising=False)
311
+ (tmp_path / ".env").write_text("ET_TEST_DOTENV=from-file\n", encoding="utf-8")
312
+
313
+ invoke(runner, "rules", "explain", "loss > 1")
314
+
315
+ assert "ET_TEST_DOTENV" not in os.environ
316
+
317
+
240
318
  # ------------------------------------------------------------------ rules
241
319
 
242
320
 
@@ -463,6 +463,7 @@ dependencies = [
463
463
  { name = "click" },
464
464
  { name = "loguru" },
465
465
  { name = "pydantic" },
466
+ { name = "python-dotenv" },
466
467
  ]
467
468
 
468
469
  [package.optional-dependencies]
@@ -517,6 +518,7 @@ requires-dist = [
517
518
  { name = "polars", marker = "extra == 'all'", specifier = ">=0.20" },
518
519
  { name = "polars", marker = "extra == 'polars'", specifier = ">=0.20" },
519
520
  { name = "pydantic", specifier = ">=2.0" },
521
+ { name = "python-dotenv", specifier = ">=1.0" },
520
522
  { name = "trackio", marker = "extra == 'all'", specifier = ">=0.4.0" },
521
523
  { name = "trackio", marker = "extra == 'trackio'", specifier = ">=0.4.0" },
522
524
  { name = "wandb", marker = "extra == 'all'", specifier = ">=0.21.0" },
File without changes
File without changes
File without changes
File without changes
File without changes