htmlgraph 0.26.25__py3-none-any.whl → 0.27.1__py3-none-any.whl
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.
- htmlgraph/__init__.py +23 -1
- htmlgraph/__init__.pyi +123 -0
- htmlgraph/agent_registry.py +2 -1
- htmlgraph/analytics/cli.py +3 -3
- htmlgraph/analytics/cost_analyzer.py +5 -1
- htmlgraph/analytics/cost_monitor.py +664 -0
- htmlgraph/analytics/cross_session.py +13 -9
- htmlgraph/analytics/dependency.py +10 -6
- htmlgraph/analytics/strategic/__init__.py +80 -0
- htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
- htmlgraph/analytics/strategic/pattern_detector.py +876 -0
- htmlgraph/analytics/strategic/preference_manager.py +709 -0
- htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
- htmlgraph/analytics/work_type.py +15 -11
- htmlgraph/analytics_index.py +2 -1
- htmlgraph/api/cost_alerts_websocket.py +416 -0
- htmlgraph/api/main.py +167 -62
- htmlgraph/api/websocket.py +538 -0
- htmlgraph/attribute_index.py +2 -1
- htmlgraph/builders/base.py +2 -1
- htmlgraph/builders/bug.py +2 -1
- htmlgraph/builders/chore.py +2 -1
- htmlgraph/builders/epic.py +2 -1
- htmlgraph/builders/feature.py +2 -1
- htmlgraph/builders/insight.py +2 -1
- htmlgraph/builders/metric.py +2 -1
- htmlgraph/builders/pattern.py +2 -1
- htmlgraph/builders/phase.py +2 -1
- htmlgraph/builders/spike.py +2 -1
- htmlgraph/builders/track.py +2 -1
- htmlgraph/cli/analytics.py +2 -1
- htmlgraph/cli/base.py +2 -1
- htmlgraph/cli/core.py +2 -1
- htmlgraph/cli/main.py +2 -1
- htmlgraph/cli/models.py +2 -1
- htmlgraph/cli/templates/cost_dashboard.py +2 -1
- htmlgraph/cli/work/__init__.py +2 -1
- htmlgraph/cli/work/browse.py +2 -1
- htmlgraph/cli/work/features.py +2 -1
- htmlgraph/cli/work/orchestration.py +2 -1
- htmlgraph/cli/work/report.py +2 -1
- htmlgraph/cli/work/sessions.py +2 -1
- htmlgraph/cli/work/snapshot.py +2 -1
- htmlgraph/cli/work/tracks.py +2 -1
- htmlgraph/collections/base.py +10 -5
- htmlgraph/collections/bug.py +2 -1
- htmlgraph/collections/chore.py +2 -1
- htmlgraph/collections/epic.py +2 -1
- htmlgraph/collections/feature.py +2 -1
- htmlgraph/collections/insight.py +2 -1
- htmlgraph/collections/metric.py +2 -1
- htmlgraph/collections/pattern.py +2 -1
- htmlgraph/collections/phase.py +2 -1
- htmlgraph/collections/session.py +12 -7
- htmlgraph/collections/spike.py +6 -1
- htmlgraph/collections/task_delegation.py +7 -2
- htmlgraph/collections/todo.py +2 -1
- htmlgraph/collections/traces.py +15 -10
- htmlgraph/config/cost_models.json +56 -0
- htmlgraph/context_analytics.py +2 -1
- htmlgraph/db/schema.py +67 -6
- htmlgraph/dependency_models.py +2 -1
- htmlgraph/edge_index.py +2 -1
- htmlgraph/event_log.py +83 -64
- htmlgraph/event_migration.py +2 -1
- htmlgraph/file_watcher.py +12 -8
- htmlgraph/find_api.py +2 -1
- htmlgraph/git_events.py +6 -2
- htmlgraph/hooks/cigs_pretool_enforcer.py +5 -1
- htmlgraph/hooks/drift_handler.py +3 -3
- htmlgraph/hooks/event_tracker.py +40 -61
- htmlgraph/hooks/installer.py +5 -1
- htmlgraph/hooks/orchestrator.py +4 -0
- htmlgraph/hooks/orchestrator_reflector.py +4 -0
- htmlgraph/hooks/post_tool_use_failure.py +7 -3
- htmlgraph/hooks/posttooluse.py +4 -0
- htmlgraph/hooks/prompt_analyzer.py +5 -5
- htmlgraph/hooks/session_handler.py +2 -1
- htmlgraph/hooks/session_summary.py +6 -2
- htmlgraph/hooks/validator.py +8 -4
- htmlgraph/ids.py +2 -1
- htmlgraph/learning.py +2 -1
- htmlgraph/mcp_server.py +2 -1
- htmlgraph/operations/analytics.py +2 -1
- htmlgraph/operations/bootstrap.py +2 -1
- htmlgraph/operations/events.py +2 -1
- htmlgraph/operations/fastapi_server.py +2 -1
- htmlgraph/operations/hooks.py +2 -1
- htmlgraph/operations/initialization.py +2 -1
- htmlgraph/operations/server.py +2 -1
- htmlgraph/orchestration/claude_launcher.py +23 -20
- htmlgraph/orchestration/command_builder.py +2 -1
- htmlgraph/orchestration/headless_spawner.py +6 -2
- htmlgraph/orchestration/model_selection.py +7 -3
- htmlgraph/orchestration/plugin_manager.py +24 -19
- htmlgraph/orchestration/spawners/claude.py +5 -2
- htmlgraph/orchestration/spawners/codex.py +12 -19
- htmlgraph/orchestration/spawners/copilot.py +13 -18
- htmlgraph/orchestration/spawners/gemini.py +12 -19
- htmlgraph/orchestration/subprocess_runner.py +6 -3
- htmlgraph/orchestration/task_coordination.py +16 -8
- htmlgraph/orchestrator.py +2 -1
- htmlgraph/parallel.py +2 -1
- htmlgraph/query_builder.py +2 -1
- htmlgraph/reflection.py +2 -1
- htmlgraph/refs.py +2 -1
- htmlgraph/repo_hash.py +2 -1
- htmlgraph/repositories/__init__.py +292 -0
- htmlgraph/repositories/analytics_repository.py +455 -0
- htmlgraph/repositories/analytics_repository_standard.py +628 -0
- htmlgraph/repositories/feature_repository.py +581 -0
- htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
- htmlgraph/repositories/feature_repository_memory.py +607 -0
- htmlgraph/repositories/feature_repository_sqlite.py +858 -0
- htmlgraph/repositories/filter_service.py +620 -0
- htmlgraph/repositories/filter_service_standard.py +445 -0
- htmlgraph/repositories/shared_cache.py +621 -0
- htmlgraph/repositories/shared_cache_memory.py +395 -0
- htmlgraph/repositories/track_repository.py +552 -0
- htmlgraph/repositories/track_repository_htmlfile.py +619 -0
- htmlgraph/repositories/track_repository_memory.py +508 -0
- htmlgraph/repositories/track_repository_sqlite.py +711 -0
- htmlgraph/sdk/__init__.py +398 -0
- htmlgraph/sdk/__init__.pyi +14 -0
- htmlgraph/sdk/analytics/__init__.py +19 -0
- htmlgraph/sdk/analytics/engine.py +155 -0
- htmlgraph/sdk/analytics/helpers.py +178 -0
- htmlgraph/sdk/analytics/registry.py +109 -0
- htmlgraph/sdk/base.py +484 -0
- htmlgraph/sdk/constants.py +216 -0
- htmlgraph/sdk/core.pyi +308 -0
- htmlgraph/sdk/discovery.py +120 -0
- htmlgraph/sdk/help/__init__.py +12 -0
- htmlgraph/sdk/help/mixin.py +699 -0
- htmlgraph/sdk/mixins/__init__.py +15 -0
- htmlgraph/sdk/mixins/attribution.py +113 -0
- htmlgraph/sdk/mixins/mixin.py +410 -0
- htmlgraph/sdk/operations/__init__.py +12 -0
- htmlgraph/sdk/operations/mixin.py +427 -0
- htmlgraph/sdk/orchestration/__init__.py +17 -0
- htmlgraph/sdk/orchestration/coordinator.py +203 -0
- htmlgraph/sdk/orchestration/spawner.py +204 -0
- htmlgraph/sdk/planning/__init__.py +19 -0
- htmlgraph/sdk/planning/bottlenecks.py +93 -0
- htmlgraph/sdk/planning/mixin.py +211 -0
- htmlgraph/sdk/planning/parallel.py +186 -0
- htmlgraph/sdk/planning/queue.py +210 -0
- htmlgraph/sdk/planning/recommendations.py +87 -0
- htmlgraph/sdk/planning/smart_planning.py +319 -0
- htmlgraph/sdk/session/__init__.py +19 -0
- htmlgraph/sdk/session/continuity.py +57 -0
- htmlgraph/sdk/session/handoff.py +110 -0
- htmlgraph/sdk/session/info.py +309 -0
- htmlgraph/sdk/session/manager.py +103 -0
- htmlgraph/sdk/strategic/__init__.py +26 -0
- htmlgraph/sdk/strategic/mixin.py +563 -0
- htmlgraph/server.py +21 -17
- htmlgraph/session_warning.py +2 -1
- htmlgraph/sessions/handoff.py +4 -3
- htmlgraph/system_prompts.py +2 -1
- htmlgraph/track_builder.py +2 -1
- htmlgraph/transcript.py +2 -1
- htmlgraph/watch.py +2 -1
- htmlgraph/work_type_utils.py +2 -1
- {htmlgraph-0.26.25.dist-info → htmlgraph-0.27.1.dist-info}/METADATA +1 -1
- htmlgraph-0.27.1.dist-info/RECORD +332 -0
- htmlgraph/sdk.py +0 -3500
- htmlgraph-0.26.25.dist-info/RECORD +0 -274
- {htmlgraph-0.26.25.data → htmlgraph-0.27.1.data}/data/htmlgraph/dashboard.html +0 -0
- {htmlgraph-0.26.25.data → htmlgraph-0.27.1.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.26.25.data → htmlgraph-0.27.1.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.26.25.data → htmlgraph-0.27.1.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.26.25.data → htmlgraph-0.27.1.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.26.25.dist-info → htmlgraph-0.27.1.dist-info}/WHEEL +0 -0
- {htmlgraph-0.26.25.dist-info → htmlgraph-0.27.1.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TrackRepository - Abstract interface for Track data access.
|
|
3
|
+
|
|
4
|
+
Unifies all data access patterns for Tracks across HtmlGraph.
|
|
5
|
+
Implementations handle:
|
|
6
|
+
- HTML file storage + SQLite database
|
|
7
|
+
- Lazy loading and caching
|
|
8
|
+
- Query building and filtering
|
|
9
|
+
- Concurrent access safety
|
|
10
|
+
- Event logging and session tracking
|
|
11
|
+
|
|
12
|
+
All implementations MUST pass TrackRepositoryComplianceTests.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import builtins
|
|
16
|
+
from abc import ABC, abstractmethod
|
|
17
|
+
from collections.abc import Callable
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class RepositoryQuery:
|
|
24
|
+
"""
|
|
25
|
+
Query builder for chaining filters.
|
|
26
|
+
|
|
27
|
+
Supports method chaining:
|
|
28
|
+
repo.where(status='active').where(priority='high').execute()
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
filters: dict[str, Any]
|
|
32
|
+
|
|
33
|
+
def execute(self) -> list[Any]:
|
|
34
|
+
"""Execute the query and return results."""
|
|
35
|
+
raise NotImplementedError("Subclass must implement")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TrackRepositoryError(Exception):
|
|
39
|
+
"""Base exception for repository operations."""
|
|
40
|
+
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TrackNotFoundError(TrackRepositoryError):
|
|
45
|
+
"""Raised when a track is not found."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, track_id: str):
|
|
48
|
+
self.track_id = track_id
|
|
49
|
+
super().__init__(f"Track not found: {track_id}")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class TrackValidationError(TrackRepositoryError):
|
|
53
|
+
"""Raised when track data fails validation."""
|
|
54
|
+
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class TrackConcurrencyError(TrackRepositoryError):
|
|
59
|
+
"""Raised when concurrent modification detected."""
|
|
60
|
+
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class TrackRepository(ABC):
|
|
65
|
+
"""
|
|
66
|
+
Abstract interface for Track data access.
|
|
67
|
+
|
|
68
|
+
Unifies access to Tracks stored in HTML files and SQLite database.
|
|
69
|
+
|
|
70
|
+
CONTRACT:
|
|
71
|
+
1. **Identity Invariant**: get(id) returns same object instance for same track
|
|
72
|
+
2. **Atomicity**: write operations are atomic (all-or-nothing)
|
|
73
|
+
3. **Consistency**: cache stays in sync with storage
|
|
74
|
+
4. **Isolation**: concurrent operations don't corrupt state
|
|
75
|
+
5. **Error Handling**: all errors preserve full context
|
|
76
|
+
|
|
77
|
+
CACHING BEHAVIOR:
|
|
78
|
+
- Single object instances per track (identity, not just equality)
|
|
79
|
+
- Automatic cache invalidation on writes
|
|
80
|
+
- Optional auto-load on first access
|
|
81
|
+
|
|
82
|
+
PERFORMANCE:
|
|
83
|
+
- get(id): O(1) cached, O(log n) uncached
|
|
84
|
+
- list(): O(n) where n = tracks
|
|
85
|
+
- where(**kwargs): O(n) with early termination
|
|
86
|
+
- batch_get(): O(k) where k = batch size
|
|
87
|
+
- batch_update(): O(k) vectorized
|
|
88
|
+
|
|
89
|
+
THREAD SAFETY:
|
|
90
|
+
- Implementations should be thread-safe
|
|
91
|
+
- Concurrent reads allowed
|
|
92
|
+
- Concurrent writes serialized (via database locks or explicit locking)
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
# ===== READ OPERATIONS =====
|
|
96
|
+
|
|
97
|
+
@abstractmethod
|
|
98
|
+
def get(self, track_id: str) -> Any | None:
|
|
99
|
+
"""
|
|
100
|
+
Get single track by ID.
|
|
101
|
+
|
|
102
|
+
Returns same object instance for multiple calls with same ID.
|
|
103
|
+
Implements identity caching (is, not ==).
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
track_id: Track ID to retrieve (e.g., "track-001")
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Track object if found, None if not found
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
ValueError: If track_id is invalid format
|
|
113
|
+
|
|
114
|
+
Performance: O(1) if cached, O(log n) if uncached
|
|
115
|
+
|
|
116
|
+
Examples:
|
|
117
|
+
>>> track = repo.get("track-001")
|
|
118
|
+
>>> track2 = repo.get("track-001")
|
|
119
|
+
>>> assert track is track2 # Identity, not just equality
|
|
120
|
+
>>> assert track is not None
|
|
121
|
+
"""
|
|
122
|
+
...
|
|
123
|
+
|
|
124
|
+
@abstractmethod
|
|
125
|
+
def list(self, filters: dict[str, Any] | None = None) -> list[Any]:
|
|
126
|
+
"""
|
|
127
|
+
List all tracks with optional filters.
|
|
128
|
+
|
|
129
|
+
Returns empty list if no matches, never None.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
filters: Optional dict of attribute->value filters.
|
|
133
|
+
Empty/None dict means no filters (returns all).
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
List of Track objects (empty list if no matches)
|
|
137
|
+
|
|
138
|
+
Raises:
|
|
139
|
+
TrackValidationError: If filter keys are invalid
|
|
140
|
+
|
|
141
|
+
Performance: O(n) where n = total tracks
|
|
142
|
+
|
|
143
|
+
Examples:
|
|
144
|
+
>>> all_tracks = repo.list()
|
|
145
|
+
>>> assert isinstance(all_tracks, list)
|
|
146
|
+
>>> active_tracks = repo.list({"status": "active"})
|
|
147
|
+
>>> multiple = repo.list({"status": "active", "priority": "high"})
|
|
148
|
+
"""
|
|
149
|
+
...
|
|
150
|
+
|
|
151
|
+
@abstractmethod
|
|
152
|
+
def where(self, **kwargs: Any) -> RepositoryQuery:
|
|
153
|
+
"""
|
|
154
|
+
Build a filtered query with chaining support.
|
|
155
|
+
|
|
156
|
+
Supports method chaining for composable queries:
|
|
157
|
+
repo.where(status='active').where(priority='high').execute()
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
**kwargs: Attribute->value filter pairs.
|
|
161
|
+
Common: status, priority, has_spec, has_plan
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
RepositoryQuery object that can be further filtered or executed
|
|
165
|
+
|
|
166
|
+
Raises:
|
|
167
|
+
TrackValidationError: If invalid attribute names
|
|
168
|
+
|
|
169
|
+
Examples:
|
|
170
|
+
>>> query = repo.where(status='active')
|
|
171
|
+
>>> query2 = query.where(priority='high') # Chaining
|
|
172
|
+
>>> results = query2.execute()
|
|
173
|
+
>>> assert all(t.status == 'active' for t in results)
|
|
174
|
+
>>> assert all(t.priority == 'high' for t in results)
|
|
175
|
+
"""
|
|
176
|
+
...
|
|
177
|
+
|
|
178
|
+
@abstractmethod
|
|
179
|
+
def by_status(self, status: str) -> builtins.list[Any]:
|
|
180
|
+
"""
|
|
181
|
+
Filter tracks by status.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
status: Status to filter by (e.g., 'planned', 'active', 'completed', 'abandoned')
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
List of matching tracks (empty if no matches)
|
|
188
|
+
|
|
189
|
+
Performance: O(n) with early termination
|
|
190
|
+
|
|
191
|
+
Examples:
|
|
192
|
+
>>> active_tracks = repo.by_status("active")
|
|
193
|
+
>>> completed = repo.by_status("completed")
|
|
194
|
+
"""
|
|
195
|
+
...
|
|
196
|
+
|
|
197
|
+
@abstractmethod
|
|
198
|
+
def by_priority(self, priority: str) -> builtins.list[Any]:
|
|
199
|
+
"""
|
|
200
|
+
Filter tracks by priority.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
priority: Priority level (e.g., 'low', 'medium', 'high', 'critical')
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
List of matching tracks
|
|
207
|
+
|
|
208
|
+
Performance: O(n)
|
|
209
|
+
|
|
210
|
+
Examples:
|
|
211
|
+
>>> critical = repo.by_priority("critical")
|
|
212
|
+
>>> important = repo.by_priority("high")
|
|
213
|
+
"""
|
|
214
|
+
...
|
|
215
|
+
|
|
216
|
+
@abstractmethod
|
|
217
|
+
def active_tracks(self) -> builtins.list[Any]:
|
|
218
|
+
"""
|
|
219
|
+
Get all tracks currently in progress.
|
|
220
|
+
|
|
221
|
+
Convenience method for status='active' filter.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
List of active tracks
|
|
225
|
+
|
|
226
|
+
Performance: O(n) with early termination
|
|
227
|
+
|
|
228
|
+
Examples:
|
|
229
|
+
>>> current_work = repo.active_tracks()
|
|
230
|
+
>>> assert all(t.status == 'active' for t in current_work)
|
|
231
|
+
"""
|
|
232
|
+
...
|
|
233
|
+
|
|
234
|
+
@abstractmethod
|
|
235
|
+
def batch_get(self, track_ids: builtins.list[str]) -> builtins.list[Any]:
|
|
236
|
+
"""
|
|
237
|
+
Bulk retrieve multiple tracks.
|
|
238
|
+
|
|
239
|
+
More efficient than multiple get() calls (vectorized).
|
|
240
|
+
Returns partial results if some tracks not found.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
track_ids: List of track IDs
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
List of found tracks (in order of input, with None for missing)
|
|
247
|
+
or list of only found tracks (implementation-dependent)
|
|
248
|
+
|
|
249
|
+
Raises:
|
|
250
|
+
ValueError: If track_ids is not a list
|
|
251
|
+
|
|
252
|
+
Performance: O(k) where k = batch size
|
|
253
|
+
|
|
254
|
+
Examples:
|
|
255
|
+
>>> ids = ["track-001", "track-002", "track-003"]
|
|
256
|
+
>>> tracks = repo.batch_get(ids)
|
|
257
|
+
>>> assert len(tracks) <= len(ids)
|
|
258
|
+
"""
|
|
259
|
+
...
|
|
260
|
+
|
|
261
|
+
# ===== WRITE OPERATIONS =====
|
|
262
|
+
|
|
263
|
+
@abstractmethod
|
|
264
|
+
def create(self, title: str, **kwargs: Any) -> Any:
|
|
265
|
+
"""
|
|
266
|
+
Create new track.
|
|
267
|
+
|
|
268
|
+
Generates ID if not provided.
|
|
269
|
+
Saves to storage immediately.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
title: Track title (required)
|
|
273
|
+
**kwargs: Additional properties (priority, status, description, etc.)
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
Created Track object (with generated ID)
|
|
277
|
+
|
|
278
|
+
Raises:
|
|
279
|
+
TrackValidationError: If invalid data provided
|
|
280
|
+
TrackRepositoryError: If create fails
|
|
281
|
+
|
|
282
|
+
Performance: O(1) cached write
|
|
283
|
+
|
|
284
|
+
Examples:
|
|
285
|
+
>>> track = repo.create("Planning Phase 1")
|
|
286
|
+
>>> assert track.id is not None
|
|
287
|
+
>>> track2 = repo.create("Feature Development", priority="high", status="active")
|
|
288
|
+
"""
|
|
289
|
+
...
|
|
290
|
+
|
|
291
|
+
@abstractmethod
|
|
292
|
+
def save(self, track: Any) -> Any:
|
|
293
|
+
"""
|
|
294
|
+
Save existing track (update or insert).
|
|
295
|
+
|
|
296
|
+
If track.id exists in repo, updates. Otherwise inserts.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
track: Track object to save
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
Saved track (same instance)
|
|
303
|
+
|
|
304
|
+
Raises:
|
|
305
|
+
TrackValidationError: If track is invalid
|
|
306
|
+
TrackConcurrencyError: If track was modified elsewhere
|
|
307
|
+
|
|
308
|
+
Performance: O(1)
|
|
309
|
+
|
|
310
|
+
Examples:
|
|
311
|
+
>>> track = repo.get("track-001")
|
|
312
|
+
>>> track.status = "completed"
|
|
313
|
+
>>> repo.save(track)
|
|
314
|
+
"""
|
|
315
|
+
...
|
|
316
|
+
|
|
317
|
+
@abstractmethod
|
|
318
|
+
def batch_update(
|
|
319
|
+
self, track_ids: builtins.list[str], updates: dict[str, Any]
|
|
320
|
+
) -> int:
|
|
321
|
+
"""
|
|
322
|
+
Vectorized batch update operation.
|
|
323
|
+
|
|
324
|
+
Updates all specified tracks with same values.
|
|
325
|
+
More efficient than individual saves.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
track_ids: List of track IDs to update
|
|
329
|
+
updates: Dict of attribute->value to set
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
Number of tracks successfully updated
|
|
333
|
+
|
|
334
|
+
Raises:
|
|
335
|
+
TrackValidationError: If invalid updates
|
|
336
|
+
|
|
337
|
+
Performance: O(k) vectorized where k = batch size
|
|
338
|
+
|
|
339
|
+
Examples:
|
|
340
|
+
>>> count = repo.batch_update(
|
|
341
|
+
... ["track-1", "track-2", "track-3"],
|
|
342
|
+
... {"status": "completed", "priority": "low"}
|
|
343
|
+
... )
|
|
344
|
+
>>> assert count == 3
|
|
345
|
+
"""
|
|
346
|
+
...
|
|
347
|
+
|
|
348
|
+
@abstractmethod
|
|
349
|
+
def delete(self, track_id: str) -> bool:
|
|
350
|
+
"""
|
|
351
|
+
Delete a track by ID.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
track_id: Track ID to delete
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
True if deleted, False if not found
|
|
358
|
+
|
|
359
|
+
Raises:
|
|
360
|
+
TrackValidationError: If track_id invalid
|
|
361
|
+
|
|
362
|
+
Performance: O(1) cache removal, O(log n) storage deletion
|
|
363
|
+
|
|
364
|
+
Examples:
|
|
365
|
+
>>> success = repo.delete("track-001")
|
|
366
|
+
>>> assert success is True or success is False
|
|
367
|
+
"""
|
|
368
|
+
...
|
|
369
|
+
|
|
370
|
+
@abstractmethod
|
|
371
|
+
def batch_delete(self, track_ids: builtins.list[str]) -> int:
|
|
372
|
+
"""
|
|
373
|
+
Delete multiple tracks.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
track_ids: List of track IDs to delete
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
Number of tracks successfully deleted
|
|
380
|
+
|
|
381
|
+
Raises:
|
|
382
|
+
ValueError: If track_ids not a list
|
|
383
|
+
|
|
384
|
+
Performance: O(k) where k = batch size
|
|
385
|
+
|
|
386
|
+
Examples:
|
|
387
|
+
>>> count = repo.batch_delete(["track-1", "track-2"])
|
|
388
|
+
>>> assert count == 2
|
|
389
|
+
"""
|
|
390
|
+
...
|
|
391
|
+
|
|
392
|
+
# ===== ADVANCED QUERIES =====
|
|
393
|
+
|
|
394
|
+
@abstractmethod
|
|
395
|
+
def find_by_features(self, feature_ids: builtins.list[str]) -> builtins.list[Any]:
|
|
396
|
+
"""
|
|
397
|
+
Find tracks containing any of the specified features.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
feature_ids: List of feature IDs to search for
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
Tracks that contain at least one of these features
|
|
404
|
+
|
|
405
|
+
Raises:
|
|
406
|
+
ValueError: If feature_ids is not a list
|
|
407
|
+
|
|
408
|
+
Performance: O(n) with early termination
|
|
409
|
+
|
|
410
|
+
Examples:
|
|
411
|
+
>>> features = ["feat-001", "feat-002"]
|
|
412
|
+
>>> tracks = repo.find_by_features(features)
|
|
413
|
+
>>> # Returns all tracks that contain feat-001 or feat-002
|
|
414
|
+
"""
|
|
415
|
+
...
|
|
416
|
+
|
|
417
|
+
@abstractmethod
|
|
418
|
+
def with_feature_count(self) -> builtins.list[Any]:
|
|
419
|
+
"""
|
|
420
|
+
Get all tracks with feature count calculated.
|
|
421
|
+
|
|
422
|
+
Convenience method for calculating feature counts across all tracks.
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
All tracks with feature_count property set
|
|
426
|
+
|
|
427
|
+
Examples:
|
|
428
|
+
>>> tracks = repo.with_feature_count()
|
|
429
|
+
>>> for t in tracks:
|
|
430
|
+
... print(f"{t.title}: {len(t.features)} features")
|
|
431
|
+
"""
|
|
432
|
+
...
|
|
433
|
+
|
|
434
|
+
@abstractmethod
|
|
435
|
+
def filter(self, predicate: Callable[[Any], bool]) -> builtins.list[Any]:
|
|
436
|
+
"""
|
|
437
|
+
Filter tracks with custom predicate function.
|
|
438
|
+
|
|
439
|
+
For complex queries not covered by standard filters.
|
|
440
|
+
|
|
441
|
+
Args:
|
|
442
|
+
predicate: Function that takes Track and returns True/False
|
|
443
|
+
|
|
444
|
+
Returns:
|
|
445
|
+
Tracks matching predicate
|
|
446
|
+
|
|
447
|
+
Examples:
|
|
448
|
+
>>> recent = repo.filter(
|
|
449
|
+
... lambda t: (datetime.now() - t.created).days < 7
|
|
450
|
+
... )
|
|
451
|
+
>>> with_spec_and_plan = repo.filter(
|
|
452
|
+
... lambda t: t.has_spec and t.has_plan
|
|
453
|
+
... )
|
|
454
|
+
"""
|
|
455
|
+
...
|
|
456
|
+
|
|
457
|
+
# ===== CACHE/LIFECYCLE MANAGEMENT =====
|
|
458
|
+
|
|
459
|
+
@abstractmethod
|
|
460
|
+
def invalidate_cache(self, track_id: str | None = None) -> None:
|
|
461
|
+
"""
|
|
462
|
+
Invalidate cache for single track or all tracks.
|
|
463
|
+
|
|
464
|
+
Forces reload from storage on next access.
|
|
465
|
+
Used when external process modifies storage.
|
|
466
|
+
|
|
467
|
+
Args:
|
|
468
|
+
track_id: Specific track to invalidate, or None for all
|
|
469
|
+
|
|
470
|
+
Examples:
|
|
471
|
+
>>> repo.invalidate_cache("track-001") # Single track
|
|
472
|
+
>>> repo.invalidate_cache() # Clear entire cache
|
|
473
|
+
"""
|
|
474
|
+
...
|
|
475
|
+
|
|
476
|
+
@abstractmethod
|
|
477
|
+
def reload(self) -> None:
|
|
478
|
+
"""
|
|
479
|
+
Force reload all tracks from storage.
|
|
480
|
+
|
|
481
|
+
Invalidates all caches and reloads from disk/database.
|
|
482
|
+
Useful for external changes or cache reconciliation.
|
|
483
|
+
|
|
484
|
+
Examples:
|
|
485
|
+
>>> repo.reload() # Force refresh from storage
|
|
486
|
+
"""
|
|
487
|
+
...
|
|
488
|
+
|
|
489
|
+
@property
|
|
490
|
+
@abstractmethod
|
|
491
|
+
def auto_load(self) -> bool:
|
|
492
|
+
"""
|
|
493
|
+
Whether auto-loading is enabled.
|
|
494
|
+
|
|
495
|
+
If True, tracks auto-load on first access.
|
|
496
|
+
If False, manual reload() required.
|
|
497
|
+
|
|
498
|
+
Returns:
|
|
499
|
+
True if auto-loading enabled, False otherwise
|
|
500
|
+
"""
|
|
501
|
+
...
|
|
502
|
+
|
|
503
|
+
@auto_load.setter
|
|
504
|
+
@abstractmethod
|
|
505
|
+
def auto_load(self, enabled: bool) -> None:
|
|
506
|
+
"""
|
|
507
|
+
Enable/disable auto-loading.
|
|
508
|
+
|
|
509
|
+
Args:
|
|
510
|
+
enabled: True to enable auto-load, False to disable
|
|
511
|
+
"""
|
|
512
|
+
...
|
|
513
|
+
|
|
514
|
+
# ===== UTILITY METHODS =====
|
|
515
|
+
|
|
516
|
+
@abstractmethod
|
|
517
|
+
def count(self, filters: dict[str, Any] | None = None) -> int:
|
|
518
|
+
"""
|
|
519
|
+
Count tracks matching filters.
|
|
520
|
+
|
|
521
|
+
Args:
|
|
522
|
+
filters: Optional filters (same as list())
|
|
523
|
+
|
|
524
|
+
Returns:
|
|
525
|
+
Number of matching tracks
|
|
526
|
+
|
|
527
|
+
Performance: O(n) or O(1) if optimized with SQL count
|
|
528
|
+
|
|
529
|
+
Examples:
|
|
530
|
+
>>> total = repo.count()
|
|
531
|
+
>>> active_count = repo.count({"status": "active"})
|
|
532
|
+
"""
|
|
533
|
+
...
|
|
534
|
+
|
|
535
|
+
@abstractmethod
|
|
536
|
+
def exists(self, track_id: str) -> bool:
|
|
537
|
+
"""
|
|
538
|
+
Check if track exists without loading it.
|
|
539
|
+
|
|
540
|
+
Args:
|
|
541
|
+
track_id: Track ID to check
|
|
542
|
+
|
|
543
|
+
Returns:
|
|
544
|
+
True if exists, False otherwise
|
|
545
|
+
|
|
546
|
+
Performance: O(1) if optimized
|
|
547
|
+
|
|
548
|
+
Examples:
|
|
549
|
+
>>> if repo.exists("track-001"):
|
|
550
|
+
... track = repo.get("track-001")
|
|
551
|
+
"""
|
|
552
|
+
...
|