queryapigate 0.7.1__tar.gz → 0.9.0__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.
- {queryapigate-0.7.1/queryapigate.egg-info → queryapigate-0.9.0}/PKG-INFO +18 -10
- {queryapigate-0.7.1 → queryapigate-0.9.0}/README.md +14 -8
- {queryapigate-0.7.1 → queryapigate-0.9.0}/pyproject.toml +2 -1
- {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/__init__.py +1 -1
- {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/app.py +141 -12
- {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/bundle.py +2 -1
- {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/config.py +78 -1
- queryapigate-0.9.0/queryapigate/definitions.py +98 -0
- queryapigate-0.9.0/queryapigate/engine.py +314 -0
- {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/formats.py +6 -0
- queryapigate-0.9.0/queryapigate/mongotools.py +82 -0
- {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/openapi.py +76 -5
- {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/runners.py +85 -0
- queryapigate-0.9.0/queryapigate/schema.py +134 -0
- {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/store.py +23 -0
- queryapigate-0.9.0/queryapigate/ui.py +4448 -0
- {queryapigate-0.7.1 → queryapigate-0.9.0/queryapigate.egg-info}/PKG-INFO +18 -10
- {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate.egg-info/SOURCES.txt +3 -0
- {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate.egg-info/requires.txt +4 -1
- {queryapigate-0.7.1 → queryapigate-0.9.0}/tests/test_audit_log.py +8 -1
- queryapigate-0.9.0/tests/test_mongotools.py +81 -0
- {queryapigate-0.7.1 → queryapigate-0.9.0}/tests/test_queryapigate.py +340 -3
- queryapigate-0.9.0/tests/test_settings.py +73 -0
- queryapigate-0.7.1/queryapigate/definitions.py +0 -66
- queryapigate-0.7.1/queryapigate/engine.py +0 -166
- queryapigate-0.7.1/queryapigate/schema.py +0 -99
- queryapigate-0.7.1/queryapigate/ui.py +0 -2655
- {queryapigate-0.7.1 → queryapigate-0.9.0}/LICENSE +0 -0
- {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/__main__.py +0 -0
- {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/apikeys.py +0 -0
- {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/cache.py +0 -0
- {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/cli.py +0 -0
- {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/collection_admin.py +0 -0
- {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/cors.py +0 -0
- {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/errors.py +0 -0
- {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/examples.py +0 -0
- {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/lib/h2-2.2.224.jar +0 -0
- {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/logging_setup.py +0 -0
- {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/metrics.py +0 -0
- {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/params.py +0 -0
- {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/pool.py +0 -0
- {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/postman.py +0 -0
- {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/ratelimit.py +0 -0
- {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/sqltools.py +0 -0
- {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate.egg-info/dependency_links.txt +0 -0
- {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate.egg-info/entry_points.txt +0 -0
- {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate.egg-info/top_level.txt +0 -0
- {queryapigate-0.7.1 → queryapigate-0.9.0}/setup.cfg +0 -0
- {queryapigate-0.7.1 → queryapigate-0.9.0}/tests/test_api_keys.py +0 -0
- {queryapigate-0.7.1 → queryapigate-0.9.0}/tests/test_cli.py +0 -0
- {queryapigate-0.7.1 → queryapigate-0.9.0}/tests/test_collection_bundle.py +0 -0
- {queryapigate-0.7.1 → queryapigate-0.9.0}/tests/test_collection_postman.py +0 -0
- {queryapigate-0.7.1 → queryapigate-0.9.0}/tests/test_collections.py +0 -0
- {queryapigate-0.7.1 → queryapigate-0.9.0}/tests/test_connection_encryption.py +0 -0
- {queryapigate-0.7.1 → queryapigate-0.9.0}/tests/test_cors_and_rate_limit.py +0 -0
- {queryapigate-0.7.1 → queryapigate-0.9.0}/tests/test_duckdb.py +0 -0
- {queryapigate-0.7.1 → queryapigate-0.9.0}/tests/test_examples.py +0 -0
- {queryapigate-0.7.1 → queryapigate-0.9.0}/tests/test_integration.py +0 -0
- {queryapigate-0.7.1 → queryapigate-0.9.0}/tests/test_jdbc.py +0 -0
- {queryapigate-0.7.1 → queryapigate-0.9.0}/tests/test_observability.py +0 -0
- {queryapigate-0.7.1 → queryapigate-0.9.0}/tests/test_params.py +0 -0
- {queryapigate-0.7.1 → queryapigate-0.9.0}/tests/test_release_check.py +0 -0
- {queryapigate-0.7.1 → queryapigate-0.9.0}/tests/test_response_cache.py +0 -0
- {queryapigate-0.7.1 → queryapigate-0.9.0}/tests/test_roles.py +0 -0
- {queryapigate-0.7.1 → queryapigate-0.9.0}/tests/test_sql_guard_fuzz.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: queryapigate
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: Turn SQL into a REST API: run queries against MySQL, PostgreSQL, ClickHouse, SQLite or H2 over HTTP and get JSON, CSV, XML, YAML or XLSX back.
|
|
5
5
|
Author-email: Anantha Raju C <arcswdev@gmail.com>
|
|
6
6
|
License-Expression: FSL-1.1-MIT
|
|
@@ -41,10 +41,12 @@ Requires-Dist: JayDeBeApi>=1.2; extra == "h2"
|
|
|
41
41
|
Requires-Dist: JPype1>=1.4; extra == "h2"
|
|
42
42
|
Provides-Extra: duckdb
|
|
43
43
|
Requires-Dist: duckdb>=1.0; extra == "duckdb"
|
|
44
|
+
Provides-Extra: mongo
|
|
45
|
+
Requires-Dist: pymongo>=4.0; extra == "mongo"
|
|
44
46
|
Provides-Extra: encryption
|
|
45
47
|
Requires-Dist: cryptography>=41; extra == "encryption"
|
|
46
48
|
Provides-Extra: all
|
|
47
|
-
Requires-Dist: queryapigate[clickhouse,duckdb,encryption,h2,mysql,postgres]; extra == "all"
|
|
49
|
+
Requires-Dist: queryapigate[clickhouse,duckdb,encryption,h2,mongo,mysql,postgres]; extra == "all"
|
|
48
50
|
Provides-Extra: server
|
|
49
51
|
Requires-Dist: gunicorn>=21; extra == "server"
|
|
50
52
|
Provides-Extra: docs
|
|
@@ -74,8 +76,8 @@ Dynamic: license-file
|
|
|
74
76
|
[**Full documentation**](https://AnanthaRajuC.github.io/QueryAPIGate/)
|
|
75
77
|
|
|
76
78
|
<p align="center">
|
|
77
|
-
<a href="https://www.youtube.com/watch?v=
|
|
78
|
-
<img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/demo-thumbnail.png" alt="Watch the
|
|
79
|
+
<a href="https://www.youtube.com/watch?v=WWImFj4m95o">
|
|
80
|
+
<img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/demo-thumbnail.png" alt="Watch the demo: saved SQL queries as REST endpoints, scoped API keys, and the Move preview showing which keys gain or lose access" width="640">
|
|
79
81
|
</a>
|
|
80
82
|
</p>
|
|
81
83
|
|
|
@@ -109,7 +111,7 @@ Crimson Garden,Comedy,PG-13,631,2
|
|
|
109
111
|
<tr>
|
|
110
112
|
<td width="25%">
|
|
111
113
|
<a href="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/connections.png">
|
|
112
|
-
<img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/connections.png" alt="Connections
|
|
114
|
+
<img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/connections.png" alt="Connections screen with a sidebar and All/Active/Inactive tabs, listing ClickHouse, MySQL, PostgreSQL and SQLite connections with active/inactive status and each connection's live usage: queries run, failures and average latency">
|
|
113
115
|
</a>
|
|
114
116
|
<br><sub>Manage connections across every supported database, with live usage per connection</sub>
|
|
115
117
|
</td>
|
|
@@ -121,13 +123,13 @@ Crimson Garden,Comedy,PG-13,631,2
|
|
|
121
123
|
</td>
|
|
122
124
|
<td width="25%">
|
|
123
125
|
<a href="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/api-keys.png">
|
|
124
|
-
<img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/api-keys.png" alt="API keys
|
|
126
|
+
<img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/api-keys.png" alt="API keys screen showing keys created from roles and scoped to a collection (shown as a dashed name/ tag), an expiring partner key, per-key rate limits, and each key's last-used time and live usage">
|
|
125
127
|
</a>
|
|
126
128
|
<br><sub>Per-key permissions: connections, specific saved queries, expiry, rate limits - and what each key has actually done</sub>
|
|
127
129
|
</td>
|
|
128
130
|
<td width="25%">
|
|
129
131
|
<a href="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/roles.png">
|
|
130
|
-
<img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/roles.png" alt="Roles
|
|
132
|
+
<img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/roles.png" alt="Roles screen listing reusable permission templates - connections, read/write access, rate limit and IP allowlisting - with a New key from this shortcut">
|
|
131
133
|
</a>
|
|
132
134
|
<br><sub>Named roles: a reusable grant template, copied onto a key once at creation</sub>
|
|
133
135
|
</td>
|
|
@@ -138,19 +140,19 @@ Crimson Garden,Comedy,PG-13,631,2
|
|
|
138
140
|
<tr>
|
|
139
141
|
<td width="33%">
|
|
140
142
|
<a href="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/saved-query-history.png">
|
|
141
|
-
<img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/saved-query-history.png" alt="A saved query's History tab: every run with its time,
|
|
143
|
+
<img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/saved-query-history.png" alt="A saved query's History tab: every run with its time, calling API key, rows, duration and request ID, with a status filter and search box">
|
|
142
144
|
</a>
|
|
143
145
|
<br><sub>Run history per saved query: who called it, how long it took, and the request ID to find it in the logs</sub>
|
|
144
146
|
</td>
|
|
145
147
|
<td width="33%">
|
|
146
148
|
<a href="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/audit-log.png">
|
|
147
|
-
<img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/audit-log.png" alt="Audit
|
|
149
|
+
<img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/audit-log.png" alt="Audit log screen listing every administrative change - saved queries, roles and API keys created or updated - with an action filter and search box">
|
|
148
150
|
</a>
|
|
149
151
|
<br><sub>A durable audit log of every configuration change, filterable by action, actor and target</sub>
|
|
150
152
|
</td>
|
|
151
153
|
<td width="33%">
|
|
152
154
|
<a href="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/metrics.png">
|
|
153
|
-
<img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/metrics.png" alt="Metrics
|
|
155
|
+
<img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/metrics.png" alt="Metrics screen with stat tiles for requests, error rate, active queries, pool and rate limiting, bars for requests by status and queries by connection, and a per-connection latency table">
|
|
154
156
|
</a>
|
|
155
157
|
<br><sub>Live metrics with no Prometheus required - or import the bundled Grafana dashboard for history</sub>
|
|
156
158
|
</td>
|
|
@@ -341,6 +343,11 @@ version management, API key management, an audit log of administrative changes,
|
|
|
341
343
|
response inspection with a collapsible JSON tree for non-tabular results, a quick bar chart of any numeric
|
|
342
344
|
result, and one-click "copy as curl" / "copy as TSV" for any result.
|
|
343
345
|
|
|
346
|
+
The UI has a collapsible sidebar (Data, Access, Observability; `Ctrl`+`B` toggles it) and a read-only **Settings**
|
|
347
|
+
screen that shows every environment variable, its effective value and whether it was set or is the default (secrets are
|
|
348
|
+
reported as configured or not, never shown), with a "Copy as .env" button. Theme, table density and the default
|
|
349
|
+
result format are per-browser preferences.
|
|
350
|
+
|
|
344
351
|
```text
|
|
345
352
|
Connect Database
|
|
346
353
|
|
|
|
@@ -666,6 +673,7 @@ Clean up with `docker compose down -v`.
|
|
|
666
673
|
| `/api_keys` | GET, POST | List / create scoped API keys (admin only). |
|
|
667
674
|
| `/api_keys/<name>` | PATCH, DELETE | Update / revoke a scoped API key (admin only). |
|
|
668
675
|
| `/audit_log` | GET | Durable record of administrative changes - keys, connections, saved queries (admin only). |
|
|
676
|
+
| `/settings` | GET | The server's own configuration - each setting's effective value and whether it comes from the environment or the default; read-only, secrets never returned (admin only). |
|
|
669
677
|
| `/health`, `/docs`, `/openapi.json` | GET | Liveness, Swagger UI, OpenAPI spec. |
|
|
670
678
|
| `/metrics` | GET | Prometheus text-format metrics: request/query counts and latencies, pool occupancy, rate-limit rejections. |
|
|
671
679
|
| `/ui` | GET | A small admin UI: manage connections and saved queries, run ad-hoc SQL. |
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
[**Full documentation**](https://AnanthaRajuC.github.io/QueryAPIGate/)
|
|
15
15
|
|
|
16
16
|
<p align="center">
|
|
17
|
-
<a href="https://www.youtube.com/watch?v=
|
|
18
|
-
<img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/demo-thumbnail.png" alt="Watch the
|
|
17
|
+
<a href="https://www.youtube.com/watch?v=WWImFj4m95o">
|
|
18
|
+
<img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/demo-thumbnail.png" alt="Watch the demo: saved SQL queries as REST endpoints, scoped API keys, and the Move preview showing which keys gain or lose access" width="640">
|
|
19
19
|
</a>
|
|
20
20
|
</p>
|
|
21
21
|
|
|
@@ -49,7 +49,7 @@ Crimson Garden,Comedy,PG-13,631,2
|
|
|
49
49
|
<tr>
|
|
50
50
|
<td width="25%">
|
|
51
51
|
<a href="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/connections.png">
|
|
52
|
-
<img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/connections.png" alt="Connections
|
|
52
|
+
<img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/connections.png" alt="Connections screen with a sidebar and All/Active/Inactive tabs, listing ClickHouse, MySQL, PostgreSQL and SQLite connections with active/inactive status and each connection's live usage: queries run, failures and average latency">
|
|
53
53
|
</a>
|
|
54
54
|
<br><sub>Manage connections across every supported database, with live usage per connection</sub>
|
|
55
55
|
</td>
|
|
@@ -61,13 +61,13 @@ Crimson Garden,Comedy,PG-13,631,2
|
|
|
61
61
|
</td>
|
|
62
62
|
<td width="25%">
|
|
63
63
|
<a href="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/api-keys.png">
|
|
64
|
-
<img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/api-keys.png" alt="API keys
|
|
64
|
+
<img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/api-keys.png" alt="API keys screen showing keys created from roles and scoped to a collection (shown as a dashed name/ tag), an expiring partner key, per-key rate limits, and each key's last-used time and live usage">
|
|
65
65
|
</a>
|
|
66
66
|
<br><sub>Per-key permissions: connections, specific saved queries, expiry, rate limits - and what each key has actually done</sub>
|
|
67
67
|
</td>
|
|
68
68
|
<td width="25%">
|
|
69
69
|
<a href="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/roles.png">
|
|
70
|
-
<img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/roles.png" alt="Roles
|
|
70
|
+
<img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/roles.png" alt="Roles screen listing reusable permission templates - connections, read/write access, rate limit and IP allowlisting - with a New key from this shortcut">
|
|
71
71
|
</a>
|
|
72
72
|
<br><sub>Named roles: a reusable grant template, copied onto a key once at creation</sub>
|
|
73
73
|
</td>
|
|
@@ -78,19 +78,19 @@ Crimson Garden,Comedy,PG-13,631,2
|
|
|
78
78
|
<tr>
|
|
79
79
|
<td width="33%">
|
|
80
80
|
<a href="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/saved-query-history.png">
|
|
81
|
-
<img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/saved-query-history.png" alt="A saved query's History tab: every run with its time,
|
|
81
|
+
<img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/saved-query-history.png" alt="A saved query's History tab: every run with its time, calling API key, rows, duration and request ID, with a status filter and search box">
|
|
82
82
|
</a>
|
|
83
83
|
<br><sub>Run history per saved query: who called it, how long it took, and the request ID to find it in the logs</sub>
|
|
84
84
|
</td>
|
|
85
85
|
<td width="33%">
|
|
86
86
|
<a href="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/audit-log.png">
|
|
87
|
-
<img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/audit-log.png" alt="Audit
|
|
87
|
+
<img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/audit-log.png" alt="Audit log screen listing every administrative change - saved queries, roles and API keys created or updated - with an action filter and search box">
|
|
88
88
|
</a>
|
|
89
89
|
<br><sub>A durable audit log of every configuration change, filterable by action, actor and target</sub>
|
|
90
90
|
</td>
|
|
91
91
|
<td width="33%">
|
|
92
92
|
<a href="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/metrics.png">
|
|
93
|
-
<img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/metrics.png" alt="Metrics
|
|
93
|
+
<img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/metrics.png" alt="Metrics screen with stat tiles for requests, error rate, active queries, pool and rate limiting, bars for requests by status and queries by connection, and a per-connection latency table">
|
|
94
94
|
</a>
|
|
95
95
|
<br><sub>Live metrics with no Prometheus required - or import the bundled Grafana dashboard for history</sub>
|
|
96
96
|
</td>
|
|
@@ -281,6 +281,11 @@ version management, API key management, an audit log of administrative changes,
|
|
|
281
281
|
response inspection with a collapsible JSON tree for non-tabular results, a quick bar chart of any numeric
|
|
282
282
|
result, and one-click "copy as curl" / "copy as TSV" for any result.
|
|
283
283
|
|
|
284
|
+
The UI has a collapsible sidebar (Data, Access, Observability; `Ctrl`+`B` toggles it) and a read-only **Settings**
|
|
285
|
+
screen that shows every environment variable, its effective value and whether it was set or is the default (secrets are
|
|
286
|
+
reported as configured or not, never shown), with a "Copy as .env" button. Theme, table density and the default
|
|
287
|
+
result format are per-browser preferences.
|
|
288
|
+
|
|
284
289
|
```text
|
|
285
290
|
Connect Database
|
|
286
291
|
|
|
|
@@ -606,6 +611,7 @@ Clean up with `docker compose down -v`.
|
|
|
606
611
|
| `/api_keys` | GET, POST | List / create scoped API keys (admin only). |
|
|
607
612
|
| `/api_keys/<name>` | PATCH, DELETE | Update / revoke a scoped API key (admin only). |
|
|
608
613
|
| `/audit_log` | GET | Durable record of administrative changes - keys, connections, saved queries (admin only). |
|
|
614
|
+
| `/settings` | GET | The server's own configuration - each setting's effective value and whether it comes from the environment or the default; read-only, secrets never returned (admin only). |
|
|
609
615
|
| `/health`, `/docs`, `/openapi.json` | GET | Liveness, Swagger UI, OpenAPI spec. |
|
|
610
616
|
| `/metrics` | GET | Prometheus text-format metrics: request/query counts and latencies, pool occupancy, rate-limit rejections. |
|
|
611
617
|
| `/ui` | GET | A small admin UI: manage connections and saved queries, run ad-hoc SQL. |
|
|
@@ -35,8 +35,9 @@ postgres = ["psycopg2-binary>=2.9"]
|
|
|
35
35
|
clickhouse = ["clickhouse-driver>=0.2"]
|
|
36
36
|
h2 = ["JayDeBeApi>=1.2", "JPype1>=1.4"] # also needs a Java runtime
|
|
37
37
|
duckdb = ["duckdb>=1.0"] # Connection.interrupt()/InterruptException, relied on for query timeouts, verified from here
|
|
38
|
+
mongo = ["pymongo>=4.0"]
|
|
38
39
|
encryption = ["cryptography>=41"] # only needed when QUERYAPIGATE_SECRET_KEY is set - see store.py's _get_fernet()
|
|
39
|
-
all = ["queryapigate[mysql,postgres,clickhouse,h2,duckdb,encryption]"]
|
|
40
|
+
all = ["queryapigate[mysql,postgres,clickhouse,h2,duckdb,mongo,encryption]"]
|
|
40
41
|
server = ["gunicorn>=21"]
|
|
41
42
|
docs = ["mkdocs-material>=9,<10"]
|
|
42
43
|
dev = ["queryapigate[all]", "ruff>=0.5", "openapi-spec-validator>=0.7", "hypothesis>=6.100", "mypy>=1.10",
|
|
@@ -21,6 +21,7 @@ from . import (
|
|
|
21
21
|
examples,
|
|
22
22
|
logging_setup,
|
|
23
23
|
metrics,
|
|
24
|
+
mongotools,
|
|
24
25
|
openapi,
|
|
25
26
|
pool,
|
|
26
27
|
postman,
|
|
@@ -411,6 +412,12 @@ def execute_sql_endpoint():
|
|
|
411
412
|
if not data.get('connection_name'):
|
|
412
413
|
raise ApiError('Connection name is missing')
|
|
413
414
|
require_connection(data['connection_name'])
|
|
415
|
+
database = data.get('database')
|
|
416
|
+
if database and not g.permission.admin:
|
|
417
|
+
# Every other scoped-key boundary is per connection, never per database within one - a key granted a
|
|
418
|
+
# connection may run any SQL that connection's own configured database allows, but not redirect that
|
|
419
|
+
# same connection at a sibling database on the same server the admin never listed it for.
|
|
420
|
+
raise ApiError('Only the admin key may run a query against a different database on this connection', 403)
|
|
414
421
|
params = get_object(data.get('params'), 'params')
|
|
415
422
|
output_format = get_output_format(data)
|
|
416
423
|
timeout = get_timeout(data)
|
|
@@ -420,7 +427,29 @@ def execute_sql_endpoint():
|
|
|
420
427
|
limit, offset, page = get_pagination()
|
|
421
428
|
result = engine.execute_sql(data['sql'], data['connection_name'], limit, offset, params, timeout,
|
|
422
429
|
allow_writes=g.permission.allow_writes, key_name=caller_key_name(),
|
|
423
|
-
allowed_write_ops=g.permission.allowed_write_ops)
|
|
430
|
+
allowed_write_ops=g.permission.allowed_write_ops, database=database)
|
|
431
|
+
return render(result, output_format, page, limit)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
@bp.route('/execute_mongo', methods=['POST'])
|
|
435
|
+
def execute_mongo_endpoint():
|
|
436
|
+
"""The Mongo sibling of /execute_sql - a find() query, ad-hoc, against a saved connection. Read-only,
|
|
437
|
+
full stop (BACKLOG #36: MongoDB support is find-only in this version) - unlike /execute_sql there is no
|
|
438
|
+
write permission to check."""
|
|
439
|
+
data = get_json_body()
|
|
440
|
+
if not data.get('collection'):
|
|
441
|
+
raise ApiError('Collection is missing')
|
|
442
|
+
if not data.get('connection_name'):
|
|
443
|
+
raise ApiError('Connection name is missing')
|
|
444
|
+
require_connection(data['connection_name'])
|
|
445
|
+
filter_doc = mongotools.validate_filter(get_object(data.get('filter'), 'filter'))
|
|
446
|
+
params = get_object(data.get('params'), 'params')
|
|
447
|
+
output_format = get_output_format(data)
|
|
448
|
+
timeout = get_timeout(data)
|
|
449
|
+
limit, offset, page = get_pagination()
|
|
450
|
+
result = engine.execute_mongo(data['collection'], filter_doc, data['connection_name'], limit, offset,
|
|
451
|
+
params, timeout, projection=data.get('projection'), sort=data.get('sort'),
|
|
452
|
+
key_name=caller_key_name())
|
|
424
453
|
return render(result, output_format, page, limit)
|
|
425
454
|
|
|
426
455
|
|
|
@@ -477,19 +506,24 @@ def run_saved(ref, body, url_params):
|
|
|
477
506
|
or apikeys.can_use_collection(g.permission, store.read_collection(content)))
|
|
478
507
|
if not (granted_by_name and connection_name == saved.get('connection_name')):
|
|
479
508
|
require_connection(connection_name)
|
|
509
|
+
|
|
510
|
+
raw = {**url_params, **get_object(body.get('params'), 'params'),
|
|
511
|
+
**get_object(body.get('placeholders'), 'placeholders')}
|
|
512
|
+
output_format = get_output_format(body)
|
|
513
|
+
timeout = get_timeout(body)
|
|
514
|
+
|
|
515
|
+
if saved.get('query_type') == 'mongo':
|
|
516
|
+
return run_saved_mongo(saved, path, number, ref, connection_name, raw, output_format, timeout)
|
|
517
|
+
|
|
480
518
|
if not isinstance(saved.get('sql_query'), str):
|
|
481
519
|
raise ApiError('Saved query has no SQL', 500)
|
|
482
520
|
# Per-query write curation (apikeys.can_write_query()) only ever adds write reach for this one named
|
|
483
521
|
# query on top of whatever the key's blanket allow_writes already grants - never the other way round.
|
|
484
522
|
effective_allow_writes = g.permission.allow_writes or apikeys.can_write_query(g.permission, query_name)
|
|
485
523
|
|
|
486
|
-
raw = {**url_params, **get_object(body.get('params'), 'params'),
|
|
487
|
-
**get_object(body.get('placeholders'), 'placeholders')}
|
|
488
524
|
used = set(sqltools.placeholder_names(saved['sql_query']))
|
|
489
525
|
values = param_rules.resolve(saved.get('query_parameters'), raw, used=used)
|
|
490
526
|
sql = sqltools.fill_placeholders(saved['sql_query'], values)
|
|
491
|
-
output_format = get_output_format(body)
|
|
492
|
-
timeout = get_timeout(body)
|
|
493
527
|
|
|
494
528
|
if get_stream_flag():
|
|
495
529
|
# No caching for a streamed export - caching would require materialising the whole body anyway,
|
|
@@ -529,6 +563,32 @@ def run_saved(ref, body, url_params):
|
|
|
529
563
|
return response
|
|
530
564
|
|
|
531
565
|
|
|
566
|
+
def run_saved_mongo(saved, path, number, ref, connection_name, raw, output_format, timeout):
|
|
567
|
+
"""The mongo query_type branch of run_saved(): a find() query instead of SQL. No caching (cache_ttl is
|
|
568
|
+
not yet supported for a mongo saved query) and no streaming (?stream=true) in this version - see
|
|
569
|
+
BACKLOG #36. Always read-only, same as the ad-hoc /execute_mongo."""
|
|
570
|
+
if get_stream_flag():
|
|
571
|
+
raise ApiError('Streaming is not supported for Mongo queries yet', 400)
|
|
572
|
+
used = set(mongotools.placeholder_names(saved.get('mongo_filter') or {}))
|
|
573
|
+
values = param_rules.resolve(saved.get('query_parameters'), raw, used=used)
|
|
574
|
+
filter_doc = mongotools.fill_placeholders(saved.get('mongo_filter') or {}, values)
|
|
575
|
+
limit, offset, page = get_pagination()
|
|
576
|
+
entry = {'executed_at': store.now(), 'connection_name': connection_name,
|
|
577
|
+
'request_id': g.get('request_id'), 'key_name': caller_key_name()}
|
|
578
|
+
try:
|
|
579
|
+
result, elapsed_ms = engine.timed(engine.execute_mongo, saved['mongo_collection'], filter_doc,
|
|
580
|
+
connection_name, limit, offset, timeout=timeout,
|
|
581
|
+
projection=saved.get('mongo_projection'), sort=saved.get('mongo_sort'),
|
|
582
|
+
key_name=caller_key_name())
|
|
583
|
+
except ApiError as error:
|
|
584
|
+
store.record_execution(path, number, {**entry, 'status': 'error', 'error': error.message})
|
|
585
|
+
raise
|
|
586
|
+
response = render(result, output_format, page, limit) # sets g.serialization_ms - see render()
|
|
587
|
+
store.record_execution(path, number, {**entry, 'status': 'success', 'rows': len(result.rows),
|
|
588
|
+
'duration_ms': elapsed_ms, 'serialization_ms': g.serialization_ms})
|
|
589
|
+
return response
|
|
590
|
+
|
|
591
|
+
|
|
532
592
|
@bp.route('/execute_sql_from_file', methods=['POST'])
|
|
533
593
|
@bp.route('/execute_sql_with_parameters_from_file', methods=['POST'])
|
|
534
594
|
def execute_sql_from_file():
|
|
@@ -722,13 +782,63 @@ def update_connections():
|
|
|
722
782
|
return jsonify({'message': 'Connections updated successfully'}), 200
|
|
723
783
|
|
|
724
784
|
|
|
785
|
+
@bp.route('/connections/databases', methods=['POST'])
|
|
786
|
+
def list_databases_route():
|
|
787
|
+
"""Every database on the server a connection points at - saved, or not yet saved. Give 'name' alone for
|
|
788
|
+
an already-saved connection (Run SQL's database picker); give the connection's own fields (as
|
|
789
|
+
/connections/test does) to probe one that isn't saved yet - the New/Edit connection form's 'Default
|
|
790
|
+
database' dropdown - or to try changed fields before saving them. Admin only, like every other endpoint
|
|
791
|
+
that reveals the server's own configuration or reaches into a connection's server ahead of a real query."""
|
|
792
|
+
require_admin()
|
|
793
|
+
body = get_json_body()
|
|
794
|
+
if not isinstance(body, dict):
|
|
795
|
+
raise ApiError('Connection fields are missing')
|
|
796
|
+
if body.get('name') and not body.get('db'):
|
|
797
|
+
databases = schema.list_databases(body['name'])
|
|
798
|
+
else:
|
|
799
|
+
if body.get('db') not in config.SUPPORTED_DB_TYPES:
|
|
800
|
+
raise ApiError(f"'db' must be one of: {', '.join(config.SUPPORTED_DB_TYPES)}")
|
|
801
|
+
if body.get('password') == config.PASSWORD_MASK:
|
|
802
|
+
existing = store.read_connections().get(body.get('name') or '', {})
|
|
803
|
+
body = {**body, 'password': existing.get('password', '')}
|
|
804
|
+
databases = engine.list_databases(store.resolve_ad_hoc(body))
|
|
805
|
+
return jsonify({'databases': databases}), 200
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
@bp.route('/connections/test', methods=['POST'])
|
|
809
|
+
def test_connection_route():
|
|
810
|
+
"""Try to connect with the given fields - not a saved connection's name, the fields themselves, exactly
|
|
811
|
+
as the New/Edit connection form has them right now. Nothing is written anywhere; a masked password
|
|
812
|
+
(unchanged from an existing connection's form) is resolved to the real stored value first, the same way
|
|
813
|
+
update_connections() already treats one."""
|
|
814
|
+
require_admin()
|
|
815
|
+
details = get_json_body()
|
|
816
|
+
if not isinstance(details, dict) or details.get('db') not in config.SUPPORTED_DB_TYPES:
|
|
817
|
+
raise ApiError(f"'db' must be one of: {', '.join(config.SUPPORTED_DB_TYPES)}")
|
|
818
|
+
if details.get('password') == config.PASSWORD_MASK:
|
|
819
|
+
existing = store.read_connections().get(details.get('name') or '', {})
|
|
820
|
+
details = {**details, 'password': existing.get('password', '')}
|
|
821
|
+
result = engine.test_connection(store.resolve_ad_hoc(details))
|
|
822
|
+
return jsonify({'message': 'Connected', **result}), 200
|
|
823
|
+
|
|
824
|
+
|
|
725
825
|
@bp.route('/connections/<name>', methods=['DELETE'])
|
|
726
826
|
def delete_connection(name):
|
|
827
|
+
"""Deleting a connection is destructive and hard to reason about after the fact - every saved query that
|
|
828
|
+
used it stops working - so the caller must say why. The reason is stored nowhere but the audit log (there
|
|
829
|
+
is no separate "deleted connections" table); the admin UI's own Deleted tab reads it straight from there,
|
|
830
|
+
the same as everything else on this route already does."""
|
|
727
831
|
require_admin()
|
|
728
832
|
before = store.read_connections().get(name)
|
|
833
|
+
if before is None:
|
|
834
|
+
raise ApiError(f"Connection '{name}' not found", 404)
|
|
835
|
+
data = get_json_body(required=False)
|
|
836
|
+
reason = (data.get('reason') or '').strip()
|
|
837
|
+
if not reason:
|
|
838
|
+
raise ApiError('A reason is required to delete a connection')
|
|
729
839
|
store.delete_connection(name)
|
|
730
|
-
|
|
731
|
-
|
|
840
|
+
changes = {**store.mask_passwords({name: before})[name], 'deleted_reason': reason}
|
|
841
|
+
store.record_audit(caller_key_name(), 'delete_connection', name, changes)
|
|
732
842
|
pool.close_pooled_connections() # a removed connection must not keep serving from idle sockets
|
|
733
843
|
return jsonify({'message': f"Connection '{name}' deleted"}), 200
|
|
734
844
|
|
|
@@ -736,7 +846,10 @@ def delete_connection(name):
|
|
|
736
846
|
@bp.route('/connections/<name>/schema', methods=['GET'])
|
|
737
847
|
def connection_schema(name):
|
|
738
848
|
require_connection(name)
|
|
739
|
-
|
|
849
|
+
database = request.args.get('database') or None
|
|
850
|
+
if database and not g.permission.admin:
|
|
851
|
+
raise ApiError('Only the admin key may browse a different database on this connection', 403)
|
|
852
|
+
return jsonify(schema.fetch_schema(name, database=database)), 200
|
|
740
853
|
|
|
741
854
|
|
|
742
855
|
# --------------------------------------------------------------------------------------
|
|
@@ -846,6 +959,16 @@ def audit_log_endpoint():
|
|
|
846
959
|
return jsonify({'entries': list(reversed(store.read_audit_log()))}), 200
|
|
847
960
|
|
|
848
961
|
|
|
962
|
+
@bp.route('/settings', methods=['GET'])
|
|
963
|
+
def settings_endpoint():
|
|
964
|
+
"""The server's own configuration as the admin UI's Settings screen shows it: every environment variable
|
|
965
|
+
that matters, its effective value and whether it was set or is the default. Read-only, and admin only
|
|
966
|
+
like the rest of what reveals the server's configuration. Secrets are reported as configured or not,
|
|
967
|
+
never returned."""
|
|
968
|
+
require_admin()
|
|
969
|
+
return jsonify({'sections': config.describe_settings()}), 200
|
|
970
|
+
|
|
971
|
+
|
|
849
972
|
# --------------------------------------------------------------------------------------
|
|
850
973
|
# Service endpoints
|
|
851
974
|
# --------------------------------------------------------------------------------------
|
|
@@ -871,6 +994,14 @@ def metrics_endpoint():
|
|
|
871
994
|
return Response(metrics.render(), mimetype='text/plain; version=0.0.4; charset=utf-8')
|
|
872
995
|
|
|
873
996
|
|
|
997
|
+
def _is_runnable(data):
|
|
998
|
+
"""Whether a saved-query version has something to actually execute - a SQL string, or (query_type
|
|
999
|
+
'mongo') a Mongo collection - so a definition left mid-edit or otherwise malformed never gets listed
|
|
1000
|
+
somewhere a caller might then try to run it."""
|
|
1001
|
+
return isinstance(data.get('sql_query'), str) or (data.get('query_type') == 'mongo'
|
|
1002
|
+
and isinstance(data.get('mongo_collection'), str))
|
|
1003
|
+
|
|
1004
|
+
|
|
874
1005
|
def describe_saved_queries(permission):
|
|
875
1006
|
"""What the OpenAPI document needs to know about each saved query (never its SQL text) that
|
|
876
1007
|
``permission`` may actually call - the same test run_saved() applies, so a key scoped to specific
|
|
@@ -878,8 +1009,7 @@ def describe_saved_queries(permission):
|
|
|
878
1009
|
unrestricted (admin, or connection-wide) key sees everything, unchanged from before this filter."""
|
|
879
1010
|
described = []
|
|
880
1011
|
for name, number, data, collection in store.latest_versions():
|
|
881
|
-
|
|
882
|
-
if not isinstance(sql, str):
|
|
1012
|
+
if not _is_runnable(data):
|
|
883
1013
|
continue
|
|
884
1014
|
connection_name = data.get('connection_name')
|
|
885
1015
|
if not apikeys.can_run_saved(permission, name, collection, connection_name):
|
|
@@ -910,8 +1040,7 @@ def describe_catalog(permission):
|
|
|
910
1040
|
caller* can write through it (apikeys.can_write_query() - independent of their blanket allow_writes)."""
|
|
911
1041
|
catalog = []
|
|
912
1042
|
for name, number, data, collection in store.latest_versions():
|
|
913
|
-
|
|
914
|
-
if not isinstance(sql, str):
|
|
1043
|
+
if not _is_runnable(data):
|
|
915
1044
|
continue
|
|
916
1045
|
connection_name = data.get('connection_name')
|
|
917
1046
|
if not apikeys.can_run_saved(permission, name, collection, connection_name):
|
|
@@ -18,7 +18,8 @@ FORMAT_VERSION = 1
|
|
|
18
18
|
POLICIES = ('fail', 'skip', 'new-version')
|
|
19
19
|
# Everything an entry may carry. Anything else is an error rather than silently dropped: a field the importer
|
|
20
20
|
# ignores is a field whose loss nobody notices.
|
|
21
|
-
ENTRY_FIELDS = {'name', 'sql_query', '
|
|
21
|
+
ENTRY_FIELDS = {'name', 'sql_query', 'query_type', 'mongo_collection', 'mongo_filter', 'mongo_projection',
|
|
22
|
+
'mongo_sort', 'author', 'description', 'tags', 'query_parameters', 'connection_name',
|
|
22
23
|
'cache_ttl'}
|
|
23
24
|
|
|
24
25
|
|
|
@@ -3,7 +3,7 @@ import os
|
|
|
3
3
|
import re
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
|
-
SUPPORTED_DB_TYPES = ('mysql', 'postgres', 'clickhouse', 'sqlite', 'h2', 'jdbc', 'duckdb')
|
|
6
|
+
SUPPORTED_DB_TYPES = ('mysql', 'postgres', 'clickhouse', 'sqlite', 'h2', 'jdbc', 'duckdb', 'mongo')
|
|
7
7
|
PASSWORD_MASK = '********'
|
|
8
8
|
CONNECT_TIMEOUT = 10 # seconds
|
|
9
9
|
HISTORY_LIMIT = 50 # executions remembered per saved-query version
|
|
@@ -278,3 +278,80 @@ def slow_query_threshold():
|
|
|
278
278
|
if value == 0:
|
|
279
279
|
return None
|
|
280
280
|
return value if value > 0 else DEFAULT_SLOW_QUERY_THRESHOLD
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _seconds(value):
|
|
284
|
+
return None if value is None else f'{value:g} s'
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def describe_settings():
|
|
288
|
+
"""Every server setting the admin UI's Settings screen shows, grouped: [{id, title, description, rows}].
|
|
289
|
+
|
|
290
|
+
Each row is {label, description, env, value, source, env_value}. ``value`` is what to display (secrets are
|
|
291
|
+
reduced to "configured" / "enabled" - never the secret itself), ``source`` is "env" when the variable is set
|
|
292
|
+
and "default" when the built-in default applies, and ``env_value`` is the raw value for a "copy as .env"
|
|
293
|
+
export, or None for a secret so that export can never leak one. Read-only: settings are environment
|
|
294
|
+
variables, changed by restarting the server, not through the API."""
|
|
295
|
+
def row(label, description, env, value, secret=False):
|
|
296
|
+
raw = os.environ.get(env, '').strip()
|
|
297
|
+
return {'label': label, 'description': description, 'env': env, 'value': value,
|
|
298
|
+
'source': 'env' if raw else 'default', 'env_value': None if secret or not raw else raw}
|
|
299
|
+
|
|
300
|
+
def on_off(flag):
|
|
301
|
+
return 'on' if flag else 'off'
|
|
302
|
+
|
|
303
|
+
stream = stream_max_rows()
|
|
304
|
+
timeout, slow = query_timeout(), slow_query_threshold()
|
|
305
|
+
limit, export = rate_limit(), audit_log_export_file()
|
|
306
|
+
cors = cors_origins()
|
|
307
|
+
return [
|
|
308
|
+
{'id': 'general', 'title': 'General',
|
|
309
|
+
'description': 'Where this server keeps its files and what it loads at startup.', 'rows': [
|
|
310
|
+
row('Home directory', 'Folder holding db_connections.json, api_keys.json, roles.json and saved_sql/.',
|
|
311
|
+
'QUERYAPIGATE_HOME', str(home())),
|
|
312
|
+
row('Load examples', 'Load the example APIs at startup. Idempotent.',
|
|
313
|
+
'QUERYAPIGATE_LOAD_EXAMPLES', on_off(load_examples())),
|
|
314
|
+
row('H2 driver', 'JAR used for h2 connections.', 'QUERYAPIGATE_H2_JAR',
|
|
315
|
+
Path(h2_jar()).name + ('' if os.environ.get('QUERYAPIGATE_H2_JAR') else ' (bundled)'))]},
|
|
316
|
+
{'id': 'security', 'title': 'Security',
|
|
317
|
+
'description': 'Admin access, write protection and secrets at rest.', 'rows': [
|
|
318
|
+
row('Admin API key', 'Key with full access to this UI and the admin API. Unset means open access.',
|
|
319
|
+
'QUERYAPIGATE_API_KEY', 'configured' if api_key() else 'not set - open access', secret=True),
|
|
320
|
+
row('Allow writes', 'When off, statements that modify data are rejected.',
|
|
321
|
+
'QUERYAPIGATE_ALLOW_WRITES', on_off(allow_writes())),
|
|
322
|
+
row('Encryption at rest', 'Fernet key used to encrypt connection passwords on disk.',
|
|
323
|
+
'QUERYAPIGATE_SECRET_KEY', 'enabled' if secret_key() else 'off', secret=True),
|
|
324
|
+
row('Trusted proxy hops', 'Reverse proxies whose X-Forwarded-* headers are trusted.',
|
|
325
|
+
'QUERYAPIGATE_TRUST_PROXY', str(proxy_hops()))]},
|
|
326
|
+
{'id': 'execution', 'title': 'Query execution',
|
|
327
|
+
'description': 'Limits applied to every statement this server runs.', 'rows': [
|
|
328
|
+
row('Query timeout', 'Server-wide statement limit. A request may ask for less, never more. '
|
|
329
|
+
'0 disables it.', 'QUERYAPIGATE_QUERY_TIMEOUT', _seconds(timeout) or 'off'),
|
|
330
|
+
row('Max page size', 'Largest page_size a caller may request.', 'QUERYAPIGATE_MAX_PAGE_SIZE',
|
|
331
|
+
f'{max_page_size()} rows'),
|
|
332
|
+
row('Stream row cap', 'Row cap for ?stream=true exports.', 'QUERYAPIGATE_STREAM_MAX_ROWS',
|
|
333
|
+
'unbounded' if stream is None else f'{stream} rows'),
|
|
334
|
+
row('Slow query threshold', 'Queries slower than this are logged as warnings. 0 disables it.',
|
|
335
|
+
'QUERYAPIGATE_SLOW_QUERY_THRESHOLD', _seconds(slow) or 'off')]},
|
|
336
|
+
{'id': 'pool', 'title': 'Connection pool',
|
|
337
|
+
'description': 'Idle connections kept open per distinct connection.', 'rows': [
|
|
338
|
+
row('Pool size', 'Idle connections kept per connection. 0 disables pooling.',
|
|
339
|
+
'QUERYAPIGATE_POOL_SIZE', str(pool_size())),
|
|
340
|
+
row('Idle timeout', 'How long an idle pooled connection is kept before it is closed.',
|
|
341
|
+
'QUERYAPIGATE_POOL_IDLE_TIMEOUT', _seconds(pool_idle_timeout()))]},
|
|
342
|
+
{'id': 'traffic', 'title': 'Rate limits & CORS',
|
|
343
|
+
'description': 'Per-client throttling and browser origins allowed to call the API.', 'rows': [
|
|
344
|
+
row('Rate limit', 'Requests allowed per client. API keys and roles can set their own.',
|
|
345
|
+
'QUERYAPIGATE_RATE_LIMIT', format_rate_limit(limit) or 'off'),
|
|
346
|
+
row('CORS origins', 'Comma-separated origins, or * for any. Unset turns CORS off.',
|
|
347
|
+
'QUERYAPIGATE_CORS_ORIGINS',
|
|
348
|
+
'off' if cors is None else '*' if cors == '*' else ', '.join(sorted(cors)))]},
|
|
349
|
+
{'id': 'audit', 'title': 'Audit & logging',
|
|
350
|
+
'description': 'Retention of administrative actions and log format.', 'rows': [
|
|
351
|
+
row('Audit log limit', 'Entries kept in audit_log.json.', 'QUERYAPIGATE_AUDIT_LOG_LIMIT',
|
|
352
|
+
str(audit_log_limit())),
|
|
353
|
+
row('Audit export file', 'Append-only JSON-lines copy of every audit entry, never rolled off.',
|
|
354
|
+
'QUERYAPIGATE_AUDIT_LOG_EXPORT_FILE', 'not set' if export is None else str(export)),
|
|
355
|
+
row('JSON logs', 'One JSON object per line instead of plain text.', 'QUERYAPIGATE_JSON_LOGS',
|
|
356
|
+
on_off(json_logs()))]},
|
|
357
|
+
]
|