queryapigate 0.7.0__tar.gz → 0.8.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.0/queryapigate.egg-info → queryapigate-0.8.0}/PKG-INFO +26 -12
- {queryapigate-0.7.0 → queryapigate-0.8.0}/README.md +25 -11
- {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/__init__.py +1 -1
- {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/app.py +14 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/config.py +77 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/examples.py +6 -2
- {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/openapi.py +5 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/ui.py +785 -310
- {queryapigate-0.7.0 → queryapigate-0.8.0/queryapigate.egg-info}/PKG-INFO +26 -12
- {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate.egg-info/SOURCES.txt +1 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/tests/test_examples.py +40 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/tests/test_queryapigate.py +16 -0
- queryapigate-0.8.0/tests/test_settings.py +73 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/LICENSE +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/pyproject.toml +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/__main__.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/apikeys.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/bundle.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/cache.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/cli.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/collection_admin.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/cors.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/definitions.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/engine.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/errors.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/formats.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/lib/h2-2.2.224.jar +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/logging_setup.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/metrics.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/params.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/pool.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/postman.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/ratelimit.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/runners.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/schema.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/sqltools.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/store.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate.egg-info/dependency_links.txt +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate.egg-info/entry_points.txt +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate.egg-info/requires.txt +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate.egg-info/top_level.txt +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/setup.cfg +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/tests/test_api_keys.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/tests/test_audit_log.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/tests/test_cli.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/tests/test_collection_bundle.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/tests/test_collection_postman.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/tests/test_collections.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/tests/test_connection_encryption.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/tests/test_cors_and_rate_limit.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/tests/test_duckdb.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/tests/test_integration.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/tests/test_jdbc.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/tests/test_observability.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/tests/test_params.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/tests/test_release_check.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/tests/test_response_cache.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.0}/tests/test_roles.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.8.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.8.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
|
|
@@ -73,6 +73,12 @@ Dynamic: license-file
|
|
|
73
73
|
|
|
74
74
|
[**Full documentation**](https://AnanthaRajuC.github.io/QueryAPIGate/)
|
|
75
75
|
|
|
76
|
+
<p align="center">
|
|
77
|
+
<a href="https://www.youtube.com/watch?v=WWImFj4m95o">
|
|
78
|
+
<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
|
+
</a>
|
|
80
|
+
</p>
|
|
81
|
+
|
|
76
82
|
**QueryAPIGate** is a self-hosted, single Flask service that runs SQL against your databases and returns the results as
|
|
77
83
|
JSON, NDJSON, XML, YAML, CSV, TSV or Excel. Save a query once and it becomes a versioned endpoint with typed,
|
|
78
84
|
injection-safe parameters and run history - without writing a controller, a repository layer, pagination, auth or
|
|
@@ -103,7 +109,7 @@ Crimson Garden,Comedy,PG-13,631,2
|
|
|
103
109
|
<tr>
|
|
104
110
|
<td width="25%">
|
|
105
111
|
<a href="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/connections.png">
|
|
106
|
-
<img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/connections.png" alt="Connections
|
|
112
|
+
<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">
|
|
107
113
|
</a>
|
|
108
114
|
<br><sub>Manage connections across every supported database, with live usage per connection</sub>
|
|
109
115
|
</td>
|
|
@@ -115,13 +121,13 @@ Crimson Garden,Comedy,PG-13,631,2
|
|
|
115
121
|
</td>
|
|
116
122
|
<td width="25%">
|
|
117
123
|
<a href="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/api-keys.png">
|
|
118
|
-
<img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/api-keys.png" alt="API keys
|
|
124
|
+
<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">
|
|
119
125
|
</a>
|
|
120
126
|
<br><sub>Per-key permissions: connections, specific saved queries, expiry, rate limits - and what each key has actually done</sub>
|
|
121
127
|
</td>
|
|
122
128
|
<td width="25%">
|
|
123
129
|
<a href="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/roles.png">
|
|
124
|
-
<img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/roles.png" alt="Roles
|
|
130
|
+
<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">
|
|
125
131
|
</a>
|
|
126
132
|
<br><sub>Named roles: a reusable grant template, copied onto a key once at creation</sub>
|
|
127
133
|
</td>
|
|
@@ -132,19 +138,19 @@ Crimson Garden,Comedy,PG-13,631,2
|
|
|
132
138
|
<tr>
|
|
133
139
|
<td width="33%">
|
|
134
140
|
<a href="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/saved-query-history.png">
|
|
135
|
-
<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,
|
|
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, calling API key, rows, duration and request ID, with a status filter and search box">
|
|
136
142
|
</a>
|
|
137
143
|
<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>
|
|
138
144
|
</td>
|
|
139
145
|
<td width="33%">
|
|
140
146
|
<a href="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/audit-log.png">
|
|
141
|
-
<img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/audit-log.png" alt="Audit
|
|
147
|
+
<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">
|
|
142
148
|
</a>
|
|
143
149
|
<br><sub>A durable audit log of every configuration change, filterable by action, actor and target</sub>
|
|
144
150
|
</td>
|
|
145
151
|
<td width="33%">
|
|
146
152
|
<a href="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/metrics.png">
|
|
147
|
-
<img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/metrics.png" alt="Metrics
|
|
153
|
+
<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">
|
|
148
154
|
</a>
|
|
149
155
|
<br><sub>Live metrics with no Prometheus required - or import the bundled Grafana dashboard for history</sub>
|
|
150
156
|
</td>
|
|
@@ -335,6 +341,11 @@ version management, API key management, an audit log of administrative changes,
|
|
|
335
341
|
response inspection with a collapsible JSON tree for non-tabular results, a quick bar chart of any numeric
|
|
336
342
|
result, and one-click "copy as curl" / "copy as TSV" for any result.
|
|
337
343
|
|
|
344
|
+
The UI has a collapsible sidebar (Data, Access, Observability; `Ctrl`+`B` toggles it) and a read-only **Settings**
|
|
345
|
+
screen that shows every environment variable, its effective value and whether it was set or is the default (secrets are
|
|
346
|
+
reported as configured or not, never shown), with a "Copy as .env" button. Theme, table density and the default
|
|
347
|
+
result format are per-browser preferences.
|
|
348
|
+
|
|
338
349
|
```text
|
|
339
350
|
Connect Database
|
|
340
351
|
|
|
|
@@ -610,7 +621,7 @@ instead of it - a request has to pass both. See [Per-key rate limiting](document
|
|
|
610
621
|
Every release is published to GitHub Container Registry for `linux/amd64` and `linux/arm64`:
|
|
611
622
|
|
|
612
623
|
~~~bash
|
|
613
|
-
docker run -p 5000:5000 -v
|
|
624
|
+
docker run -p 5000:5000 -v queryapigate-data:/data -e QUERYAPIGATE_API_KEY=change-me ghcr.io/anantharajuc/queryapigate:latest
|
|
614
625
|
~~~
|
|
615
626
|
|
|
616
627
|
| Tag | Contents |
|
|
@@ -619,7 +630,10 @@ docker run -p 5000:5000 -v "$PWD/data:/data" -e QUERYAPIGATE_API_KEY=change-me g
|
|
|
619
630
|
| `X.Y.Z-h2`, `latest-h2` | The same plus Java and the H2 driver - also the variant to use for a generic `jdbc` connection (mount your vendor's jar) |
|
|
620
631
|
|
|
621
632
|
The container keeps `db_connections.json` and `saved_sql/` in `/data` (create a starter with
|
|
622
|
-
`docker run --rm -v
|
|
633
|
+
`docker run --rm -v queryapigate-data:/data ghcr.io/anantharajuc/queryapigate queryapigate init`). The named volume above works
|
|
634
|
+
out of the box. To use a folder on the host instead (`-v "$PWD/data:/data"`), create it yourself first (`mkdir data`) and make sure
|
|
635
|
+
it is writable by uid 1000, the container's user - a folder that Docker creates for you is owned by root, which the container
|
|
636
|
+
cannot write to (or run with `--user "$(id -u):$(id -g)"`). It runs as a non-root user under
|
|
623
637
|
gunicorn with one worker (the files are protected by an in-process lock) and a health check on `/health`. Behind a
|
|
624
638
|
reverse proxy or load balancer, set `QUERYAPIGATE_TRUST_PROXY=1`. To build it yourself:
|
|
625
639
|
`docker build -t queryapigate .` (add `--build-arg WITH_H2=true` for H2).
|
|
@@ -657,6 +671,7 @@ Clean up with `docker compose down -v`.
|
|
|
657
671
|
| `/api_keys` | GET, POST | List / create scoped API keys (admin only). |
|
|
658
672
|
| `/api_keys/<name>` | PATCH, DELETE | Update / revoke a scoped API key (admin only). |
|
|
659
673
|
| `/audit_log` | GET | Durable record of administrative changes - keys, connections, saved queries (admin only). |
|
|
674
|
+
| `/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). |
|
|
660
675
|
| `/health`, `/docs`, `/openapi.json` | GET | Liveness, Swagger UI, OpenAPI spec. |
|
|
661
676
|
| `/metrics` | GET | Prometheus text-format metrics: request/query counts and latencies, pool occupancy, rate-limit rejections. |
|
|
662
677
|
| `/ui` | GET | A small admin UI: manage connections and saved queries, run ad-hoc SQL. |
|
|
@@ -734,9 +749,8 @@ In plain terms - the [LICENSE](https://github.com/AnanthaRajuC/QueryAPIGate/blob
|
|
|
734
749
|
- **You may not** make it available to others in a commercial product or service that substitutes for QueryAPIGate or
|
|
735
750
|
offers the same or substantially similar functionality (for example, selling it, or hosting it as a paid service).
|
|
736
751
|
- **Each version becomes MIT** on the second anniversary of the date it was released, and can then be used under MIT terms.
|
|
737
|
-
- **Versions up to and including 0.6.1** were released under the MIT license
|
|
738
|
-
|
|
739
|
-
[`sql2api`](https://pypi.org/project/sql2api/) 0.1.0-0.4.0).
|
|
752
|
+
- **Versions up to and including 0.6.1** were released under the MIT license. They are no longer distributed (and the
|
|
753
|
+
repository history was reset in 0.7.0), but a copy you already obtained under MIT remains yours under those terms.
|
|
740
754
|
|
|
741
755
|
Need something the license does not cover, such as offering it as part of a hosted service? Get in touch - commercial
|
|
742
756
|
licensing may be available. This is a summary, not legal advice.
|
|
@@ -13,6 +13,12 @@
|
|
|
13
13
|
|
|
14
14
|
[**Full documentation**](https://AnanthaRajuC.github.io/QueryAPIGate/)
|
|
15
15
|
|
|
16
|
+
<p align="center">
|
|
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
|
+
</a>
|
|
20
|
+
</p>
|
|
21
|
+
|
|
16
22
|
**QueryAPIGate** is a self-hosted, single Flask service that runs SQL against your databases and returns the results as
|
|
17
23
|
JSON, NDJSON, XML, YAML, CSV, TSV or Excel. Save a query once and it becomes a versioned endpoint with typed,
|
|
18
24
|
injection-safe parameters and run history - without writing a controller, a repository layer, pagination, auth or
|
|
@@ -43,7 +49,7 @@ Crimson Garden,Comedy,PG-13,631,2
|
|
|
43
49
|
<tr>
|
|
44
50
|
<td width="25%">
|
|
45
51
|
<a href="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/connections.png">
|
|
46
|
-
<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">
|
|
47
53
|
</a>
|
|
48
54
|
<br><sub>Manage connections across every supported database, with live usage per connection</sub>
|
|
49
55
|
</td>
|
|
@@ -55,13 +61,13 @@ Crimson Garden,Comedy,PG-13,631,2
|
|
|
55
61
|
</td>
|
|
56
62
|
<td width="25%">
|
|
57
63
|
<a href="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/api-keys.png">
|
|
58
|
-
<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">
|
|
59
65
|
</a>
|
|
60
66
|
<br><sub>Per-key permissions: connections, specific saved queries, expiry, rate limits - and what each key has actually done</sub>
|
|
61
67
|
</td>
|
|
62
68
|
<td width="25%">
|
|
63
69
|
<a href="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/roles.png">
|
|
64
|
-
<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">
|
|
65
71
|
</a>
|
|
66
72
|
<br><sub>Named roles: a reusable grant template, copied onto a key once at creation</sub>
|
|
67
73
|
</td>
|
|
@@ -72,19 +78,19 @@ Crimson Garden,Comedy,PG-13,631,2
|
|
|
72
78
|
<tr>
|
|
73
79
|
<td width="33%">
|
|
74
80
|
<a href="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/saved-query-history.png">
|
|
75
|
-
<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">
|
|
76
82
|
</a>
|
|
77
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>
|
|
78
84
|
</td>
|
|
79
85
|
<td width="33%">
|
|
80
86
|
<a href="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/audit-log.png">
|
|
81
|
-
<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">
|
|
82
88
|
</a>
|
|
83
89
|
<br><sub>A durable audit log of every configuration change, filterable by action, actor and target</sub>
|
|
84
90
|
</td>
|
|
85
91
|
<td width="33%">
|
|
86
92
|
<a href="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/metrics.png">
|
|
87
|
-
<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">
|
|
88
94
|
</a>
|
|
89
95
|
<br><sub>Live metrics with no Prometheus required - or import the bundled Grafana dashboard for history</sub>
|
|
90
96
|
</td>
|
|
@@ -275,6 +281,11 @@ version management, API key management, an audit log of administrative changes,
|
|
|
275
281
|
response inspection with a collapsible JSON tree for non-tabular results, a quick bar chart of any numeric
|
|
276
282
|
result, and one-click "copy as curl" / "copy as TSV" for any result.
|
|
277
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
|
+
|
|
278
289
|
```text
|
|
279
290
|
Connect Database
|
|
280
291
|
|
|
|
@@ -550,7 +561,7 @@ instead of it - a request has to pass both. See [Per-key rate limiting](document
|
|
|
550
561
|
Every release is published to GitHub Container Registry for `linux/amd64` and `linux/arm64`:
|
|
551
562
|
|
|
552
563
|
~~~bash
|
|
553
|
-
docker run -p 5000:5000 -v
|
|
564
|
+
docker run -p 5000:5000 -v queryapigate-data:/data -e QUERYAPIGATE_API_KEY=change-me ghcr.io/anantharajuc/queryapigate:latest
|
|
554
565
|
~~~
|
|
555
566
|
|
|
556
567
|
| Tag | Contents |
|
|
@@ -559,7 +570,10 @@ docker run -p 5000:5000 -v "$PWD/data:/data" -e QUERYAPIGATE_API_KEY=change-me g
|
|
|
559
570
|
| `X.Y.Z-h2`, `latest-h2` | The same plus Java and the H2 driver - also the variant to use for a generic `jdbc` connection (mount your vendor's jar) |
|
|
560
571
|
|
|
561
572
|
The container keeps `db_connections.json` and `saved_sql/` in `/data` (create a starter with
|
|
562
|
-
`docker run --rm -v
|
|
573
|
+
`docker run --rm -v queryapigate-data:/data ghcr.io/anantharajuc/queryapigate queryapigate init`). The named volume above works
|
|
574
|
+
out of the box. To use a folder on the host instead (`-v "$PWD/data:/data"`), create it yourself first (`mkdir data`) and make sure
|
|
575
|
+
it is writable by uid 1000, the container's user - a folder that Docker creates for you is owned by root, which the container
|
|
576
|
+
cannot write to (or run with `--user "$(id -u):$(id -g)"`). It runs as a non-root user under
|
|
563
577
|
gunicorn with one worker (the files are protected by an in-process lock) and a health check on `/health`. Behind a
|
|
564
578
|
reverse proxy or load balancer, set `QUERYAPIGATE_TRUST_PROXY=1`. To build it yourself:
|
|
565
579
|
`docker build -t queryapigate .` (add `--build-arg WITH_H2=true` for H2).
|
|
@@ -597,6 +611,7 @@ Clean up with `docker compose down -v`.
|
|
|
597
611
|
| `/api_keys` | GET, POST | List / create scoped API keys (admin only). |
|
|
598
612
|
| `/api_keys/<name>` | PATCH, DELETE | Update / revoke a scoped API key (admin only). |
|
|
599
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). |
|
|
600
615
|
| `/health`, `/docs`, `/openapi.json` | GET | Liveness, Swagger UI, OpenAPI spec. |
|
|
601
616
|
| `/metrics` | GET | Prometheus text-format metrics: request/query counts and latencies, pool occupancy, rate-limit rejections. |
|
|
602
617
|
| `/ui` | GET | A small admin UI: manage connections and saved queries, run ad-hoc SQL. |
|
|
@@ -674,9 +689,8 @@ In plain terms - the [LICENSE](https://github.com/AnanthaRajuC/QueryAPIGate/blob
|
|
|
674
689
|
- **You may not** make it available to others in a commercial product or service that substitutes for QueryAPIGate or
|
|
675
690
|
offers the same or substantially similar functionality (for example, selling it, or hosting it as a paid service).
|
|
676
691
|
- **Each version becomes MIT** on the second anniversary of the date it was released, and can then be used under MIT terms.
|
|
677
|
-
- **Versions up to and including 0.6.1** were released under the MIT license
|
|
678
|
-
|
|
679
|
-
[`sql2api`](https://pypi.org/project/sql2api/) 0.1.0-0.4.0).
|
|
692
|
+
- **Versions up to and including 0.6.1** were released under the MIT license. They are no longer distributed (and the
|
|
693
|
+
repository history was reset in 0.7.0), but a copy you already obtained under MIT remains yours under those terms.
|
|
680
694
|
|
|
681
695
|
Need something the license does not cover, such as offering it as part of a hosted service? Get in touch - commercial
|
|
682
696
|
licensing may be available. This is a summary, not legal advice.
|
|
@@ -76,6 +76,10 @@ def load_examples_at_startup():
|
|
|
76
76
|
log.warning('QUERYAPIGATE_LOAD_EXAMPLES is set but the examples could not be loaded: %s',
|
|
77
77
|
getattr(error, 'message', error))
|
|
78
78
|
return
|
|
79
|
+
except Exception as error: # a convenience must never take the server down, whatever went wrong
|
|
80
|
+
log.warning('QUERYAPIGATE_LOAD_EXAMPLES is set but the examples could not be loaded (unexpected error: %s)',
|
|
81
|
+
error, exc_info=True)
|
|
82
|
+
return
|
|
79
83
|
if added['connection'] or added['queries'] or added['roles']:
|
|
80
84
|
store.record_audit('startup', 'load_examples', 'examples', added)
|
|
81
85
|
log.info('Loaded the example APIs: %d queries, %d roles', len(added['queries']), len(added['roles']))
|
|
@@ -842,6 +846,16 @@ def audit_log_endpoint():
|
|
|
842
846
|
return jsonify({'entries': list(reversed(store.read_audit_log()))}), 200
|
|
843
847
|
|
|
844
848
|
|
|
849
|
+
@bp.route('/settings', methods=['GET'])
|
|
850
|
+
def settings_endpoint():
|
|
851
|
+
"""The server's own configuration as the admin UI's Settings screen shows it: every environment variable
|
|
852
|
+
that matters, its effective value and whether it was set or is the default. Read-only, and admin only
|
|
853
|
+
like the rest of what reveals the server's configuration. Secrets are reported as configured or not,
|
|
854
|
+
never returned."""
|
|
855
|
+
require_admin()
|
|
856
|
+
return jsonify({'sections': config.describe_settings()}), 200
|
|
857
|
+
|
|
858
|
+
|
|
845
859
|
# --------------------------------------------------------------------------------------
|
|
846
860
|
# Service endpoints
|
|
847
861
|
# --------------------------------------------------------------------------------------
|
|
@@ -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
|
+
]
|
|
@@ -19,6 +19,7 @@ walkthrough in ``documentation/EXAMPLES.md`` shows how to create one from the ex
|
|
|
19
19
|
"""
|
|
20
20
|
import os
|
|
21
21
|
import random
|
|
22
|
+
import sqlite3
|
|
22
23
|
from datetime import datetime, timedelta, timezone
|
|
23
24
|
from typing import Any
|
|
24
25
|
|
|
@@ -78,7 +79,6 @@ CREATE INDEX rental_film_idx ON rental(film_id);
|
|
|
78
79
|
def build_database(path, now=None, seed=42):
|
|
79
80
|
"""Create the example SQLite database at ``path`` (which must not exist). Deterministic for a given ``now`` and
|
|
80
81
|
``seed``. Dates are UTC, to match SQLite's own ``'now'`` that the example queries compare against."""
|
|
81
|
-
import sqlite3
|
|
82
82
|
rng = random.Random(seed)
|
|
83
83
|
now = now or datetime.now(timezone.utc).replace(tzinfo=None)
|
|
84
84
|
titles = [f'{a} {n}' for a in _ADJECTIVES for n in _NOUNS]
|
|
@@ -274,7 +274,11 @@ def load(now=None):
|
|
|
274
274
|
db_path = config.home() / DB_FILE
|
|
275
275
|
if _marked_connection() is None:
|
|
276
276
|
if not db_path.exists():
|
|
277
|
-
|
|
277
|
+
try:
|
|
278
|
+
build_database(str(db_path), now)
|
|
279
|
+
except sqlite3.Error as error: # SQLite's own error type - not an OSError, so callers would miss it
|
|
280
|
+
raise ApiError(f'Could not create the example database in {config.home()} ({error}). Is the data '
|
|
281
|
+
'folder writable by the user QueryAPIGate runs as?', 500) from None
|
|
278
282
|
store.update_connections({CONNECTION: {'db': 'sqlite', 'database': DB_FILE, 'active': True,
|
|
279
283
|
'example': True}})
|
|
280
284
|
added['connection'] = True
|
|
@@ -400,6 +400,11 @@ def build_spec(version, saved_queries=None):
|
|
|
400
400
|
'summary': 'A durable record of administrative changes - API keys, roles, connections and '
|
|
401
401
|
'saved queries created, changed or removed (admin only)', 'tags': ['API keys'],
|
|
402
402
|
'responses': {'200': {'description': 'Newest entry first, capped at 500 entries'}, **_ERRORS}}},
|
|
403
|
+
'/settings': {'get': {
|
|
404
|
+
'summary': "The server's own configuration: each setting's effective value and whether it comes "
|
|
405
|
+
'from the environment or is the default (admin only, read-only; secrets are reported '
|
|
406
|
+
'as configured or not, never returned)', 'tags': ['Service'],
|
|
407
|
+
'responses': {'200': {'description': 'Settings grouped into sections'}, **_ERRORS}}},
|
|
403
408
|
'/catalog': {'get': {
|
|
404
409
|
'summary': 'Every saved query this caller can reach, and the terms it is offered under',
|
|
405
410
|
'tags': ['Saved queries'],
|