marqeta-diva-mcp 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.
- marqeta_diva_mcp/__init__.py +3 -0
- marqeta_diva_mcp/__main__.py +6 -0
- marqeta_diva_mcp/client.py +471 -0
- marqeta_diva_mcp/embeddings.py +131 -0
- marqeta_diva_mcp/local_storage.py +348 -0
- marqeta_diva_mcp/rag_tools.py +366 -0
- marqeta_diva_mcp/server.py +940 -0
- marqeta_diva_mcp/vector_store.py +274 -0
- marqeta_diva_mcp-0.2.0.dist-info/METADATA +515 -0
- marqeta_diva_mcp-0.2.0.dist-info/RECORD +13 -0
- marqeta_diva_mcp-0.2.0.dist-info/WHEEL +4 -0
- marqeta_diva_mcp-0.2.0.dist-info/entry_points.txt +2 -0
- marqeta_diva_mcp-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""Local SQLite storage for complete transaction data."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sqlite3
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TransactionStorage:
|
|
11
|
+
"""Local SQLite storage for complete transaction data."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, db_path: str = "./transactions.db"):
|
|
14
|
+
"""
|
|
15
|
+
Initialize the transaction storage.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
db_path: Path to SQLite database file
|
|
19
|
+
"""
|
|
20
|
+
self.db_path = Path(db_path)
|
|
21
|
+
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
|
|
23
|
+
print(f"[Storage] Initializing SQLite database at {self.db_path}...", file=sys.stderr)
|
|
24
|
+
|
|
25
|
+
self.conn = sqlite3.connect(str(self.db_path))
|
|
26
|
+
self.conn.row_factory = sqlite3.Row # Return rows as dictionaries
|
|
27
|
+
|
|
28
|
+
self._create_tables()
|
|
29
|
+
|
|
30
|
+
print(f"[Storage] SQLite database ready", file=sys.stderr)
|
|
31
|
+
|
|
32
|
+
def _create_tables(self) -> None:
|
|
33
|
+
"""Create database tables if they don't exist."""
|
|
34
|
+
cursor = self.conn.cursor()
|
|
35
|
+
|
|
36
|
+
# Main transactions table
|
|
37
|
+
cursor.execute("""
|
|
38
|
+
CREATE TABLE IF NOT EXISTS transactions (
|
|
39
|
+
transaction_token TEXT PRIMARY KEY,
|
|
40
|
+
view_name TEXT NOT NULL,
|
|
41
|
+
aggregation TEXT NOT NULL,
|
|
42
|
+
merchant_name TEXT,
|
|
43
|
+
transaction_amount REAL,
|
|
44
|
+
transaction_type TEXT,
|
|
45
|
+
state TEXT,
|
|
46
|
+
user_token TEXT,
|
|
47
|
+
card_token TEXT,
|
|
48
|
+
business_user_token TEXT,
|
|
49
|
+
created_time TEXT,
|
|
50
|
+
transaction_timestamp TEXT,
|
|
51
|
+
network TEXT,
|
|
52
|
+
merchant_category_code TEXT,
|
|
53
|
+
currency_code TEXT,
|
|
54
|
+
full_data TEXT NOT NULL,
|
|
55
|
+
indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
56
|
+
)
|
|
57
|
+
""")
|
|
58
|
+
|
|
59
|
+
# Create indexes for common queries
|
|
60
|
+
cursor.execute("""
|
|
61
|
+
CREATE INDEX IF NOT EXISTS idx_merchant_name
|
|
62
|
+
ON transactions(merchant_name)
|
|
63
|
+
""")
|
|
64
|
+
|
|
65
|
+
cursor.execute("""
|
|
66
|
+
CREATE INDEX IF NOT EXISTS idx_transaction_amount
|
|
67
|
+
ON transactions(transaction_amount)
|
|
68
|
+
""")
|
|
69
|
+
|
|
70
|
+
cursor.execute("""
|
|
71
|
+
CREATE INDEX IF NOT EXISTS idx_user_token
|
|
72
|
+
ON transactions(user_token)
|
|
73
|
+
""")
|
|
74
|
+
|
|
75
|
+
cursor.execute("""
|
|
76
|
+
CREATE INDEX IF NOT EXISTS idx_business_user_token
|
|
77
|
+
ON transactions(business_user_token)
|
|
78
|
+
""")
|
|
79
|
+
|
|
80
|
+
cursor.execute("""
|
|
81
|
+
CREATE INDEX IF NOT EXISTS idx_created_time
|
|
82
|
+
ON transactions(created_time)
|
|
83
|
+
""")
|
|
84
|
+
|
|
85
|
+
cursor.execute("""
|
|
86
|
+
CREATE INDEX IF NOT EXISTS idx_view_name
|
|
87
|
+
ON transactions(view_name)
|
|
88
|
+
""")
|
|
89
|
+
|
|
90
|
+
self.conn.commit()
|
|
91
|
+
|
|
92
|
+
def add_transactions(
|
|
93
|
+
self,
|
|
94
|
+
transactions: List[Dict[str, Any]],
|
|
95
|
+
view_name: str,
|
|
96
|
+
aggregation: str
|
|
97
|
+
) -> int:
|
|
98
|
+
"""
|
|
99
|
+
Add or update transactions in the database.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
transactions: List of transaction dictionaries
|
|
103
|
+
view_name: DiVA view name
|
|
104
|
+
aggregation: Aggregation level
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Number of transactions added/updated
|
|
108
|
+
"""
|
|
109
|
+
cursor = self.conn.cursor()
|
|
110
|
+
count = 0
|
|
111
|
+
|
|
112
|
+
for txn in transactions:
|
|
113
|
+
transaction_token = txn.get("transaction_token")
|
|
114
|
+
if not transaction_token:
|
|
115
|
+
print(f"[Storage] Warning: Transaction missing token, skipping", file=sys.stderr)
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
# Extract common fields for indexing
|
|
119
|
+
merchant_name = txn.get("merchant_name", txn.get("acquirer_merchant_name"))
|
|
120
|
+
transaction_amount = txn.get("transaction_amount")
|
|
121
|
+
transaction_type = txn.get("transaction_type")
|
|
122
|
+
state = txn.get("state", txn.get("transaction_status"))
|
|
123
|
+
user_token = txn.get("user_token", txn.get("acting_user_token"))
|
|
124
|
+
card_token = txn.get("card_token")
|
|
125
|
+
business_user_token = txn.get("business_user_token")
|
|
126
|
+
created_time = txn.get("created_time", txn.get("transaction_timestamp"))
|
|
127
|
+
transaction_timestamp = txn.get("transaction_timestamp")
|
|
128
|
+
network = txn.get("network")
|
|
129
|
+
merchant_category_code = txn.get("merchant_category_code")
|
|
130
|
+
currency_code = txn.get("currency_code")
|
|
131
|
+
|
|
132
|
+
# Store full transaction as JSON
|
|
133
|
+
full_data = json.dumps(txn)
|
|
134
|
+
|
|
135
|
+
# Upsert (insert or replace)
|
|
136
|
+
cursor.execute("""
|
|
137
|
+
INSERT OR REPLACE INTO transactions (
|
|
138
|
+
transaction_token, view_name, aggregation,
|
|
139
|
+
merchant_name, transaction_amount, transaction_type,
|
|
140
|
+
state, user_token, card_token, business_user_token,
|
|
141
|
+
created_time, transaction_timestamp, network,
|
|
142
|
+
merchant_category_code, currency_code, full_data
|
|
143
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
144
|
+
""", (
|
|
145
|
+
transaction_token, view_name, aggregation,
|
|
146
|
+
merchant_name, transaction_amount, transaction_type,
|
|
147
|
+
state, user_token, card_token, business_user_token,
|
|
148
|
+
created_time, transaction_timestamp, network,
|
|
149
|
+
merchant_category_code, currency_code, full_data
|
|
150
|
+
))
|
|
151
|
+
|
|
152
|
+
count += 1
|
|
153
|
+
|
|
154
|
+
self.conn.commit()
|
|
155
|
+
print(f"[Storage] Added/updated {count} transactions", file=sys.stderr)
|
|
156
|
+
return count
|
|
157
|
+
|
|
158
|
+
def get_transactions(
|
|
159
|
+
self,
|
|
160
|
+
transaction_tokens: List[str]
|
|
161
|
+
) -> List[Dict[str, Any]]:
|
|
162
|
+
"""
|
|
163
|
+
Get transactions by their tokens.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
transaction_tokens: List of transaction tokens
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
List of complete transaction dictionaries
|
|
170
|
+
"""
|
|
171
|
+
if not transaction_tokens:
|
|
172
|
+
return []
|
|
173
|
+
|
|
174
|
+
cursor = self.conn.cursor()
|
|
175
|
+
placeholders = ",".join("?" * len(transaction_tokens))
|
|
176
|
+
|
|
177
|
+
cursor.execute(f"""
|
|
178
|
+
SELECT full_data FROM transactions
|
|
179
|
+
WHERE transaction_token IN ({placeholders})
|
|
180
|
+
""", transaction_tokens)
|
|
181
|
+
|
|
182
|
+
results = []
|
|
183
|
+
for row in cursor.fetchall():
|
|
184
|
+
full_data = json.loads(row["full_data"])
|
|
185
|
+
results.append(full_data)
|
|
186
|
+
|
|
187
|
+
return results
|
|
188
|
+
|
|
189
|
+
def query_transactions(
|
|
190
|
+
self,
|
|
191
|
+
filters: Optional[Dict[str, Any]] = None,
|
|
192
|
+
limit: int = 100,
|
|
193
|
+
offset: int = 0,
|
|
194
|
+
order_by: str = "created_time DESC"
|
|
195
|
+
) -> Dict[str, Any]:
|
|
196
|
+
"""
|
|
197
|
+
Query transactions with filters.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
filters: Dictionary of filters (e.g., {"merchant_name": "Starbucks", "transaction_amount": {">": 10}})
|
|
201
|
+
limit: Maximum number of results
|
|
202
|
+
offset: Offset for pagination
|
|
203
|
+
order_by: SQL ORDER BY clause
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
Dictionary with results and metadata
|
|
207
|
+
"""
|
|
208
|
+
cursor = self.conn.cursor()
|
|
209
|
+
|
|
210
|
+
# Build WHERE clause
|
|
211
|
+
where_clauses = []
|
|
212
|
+
params = []
|
|
213
|
+
|
|
214
|
+
if filters:
|
|
215
|
+
for field, value in filters.items():
|
|
216
|
+
if isinstance(value, dict):
|
|
217
|
+
# Handle operators like {"transaction_amount": {">": 10}}
|
|
218
|
+
for op, val in value.items():
|
|
219
|
+
if op in [">", "<", ">=", "<=", "=", "!="]:
|
|
220
|
+
where_clauses.append(f"{field} {op} ?")
|
|
221
|
+
params.append(val)
|
|
222
|
+
elif op == "like":
|
|
223
|
+
where_clauses.append(f"{field} LIKE ?")
|
|
224
|
+
params.append(f"%{val}%")
|
|
225
|
+
else:
|
|
226
|
+
# Simple equality
|
|
227
|
+
where_clauses.append(f"{field} = ?")
|
|
228
|
+
params.append(value)
|
|
229
|
+
|
|
230
|
+
where_clause = " AND ".join(where_clauses) if where_clauses else "1=1"
|
|
231
|
+
|
|
232
|
+
# Get total count
|
|
233
|
+
cursor.execute(f"""
|
|
234
|
+
SELECT COUNT(*) as count FROM transactions WHERE {where_clause}
|
|
235
|
+
""", params)
|
|
236
|
+
total_count = cursor.fetchone()["count"]
|
|
237
|
+
|
|
238
|
+
# Get results
|
|
239
|
+
cursor.execute(f"""
|
|
240
|
+
SELECT full_data FROM transactions
|
|
241
|
+
WHERE {where_clause}
|
|
242
|
+
ORDER BY {order_by}
|
|
243
|
+
LIMIT ? OFFSET ?
|
|
244
|
+
""", params + [limit, offset])
|
|
245
|
+
|
|
246
|
+
results = []
|
|
247
|
+
for row in cursor.fetchall():
|
|
248
|
+
full_data = json.loads(row["full_data"])
|
|
249
|
+
results.append(full_data)
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
"total": total_count,
|
|
253
|
+
"count": len(results),
|
|
254
|
+
"offset": offset,
|
|
255
|
+
"limit": limit,
|
|
256
|
+
"is_more": offset + len(results) < total_count,
|
|
257
|
+
"data": results
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
261
|
+
"""
|
|
262
|
+
Get statistics about the local storage.
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
Dictionary with storage statistics
|
|
266
|
+
"""
|
|
267
|
+
cursor = self.conn.cursor()
|
|
268
|
+
|
|
269
|
+
# Total transactions
|
|
270
|
+
cursor.execute("SELECT COUNT(*) as count FROM transactions")
|
|
271
|
+
total_count = cursor.fetchone()["count"]
|
|
272
|
+
|
|
273
|
+
# By view
|
|
274
|
+
cursor.execute("""
|
|
275
|
+
SELECT view_name, aggregation, COUNT(*) as count
|
|
276
|
+
FROM transactions
|
|
277
|
+
GROUP BY view_name, aggregation
|
|
278
|
+
""")
|
|
279
|
+
by_view = [dict(row) for row in cursor.fetchall()]
|
|
280
|
+
|
|
281
|
+
# Date range
|
|
282
|
+
cursor.execute("""
|
|
283
|
+
SELECT
|
|
284
|
+
MIN(created_time) as earliest,
|
|
285
|
+
MAX(created_time) as latest
|
|
286
|
+
FROM transactions
|
|
287
|
+
""")
|
|
288
|
+
date_range = dict(cursor.fetchone())
|
|
289
|
+
|
|
290
|
+
# Database file size
|
|
291
|
+
db_size = self.db_path.stat().st_size if self.db_path.exists() else 0
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
"database_path": str(self.db_path.absolute()),
|
|
295
|
+
"total_transactions": total_count,
|
|
296
|
+
"by_view": by_view,
|
|
297
|
+
"date_range": date_range,
|
|
298
|
+
"database_size_bytes": db_size,
|
|
299
|
+
"database_size_mb": round(db_size / (1024 * 1024), 2)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
def clear(self, view_name: Optional[str] = None) -> int:
|
|
303
|
+
"""
|
|
304
|
+
Clear transactions from the database.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
view_name: If specified, only clear transactions from this view
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
Number of transactions deleted
|
|
311
|
+
"""
|
|
312
|
+
cursor = self.conn.cursor()
|
|
313
|
+
|
|
314
|
+
if view_name:
|
|
315
|
+
cursor.execute("DELETE FROM transactions WHERE view_name = ?", (view_name,))
|
|
316
|
+
count = cursor.rowcount
|
|
317
|
+
print(f"[Storage] Cleared {count} transactions from view '{view_name}'", file=sys.stderr)
|
|
318
|
+
else:
|
|
319
|
+
cursor.execute("DELETE FROM transactions")
|
|
320
|
+
count = cursor.rowcount
|
|
321
|
+
print(f"[Storage] Cleared all {count} transactions", file=sys.stderr)
|
|
322
|
+
|
|
323
|
+
self.conn.commit()
|
|
324
|
+
return count
|
|
325
|
+
|
|
326
|
+
def close(self) -> None:
|
|
327
|
+
"""Close the database connection."""
|
|
328
|
+
self.conn.close()
|
|
329
|
+
|
|
330
|
+
def __enter__(self):
|
|
331
|
+
"""Context manager entry."""
|
|
332
|
+
return self
|
|
333
|
+
|
|
334
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
335
|
+
"""Context manager exit."""
|
|
336
|
+
self.close()
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
# Global storage instance (lazy-loaded)
|
|
340
|
+
_storage: TransactionStorage | None = None
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def get_storage(db_path: str = "./transactions.db") -> TransactionStorage:
|
|
344
|
+
"""Get or create the global transaction storage instance."""
|
|
345
|
+
global _storage
|
|
346
|
+
if _storage is None:
|
|
347
|
+
_storage = TransactionStorage(db_path=db_path)
|
|
348
|
+
return _storage
|