sqlite-opus 0.2.0__tar.gz → 0.2.1__tar.gz
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.
- {sqlite_opus-0.2.0/sqlite_opus.egg-info → sqlite_opus-0.2.1}/PKG-INFO +1 -1
- {sqlite_opus-0.2.0 → sqlite_opus-0.2.1}/pyproject.toml +1 -1
- {sqlite_opus-0.2.0 → sqlite_opus-0.2.1}/sqlite_opus/__init__.py +30 -5
- {sqlite_opus-0.2.0 → sqlite_opus-0.2.1}/sqlite_opus/routes.py +33 -0
- {sqlite_opus-0.2.0 → sqlite_opus-0.2.1}/sqlite_opus/static/app.js +50 -10
- {sqlite_opus-0.2.0 → sqlite_opus-0.2.1}/sqlite_opus/static/style.css +0 -1
- {sqlite_opus-0.2.0 → sqlite_opus-0.2.1}/sqlite_opus/templates/sqlite_opus/index.html +3 -1
- {sqlite_opus-0.2.0 → sqlite_opus-0.2.1/sqlite_opus.egg-info}/PKG-INFO +1 -1
- {sqlite_opus-0.2.0 → sqlite_opus-0.2.1}/LICENSE.txt +0 -0
- {sqlite_opus-0.2.0 → sqlite_opus-0.2.1}/MANIFEST.in +0 -0
- {sqlite_opus-0.2.0 → sqlite_opus-0.2.1}/README.md +0 -0
- {sqlite_opus-0.2.0 → sqlite_opus-0.2.1}/setup.cfg +0 -0
- {sqlite_opus-0.2.0 → sqlite_opus-0.2.1}/sqlite_opus/app.py +0 -0
- {sqlite_opus-0.2.0 → sqlite_opus-0.2.1}/sqlite_opus/config.py +0 -0
- {sqlite_opus-0.2.0 → sqlite_opus-0.2.1}/sqlite_opus/core.py +0 -0
- {sqlite_opus-0.2.0 → sqlite_opus-0.2.1}/sqlite_opus/database.py +0 -0
- {sqlite_opus-0.2.0 → sqlite_opus-0.2.1}/sqlite_opus/templates/sqlite_opus/partials/query_results.html +0 -0
- {sqlite_opus-0.2.0 → sqlite_opus-0.2.1}/sqlite_opus/templates/sqlite_opus/partials/table_columns.html +0 -0
- {sqlite_opus-0.2.0 → sqlite_opus-0.2.1}/sqlite_opus/templates/sqlite_opus/partials/table_indexes.html +0 -0
- {sqlite_opus-0.2.0 → sqlite_opus-0.2.1}/sqlite_opus.egg-info/SOURCES.txt +0 -0
- {sqlite_opus-0.2.0 → sqlite_opus-0.2.1}/sqlite_opus.egg-info/dependency_links.txt +0 -0
- {sqlite_opus-0.2.0 → sqlite_opus-0.2.1}/sqlite_opus.egg-info/requires.txt +0 -0
- {sqlite_opus-0.2.0 → sqlite_opus-0.2.1}/sqlite_opus.egg-info/top_level.txt +0 -0
|
@@ -16,7 +16,7 @@ from flask import Blueprint
|
|
|
16
16
|
from sqlite_opus.core import Config, get_templates_path, get_static_path
|
|
17
17
|
from sqlite_opus.database import DatabaseManager
|
|
18
18
|
|
|
19
|
-
__version__ = "0.2.
|
|
19
|
+
__version__ = "0.2.1"
|
|
20
20
|
|
|
21
21
|
# Module-level configuration
|
|
22
22
|
config = Config()
|
|
@@ -31,20 +31,34 @@ blueprint = Blueprint(
|
|
|
31
31
|
)
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
def bind(
|
|
34
|
+
def bind(
|
|
35
|
+
app,
|
|
36
|
+
url_prefix: str = None,
|
|
37
|
+
enable_cors: bool = True,
|
|
38
|
+
max_query_results: int = 1000,
|
|
39
|
+
query_results_per_page: int = None,
|
|
40
|
+
auth_user: str = None,
|
|
41
|
+
auth_password: str = None,
|
|
42
|
+
allow_dml: bool = None,
|
|
43
|
+
db_path: str = None,
|
|
44
|
+
):
|
|
35
45
|
"""
|
|
36
46
|
Bind SQLite Opus dashboard to a Flask application.
|
|
37
|
-
|
|
47
|
+
|
|
38
48
|
This function should be called after creating your Flask app but before
|
|
39
49
|
running it. It will register all dashboard routes and initialize the
|
|
40
50
|
database manager.
|
|
41
|
-
|
|
51
|
+
|
|
42
52
|
Args:
|
|
43
53
|
app: Flask application instance to bind the dashboard to
|
|
44
54
|
url_prefix: URL prefix for dashboard routes (default: "sqlite-opus")
|
|
45
55
|
enable_cors: Enable CORS support (default: True)
|
|
46
56
|
max_query_results: Maximum number of query results to return (default: 1000)
|
|
47
57
|
query_results_per_page: Rows per page for paginated SELECT results (default: 50)
|
|
58
|
+
auth_user: Basic Auth username for dashboard routes (optional). If set with auth_password, enables HTTP Basic Auth.
|
|
59
|
+
auth_password: Basic Auth password for dashboard routes (optional).
|
|
60
|
+
allow_dml: If True, allow DML (INSERT/UPDATE/DELETE/...) in query API (default: False).
|
|
61
|
+
db_path: Path to SQLite database file for auto-connect (optional). Can also set via config.db_path before bind().
|
|
48
62
|
|
|
49
63
|
Example:
|
|
50
64
|
>>> from flask import Flask
|
|
@@ -52,10 +66,13 @@ def bind(app, url_prefix: str = None, enable_cors: bool = True, max_query_result
|
|
|
52
66
|
>>> app = Flask(__name__)
|
|
53
67
|
>>> dashboard.bind(app)
|
|
54
68
|
>>> app.run()
|
|
69
|
+
|
|
70
|
+
Example with Basic Auth (works when package is installed in other projects):
|
|
71
|
+
>>> dashboard.bind(app, auth_user="admin", auth_password="secret", db_path="db.sqlite3")
|
|
55
72
|
"""
|
|
56
73
|
# Store app reference in config
|
|
57
74
|
config.app = app
|
|
58
|
-
|
|
75
|
+
|
|
59
76
|
# Update configuration
|
|
60
77
|
if url_prefix is not None:
|
|
61
78
|
config.url_prefix = url_prefix
|
|
@@ -63,6 +80,14 @@ def bind(app, url_prefix: str = None, enable_cors: bool = True, max_query_result
|
|
|
63
80
|
config.max_query_results = max_query_results
|
|
64
81
|
if query_results_per_page is not None:
|
|
65
82
|
config.query_results_per_page = query_results_per_page
|
|
83
|
+
if auth_user is not None:
|
|
84
|
+
config.auth_user = auth_user
|
|
85
|
+
if auth_password is not None:
|
|
86
|
+
config.auth_password = auth_password
|
|
87
|
+
if allow_dml is not None:
|
|
88
|
+
config.allow_dml = allow_dml
|
|
89
|
+
if db_path is not None:
|
|
90
|
+
config.db_path = db_path
|
|
66
91
|
|
|
67
92
|
# Initialize database manager and attach to app
|
|
68
93
|
if not hasattr(app, "sqlite_opus_db_manager"):
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""Flask routes for SQLite Opus dashboard."""
|
|
2
2
|
|
|
3
|
+
import csv
|
|
4
|
+
import io
|
|
3
5
|
import re
|
|
4
6
|
from functools import wraps
|
|
5
7
|
from flask import Blueprint, render_template, request, jsonify, Flask, Response
|
|
@@ -201,6 +203,37 @@ def register_routes(bp: Blueprint, app: Flask):
|
|
|
201
203
|
page_numbers=page_numbers,
|
|
202
204
|
)
|
|
203
205
|
|
|
206
|
+
@bp.route("/api/query/export", methods=["POST"])
|
|
207
|
+
def export_query_csv():
|
|
208
|
+
"""Run the current SELECT query and return results as CSV download."""
|
|
209
|
+
data = request.get_json() or {}
|
|
210
|
+
query = (data.get("query") or "").strip()
|
|
211
|
+
if not query:
|
|
212
|
+
return jsonify({"success": False, "error": "Query required"}), 400
|
|
213
|
+
if not query.upper().startswith("SELECT"):
|
|
214
|
+
return jsonify({"success": False, "error": "Only SELECT queries can be exported as CSV"}), 400
|
|
215
|
+
if contains_dml(query) and not config.allow_dml:
|
|
216
|
+
return jsonify({
|
|
217
|
+
"success": False,
|
|
218
|
+
"error": "DML queries are not allowed. Set config.allow_dml = True to enable.",
|
|
219
|
+
}), 400
|
|
220
|
+
result = get_query_result(query, page=None, per_page=None)
|
|
221
|
+
if not result.get("success"):
|
|
222
|
+
return jsonify({"success": False, "error": result.get("error", "Query failed")}), 400
|
|
223
|
+
columns = result.get("columns", [])
|
|
224
|
+
results = result.get("results", [])
|
|
225
|
+
buf = io.StringIO()
|
|
226
|
+
writer = csv.writer(buf)
|
|
227
|
+
writer.writerow(columns)
|
|
228
|
+
for row in results:
|
|
229
|
+
writer.writerow([row.get(col) for col in columns])
|
|
230
|
+
csv_str = buf.getvalue()
|
|
231
|
+
return Response(
|
|
232
|
+
csv_str,
|
|
233
|
+
mimetype="text/csv",
|
|
234
|
+
headers={"Content-Disposition": "attachment; filename=export.csv"},
|
|
235
|
+
)
|
|
236
|
+
|
|
204
237
|
def basic_auth_required(f):
|
|
205
238
|
"""Require HTTP Basic Auth if config.auth_user and config.auth_password are set."""
|
|
206
239
|
@wraps(f)
|
|
@@ -65,8 +65,6 @@ document.getElementById('tables-list').addEventListener('click', async (e) => {
|
|
|
65
65
|
if (!tableName) return;
|
|
66
66
|
|
|
67
67
|
selectedTableName = tableName;
|
|
68
|
-
const exportBtn = document.getElementById('export-csv-btn');
|
|
69
|
-
if (exportBtn) exportBtn.disabled = false;
|
|
70
68
|
|
|
71
69
|
// Insert SELECT query into query editor when user clicks a table
|
|
72
70
|
const queryEditor = document.getElementById('query-editor');
|
|
@@ -137,6 +135,10 @@ document.getElementById('execute-btn').addEventListener('click', async () => {
|
|
|
137
135
|
return;
|
|
138
136
|
}
|
|
139
137
|
lastQuery = query;
|
|
138
|
+
const hiddenLastQuery = document.getElementById('last-executed-query');
|
|
139
|
+
if (hiddenLastQuery) {
|
|
140
|
+
hiddenLastQuery.value = query;
|
|
141
|
+
}
|
|
140
142
|
lastPerPage = 10;
|
|
141
143
|
try {
|
|
142
144
|
const html = await fetchQueryPartial(query, 1, lastPerPage);
|
|
@@ -146,9 +148,14 @@ document.getElementById('execute-btn').addEventListener('click', async () => {
|
|
|
146
148
|
}
|
|
147
149
|
});
|
|
148
150
|
|
|
149
|
-
// Clear query
|
|
151
|
+
// Clear query (also clear last executed query so export uses fresh results)
|
|
150
152
|
document.getElementById('clear-btn').addEventListener('click', () => {
|
|
151
153
|
document.getElementById('query-editor').value = '';
|
|
154
|
+
lastQuery = '';
|
|
155
|
+
const hiddenLastQuery = document.getElementById('last-executed-query');
|
|
156
|
+
if (hiddenLastQuery) {
|
|
157
|
+
hiddenLastQuery.value = '';
|
|
158
|
+
}
|
|
152
159
|
});
|
|
153
160
|
|
|
154
161
|
// Pagination: delegate on query-results-area so it works after partial inject
|
|
@@ -165,15 +172,45 @@ document.getElementById('query-results-area').addEventListener('click', async (e
|
|
|
165
172
|
}
|
|
166
173
|
});
|
|
167
174
|
|
|
168
|
-
// Export
|
|
175
|
+
// Export result of last executed query as CSV (uses same query as the results table)
|
|
169
176
|
async function exportTableToCsv() {
|
|
170
|
-
|
|
171
|
-
|
|
177
|
+
const hiddenLastQuery = document.getElementById('last-executed-query');
|
|
178
|
+
const query = hiddenLastQuery ? hiddenLastQuery.value.trim() : '';
|
|
179
|
+
if (!query) {
|
|
180
|
+
if (typeof showStatus === 'function') showStatus('Execute a query first to export results', 'error');
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (!query.toUpperCase().startsWith('SELECT')) {
|
|
184
|
+
if (typeof showStatus === 'function') showStatus('Only SELECT queries can be exported as CSV', 'error');
|
|
172
185
|
return;
|
|
173
186
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
187
|
+
const exportBtn = document.getElementById('export-csv-btn');
|
|
188
|
+
if (exportBtn) exportBtn.disabled = true;
|
|
189
|
+
try {
|
|
190
|
+
const res = await fetch('api/query/export', {
|
|
191
|
+
method: 'POST',
|
|
192
|
+
headers: { 'Content-Type': 'application/json' },
|
|
193
|
+
body: JSON.stringify({ query }),
|
|
194
|
+
});
|
|
195
|
+
if (!res.ok) {
|
|
196
|
+
const err = await res.json().catch(() => ({}));
|
|
197
|
+
if (typeof showStatus === 'function') showStatus(err.error || 'Export failed', 'error');
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const blob = await res.blob();
|
|
201
|
+
const url = URL.createObjectURL(blob);
|
|
202
|
+
const a = document.createElement('a');
|
|
203
|
+
a.href = url;
|
|
204
|
+
const csvFilename = selectedTableName ? `${selectedTableName}_export.csv` : 'export.csv';
|
|
205
|
+
a.download = csvFilename;
|
|
206
|
+
a.click();
|
|
207
|
+
URL.revokeObjectURL(url);
|
|
208
|
+
if (typeof showStatus === 'function') showStatus('CSV exported successfully', 'success');
|
|
209
|
+
} catch (err) {
|
|
210
|
+
if (typeof showStatus === 'function') showStatus('Export failed: ' + err.message, 'error');
|
|
211
|
+
} finally {
|
|
212
|
+
if (exportBtn) exportBtn.disabled = false;
|
|
213
|
+
}
|
|
177
214
|
}
|
|
178
215
|
|
|
179
216
|
const exportCsvBtn = document.getElementById('export-csv-btn');
|
|
@@ -197,6 +234,9 @@ function showStatus(message, type) {
|
|
|
197
234
|
window.addEventListener('load', async () => {
|
|
198
235
|
const status = await apiRequest('api/status');
|
|
199
236
|
if (status.connected) {
|
|
200
|
-
document.getElementById('execute-btn')
|
|
237
|
+
const executeBtn = document.getElementById('execute-btn');
|
|
238
|
+
const exportCsvBtn = document.getElementById('export-csv-btn');
|
|
239
|
+
if (executeBtn) executeBtn.disabled = false;
|
|
240
|
+
if (exportCsvBtn) exportCsvBtn.disabled = false;
|
|
201
241
|
}
|
|
202
242
|
});
|
|
@@ -108,6 +108,8 @@
|
|
|
108
108
|
placeholder="Enter your SQL query here..."
|
|
109
109
|
rows="9"
|
|
110
110
|
></textarea>
|
|
111
|
+
<!-- Hidden field to store last executed query for export/persistence -->
|
|
112
|
+
<input type="hidden" id="last-executed-query" value="">
|
|
111
113
|
<div class="button-group">
|
|
112
114
|
<button id="execute-btn" class="btn btn-primary" disabled>Execute Query</button>
|
|
113
115
|
<button id="clear-btn" class="btn btn-secondary">Clear</button>
|
|
@@ -117,7 +119,7 @@
|
|
|
117
119
|
<section class="panel results-panel mt-2">
|
|
118
120
|
<div class="panel-header panel-header--with-actions">
|
|
119
121
|
<h2>Query Results</h2>
|
|
120
|
-
<button type="button" id="export-csv-btn" class="btn btn-outline-primary btn-sm" disabled title="Export
|
|
122
|
+
<button type="button" id="export-csv-btn" class="btn btn-outline-primary btn-sm" disabled title="Export current SELECT query result as CSV">
|
|
121
123
|
<i class="fas fa-file-csv me-1"></i> Export CSV
|
|
122
124
|
</button>
|
|
123
125
|
</div>
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|