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.
Files changed (23) hide show
  1. {sqlite_opus-0.2.0/sqlite_opus.egg-info → sqlite_opus-0.2.1}/PKG-INFO +1 -1
  2. {sqlite_opus-0.2.0 → sqlite_opus-0.2.1}/pyproject.toml +1 -1
  3. {sqlite_opus-0.2.0 → sqlite_opus-0.2.1}/sqlite_opus/__init__.py +30 -5
  4. {sqlite_opus-0.2.0 → sqlite_opus-0.2.1}/sqlite_opus/routes.py +33 -0
  5. {sqlite_opus-0.2.0 → sqlite_opus-0.2.1}/sqlite_opus/static/app.js +50 -10
  6. {sqlite_opus-0.2.0 → sqlite_opus-0.2.1}/sqlite_opus/static/style.css +0 -1
  7. {sqlite_opus-0.2.0 → sqlite_opus-0.2.1}/sqlite_opus/templates/sqlite_opus/index.html +3 -1
  8. {sqlite_opus-0.2.0 → sqlite_opus-0.2.1/sqlite_opus.egg-info}/PKG-INFO +1 -1
  9. {sqlite_opus-0.2.0 → sqlite_opus-0.2.1}/LICENSE.txt +0 -0
  10. {sqlite_opus-0.2.0 → sqlite_opus-0.2.1}/MANIFEST.in +0 -0
  11. {sqlite_opus-0.2.0 → sqlite_opus-0.2.1}/README.md +0 -0
  12. {sqlite_opus-0.2.0 → sqlite_opus-0.2.1}/setup.cfg +0 -0
  13. {sqlite_opus-0.2.0 → sqlite_opus-0.2.1}/sqlite_opus/app.py +0 -0
  14. {sqlite_opus-0.2.0 → sqlite_opus-0.2.1}/sqlite_opus/config.py +0 -0
  15. {sqlite_opus-0.2.0 → sqlite_opus-0.2.1}/sqlite_opus/core.py +0 -0
  16. {sqlite_opus-0.2.0 → sqlite_opus-0.2.1}/sqlite_opus/database.py +0 -0
  17. {sqlite_opus-0.2.0 → sqlite_opus-0.2.1}/sqlite_opus/templates/sqlite_opus/partials/query_results.html +0 -0
  18. {sqlite_opus-0.2.0 → sqlite_opus-0.2.1}/sqlite_opus/templates/sqlite_opus/partials/table_columns.html +0 -0
  19. {sqlite_opus-0.2.0 → sqlite_opus-0.2.1}/sqlite_opus/templates/sqlite_opus/partials/table_indexes.html +0 -0
  20. {sqlite_opus-0.2.0 → sqlite_opus-0.2.1}/sqlite_opus.egg-info/SOURCES.txt +0 -0
  21. {sqlite_opus-0.2.0 → sqlite_opus-0.2.1}/sqlite_opus.egg-info/dependency_links.txt +0 -0
  22. {sqlite_opus-0.2.0 → sqlite_opus-0.2.1}/sqlite_opus.egg-info/requires.txt +0 -0
  23. {sqlite_opus-0.2.0 → sqlite_opus-0.2.1}/sqlite_opus.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlite-opus
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: A Flask-based web dashboard for SQLite database query and management
5
5
  Author-email: Your Name <hungle2048@gmail.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sqlite-opus"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "A Flask-based web dashboard for SQLite database query and management"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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.0"
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(app, url_prefix: str = None, enable_cors: bool = True, max_query_results: int = 1000, query_results_per_page: int = None):
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 selected table as CSV via API
175
+ // Export result of last executed query as CSV (uses same query as the results table)
169
176
  async function exportTableToCsv() {
170
- if (!selectedTableName) {
171
- if (typeof showStatus === 'function') showStatus('Select a table first', 'error');
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
- // const exportBtn = document.getElementById('export-csv-btn');
175
- // if (exportBtn) exportBtn.disabled = true;
176
- // call api/table/${encodeURIComponent(selectedTableName)}/export
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').disabled = false;
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
  });
@@ -312,7 +312,6 @@ body {
312
312
  margin-top: 0;
313
313
  }
314
314
 
315
- .table-columns-panel,
316
315
  .table-indexes-panel {
317
316
  margin-top: 1.5rem;
318
317
  }
@@ -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 selected table to CSV">
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>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlite-opus
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: A Flask-based web dashboard for SQLite database query and management
5
5
  Author-email: Your Name <hungle2048@gmail.com>
6
6
  License: MIT
File without changes
File without changes
File without changes
File without changes