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.
Files changed (164) hide show
  1. atif_analytics/__init__.py +9 -0
  2. atif_analytics/application/__init__.py +3 -0
  3. atif_analytics/application/analyze.py +158 -0
  4. atif_analytics/application/prompts.py +750 -0
  5. atif_analytics/application/use_cases/__init__.py +3 -0
  6. atif_analytics/application/use_cases/_shared.py +264 -0
  7. atif_analytics/application/use_cases/classify.py +430 -0
  8. atif_analytics/application/use_cases/conflicts.py +448 -0
  9. atif_analytics/application/use_cases/friction.py +634 -0
  10. atif_analytics/application/use_cases/perceived.py +451 -0
  11. atif_analytics/domain/__init__.py +3 -0
  12. atif_analytics/domain/authorship.py +166 -0
  13. atif_analytics/domain/config.py +30 -0
  14. atif_analytics/domain/costs.py +50 -0
  15. atif_analytics/domain/friction.py +117 -0
  16. atif_analytics/domain/layout.py +94 -0
  17. atif_analytics/domain/models.py +385 -0
  18. atif_analytics/domain/ports.py +100 -0
  19. atif_analytics/domain/transcript.py +359 -0
  20. atif_analytics/infrastructure/__init__.py +3 -0
  21. atif_analytics/infrastructure/corpus_reader.py +546 -0
  22. atif_analytics/infrastructure/parquet_cache.py +186 -0
  23. atif_analytics/infrastructure/settings.py +114 -0
  24. atif_analytics/infrastructure/sqlite_state/__init__.py +3 -0
  25. atif_analytics/infrastructure/sqlite_state/checkpointer.py +326 -0
  26. atif_analytics/infrastructure/sqlite_state/retry_queue.py +296 -0
  27. atif_analytics/py.typed +0 -0
  28. atif_cli/__init__.py +3 -0
  29. atif_cli/__main__.py +8 -0
  30. atif_cli/app.py +2459 -0
  31. atif_cli/converter_adapter.py +151 -0
  32. atif_cli/corpus.py +409 -0
  33. atif_cli/cron.py +311 -0
  34. atif_cli/duck_errors.py +97 -0
  35. atif_cli/embed_lake.py +101 -0
  36. atif_cli/errors.py +89 -0
  37. atif_cli/lake.py +373 -0
  38. atif_cli/lake_sessions.py +270 -0
  39. atif_cli/output.py +297 -0
  40. atif_cli/py.typed +0 -0
  41. atif_converter/__init__.py +3 -0
  42. atif_converter/application/__init__.py +3 -0
  43. atif_converter/application/convert_and_audit.py +294 -0
  44. atif_converter/application/convert_codex.py +210 -0
  45. atif_converter/domain/__init__.py +9 -0
  46. atif_converter/domain/agents.py +40 -0
  47. atif_converter/domain/atif/__init__.py +55 -0
  48. atif_converter/domain/atif/agent.py +37 -0
  49. atif_converter/domain/atif/content.py +161 -0
  50. atif_converter/domain/atif/final_metrics.py +45 -0
  51. atif_converter/domain/atif/metrics.py +49 -0
  52. atif_converter/domain/atif/observation.py +21 -0
  53. atif_converter/domain/atif/observation_result.py +47 -0
  54. atif_converter/domain/atif/step.py +144 -0
  55. atif_converter/domain/atif/subagent_trajectory_ref.py +90 -0
  56. atif_converter/domain/atif/tool_call.py +36 -0
  57. atif_converter/domain/atif/trajectory.py +211 -0
  58. atif_converter/domain/atif/trajectory_validator.py +299 -0
  59. atif_converter/domain/blobs.py +510 -0
  60. atif_converter/domain/claude_code_conversion.py +1077 -0
  61. atif_converter/domain/codex_conversion.py +1036 -0
  62. atif_converter/domain/codex_edges.py +192 -0
  63. atif_converter/domain/codex_enrichment.py +419 -0
  64. atif_converter/domain/codex_fidelity.py +124 -0
  65. atif_converter/domain/codex_nested_calls.py +165 -0
  66. atif_converter/domain/edges.py +113 -0
  67. atif_converter/domain/enrichment.py +412 -0
  68. atif_converter/domain/errors.py +60 -0
  69. atif_converter/domain/fidelity.py +171 -0
  70. atif_converter/domain/model_prices.json +8538 -0
  71. atif_converter/domain/pricing.py +790 -0
  72. atif_converter/domain/result_signals.py +774 -0
  73. atif_converter/domain/schema_version.py +90 -0
  74. atif_converter/domain/session_events.py +408 -0
  75. atif_converter/infrastructure/__init__.py +3 -0
  76. atif_converter/infrastructure/census.py +89 -0
  77. atif_converter/infrastructure/claude_code_converter.py +127 -0
  78. atif_converter/infrastructure/codex_adapter.py +132 -0
  79. atif_converter/infrastructure/codex_census.py +100 -0
  80. atif_converter/infrastructure/codex_converter.py +82 -0
  81. atif_converter/infrastructure/harbor_adapter.py +187 -0
  82. atif_converter/infrastructure/raw_records.py +426 -0
  83. atif_converter/infrastructure/source_archive.py +154 -0
  84. atif_converter/py.typed +0 -0
  85. atif_corpus/__init__.py +13 -0
  86. atif_corpus/application/__init__.py +7 -0
  87. atif_corpus/application/materialize.py +1826 -0
  88. atif_corpus/domain/__init__.py +9 -0
  89. atif_corpus/domain/agents.py +33 -0
  90. atif_corpus/domain/generation.py +34 -0
  91. atif_corpus/domain/layout.py +168 -0
  92. atif_corpus/domain/ports.py +175 -0
  93. atif_corpus/domain/session_id.py +68 -0
  94. atif_corpus/domain/sessions.py +228 -0
  95. atif_corpus/domain/slug.py +72 -0
  96. atif_corpus/domain/source_layout.py +143 -0
  97. atif_corpus/domain/watermark.py +76 -0
  98. atif_corpus/infrastructure/__init__.py +7 -0
  99. atif_corpus/infrastructure/atomic.py +266 -0
  100. atif_corpus/infrastructure/compress_artifacts.py +161 -0
  101. atif_corpus/infrastructure/fake_converter.py +174 -0
  102. atif_corpus/infrastructure/scanner.py +316 -0
  103. atif_corpus/infrastructure/settings.py +130 -0
  104. atif_corpus/infrastructure/source_archive.py +202 -0
  105. atif_corpus/py.typed +0 -0
  106. atif_duck/__init__.py +43 -0
  107. atif_duck/domain/__init__.py +32 -0
  108. atif_duck/domain/artifacts.py +66 -0
  109. atif_duck/domain/authorship.py +165 -0
  110. atif_duck/domain/catalog.py +626 -0
  111. atif_duck/domain/columnar.py +149 -0
  112. atif_duck/domain/embedding_guard.py +88 -0
  113. atif_duck/domain/examples.py +265 -0
  114. atif_duck/domain/lake.py +427 -0
  115. atif_duck/domain/lake_sessions.py +97 -0
  116. atif_duck/domain/raw_readers.py +140 -0
  117. atif_duck/domain/session_id.py +69 -0
  118. atif_duck/domain/sql_literal.py +50 -0
  119. atif_duck/infrastructure/__init__.py +34 -0
  120. atif_duck/infrastructure/analytics.py +469 -0
  121. atif_duck/infrastructure/authorship.py +139 -0
  122. atif_duck/infrastructure/columnar.py +641 -0
  123. atif_duck/infrastructure/lake.py +1383 -0
  124. atif_duck/infrastructure/lake_sessions.py +194 -0
  125. atif_duck/infrastructure/lake_settings.py +63 -0
  126. atif_duck/infrastructure/lake_steps.py +225 -0
  127. atif_duck/infrastructure/projections.py +221 -0
  128. atif_duck/infrastructure/registry.py +2194 -0
  129. atif_duck/infrastructure/stored_artifacts.py +115 -0
  130. atif_duck/py.typed +0 -0
  131. atif_embed/__init__.py +12 -0
  132. atif_embed/application/__init__.py +3 -0
  133. atif_embed/application/embed.py +293 -0
  134. atif_embed/application/prune.py +80 -0
  135. atif_embed/domain/__init__.py +3 -0
  136. atif_embed/domain/discovery.py +70 -0
  137. atif_embed/domain/embedding_guard.py +83 -0
  138. atif_embed/domain/errors.py +94 -0
  139. atif_embed/domain/ports.py +223 -0
  140. atif_embed/domain/sql_literal.py +49 -0
  141. atif_embed/domain/text_stamp.py +71 -0
  142. atif_embed/infrastructure/__init__.py +3 -0
  143. atif_embed/infrastructure/cohere_bedrock.py +433 -0
  144. atif_embed/infrastructure/corpus_text_rows.py +273 -0
  145. atif_embed/infrastructure/gemma_local.py +355 -0
  146. atif_embed/infrastructure/lake_text_rows.py +203 -0
  147. atif_embed/infrastructure/lance_store.py +440 -0
  148. atif_embed/infrastructure/providers.py +30 -0
  149. atif_embed/infrastructure/settings.py +154 -0
  150. atif_embed/py.typed +0 -0
  151. atif_models/__init__.py +14 -0
  152. atif_models/domain/__init__.py +9 -0
  153. atif_models/domain/ports.py +143 -0
  154. atif_models/domain/registry.py +151 -0
  155. atif_models/domain/schema.py +109 -0
  156. atif_models/infrastructure/__init__.py +8 -0
  157. atif_models/infrastructure/openai_bedrock.py +314 -0
  158. atif_models/infrastructure/settings.py +85 -0
  159. atif_models/py.typed +0 -0
  160. atif_sql-0.2.0.dist-info/METADATA +272 -0
  161. atif_sql-0.2.0.dist-info/RECORD +164 -0
  162. atif_sql-0.2.0.dist-info/WHEEL +4 -0
  163. atif_sql-0.2.0.dist-info/entry_points.txt +2 -0
  164. 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
+ ]
File without changes
atif_cli/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+
3
+ """atif-cli: the cyclopts CLI composing the atif-sql workspace."""
atif_cli/__main__.py ADDED
@@ -0,0 +1,8 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+
3
+ """``python -m atif_cli`` — same entry point as the ``atif-sql`` script."""
4
+
5
+ from atif_cli.app import main
6
+
7
+ if __name__ == "__main__":
8
+ main()