prompture 0.0.39.dev1__py3-none-any.whl → 0.0.40__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.
prompture/__init__.py CHANGED
@@ -110,6 +110,7 @@ from .image import (
110
110
  image_from_url,
111
111
  make_image,
112
112
  )
113
+ from .ledger import ModelUsageLedger, get_recently_used_models
113
114
  from .logging import JSONFormatter, configure_logging
114
115
  from .model_rates import (
115
116
  ModelCapabilities,
@@ -221,6 +222,7 @@ __all__ = [
221
222
  "MemoryCacheBackend",
222
223
  "ModelCapabilities",
223
224
  "ModelRetry",
225
+ "ModelUsageLedger",
224
226
  "OllamaDriver",
225
227
  "OpenAIDriver",
226
228
  "OpenRouterDriver",
@@ -268,6 +270,7 @@ __all__ = [
268
270
  "get_persona",
269
271
  "get_persona_names",
270
272
  "get_persona_registry_snapshot",
273
+ "get_recently_used_models",
271
274
  "get_registry_snapshot",
272
275
  "get_required_fields",
273
276
  "get_trait",
prompture/_version.py CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.0.39.dev1'
32
- __version_tuple__ = version_tuple = (0, 0, 39, 'dev1')
31
+ __version__ = version = '0.0.40'
32
+ __version_tuple__ = version_tuple = (0, 0, 40)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -304,6 +304,15 @@ class AsyncConversation:
304
304
  self._usage["turns"] += 1
305
305
  self._maybe_auto_save()
306
306
 
307
+ from .ledger import _resolve_api_key_hash, record_model_usage
308
+
309
+ record_model_usage(
310
+ self._model_name,
311
+ api_key_hash=_resolve_api_key_hash(self._model_name),
312
+ tokens=meta.get("total_tokens", 0),
313
+ cost=meta.get("cost", 0.0),
314
+ )
315
+
307
316
  async def ask(
308
317
  self,
309
318
  content: str,
prompture/async_core.py CHANGED
@@ -35,6 +35,18 @@ from .tools import (
35
35
  logger = logging.getLogger("prompture.async_core")
36
36
 
37
37
 
38
+ def _record_usage_to_ledger(model_name: str, meta: dict[str, Any]) -> None:
39
+ """Fire-and-forget ledger recording for standalone async core functions."""
40
+ from .ledger import _resolve_api_key_hash, record_model_usage
41
+
42
+ record_model_usage(
43
+ model_name,
44
+ api_key_hash=_resolve_api_key_hash(model_name),
45
+ tokens=meta.get("total_tokens", 0),
46
+ cost=meta.get("cost", 0.0),
47
+ )
48
+
49
+
38
50
  async def clean_json_text_with_ai(
39
51
  driver: AsyncDriver, text: str, model_name: str = "", options: dict[str, Any] | None = None
40
52
  ) -> str:
@@ -117,6 +129,8 @@ async def render_output(
117
129
  "model_name": model_name or getattr(driver, "model", ""),
118
130
  }
119
131
 
132
+ _record_usage_to_ledger(model_name, resp.get("meta", {}))
133
+
120
134
  return {"text": raw, "usage": usage, "output_format": output_format}
121
135
 
122
136
 
@@ -211,6 +225,8 @@ async def ask_for_json(
211
225
  raw = resp.get("text", "")
212
226
  cleaned = clean_json_text(raw)
213
227
 
228
+ _record_usage_to_ledger(model_name, resp.get("meta", {}))
229
+
214
230
  try:
215
231
  json_obj = json.loads(cleaned)
216
232
  json_string = cleaned
prompture/conversation.py CHANGED
@@ -311,6 +311,15 @@ class Conversation:
311
311
  self._usage["turns"] += 1
312
312
  self._maybe_auto_save()
313
313
 
314
+ from .ledger import _resolve_api_key_hash, record_model_usage
315
+
316
+ record_model_usage(
317
+ self._model_name,
318
+ api_key_hash=_resolve_api_key_hash(self._model_name),
319
+ tokens=meta.get("total_tokens", 0),
320
+ cost=meta.get("cost", 0.0),
321
+ )
322
+
314
323
  def ask(
315
324
  self,
316
325
  content: str,
prompture/core.py CHANGED
@@ -31,6 +31,18 @@ from .tools import (
31
31
  logger = logging.getLogger("prompture.core")
32
32
 
33
33
 
34
+ def _record_usage_to_ledger(model_name: str, meta: dict[str, Any]) -> None:
35
+ """Fire-and-forget ledger recording for standalone core functions."""
36
+ from .ledger import _resolve_api_key_hash, record_model_usage
37
+
38
+ record_model_usage(
39
+ model_name,
40
+ api_key_hash=_resolve_api_key_hash(model_name),
41
+ tokens=meta.get("total_tokens", 0),
42
+ cost=meta.get("cost", 0.0),
43
+ )
44
+
45
+
34
46
  def _build_content_with_images(text: str, images: list[ImageInput] | None = None) -> str | list[dict[str, Any]]:
35
47
  """Return plain string when no images, or a list of content blocks."""
36
48
  if not images:
@@ -231,6 +243,8 @@ def render_output(
231
243
  "model_name": model_name or getattr(driver, "model", ""),
232
244
  }
233
245
 
246
+ _record_usage_to_ledger(model_name, resp.get("meta", {}))
247
+
234
248
  return {"text": raw, "usage": usage, "output_format": output_format}
235
249
 
236
250
 
@@ -353,6 +367,8 @@ def ask_for_json(
353
367
  raw = resp.get("text", "")
354
368
  cleaned = clean_json_text(raw)
355
369
 
370
+ _record_usage_to_ledger(model_name, resp.get("meta", {}))
371
+
356
372
  try:
357
373
  json_obj = json.loads(cleaned)
358
374
  json_string = cleaned
prompture/discovery.py CHANGED
@@ -27,14 +27,18 @@ logger = logging.getLogger(__name__)
27
27
 
28
28
 
29
29
  @overload
30
- def get_available_models(*, include_capabilities: bool = False) -> list[str]: ...
30
+ def get_available_models(*, include_capabilities: bool = False, verified_only: bool = False) -> list[str]: ...
31
31
 
32
32
 
33
33
  @overload
34
- def get_available_models(*, include_capabilities: bool = True) -> list[dict[str, Any]]: ...
34
+ def get_available_models(*, include_capabilities: bool = True, verified_only: bool = False) -> list[dict[str, Any]]: ...
35
35
 
36
36
 
37
- def get_available_models(*, include_capabilities: bool = False) -> list[str] | list[dict[str, Any]]:
37
+ def get_available_models(
38
+ *,
39
+ include_capabilities: bool = False,
40
+ verified_only: bool = False,
41
+ ) -> list[str] | list[dict[str, Any]]:
38
42
  """Auto-detect available models based on configured drivers and environment variables.
39
43
 
40
44
  Iterates through supported providers and checks if they are configured
@@ -46,6 +50,8 @@ def get_available_models(*, include_capabilities: bool = False) -> list[str] | l
46
50
  include_capabilities: When ``True``, return enriched dicts with
47
51
  ``model``, ``provider``, ``model_id``, and ``capabilities``
48
52
  fields instead of plain ``"provider/model_id"`` strings.
53
+ verified_only: When ``True``, only return models that have been
54
+ successfully used (as recorded by the usage ledger).
49
55
 
50
56
  Returns:
51
57
  A sorted list of unique model strings (default) or enriched dicts.
@@ -96,11 +102,11 @@ def get_available_models(*, include_capabilities: bool = False) -> list[str] | l
96
102
  elif provider == "grok":
97
103
  if settings.grok_api_key or os.getenv("GROK_API_KEY"):
98
104
  is_configured = True
99
- elif provider == "ollama":
100
- is_configured = True
101
- elif provider == "lmstudio":
102
- is_configured = True
103
- elif provider == "local_http" and os.getenv("LOCAL_HTTP_ENDPOINT"):
105
+ elif (
106
+ provider == "ollama"
107
+ or provider == "lmstudio"
108
+ or (provider == "local_http" and os.getenv("LOCAL_HTTP_ENDPOINT"))
109
+ ):
104
110
  is_configured = True
105
111
 
106
112
  if not is_configured:
@@ -175,12 +181,47 @@ def get_available_models(*, include_capabilities: bool = False) -> list[str] | l
175
181
 
176
182
  sorted_models = sorted(available_models)
177
183
 
184
+ # --- verified_only filtering ---
185
+ verified_set: set[str] | None = None
186
+ if verified_only or include_capabilities:
187
+ try:
188
+ from .ledger import _get_ledger
189
+
190
+ ledger = _get_ledger()
191
+ verified_set = ledger.get_verified_models()
192
+ except Exception:
193
+ logger.debug("Could not load ledger for verified models", exc_info=True)
194
+ verified_set = set()
195
+
196
+ if verified_only and verified_set is not None:
197
+ sorted_models = [m for m in sorted_models if m in verified_set]
198
+
178
199
  if not include_capabilities:
179
200
  return sorted_models
180
201
 
181
202
  # Build enriched dicts with capabilities from models.dev
182
203
  from .model_rates import get_model_capabilities
183
204
 
205
+ # Fetch all ledger stats for annotation (keyed by model_name)
206
+ ledger_stats: dict[str, dict[str, Any]] = {}
207
+ try:
208
+ from .ledger import _get_ledger
209
+
210
+ for row in _get_ledger().get_all_stats():
211
+ name = row["model_name"]
212
+ if name not in ledger_stats:
213
+ ledger_stats[name] = row
214
+ else:
215
+ # Aggregate across API key hashes
216
+ existing = ledger_stats[name]
217
+ existing["use_count"] += row["use_count"]
218
+ existing["total_tokens"] += row["total_tokens"]
219
+ existing["total_cost"] += row["total_cost"]
220
+ if row["last_used"] > existing["last_used"]:
221
+ existing["last_used"] = row["last_used"]
222
+ except Exception:
223
+ logger.debug("Could not load ledger stats for enrichment", exc_info=True)
224
+
184
225
  enriched: list[dict[str, Any]] = []
185
226
  for model_str in sorted_models:
186
227
  parts = model_str.split("/", 1)
@@ -190,13 +231,22 @@ def get_available_models(*, include_capabilities: bool = False) -> list[str] | l
190
231
  caps = get_model_capabilities(provider, model_id)
191
232
  caps_dict = dataclasses.asdict(caps) if caps is not None else None
192
233
 
193
- enriched.append(
194
- {
195
- "model": model_str,
196
- "provider": provider,
197
- "model_id": model_id,
198
- "capabilities": caps_dict,
199
- }
200
- )
234
+ entry: dict[str, Any] = {
235
+ "model": model_str,
236
+ "provider": provider,
237
+ "model_id": model_id,
238
+ "capabilities": caps_dict,
239
+ "verified": verified_set is not None and model_str in verified_set,
240
+ }
241
+
242
+ stats = ledger_stats.get(model_str)
243
+ if stats:
244
+ entry["last_used"] = stats["last_used"]
245
+ entry["use_count"] = stats["use_count"]
246
+ else:
247
+ entry["last_used"] = None
248
+ entry["use_count"] = 0
249
+
250
+ enriched.append(entry)
201
251
 
202
252
  return enriched
prompture/ledger.py ADDED
@@ -0,0 +1,252 @@
1
+ """Persistent model usage ledger — tracks which LLM models have been used.
2
+
3
+ Stores per-model usage stats (call count, tokens, cost, timestamps) in a
4
+ SQLite database at ``~/.prompture/usage/model_ledger.db``. The public
5
+ convenience functions are fire-and-forget: they never raise exceptions so
6
+ they cannot break existing extraction/conversation flows.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ import logging
13
+ import sqlite3
14
+ import threading
15
+ from datetime import datetime, timezone
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ logger = logging.getLogger("prompture.ledger")
20
+
21
+ _DEFAULT_DB_DIR = Path.home() / ".prompture" / "usage"
22
+ _DEFAULT_DB_PATH = _DEFAULT_DB_DIR / "model_ledger.db"
23
+
24
+ _SCHEMA_SQL = """
25
+ CREATE TABLE IF NOT EXISTS model_usage (
26
+ model_name TEXT NOT NULL,
27
+ api_key_hash TEXT NOT NULL,
28
+ use_count INTEGER NOT NULL DEFAULT 1,
29
+ total_tokens INTEGER NOT NULL DEFAULT 0,
30
+ total_cost REAL NOT NULL DEFAULT 0.0,
31
+ first_used TEXT NOT NULL,
32
+ last_used TEXT NOT NULL,
33
+ last_status TEXT NOT NULL DEFAULT 'success',
34
+ PRIMARY KEY (model_name, api_key_hash)
35
+ );
36
+ """
37
+
38
+
39
+ class ModelUsageLedger:
40
+ """SQLite-backed model usage tracker.
41
+
42
+ Thread-safe via an internal :class:`threading.Lock`.
43
+
44
+ Args:
45
+ db_path: Path to the SQLite database file. Defaults to
46
+ ``~/.prompture/usage/model_ledger.db``.
47
+ """
48
+
49
+ def __init__(self, db_path: str | Path | None = None) -> None:
50
+ self._db_path = Path(db_path) if db_path else _DEFAULT_DB_PATH
51
+ self._db_path.parent.mkdir(parents=True, exist_ok=True)
52
+ self._lock = threading.Lock()
53
+ self._init_db()
54
+
55
+ def _init_db(self) -> None:
56
+ with self._lock:
57
+ conn = sqlite3.connect(str(self._db_path))
58
+ try:
59
+ conn.executescript(_SCHEMA_SQL)
60
+ conn.commit()
61
+ finally:
62
+ conn.close()
63
+
64
+ def _connect(self) -> sqlite3.Connection:
65
+ conn = sqlite3.connect(str(self._db_path))
66
+ conn.row_factory = sqlite3.Row
67
+ return conn
68
+
69
+ # ------------------------------------------------------------------ #
70
+ # Recording
71
+ # ------------------------------------------------------------------ #
72
+
73
+ def record_usage(
74
+ self,
75
+ model_name: str,
76
+ *,
77
+ api_key_hash: str = "",
78
+ tokens: int = 0,
79
+ cost: float = 0.0,
80
+ status: str = "success",
81
+ ) -> None:
82
+ """Record a model usage event (upsert).
83
+
84
+ On conflict the row's counters are incremented and ``last_used``
85
+ is updated.
86
+ """
87
+ now = datetime.now(timezone.utc).isoformat()
88
+ with self._lock:
89
+ conn = self._connect()
90
+ try:
91
+ conn.execute(
92
+ """
93
+ INSERT INTO model_usage
94
+ (model_name, api_key_hash, use_count, total_tokens, total_cost,
95
+ first_used, last_used, last_status)
96
+ VALUES (?, ?, 1, ?, ?, ?, ?, ?)
97
+ ON CONFLICT(model_name, api_key_hash) DO UPDATE SET
98
+ use_count = use_count + 1,
99
+ total_tokens = total_tokens + excluded.total_tokens,
100
+ total_cost = total_cost + excluded.total_cost,
101
+ last_used = excluded.last_used,
102
+ last_status = excluded.last_status
103
+ """,
104
+ (model_name, api_key_hash, tokens, cost, now, now, status),
105
+ )
106
+ conn.commit()
107
+ finally:
108
+ conn.close()
109
+
110
+ # ------------------------------------------------------------------ #
111
+ # Queries
112
+ # ------------------------------------------------------------------ #
113
+
114
+ def get_model_stats(self, model_name: str, api_key_hash: str = "") -> dict[str, Any] | None:
115
+ """Return stats for a specific model + key combination, or ``None``."""
116
+ with self._lock:
117
+ conn = self._connect()
118
+ try:
119
+ row = conn.execute(
120
+ "SELECT * FROM model_usage WHERE model_name = ? AND api_key_hash = ?",
121
+ (model_name, api_key_hash),
122
+ ).fetchone()
123
+ if row is None:
124
+ return None
125
+ return dict(row)
126
+ finally:
127
+ conn.close()
128
+
129
+ def get_verified_models(self) -> set[str]:
130
+ """Return model names that have at least one successful usage."""
131
+ with self._lock:
132
+ conn = self._connect()
133
+ try:
134
+ rows = conn.execute(
135
+ "SELECT DISTINCT model_name FROM model_usage WHERE last_status = 'success'"
136
+ ).fetchall()
137
+ return {r["model_name"] for r in rows}
138
+ finally:
139
+ conn.close()
140
+
141
+ def get_recently_used(self, limit: int = 10) -> list[dict[str, Any]]:
142
+ """Return recent model usage rows ordered by ``last_used`` descending."""
143
+ with self._lock:
144
+ conn = self._connect()
145
+ try:
146
+ rows = conn.execute(
147
+ "SELECT * FROM model_usage ORDER BY last_used DESC LIMIT ?",
148
+ (limit,),
149
+ ).fetchall()
150
+ return [dict(r) for r in rows]
151
+ finally:
152
+ conn.close()
153
+
154
+ def get_all_stats(self) -> list[dict[str, Any]]:
155
+ """Return all usage rows."""
156
+ with self._lock:
157
+ conn = self._connect()
158
+ try:
159
+ rows = conn.execute("SELECT * FROM model_usage ORDER BY last_used DESC").fetchall()
160
+ return [dict(r) for r in rows]
161
+ finally:
162
+ conn.close()
163
+
164
+
165
+ # ------------------------------------------------------------------
166
+ # Module-level singleton
167
+ # ------------------------------------------------------------------
168
+
169
+ _ledger: ModelUsageLedger | None = None
170
+ _ledger_lock = threading.Lock()
171
+
172
+
173
+ def _get_ledger() -> ModelUsageLedger:
174
+ """Return (and lazily create) the module-level singleton ledger."""
175
+ global _ledger
176
+ if _ledger is None:
177
+ with _ledger_lock:
178
+ if _ledger is None:
179
+ _ledger = ModelUsageLedger()
180
+ return _ledger
181
+
182
+
183
+ # ------------------------------------------------------------------
184
+ # Public convenience functions (fire-and-forget)
185
+ # ------------------------------------------------------------------
186
+
187
+
188
+ def record_model_usage(
189
+ model_name: str,
190
+ *,
191
+ api_key_hash: str = "",
192
+ tokens: int = 0,
193
+ cost: float = 0.0,
194
+ status: str = "success",
195
+ ) -> None:
196
+ """Record a model usage event. Never raises — all exceptions are swallowed."""
197
+ try:
198
+ _get_ledger().record_usage(
199
+ model_name,
200
+ api_key_hash=api_key_hash,
201
+ tokens=tokens,
202
+ cost=cost,
203
+ status=status,
204
+ )
205
+ except Exception:
206
+ logger.debug("Failed to record model usage for %s", model_name, exc_info=True)
207
+
208
+
209
+ def get_recently_used_models(limit: int = 10) -> list[dict[str, Any]]:
210
+ """Return recently used models. Returns empty list on error."""
211
+ try:
212
+ return _get_ledger().get_recently_used(limit)
213
+ except Exception:
214
+ logger.debug("Failed to get recently used models", exc_info=True)
215
+ return []
216
+
217
+
218
+ # ------------------------------------------------------------------
219
+ # API key hash helper
220
+ # ------------------------------------------------------------------
221
+
222
+ _LOCAL_PROVIDERS = frozenset({"ollama", "lmstudio", "local_http", "airllm"})
223
+
224
+
225
+ def _resolve_api_key_hash(model_name: str) -> str:
226
+ """Derive an 8-char hex hash of the API key for the given model's provider.
227
+
228
+ Local providers (ollama, lmstudio, etc.) return ``""``.
229
+ """
230
+ try:
231
+ provider = model_name.split("/", 1)[0].lower() if "/" in model_name else model_name.lower()
232
+ if provider in _LOCAL_PROVIDERS:
233
+ return ""
234
+
235
+ from .settings import settings
236
+
237
+ key_map: dict[str, str | None] = {
238
+ "openai": settings.openai_api_key,
239
+ "claude": settings.claude_api_key,
240
+ "google": settings.google_api_key,
241
+ "groq": settings.groq_api_key,
242
+ "grok": settings.grok_api_key,
243
+ "openrouter": settings.openrouter_api_key,
244
+ "azure": settings.azure_api_key,
245
+ "huggingface": settings.hf_token,
246
+ }
247
+ api_key = key_map.get(provider)
248
+ if not api_key:
249
+ return ""
250
+ return hashlib.sha256(api_key.encode()).hexdigest()[:8]
251
+ except Exception:
252
+ return ""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: prompture
3
- Version: 0.0.39.dev1
3
+ Version: 0.0.40
4
4
  Summary: Ask LLMs to return structured JSON and run cross-model tests. API-first.
5
5
  Author-email: Juan Denis <juan@vene.co>
6
6
  License-Expression: MIT
@@ -1,24 +1,25 @@
1
- prompture/__init__.py,sha256=pRD1BjR9wHMmbX_8gqBYfQrZk6aOH9kJfsd7klOEyzA,7336
2
- prompture/_version.py,sha256=vmkhULu1sKz5TZsHnh6Ft106Bg-CVqwMF0zt2_28S5U,719
1
+ prompture/__init__.py,sha256=cJnkefDpiyFbU77juw4tXPdKJQWoJ-c6XBFt2v-e5Q4,7455
2
+ prompture/_version.py,sha256=iVCKwk3ALZWN3TArFywN2NsKO1DSIjrjwe22g3qygd0,706
3
3
  prompture/agent.py,sha256=xe_yFHGDzTxaU4tmaLt5AQnzrN0I72hBGwGVrCxg2D0,34704
4
4
  prompture/agent_types.py,sha256=Icl16PQI-ThGLMFCU43adtQA6cqETbsPn4KssKBI4xc,4664
5
5
  prompture/async_agent.py,sha256=nOLOQCNkg0sKKTpryIiidmIcAAlA3FR2NfnZwrNBuCg,33066
6
- prompture/async_conversation.py,sha256=7NOkXdT5LnUNPVzTammtHeiOV2vB6IfZGuysbtNfcHQ,30861
7
- prompture/async_core.py,sha256=s8G0nGUGR1Bf_BQG9_FcQRpveSnJKkEwcWNfbAJaSkg,29208
6
+ prompture/async_conversation.py,sha256=m9sdKBu1wxo5veGwO6g6Zvf1sBzpuxP-mSIEeNKlBjQ,31155
7
+ prompture/async_core.py,sha256=hbRXLvsBJv3JAnUwGZbazsL6x022FrsJU6swmZolgxY,29745
8
8
  prompture/async_driver.py,sha256=4VQ9Q_tI6Ufw6W1CYJ5j8hVtgVdqFGuk6e2tLaSceWE,8581
9
9
  prompture/async_groups.py,sha256=8B383EF_qI9NzcG9zljLKjIZ_37bpNivvsmfJQoOGRk,19894
10
10
  prompture/cache.py,sha256=4dfQDMsEZ9JMQDXLOkiugPmmMJQIfKVE8rTAKDH4oL8,14401
11
11
  prompture/callbacks.py,sha256=JPDqWGzPIzv44l54ocmezlYVBnbKPDEEXRrLdluWGAo,1731
12
12
  prompture/cli.py,sha256=tNiIddRmgC1BomjY5O1VVVAwvqHVzF8IHmQrM-cG2wQ,2902
13
- prompture/conversation.py,sha256=jPwp5m5ZYqLHvD3zYVWJEnegnGPgexVZanZeGVOWrjE,32462
14
- prompture/core.py,sha256=ZCKhqXI7msI-r4zy_Y_Lx8Sz--OJ6qb4b6jchdb5Boo,56671
13
+ prompture/conversation.py,sha256=kBflwh7Qmw1I_jcUGyV36oskdVz4SYDSw_dCjemRRRc,32756
14
+ prompture/core.py,sha256=5FHwX7fNPwFHMbFCMvV-RH7LpPpTToLAmcyDnKbrN0E,57202
15
15
  prompture/cost_mixin.py,sha256=BR-zd42Tj4K865iRIntXlJEfryUcrd5Tuwcfx89QknE,3547
16
- prompture/discovery.py,sha256=bkGyeDp7reHS8ih5jN20g-qxsdfpQ4Lg1aVG06j6Qnk,7532
16
+ prompture/discovery.py,sha256=EWx2d-LJHmlDpm8dlpOicey6XZdDx70ZEetIlOOIlxw,9464
17
17
  prompture/driver.py,sha256=wE7K3vnqeCVT5pEEBP-3uZ6e-YyU6TXtnEKRSB25eOc,10410
18
18
  prompture/field_definitions.py,sha256=PLvxq2ot-ngJ8JbWkkZ-XLtM1wvjUQ3TL01vSEo-a6E,21368
19
19
  prompture/group_types.py,sha256=BxeFV1tI4PTH3xPOie7q3-35ivkTdB9lJUPLH0kPH7A,4731
20
20
  prompture/groups.py,sha256=q9lpD57VWw6iQgK9S0nLVidItJZmusJkmpblM4EX9Sc,18349
21
21
  prompture/image.py,sha256=3uBxC6blXRNyY5KAJ5MkG6ow8KGAslX8WxM8Is8S8cw,5620
22
+ prompture/ledger.py,sha256=2iXkd9PWiM9WpRCxvnHG1-nwh_IM4mCbxjF4LE92Gzs,8576
22
23
  prompture/logging.py,sha256=SkFO26_56Zai05vW8kTq3jvJudfLG2ipI5qNHaXKH3g,2574
23
24
  prompture/model_rates.py,sha256=w2syZCbYM3DGP978Wopgy0AbmvSQcDm-6ALLBLLrGkg,10482
24
25
  prompture/persistence.py,sha256=stcsH9Onth3BlK0QTWDKtXFp3FBmwUS5PI5R1glsIQc,9293
@@ -69,9 +70,9 @@ prompture/scaffold/templates/env.example.j2,sha256=eESKr1KWgyrczO6d-nwAhQwSpf_G-
69
70
  prompture/scaffold/templates/main.py.j2,sha256=TEgc5OvsZOEX0JthkSW1NI_yLwgoeVN_x97Ibg-vyWY,2632
70
71
  prompture/scaffold/templates/models.py.j2,sha256=JrZ99GCVK6TKWapskVRSwCssGrTu5cGZ_r46fOhY2GE,858
71
72
  prompture/scaffold/templates/requirements.txt.j2,sha256=m3S5fi1hq9KG9l_9j317rjwWww0a43WMKd8VnUWv2A4,102
72
- prompture-0.0.39.dev1.dist-info/licenses/LICENSE,sha256=0HgDepH7aaHNFhHF-iXuW6_GqDfYPnVkjtiCAZ4yS8I,1060
73
- prompture-0.0.39.dev1.dist-info/METADATA,sha256=tU2YOuZDS573rWDVQPyYWAxtysF0OFWGHdr_9dmNFoQ,10842
74
- prompture-0.0.39.dev1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
75
- prompture-0.0.39.dev1.dist-info/entry_points.txt,sha256=AFPG3lJR86g4IJMoWQUW5Ph7G6MLNWG3A2u2Tp9zkp8,48
76
- prompture-0.0.39.dev1.dist-info/top_level.txt,sha256=to86zq_kjfdoLeAxQNr420UWqT0WzkKoZ509J7Qr2t4,10
77
- prompture-0.0.39.dev1.dist-info/RECORD,,
73
+ prompture-0.0.40.dist-info/licenses/LICENSE,sha256=0HgDepH7aaHNFhHF-iXuW6_GqDfYPnVkjtiCAZ4yS8I,1060
74
+ prompture-0.0.40.dist-info/METADATA,sha256=i3x9jnjjXSpAHmmgXMF0TyYOINSnufBLCOmaxRkGwwA,10837
75
+ prompture-0.0.40.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
76
+ prompture-0.0.40.dist-info/entry_points.txt,sha256=AFPG3lJR86g4IJMoWQUW5Ph7G6MLNWG3A2u2Tp9zkp8,48
77
+ prompture-0.0.40.dist-info/top_level.txt,sha256=to86zq_kjfdoLeAxQNr420UWqT0WzkKoZ509J7Qr2t4,10
78
+ prompture-0.0.40.dist-info/RECORD,,