atif-sql 0.2.0__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.
- atif_analytics/__init__.py +9 -0
- atif_analytics/application/__init__.py +3 -0
- atif_analytics/application/analyze.py +158 -0
- atif_analytics/application/prompts.py +750 -0
- atif_analytics/application/use_cases/__init__.py +3 -0
- atif_analytics/application/use_cases/_shared.py +264 -0
- atif_analytics/application/use_cases/classify.py +430 -0
- atif_analytics/application/use_cases/conflicts.py +448 -0
- atif_analytics/application/use_cases/friction.py +634 -0
- atif_analytics/application/use_cases/perceived.py +451 -0
- atif_analytics/domain/__init__.py +3 -0
- atif_analytics/domain/authorship.py +166 -0
- atif_analytics/domain/config.py +30 -0
- atif_analytics/domain/costs.py +50 -0
- atif_analytics/domain/friction.py +117 -0
- atif_analytics/domain/layout.py +94 -0
- atif_analytics/domain/models.py +385 -0
- atif_analytics/domain/ports.py +100 -0
- atif_analytics/domain/transcript.py +359 -0
- atif_analytics/infrastructure/__init__.py +3 -0
- atif_analytics/infrastructure/corpus_reader.py +546 -0
- atif_analytics/infrastructure/parquet_cache.py +186 -0
- atif_analytics/infrastructure/settings.py +114 -0
- atif_analytics/infrastructure/sqlite_state/__init__.py +3 -0
- atif_analytics/infrastructure/sqlite_state/checkpointer.py +326 -0
- atif_analytics/infrastructure/sqlite_state/retry_queue.py +296 -0
- atif_analytics/py.typed +0 -0
- atif_cli/__init__.py +3 -0
- atif_cli/__main__.py +8 -0
- atif_cli/app.py +2459 -0
- atif_cli/converter_adapter.py +151 -0
- atif_cli/corpus.py +409 -0
- atif_cli/cron.py +311 -0
- atif_cli/duck_errors.py +97 -0
- atif_cli/embed_lake.py +101 -0
- atif_cli/errors.py +89 -0
- atif_cli/lake.py +373 -0
- atif_cli/lake_sessions.py +270 -0
- atif_cli/output.py +297 -0
- atif_cli/py.typed +0 -0
- atif_converter/__init__.py +3 -0
- atif_converter/application/__init__.py +3 -0
- atif_converter/application/convert_and_audit.py +294 -0
- atif_converter/application/convert_codex.py +210 -0
- atif_converter/domain/__init__.py +9 -0
- atif_converter/domain/agents.py +40 -0
- atif_converter/domain/atif/__init__.py +55 -0
- atif_converter/domain/atif/agent.py +37 -0
- atif_converter/domain/atif/content.py +161 -0
- atif_converter/domain/atif/final_metrics.py +45 -0
- atif_converter/domain/atif/metrics.py +49 -0
- atif_converter/domain/atif/observation.py +21 -0
- atif_converter/domain/atif/observation_result.py +47 -0
- atif_converter/domain/atif/step.py +144 -0
- atif_converter/domain/atif/subagent_trajectory_ref.py +90 -0
- atif_converter/domain/atif/tool_call.py +36 -0
- atif_converter/domain/atif/trajectory.py +211 -0
- atif_converter/domain/atif/trajectory_validator.py +299 -0
- atif_converter/domain/blobs.py +510 -0
- atif_converter/domain/claude_code_conversion.py +1077 -0
- atif_converter/domain/codex_conversion.py +1036 -0
- atif_converter/domain/codex_edges.py +192 -0
- atif_converter/domain/codex_enrichment.py +419 -0
- atif_converter/domain/codex_fidelity.py +124 -0
- atif_converter/domain/codex_nested_calls.py +165 -0
- atif_converter/domain/edges.py +113 -0
- atif_converter/domain/enrichment.py +412 -0
- atif_converter/domain/errors.py +60 -0
- atif_converter/domain/fidelity.py +171 -0
- atif_converter/domain/model_prices.json +8538 -0
- atif_converter/domain/pricing.py +790 -0
- atif_converter/domain/result_signals.py +774 -0
- atif_converter/domain/schema_version.py +90 -0
- atif_converter/domain/session_events.py +408 -0
- atif_converter/infrastructure/__init__.py +3 -0
- atif_converter/infrastructure/census.py +89 -0
- atif_converter/infrastructure/claude_code_converter.py +127 -0
- atif_converter/infrastructure/codex_adapter.py +132 -0
- atif_converter/infrastructure/codex_census.py +100 -0
- atif_converter/infrastructure/codex_converter.py +82 -0
- atif_converter/infrastructure/harbor_adapter.py +187 -0
- atif_converter/infrastructure/raw_records.py +426 -0
- atif_converter/infrastructure/source_archive.py +154 -0
- atif_converter/py.typed +0 -0
- atif_corpus/__init__.py +13 -0
- atif_corpus/application/__init__.py +7 -0
- atif_corpus/application/materialize.py +1826 -0
- atif_corpus/domain/__init__.py +9 -0
- atif_corpus/domain/agents.py +33 -0
- atif_corpus/domain/generation.py +34 -0
- atif_corpus/domain/layout.py +168 -0
- atif_corpus/domain/ports.py +175 -0
- atif_corpus/domain/session_id.py +68 -0
- atif_corpus/domain/sessions.py +228 -0
- atif_corpus/domain/slug.py +72 -0
- atif_corpus/domain/source_layout.py +143 -0
- atif_corpus/domain/watermark.py +76 -0
- atif_corpus/infrastructure/__init__.py +7 -0
- atif_corpus/infrastructure/atomic.py +266 -0
- atif_corpus/infrastructure/compress_artifacts.py +161 -0
- atif_corpus/infrastructure/fake_converter.py +174 -0
- atif_corpus/infrastructure/scanner.py +316 -0
- atif_corpus/infrastructure/settings.py +130 -0
- atif_corpus/infrastructure/source_archive.py +202 -0
- atif_corpus/py.typed +0 -0
- atif_duck/__init__.py +43 -0
- atif_duck/domain/__init__.py +32 -0
- atif_duck/domain/artifacts.py +66 -0
- atif_duck/domain/authorship.py +165 -0
- atif_duck/domain/catalog.py +626 -0
- atif_duck/domain/columnar.py +149 -0
- atif_duck/domain/embedding_guard.py +88 -0
- atif_duck/domain/examples.py +265 -0
- atif_duck/domain/lake.py +427 -0
- atif_duck/domain/lake_sessions.py +97 -0
- atif_duck/domain/raw_readers.py +140 -0
- atif_duck/domain/session_id.py +69 -0
- atif_duck/domain/sql_literal.py +50 -0
- atif_duck/infrastructure/__init__.py +34 -0
- atif_duck/infrastructure/analytics.py +469 -0
- atif_duck/infrastructure/authorship.py +139 -0
- atif_duck/infrastructure/columnar.py +641 -0
- atif_duck/infrastructure/lake.py +1383 -0
- atif_duck/infrastructure/lake_sessions.py +194 -0
- atif_duck/infrastructure/lake_settings.py +63 -0
- atif_duck/infrastructure/lake_steps.py +225 -0
- atif_duck/infrastructure/projections.py +221 -0
- atif_duck/infrastructure/registry.py +2194 -0
- atif_duck/infrastructure/stored_artifacts.py +115 -0
- atif_duck/py.typed +0 -0
- atif_embed/__init__.py +12 -0
- atif_embed/application/__init__.py +3 -0
- atif_embed/application/embed.py +293 -0
- atif_embed/application/prune.py +80 -0
- atif_embed/domain/__init__.py +3 -0
- atif_embed/domain/discovery.py +70 -0
- atif_embed/domain/embedding_guard.py +83 -0
- atif_embed/domain/errors.py +94 -0
- atif_embed/domain/ports.py +223 -0
- atif_embed/domain/sql_literal.py +49 -0
- atif_embed/domain/text_stamp.py +71 -0
- atif_embed/infrastructure/__init__.py +3 -0
- atif_embed/infrastructure/cohere_bedrock.py +433 -0
- atif_embed/infrastructure/corpus_text_rows.py +273 -0
- atif_embed/infrastructure/gemma_local.py +355 -0
- atif_embed/infrastructure/lake_text_rows.py +203 -0
- atif_embed/infrastructure/lance_store.py +440 -0
- atif_embed/infrastructure/providers.py +30 -0
- atif_embed/infrastructure/settings.py +154 -0
- atif_embed/py.typed +0 -0
- atif_models/__init__.py +14 -0
- atif_models/domain/__init__.py +9 -0
- atif_models/domain/ports.py +143 -0
- atif_models/domain/registry.py +151 -0
- atif_models/domain/schema.py +109 -0
- atif_models/infrastructure/__init__.py +8 -0
- atif_models/infrastructure/openai_bedrock.py +314 -0
- atif_models/infrastructure/settings.py +85 -0
- atif_models/py.typed +0 -0
- atif_sql-0.2.0.dist-info/METADATA +272 -0
- atif_sql-0.2.0.dist-info/RECORD +164 -0
- atif_sql-0.2.0.dist-info/WHEEL +4 -0
- atif_sql-0.2.0.dist-info/entry_points.txt +2 -0
- atif_sql-0.2.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
"""Durable retry queue backed by SQLite WAL.
|
|
4
|
+
|
|
5
|
+
When an LLM call fails in a way that's worth retrying (parse failure,
|
|
6
|
+
throttle that outlived tenacity's budget, transient model error), the unit
|
|
7
|
+
of work gets enqueued here. A later run drains the queue before starting
|
|
8
|
+
fresh work, so a mid-run crash never costs the rows already paid for.
|
|
9
|
+
|
|
10
|
+
One row per ``(pipeline, unit_id)``. ``unit_id`` is ``session_id`` for
|
|
11
|
+
``classify`` / ``conflicts`` / ``perceived`` and the message ``uuid`` for
|
|
12
|
+
``user_friction``. Semantics are "upsert with attempt counter":
|
|
13
|
+
|
|
14
|
+
- First failure → insert with attempts=1, next_attempt_at = now + 2 min.
|
|
15
|
+
- Retry failure → attempts += 1, next_attempt_at = now + 2^attempts min (cap 60).
|
|
16
|
+
- Retry success → ``completed_at`` stamped; row stays as audit trail.
|
|
17
|
+
|
|
18
|
+
Lives in the same ``state.db`` as the checkpoint table.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import sqlite3
|
|
24
|
+
from collections.abc import Iterable
|
|
25
|
+
from datetime import UTC, datetime, timedelta
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
# The two bootstrap sentinels stay module-private in `checkpointer` and are
|
|
29
|
+
# reached for here on purpose: both modules open the SAME SQLite file, so the
|
|
30
|
+
# lock has to be the SAME object in both or the WAL transition and the two
|
|
31
|
+
# CREATEs race the writer lock again ("database is locked" on a cold start).
|
|
32
|
+
# The registry is keyed per-table (`retry::<path>` here), so sharing it does
|
|
33
|
+
# not let one module's bootstrap satisfy the other's.
|
|
34
|
+
from atif_analytics.infrastructure.sqlite_state.checkpointer import (
|
|
35
|
+
_SCHEMA_BOOTSTRAP_LOCK, # pyright: ignore[reportPrivateUsage]
|
|
36
|
+
_SCHEMA_BOOTSTRAPPED, # pyright: ignore[reportPrivateUsage]
|
|
37
|
+
PIPELINE_NAMES,
|
|
38
|
+
connect as _checkpoint_connect,
|
|
39
|
+
to_iso,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
MAX_ATTEMPTS_DEFAULT: int = 5
|
|
43
|
+
_BACKOFF_CAP_MIN: int = 60
|
|
44
|
+
|
|
45
|
+
_CREATE_TABLE_SQL = """
|
|
46
|
+
CREATE TABLE IF NOT EXISTS retry_queue (
|
|
47
|
+
pipeline TEXT NOT NULL,
|
|
48
|
+
unit_id TEXT NOT NULL,
|
|
49
|
+
error TEXT NOT NULL,
|
|
50
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
51
|
+
next_attempt_at TEXT NOT NULL,
|
|
52
|
+
created_at TEXT NOT NULL,
|
|
53
|
+
completed_at TEXT,
|
|
54
|
+
PRIMARY KEY (pipeline, unit_id)
|
|
55
|
+
);
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _connect(path: Path) -> sqlite3.Connection:
|
|
60
|
+
"""Open the queue DB, reusing the checkpointer connection helper.
|
|
61
|
+
|
|
62
|
+
Both tables live in the same SQLite file; a distinct bootstrap
|
|
63
|
+
sentinel keeps ``retry_queue``'s CREATE from re-running (and racing the
|
|
64
|
+
writer lock) on every open.
|
|
65
|
+
"""
|
|
66
|
+
con = _checkpoint_connect(path)
|
|
67
|
+
key = f"retry::{path.resolve()}"
|
|
68
|
+
with _SCHEMA_BOOTSTRAP_LOCK:
|
|
69
|
+
if key not in _SCHEMA_BOOTSTRAPPED:
|
|
70
|
+
prior = con.isolation_level
|
|
71
|
+
con.isolation_level = None # autocommit for DDL
|
|
72
|
+
con.execute(_CREATE_TABLE_SQL)
|
|
73
|
+
con.isolation_level = prior
|
|
74
|
+
_SCHEMA_BOOTSTRAPPED.add(key)
|
|
75
|
+
return con
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _backoff_delta(attempts: int) -> timedelta:
|
|
79
|
+
"""Exponential backoff in minutes: 2, 4, 8, 16, 32, capped at 60."""
|
|
80
|
+
minutes = min(2**attempts, _BACKOFF_CAP_MIN)
|
|
81
|
+
return timedelta(minutes=minutes)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def enqueue(
|
|
85
|
+
db_path: Path,
|
|
86
|
+
*,
|
|
87
|
+
pipeline: str,
|
|
88
|
+
unit_id: str,
|
|
89
|
+
error: str,
|
|
90
|
+
now: datetime | None = None,
|
|
91
|
+
) -> int:
|
|
92
|
+
"""Record a failure. Increments ``attempts`` on repeat calls.
|
|
93
|
+
|
|
94
|
+
Returns the resulting ``attempts`` value for logging.
|
|
95
|
+
"""
|
|
96
|
+
if pipeline not in PIPELINE_NAMES:
|
|
97
|
+
msg = f"unknown pipeline: {pipeline!r}"
|
|
98
|
+
raise ValueError(msg)
|
|
99
|
+
cur = now or datetime.now(UTC)
|
|
100
|
+
cur_iso = to_iso(cur)
|
|
101
|
+
con = _connect(db_path)
|
|
102
|
+
prior_isolation = con.isolation_level
|
|
103
|
+
# Explicit transaction control below; the implicit DEFERRED wrapper cannot
|
|
104
|
+
# express "take the writer lock before the read".
|
|
105
|
+
con.isolation_level = None
|
|
106
|
+
try:
|
|
107
|
+
# BEGIN IMMEDIATE, not DEFERRED: the counter is a read-modify-write, so
|
|
108
|
+
# two concurrent enqueues of the SAME (pipeline, unit_id) must not both
|
|
109
|
+
# read `attempts` = N and both write N + 1. Under-counting here does not
|
|
110
|
+
# lose a row, it raises the ceiling `max_attempts` puts on Bedrock
|
|
111
|
+
# spend for a unit that keeps failing. DEFERRED would start read-only
|
|
112
|
+
# and upgrade mid-transaction, which is exactly the window that lets
|
|
113
|
+
# both sides read the pre-increment value.
|
|
114
|
+
con.execute("BEGIN IMMEDIATE")
|
|
115
|
+
row = con.execute(
|
|
116
|
+
"SELECT attempts FROM retry_queue WHERE pipeline = ? AND unit_id = ?",
|
|
117
|
+
[pipeline, unit_id],
|
|
118
|
+
).fetchone()
|
|
119
|
+
prev = int(row[0]) if row else 0
|
|
120
|
+
attempts = prev + 1
|
|
121
|
+
# The backoff stays in Python (`_backoff_delta`) rather than becoming
|
|
122
|
+
# SQLite date arithmetic, so the schedule has one definition.
|
|
123
|
+
next_at_iso = to_iso(cur + _backoff_delta(attempts))
|
|
124
|
+
con.execute(
|
|
125
|
+
"INSERT INTO retry_queue "
|
|
126
|
+
"(pipeline, unit_id, error, attempts, next_attempt_at, created_at, completed_at) "
|
|
127
|
+
"VALUES (?, ?, ?, ?, ?, ?, NULL) "
|
|
128
|
+
"ON CONFLICT(pipeline, unit_id) DO UPDATE SET "
|
|
129
|
+
"error = excluded.error, "
|
|
130
|
+
"attempts = excluded.attempts, "
|
|
131
|
+
"next_attempt_at = excluded.next_attempt_at, "
|
|
132
|
+
"created_at = excluded.created_at, "
|
|
133
|
+
"completed_at = NULL",
|
|
134
|
+
[pipeline, unit_id, error[:2000], attempts, next_at_iso, cur_iso],
|
|
135
|
+
)
|
|
136
|
+
con.execute("COMMIT")
|
|
137
|
+
finally:
|
|
138
|
+
# Closing an open transaction rolls it back, so a raise between BEGIN
|
|
139
|
+
# and COMMIT leaves the counter untouched rather than half-applied.
|
|
140
|
+
con.isolation_level = prior_isolation
|
|
141
|
+
con.close()
|
|
142
|
+
return attempts
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def drain(
|
|
146
|
+
db_path: Path,
|
|
147
|
+
*,
|
|
148
|
+
pipeline: str,
|
|
149
|
+
now: datetime | None = None,
|
|
150
|
+
max_attempts: int = MAX_ATTEMPTS_DEFAULT,
|
|
151
|
+
limit: int | None = None,
|
|
152
|
+
) -> list[str]:
|
|
153
|
+
"""Return unit_ids eligible for retry (not completed, attempts<max, due now)."""
|
|
154
|
+
if not db_path.exists():
|
|
155
|
+
return []
|
|
156
|
+
cur = now or datetime.now(UTC)
|
|
157
|
+
cur_iso = to_iso(cur)
|
|
158
|
+
con = _connect(db_path)
|
|
159
|
+
sql = (
|
|
160
|
+
"SELECT unit_id FROM retry_queue "
|
|
161
|
+
"WHERE pipeline = ? AND completed_at IS NULL "
|
|
162
|
+
" AND attempts < ? AND next_attempt_at <= ? "
|
|
163
|
+
"ORDER BY next_attempt_at"
|
|
164
|
+
)
|
|
165
|
+
params: list[object] = [pipeline, max_attempts, cur_iso]
|
|
166
|
+
if limit is not None:
|
|
167
|
+
sql += " LIMIT ?"
|
|
168
|
+
params.append(int(limit))
|
|
169
|
+
try:
|
|
170
|
+
rows = con.execute(sql, params).fetchall()
|
|
171
|
+
finally:
|
|
172
|
+
con.close()
|
|
173
|
+
return [str(r[0]) for r in rows]
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def blocked_units(
|
|
177
|
+
db_path: Path,
|
|
178
|
+
*,
|
|
179
|
+
pipeline: str,
|
|
180
|
+
now: datetime | None = None,
|
|
181
|
+
max_attempts: int = MAX_ATTEMPTS_DEFAULT,
|
|
182
|
+
) -> set[str]:
|
|
183
|
+
"""Unit_ids with a LIVE retry entry that must NOT be dispatched this run.
|
|
184
|
+
|
|
185
|
+
Live = not completed, and either the attempts budget is exhausted
|
|
186
|
+
(``attempts >= max_attempts``) or the backoff window has not elapsed
|
|
187
|
+
(``next_attempt_at`` in the future). This is the complement of
|
|
188
|
+
:func:`drain` over live rows — together they make the retry queue the
|
|
189
|
+
single re-admission gate: a failed-and-live unit re-enters ONLY through
|
|
190
|
+
``drain``, never through the checkpoint path (a failed unit is not
|
|
191
|
+
checkpointed, so ``filter_unchanged`` would otherwise re-admit it fresh
|
|
192
|
+
on every run, forever — the uncapped-retry money leak).
|
|
193
|
+
"""
|
|
194
|
+
if not db_path.exists():
|
|
195
|
+
return set()
|
|
196
|
+
cur_iso = to_iso(now or datetime.now(UTC))
|
|
197
|
+
con = _connect(db_path)
|
|
198
|
+
try:
|
|
199
|
+
rows = con.execute(
|
|
200
|
+
"SELECT unit_id FROM retry_queue "
|
|
201
|
+
"WHERE pipeline = ? AND completed_at IS NULL "
|
|
202
|
+
" AND (attempts >= ? OR next_attempt_at > ?)",
|
|
203
|
+
[pipeline, max_attempts, cur_iso],
|
|
204
|
+
).fetchall()
|
|
205
|
+
finally:
|
|
206
|
+
con.close()
|
|
207
|
+
return {str(r[0]) for r in rows}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def mark_done(
|
|
211
|
+
db_path: Path,
|
|
212
|
+
*,
|
|
213
|
+
pipeline: str,
|
|
214
|
+
unit_ids: Iterable[str],
|
|
215
|
+
now: datetime | None = None,
|
|
216
|
+
) -> int:
|
|
217
|
+
"""Mark the given unit_ids as completed. No-op if unknown."""
|
|
218
|
+
ids = list(unit_ids)
|
|
219
|
+
if not ids:
|
|
220
|
+
return 0
|
|
221
|
+
cur_iso = to_iso(now or datetime.now(UTC))
|
|
222
|
+
con = _connect(db_path)
|
|
223
|
+
try:
|
|
224
|
+
con.executemany(
|
|
225
|
+
"UPDATE retry_queue SET completed_at = ? "
|
|
226
|
+
"WHERE pipeline = ? AND unit_id = ? AND completed_at IS NULL",
|
|
227
|
+
[(cur_iso, pipeline, uid) for uid in ids],
|
|
228
|
+
)
|
|
229
|
+
con.commit()
|
|
230
|
+
finally:
|
|
231
|
+
con.close()
|
|
232
|
+
return len(ids)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def pending_count(db_path: Path, *, pipeline: str) -> int:
|
|
236
|
+
"""Count not-yet-completed rows for one pipeline."""
|
|
237
|
+
if not db_path.exists():
|
|
238
|
+
return 0
|
|
239
|
+
con = _connect(db_path)
|
|
240
|
+
try:
|
|
241
|
+
row = con.execute(
|
|
242
|
+
"SELECT count(*) FROM retry_queue WHERE pipeline = ? AND completed_at IS NULL",
|
|
243
|
+
[pipeline],
|
|
244
|
+
).fetchone()
|
|
245
|
+
finally:
|
|
246
|
+
con.close()
|
|
247
|
+
return int(row[0]) if row else 0
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class SqliteRetryQueue:
|
|
251
|
+
"""The retry-queue port over one ``state.db``."""
|
|
252
|
+
|
|
253
|
+
def __init__(self, db_path: Path) -> None:
|
|
254
|
+
self._db_path = db_path
|
|
255
|
+
|
|
256
|
+
@property
|
|
257
|
+
def db_path(self) -> Path:
|
|
258
|
+
"""The bound sqlite file."""
|
|
259
|
+
return self._db_path
|
|
260
|
+
|
|
261
|
+
def enqueue(self, *, pipeline: str, unit_id: str, error: str) -> int:
|
|
262
|
+
"""Record a failure; return the attempt counter."""
|
|
263
|
+
return enqueue(self._db_path, pipeline=pipeline, unit_id=unit_id, error=error)
|
|
264
|
+
|
|
265
|
+
def drain(
|
|
266
|
+
self,
|
|
267
|
+
*,
|
|
268
|
+
pipeline: str,
|
|
269
|
+
max_attempts: int = MAX_ATTEMPTS_DEFAULT,
|
|
270
|
+
limit: int | None = None,
|
|
271
|
+
) -> list[str]:
|
|
272
|
+
"""Unit_ids eligible for retry (not done, attempts<max, due)."""
|
|
273
|
+
return drain(self._db_path, pipeline=pipeline, max_attempts=max_attempts, limit=limit)
|
|
274
|
+
|
|
275
|
+
def blocked_units(self, *, pipeline: str, max_attempts: int = MAX_ATTEMPTS_DEFAULT) -> set[str]:
|
|
276
|
+
"""Unit_ids with a live entry that must not be dispatched this run."""
|
|
277
|
+
return blocked_units(self._db_path, pipeline=pipeline, max_attempts=max_attempts)
|
|
278
|
+
|
|
279
|
+
def mark_done(self, *, pipeline: str, unit_ids: Iterable[str]) -> int:
|
|
280
|
+
"""Mark unit_ids completed; return the count marked."""
|
|
281
|
+
return mark_done(self._db_path, pipeline=pipeline, unit_ids=unit_ids)
|
|
282
|
+
|
|
283
|
+
def pending_count(self, *, pipeline: str) -> int:
|
|
284
|
+
"""Count not-yet-completed rows for one pipeline."""
|
|
285
|
+
return pending_count(self._db_path, pipeline=pipeline)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
__all__ = [
|
|
289
|
+
"MAX_ATTEMPTS_DEFAULT",
|
|
290
|
+
"SqliteRetryQueue",
|
|
291
|
+
"blocked_units",
|
|
292
|
+
"drain",
|
|
293
|
+
"enqueue",
|
|
294
|
+
"mark_done",
|
|
295
|
+
"pending_count",
|
|
296
|
+
]
|
atif_analytics/py.typed
ADDED
|
File without changes
|
atif_cli/__init__.py
ADDED