guck-sdk 0.1.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.
@@ -0,0 +1,7 @@
1
+ node_modules/
2
+ dist/
3
+ coverage/
4
+ logs/
5
+ .hunch.json
6
+ .hunch.local.json
7
+ .DS_Store
@@ -0,0 +1,50 @@
1
+ Metadata-Version: 2.4
2
+ Name: guck-sdk
3
+ Version: 0.1.0
4
+ Summary: MCP-first telemetry store for AI debugging (Python SDK)
5
+ Author: Guck
6
+ License: MIT
7
+ Keywords: mcp,observability,telemetry
8
+ Requires-Python: >=3.9
9
+ Provides-Extra: dev
10
+ Requires-Dist: pytest>=7.0; extra == 'dev'
11
+ Description-Content-Type: text/markdown
12
+
13
+ # Guck Python SDK
14
+
15
+ Guck is a tiny, MCP-first telemetry store for AI debugging. This package is the
16
+ Python SDK that mirrors the JS `emit` behavior.
17
+
18
+ ## Install
19
+
20
+ ```sh
21
+ pip install guck-sdk
22
+ ```
23
+
24
+ The distribution is `guck-sdk`, but the import name remains `guck`.
25
+
26
+ ## Install (local dev)
27
+
28
+ ```sh
29
+ uv pip install -e .
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ ```py
35
+ from guck import emit
36
+
37
+ emit({"message": "hello from python"})
38
+ ```
39
+
40
+ ## Config
41
+
42
+ The SDK reads `.guck.json` in your repo root and honors the same environment
43
+ variables as the JS SDK:
44
+
45
+ - `GUCK_CONFIG_PATH`
46
+ - `GUCK_DIR`
47
+ - `GUCK_ENABLED`
48
+ - `GUCK_SERVICE`
49
+ - `GUCK_SESSION_ID`
50
+ - `GUCK_RUN_ID`
@@ -0,0 +1,38 @@
1
+ # Guck Python SDK
2
+
3
+ Guck is a tiny, MCP-first telemetry store for AI debugging. This package is the
4
+ Python SDK that mirrors the JS `emit` behavior.
5
+
6
+ ## Install
7
+
8
+ ```sh
9
+ pip install guck-sdk
10
+ ```
11
+
12
+ The distribution is `guck-sdk`, but the import name remains `guck`.
13
+
14
+ ## Install (local dev)
15
+
16
+ ```sh
17
+ uv pip install -e .
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ```py
23
+ from guck import emit
24
+
25
+ emit({"message": "hello from python"})
26
+ ```
27
+
28
+ ## Config
29
+
30
+ The SDK reads `.guck.json` in your repo root and honors the same environment
31
+ variables as the JS SDK:
32
+
33
+ - `GUCK_CONFIG_PATH`
34
+ - `GUCK_DIR`
35
+ - `GUCK_ENABLED`
36
+ - `GUCK_SERVICE`
37
+ - `GUCK_SESSION_ID`
38
+ - `GUCK_RUN_ID`
@@ -0,0 +1,24 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.24.0"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "guck-sdk"
7
+ dynamic = ["version"]
8
+ description = "MCP-first telemetry store for AI debugging (Python SDK)"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ keywords = ["telemetry", "mcp", "observability"]
13
+ authors = [{ name = "Guck" }]
14
+ dependencies = []
15
+
16
+ [project.optional-dependencies]
17
+ dev = ["pytest>=7.0"]
18
+
19
+ [tool.hatch.version]
20
+ path = "../../VERSION"
21
+ pattern = "(?P<version>.+)"
22
+
23
+ [tool.hatch.build.targets.wheel]
24
+ packages = ["src/guck"]
@@ -0,0 +1,11 @@
1
+ from .emit import emit
2
+ from .schema import GuckConfig, GuckEvent, GuckLevel, GuckSource, GuckSourceKind
3
+
4
+ __all__ = [
5
+ "emit",
6
+ "GuckConfig",
7
+ "GuckEvent",
8
+ "GuckLevel",
9
+ "GuckSource",
10
+ "GuckSourceKind",
11
+ ]
@@ -0,0 +1,146 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+ from typing import Any, Dict, Optional, TypedDict
7
+
8
+ from .schema import GuckConfig
9
+
10
+ DEFAULT_CONFIG: GuckConfig = {
11
+ "version": 1,
12
+ "enabled": True,
13
+ "store_dir": "logs/guck",
14
+ "default_service": "guck",
15
+ "redaction": {
16
+ "enabled": True,
17
+ "keys": ["authorization", "api_key", "token", "secret", "password"],
18
+ "patterns": ["sk-[A-Za-z0-9]{20,}", "Bearer\\s+[A-Za-z0-9._-]+"],
19
+ },
20
+ "mcp": {
21
+ "max_results": 200,
22
+ "default_lookback_ms": 300000,
23
+ },
24
+ }
25
+
26
+
27
+ class LoadedConfig(TypedDict):
28
+ root_dir: str
29
+ config_path: Optional[str]
30
+ local_config_path: Optional[str]
31
+ config: GuckConfig
32
+
33
+
34
+ def _read_json_file(file_path: Path) -> Optional[Dict[str, Any]]:
35
+ try:
36
+ raw = file_path.read_text(encoding="utf-8")
37
+ parsed = json.loads(raw)
38
+ return parsed if isinstance(parsed, dict) else None
39
+ except Exception:
40
+ return None
41
+
42
+
43
+ def _is_dir_or_file(file_path: Path) -> bool:
44
+ try:
45
+ file_path.stat()
46
+ return True
47
+ except OSError:
48
+ return False
49
+
50
+
51
+ def find_repo_root(start_dir: str) -> str:
52
+ current = Path(start_dir).resolve()
53
+ while True:
54
+ if _is_dir_or_file(current / ".git"):
55
+ return str(current)
56
+ parent = current.parent
57
+ if parent == current:
58
+ return start_dir
59
+ current = parent
60
+
61
+
62
+ def _merge_config(base: GuckConfig, override: Dict[str, Any]) -> GuckConfig:
63
+ merged: GuckConfig = {
64
+ **base,
65
+ **override,
66
+ "redaction": {
67
+ **base["redaction"],
68
+ **(override.get("redaction") or {}),
69
+ },
70
+ "mcp": {
71
+ **base["mcp"],
72
+ **(override.get("mcp") or {}),
73
+ },
74
+ }
75
+ return merged
76
+
77
+
78
+ def _parse_bool(value: Optional[str]) -> Optional[bool]:
79
+ if value is None:
80
+ return None
81
+ normalized = value.strip().lower()
82
+ if normalized == "true":
83
+ return True
84
+ if normalized == "false":
85
+ return False
86
+ return None
87
+
88
+
89
+ def load_config(*, cwd: Optional[str] = None, config_path: Optional[str] = None) -> LoadedConfig:
90
+ working_dir = (
91
+ cwd
92
+ or os.environ.get("GUCK_CWD")
93
+ or os.environ.get("INIT_CWD")
94
+ or os.getcwd()
95
+ )
96
+ explicit_config = config_path or os.environ.get("GUCK_CONFIG") or os.environ.get(
97
+ "GUCK_CONFIG_PATH"
98
+ )
99
+ if explicit_config:
100
+ resolved_explicit = Path(explicit_config)
101
+ if not resolved_explicit.is_absolute():
102
+ resolved_explicit = Path(working_dir) / resolved_explicit
103
+ resolved_explicit = resolved_explicit.resolve()
104
+ if resolved_explicit.is_dir():
105
+ root_dir = str(resolved_explicit)
106
+ resolved_config_path = resolved_explicit / ".guck.json"
107
+ else:
108
+ root_dir = str(resolved_explicit.parent)
109
+ resolved_config_path = resolved_explicit
110
+ else:
111
+ root_dir = find_repo_root(working_dir)
112
+ resolved_config_path = Path(root_dir) / ".guck.json"
113
+ config_exists = _is_dir_or_file(resolved_config_path)
114
+ config_json = _read_json_file(resolved_config_path) if config_exists else None
115
+
116
+ config: GuckConfig = DEFAULT_CONFIG
117
+ if config_json:
118
+ config = _merge_config(config, config_json)
119
+
120
+ env_enabled = _parse_bool(os.environ.get("GUCK_ENABLED"))
121
+ if env_enabled is not None:
122
+ config = {**config, "enabled": env_enabled}
123
+
124
+ if os.environ.get("GUCK_DIR"):
125
+ config = {**config, "store_dir": os.environ["GUCK_DIR"]}
126
+
127
+ if os.environ.get("GUCK_SERVICE"):
128
+ config = {**config, "default_service": os.environ["GUCK_SERVICE"]}
129
+
130
+ return {
131
+ "root_dir": root_dir,
132
+ "config_path": str(resolved_config_path) if config_exists else None,
133
+ "local_config_path": None,
134
+ "config": config,
135
+ }
136
+
137
+
138
+ def resolve_store_dir(config: GuckConfig, root_dir: str) -> str:
139
+ store_dir = config["store_dir"]
140
+ if Path(store_dir).is_absolute():
141
+ return store_dir
142
+ return str(Path(root_dir) / store_dir)
143
+
144
+
145
+ def get_default_config() -> GuckConfig:
146
+ return DEFAULT_CONFIG
@@ -0,0 +1,85 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import uuid
5
+ from datetime import datetime, timezone
6
+ from typing import Any, Dict, Optional
7
+
8
+ from .config import load_config, resolve_store_dir
9
+ from .redact import redact_event
10
+ from .schema import GuckEvent, GuckLevel
11
+ from .store import append_event
12
+
13
+ _cached: Optional[Dict[str, Any]] = None
14
+
15
+ _DEFAULT_RUN_ID = os.environ.get("GUCK_RUN_ID") or str(uuid.uuid4())
16
+ _DEFAULT_SESSION_ID = os.environ.get("GUCK_SESSION_ID")
17
+
18
+
19
+ def _now_iso() -> str:
20
+ now = datetime.now(timezone.utc)
21
+ return now.isoformat(timespec="milliseconds").replace("+00:00", "Z")
22
+
23
+
24
+ def _normalize_level(level: Optional[str]) -> GuckLevel:
25
+ if not level:
26
+ return "info"
27
+ lower = level.lower()
28
+ if lower in {"trace", "debug", "info", "warn", "error", "fatal"}:
29
+ return lower # type: ignore[return-value]
30
+ return "info"
31
+
32
+
33
+ def _coalesce(input_event: Dict[str, Any], key: str, default: Any) -> Any:
34
+ if key in input_event and input_event[key] is not None:
35
+ return input_event[key]
36
+ return default
37
+
38
+
39
+ def _to_event(input_event: Dict[str, Any], defaults: Dict[str, str]) -> GuckEvent:
40
+ event: GuckEvent = {
41
+ "id": _coalesce(input_event, "id", str(uuid.uuid4())),
42
+ "ts": _coalesce(input_event, "ts", _now_iso()),
43
+ "level": _normalize_level(input_event.get("level")),
44
+ "type": _coalesce(input_event, "type", "log"),
45
+ "service": _coalesce(input_event, "service", defaults["service"]),
46
+ "run_id": _coalesce(input_event, "run_id", _DEFAULT_RUN_ID),
47
+ "source": _coalesce(input_event, "source", {"kind": "sdk"}),
48
+ }
49
+
50
+ session_id = _coalesce(input_event, "session_id", _DEFAULT_SESSION_ID)
51
+ if session_id is not None:
52
+ event["session_id"] = session_id
53
+
54
+ if "message" in input_event and input_event["message"] is not None:
55
+ event["message"] = input_event["message"]
56
+ if "data" in input_event and input_event["data"] is not None:
57
+ event["data"] = input_event["data"]
58
+ if "tags" in input_event and input_event["tags"] is not None:
59
+ event["tags"] = input_event["tags"]
60
+ if "trace_id" in input_event and input_event["trace_id"] is not None:
61
+ event["trace_id"] = input_event["trace_id"]
62
+ if "span_id" in input_event and input_event["span_id"] is not None:
63
+ event["span_id"] = input_event["span_id"]
64
+
65
+ return event
66
+
67
+
68
+ def _get_cached() -> Dict[str, Any]:
69
+ global _cached
70
+ if _cached is not None:
71
+ return _cached
72
+ loaded = load_config()
73
+ store_dir = resolve_store_dir(loaded["config"], loaded["root_dir"])
74
+ _cached = {"store_dir": store_dir, "config": loaded["config"]}
75
+ return _cached
76
+
77
+
78
+ def emit(input_event: Dict[str, Any]) -> None:
79
+ cached = _get_cached()
80
+ config = cached["config"]
81
+ if not config["enabled"]:
82
+ return
83
+ event = _to_event(input_event, {"service": config["default_service"]})
84
+ redacted = redact_event(config, event)
85
+ append_event(cached["store_dir"], redacted)
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import Any, Dict, Iterable
5
+
6
+ from .schema import GuckConfig, GuckEvent
7
+
8
+ _REDACTED_VALUE = "[REDACTED]"
9
+
10
+
11
+ def _normalize_key_set(keys: Iterable[str]) -> set[str]:
12
+ return {key.strip().lower() for key in keys if key.strip()}
13
+
14
+
15
+ def _compile_patterns(patterns: Iterable[str]) -> list[re.Pattern[str]]:
16
+ compiled: list[re.Pattern[str]] = []
17
+ for pattern in patterns:
18
+ try:
19
+ compiled.append(re.compile(pattern, re.IGNORECASE))
20
+ except re.error:
21
+ continue
22
+ return compiled
23
+
24
+
25
+ def _redact_string(value: str, patterns: Iterable[re.Pattern[str]]) -> str:
26
+ next_value = value
27
+ for pattern in patterns:
28
+ next_value = pattern.sub(_REDACTED_VALUE, next_value)
29
+ return next_value
30
+
31
+
32
+ def _redact_value(value: Any, key_set: set[str], patterns: list[re.Pattern[str]]) -> Any:
33
+ if value is None:
34
+ return value
35
+ if isinstance(value, str):
36
+ return _redact_string(value, patterns)
37
+ if isinstance(value, (int, float, bool)):
38
+ return value
39
+ if isinstance(value, list):
40
+ return [_redact_value(entry, key_set, patterns) for entry in value]
41
+ if isinstance(value, dict):
42
+ next_value: Dict[str, Any] = {}
43
+ for key, entry in value.items():
44
+ if key.lower() in key_set:
45
+ next_value[key] = _REDACTED_VALUE
46
+ else:
47
+ next_value[key] = _redact_value(entry, key_set, patterns)
48
+ return next_value
49
+ return value
50
+
51
+
52
+ def redact_event(config: GuckConfig, event: GuckEvent) -> GuckEvent:
53
+ if not config["redaction"]["enabled"]:
54
+ return event
55
+
56
+ key_set = _normalize_key_set(config["redaction"]["keys"])
57
+ patterns = _compile_patterns(config["redaction"]["patterns"])
58
+
59
+ redacted: GuckEvent = dict(event)
60
+
61
+ message = event.get("message")
62
+ if isinstance(message, str):
63
+ redacted["message"] = _redact_string(message, patterns)
64
+
65
+ data = event.get("data")
66
+ if data is not None:
67
+ redacted["data"] = _redact_value(data, key_set, patterns)
68
+
69
+ tags = event.get("tags")
70
+ if tags is not None:
71
+ redacted["tags"] = _redact_value(tags, key_set, patterns)
72
+
73
+ return redacted
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, Literal, TypedDict
4
+
5
+ GuckLevel = Literal["trace", "debug", "info", "warn", "error", "fatal"]
6
+
7
+ GuckSourceKind = Literal["sdk", "stdout", "stderr", "mcp"]
8
+
9
+
10
+ class GuckSource(TypedDict, total=False):
11
+ kind: GuckSourceKind
12
+ file: str
13
+ line: int
14
+
15
+
16
+ class GuckEvent(TypedDict, total=False):
17
+ id: str
18
+ ts: str
19
+ level: GuckLevel
20
+ type: str
21
+ service: str
22
+ run_id: str
23
+ session_id: str
24
+ message: str
25
+ data: Dict[str, Any]
26
+ tags: Dict[str, str]
27
+ trace_id: str
28
+ span_id: str
29
+ source: GuckSource
30
+
31
+
32
+ class GuckRedactionConfig(TypedDict):
33
+ enabled: bool
34
+ keys: list[str]
35
+ patterns: list[str]
36
+
37
+
38
+ class GuckMcpConfig(TypedDict):
39
+ max_results: int
40
+ default_lookback_ms: int
41
+
42
+
43
+ class GuckConfig(TypedDict):
44
+ version: int
45
+ enabled: bool
46
+ store_dir: str
47
+ default_service: str
48
+ redaction: GuckRedactionConfig
49
+ mcp: GuckMcpConfig
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from datetime import datetime, timezone
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from .schema import GuckEvent
9
+
10
+
11
+ def _parse_timestamp(value: Any) -> datetime:
12
+ if isinstance(value, str):
13
+ candidate = value.strip()
14
+ if candidate.endswith("Z"):
15
+ candidate = candidate[:-1] + "+00:00"
16
+ try:
17
+ parsed = datetime.fromisoformat(candidate)
18
+ if parsed.tzinfo is None:
19
+ parsed = parsed.replace(tzinfo=timezone.utc)
20
+ return parsed.astimezone(timezone.utc)
21
+ except ValueError:
22
+ pass
23
+ return datetime.now(timezone.utc)
24
+
25
+
26
+ def _format_date_segment(ts: datetime) -> str:
27
+ return ts.strftime("%Y-%m-%d")
28
+
29
+
30
+ def append_event(store_dir: str, event: GuckEvent) -> str:
31
+ ts = _parse_timestamp(event.get("ts"))
32
+ date_segment = _format_date_segment(ts)
33
+ file_dir = Path(store_dir) / event["service"] / date_segment
34
+ file_dir.mkdir(parents=True, exist_ok=True)
35
+ file_path = file_dir / f"{event['run_id']}.jsonl"
36
+ with file_path.open("a", encoding="utf-8") as handle:
37
+ handle.write(json.dumps(event, ensure_ascii=False) + "\n")
38
+ return str(file_path)
@@ -0,0 +1,43 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ from guck.config import load_config, resolve_store_dir
5
+
6
+
7
+ def _write_config(path: Path, store_dir: str = "logs/guck") -> None:
8
+ path.write_text(json.dumps({"store_dir": store_dir}, indent=2), encoding="utf-8")
9
+
10
+
11
+ def test_load_config_directory_path(tmp_path, monkeypatch):
12
+ monkeypatch.delenv("GUCK_CWD", raising=False)
13
+ monkeypatch.delenv("INIT_CWD", raising=False)
14
+ monkeypatch.delenv("GUCK_CONFIG", raising=False)
15
+
16
+ config_dir = tmp_path / "repo"
17
+ config_dir.mkdir()
18
+ config_path = config_dir / ".guck.json"
19
+ _write_config(config_path)
20
+
21
+ result = load_config(config_path=str(config_dir))
22
+ assert result["root_dir"] == str(config_dir)
23
+ assert resolve_store_dir(result["config"], result["root_dir"]) == str(
24
+ config_dir / "logs/guck"
25
+ )
26
+
27
+
28
+ def test_load_config_relative_path_uses_cwd(tmp_path, monkeypatch):
29
+ monkeypatch.delenv("GUCK_CWD", raising=False)
30
+ monkeypatch.delenv("INIT_CWD", raising=False)
31
+ monkeypatch.delenv("GUCK_CONFIG", raising=False)
32
+
33
+ base_dir = tmp_path / "root"
34
+ config_dir = base_dir / "config"
35
+ config_dir.mkdir(parents=True)
36
+ config_path = config_dir / ".guck.json"
37
+ _write_config(config_path)
38
+
39
+ result = load_config(cwd=str(base_dir), config_path="config/.guck.json")
40
+ assert result["root_dir"] == str(config_dir)
41
+ assert resolve_store_dir(result["config"], result["root_dir"]) == str(
42
+ config_dir / "logs/guck"
43
+ )
@@ -0,0 +1,99 @@
1
+ import importlib
2
+ import json
3
+ import os
4
+ import re
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ import pytest
10
+
11
+ PACKAGE_ROOT = Path(__file__).resolve().parents[1]
12
+ REPO_ROOT = PACKAGE_ROOT.parents[1]
13
+ SRC_ROOT = PACKAGE_ROOT / "src"
14
+ if str(SRC_ROOT) not in sys.path:
15
+ sys.path.insert(0, str(SRC_ROOT))
16
+
17
+ CASES = json.loads((REPO_ROOT / "specs" / "emit_cases.json").read_text(encoding="utf-8"))
18
+ UUID_RE = re.compile(
19
+ r"^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
20
+ re.IGNORECASE,
21
+ )
22
+
23
+
24
+ def _collect_jsonl_files(root: Path) -> list[Path]:
25
+ if not root.exists():
26
+ return []
27
+ return [path for path in root.rglob("*.jsonl") if path.is_file()]
28
+
29
+
30
+ def _format_date_segment(ts: str) -> Optional[str]:
31
+ try:
32
+ import datetime as dt
33
+
34
+ candidate = ts.strip()
35
+ if candidate.endswith("Z"):
36
+ candidate = candidate[:-1] + "+00:00"
37
+ parsed = dt.datetime.fromisoformat(candidate)
38
+ if parsed.tzinfo is None:
39
+ parsed = parsed.replace(tzinfo=dt.timezone.utc)
40
+ parsed = parsed.astimezone(dt.timezone.utc)
41
+ return parsed.strftime("%Y-%m-%d")
42
+ except Exception:
43
+ return None
44
+
45
+
46
+ def _assert_event_matches(event: dict, test_case: dict) -> None:
47
+ expected = test_case.get("expect", {})
48
+ for key, value in expected.items():
49
+ assert event.get(key) == value, f"expected {key} to match"
50
+
51
+ for key in test_case.get("expect_missing", []):
52
+ assert key not in event, f"expected {key} to be absent"
53
+
54
+ for key, rule in test_case.get("expect_regex", {}).items():
55
+ value = event.get(key)
56
+ assert isinstance(value, str), f"expected {key} to be a string"
57
+ if rule == "uuid":
58
+ assert UUID_RE.match(value), f"expected {key} to be uuid"
59
+ elif rule == "iso":
60
+ assert _format_date_segment(value) is not None, f"expected {key} to be ISO"
61
+
62
+
63
+ @pytest.mark.parametrize("test_case", CASES, ids=[case["name"] for case in CASES])
64
+ def test_emit_contract(test_case, tmp_path, monkeypatch):
65
+ config_path = tmp_path / ".guck.json"
66
+ config_path.write_text(json.dumps(test_case["config"], indent=2), encoding="utf-8")
67
+
68
+ for key, value in test_case.get("env", {}).items():
69
+ monkeypatch.setenv(key, value)
70
+ monkeypatch.setenv("GUCK_CONFIG_PATH", str(config_path))
71
+
72
+ emit_module = importlib.import_module("guck.emit")
73
+ importlib.reload(emit_module)
74
+ emit_module.emit(test_case.get("input", {}))
75
+
76
+ store_dir_value = test_case.get("expect_store_dir") or test_case["config"]["store_dir"]
77
+ if os.path.isabs(store_dir_value):
78
+ store_dir = Path(store_dir_value)
79
+ else:
80
+ store_dir = Path(config_path).parent / store_dir_value
81
+
82
+ if test_case.get("expect_no_write"):
83
+ files = _collect_jsonl_files(store_dir)
84
+ assert len(files) == 0, "expected no JSONL files"
85
+ return
86
+
87
+ files = _collect_jsonl_files(store_dir)
88
+ assert len(files) == 1, "expected one JSONL file"
89
+
90
+ content = files[0].read_text(encoding="utf-8").strip()
91
+ line = [entry for entry in content.splitlines() if entry][0]
92
+ event = json.loads(line)
93
+
94
+ _assert_event_matches(event, test_case)
95
+
96
+ date_segment = _format_date_segment(event["ts"])
97
+ assert date_segment is not None, "expected valid timestamp"
98
+ expected_path = store_dir / event["service"] / date_segment / f"{event['run_id']}.jsonl"
99
+ assert files[0] == expected_path
guck_sdk-0.1.0/uv.lock ADDED
@@ -0,0 +1,198 @@
1
+ version = 1
2
+ revision = 1
3
+ requires-python = ">=3.9"
4
+ resolution-markers = [
5
+ "python_full_version >= '3.10'",
6
+ "python_full_version < '3.10'",
7
+ ]
8
+
9
+ [[package]]
10
+ name = "colorama"
11
+ version = "0.4.6"
12
+ source = { registry = "https://pypi.org/simple" }
13
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
14
+ wheels = [
15
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
16
+ ]
17
+
18
+ [[package]]
19
+ name = "exceptiongroup"
20
+ version = "1.3.1"
21
+ source = { registry = "https://pypi.org/simple" }
22
+ dependencies = [
23
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
24
+ ]
25
+ sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371 }
26
+ wheels = [
27
+ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740 },
28
+ ]
29
+
30
+ [[package]]
31
+ name = "guck-sdk"
32
+ source = { editable = "." }
33
+
34
+ [package.optional-dependencies]
35
+ dev = [
36
+ { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
37
+ { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
38
+ ]
39
+
40
+ [package.metadata]
41
+ requires-dist = [{ name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0" }]
42
+ provides-extras = ["dev"]
43
+
44
+ [[package]]
45
+ name = "iniconfig"
46
+ version = "2.1.0"
47
+ source = { registry = "https://pypi.org/simple" }
48
+ resolution-markers = [
49
+ "python_full_version < '3.10'",
50
+ ]
51
+ sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 }
52
+ wheels = [
53
+ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 },
54
+ ]
55
+
56
+ [[package]]
57
+ name = "iniconfig"
58
+ version = "2.3.0"
59
+ source = { registry = "https://pypi.org/simple" }
60
+ resolution-markers = [
61
+ "python_full_version >= '3.10'",
62
+ ]
63
+ sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503 }
64
+ wheels = [
65
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484 },
66
+ ]
67
+
68
+ [[package]]
69
+ name = "packaging"
70
+ version = "26.0"
71
+ source = { registry = "https://pypi.org/simple" }
72
+ sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416 }
73
+ wheels = [
74
+ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366 },
75
+ ]
76
+
77
+ [[package]]
78
+ name = "pluggy"
79
+ version = "1.6.0"
80
+ source = { registry = "https://pypi.org/simple" }
81
+ sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 }
82
+ wheels = [
83
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 },
84
+ ]
85
+
86
+ [[package]]
87
+ name = "pygments"
88
+ version = "2.19.2"
89
+ source = { registry = "https://pypi.org/simple" }
90
+ sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631 }
91
+ wheels = [
92
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 },
93
+ ]
94
+
95
+ [[package]]
96
+ name = "pytest"
97
+ version = "8.4.2"
98
+ source = { registry = "https://pypi.org/simple" }
99
+ resolution-markers = [
100
+ "python_full_version < '3.10'",
101
+ ]
102
+ dependencies = [
103
+ { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" },
104
+ { name = "exceptiongroup", marker = "python_full_version < '3.10'" },
105
+ { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
106
+ { name = "packaging", marker = "python_full_version < '3.10'" },
107
+ { name = "pluggy", marker = "python_full_version < '3.10'" },
108
+ { name = "pygments", marker = "python_full_version < '3.10'" },
109
+ { name = "tomli", marker = "python_full_version < '3.10'" },
110
+ ]
111
+ sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618 }
112
+ wheels = [
113
+ { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750 },
114
+ ]
115
+
116
+ [[package]]
117
+ name = "pytest"
118
+ version = "9.0.2"
119
+ source = { registry = "https://pypi.org/simple" }
120
+ resolution-markers = [
121
+ "python_full_version >= '3.10'",
122
+ ]
123
+ dependencies = [
124
+ { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" },
125
+ { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" },
126
+ { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
127
+ { name = "packaging", marker = "python_full_version >= '3.10'" },
128
+ { name = "pluggy", marker = "python_full_version >= '3.10'" },
129
+ { name = "pygments", marker = "python_full_version >= '3.10'" },
130
+ { name = "tomli", marker = "python_full_version == '3.10.*'" },
131
+ ]
132
+ sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901 }
133
+ wheels = [
134
+ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801 },
135
+ ]
136
+
137
+ [[package]]
138
+ name = "tomli"
139
+ version = "2.4.0"
140
+ source = { registry = "https://pypi.org/simple" }
141
+ sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477 }
142
+ wheels = [
143
+ { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663 },
144
+ { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469 },
145
+ { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039 },
146
+ { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007 },
147
+ { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875 },
148
+ { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271 },
149
+ { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770 },
150
+ { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626 },
151
+ { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842 },
152
+ { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894 },
153
+ { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053 },
154
+ { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481 },
155
+ { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720 },
156
+ { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014 },
157
+ { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820 },
158
+ { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712 },
159
+ { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296 },
160
+ { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553 },
161
+ { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915 },
162
+ { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038 },
163
+ { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245 },
164
+ { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335 },
165
+ { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962 },
166
+ { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396 },
167
+ { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530 },
168
+ { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227 },
169
+ { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748 },
170
+ { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725 },
171
+ { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901 },
172
+ { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375 },
173
+ { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639 },
174
+ { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897 },
175
+ { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697 },
176
+ { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567 },
177
+ { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556 },
178
+ { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014 },
179
+ { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339 },
180
+ { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490 },
181
+ { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398 },
182
+ { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515 },
183
+ { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806 },
184
+ { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340 },
185
+ { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106 },
186
+ { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504 },
187
+ { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561 },
188
+ { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477 },
189
+ ]
190
+
191
+ [[package]]
192
+ name = "typing-extensions"
193
+ version = "4.15.0"
194
+ source = { registry = "https://pypi.org/simple" }
195
+ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 }
196
+ wheels = [
197
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 },
198
+ ]