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.
Files changed (59) hide show
  1. {queryapigate-0.7.0/queryapigate.egg-info → queryapigate-0.8.0}/PKG-INFO +26 -12
  2. {queryapigate-0.7.0 → queryapigate-0.8.0}/README.md +25 -11
  3. {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/__init__.py +1 -1
  4. {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/app.py +14 -0
  5. {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/config.py +77 -0
  6. {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/examples.py +6 -2
  7. {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/openapi.py +5 -0
  8. {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/ui.py +785 -310
  9. {queryapigate-0.7.0 → queryapigate-0.8.0/queryapigate.egg-info}/PKG-INFO +26 -12
  10. {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate.egg-info/SOURCES.txt +1 -0
  11. {queryapigate-0.7.0 → queryapigate-0.8.0}/tests/test_examples.py +40 -0
  12. {queryapigate-0.7.0 → queryapigate-0.8.0}/tests/test_queryapigate.py +16 -0
  13. queryapigate-0.8.0/tests/test_settings.py +73 -0
  14. {queryapigate-0.7.0 → queryapigate-0.8.0}/LICENSE +0 -0
  15. {queryapigate-0.7.0 → queryapigate-0.8.0}/pyproject.toml +0 -0
  16. {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/__main__.py +0 -0
  17. {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/apikeys.py +0 -0
  18. {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/bundle.py +0 -0
  19. {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/cache.py +0 -0
  20. {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/cli.py +0 -0
  21. {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/collection_admin.py +0 -0
  22. {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/cors.py +0 -0
  23. {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/definitions.py +0 -0
  24. {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/engine.py +0 -0
  25. {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/errors.py +0 -0
  26. {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/formats.py +0 -0
  27. {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/lib/h2-2.2.224.jar +0 -0
  28. {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/logging_setup.py +0 -0
  29. {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/metrics.py +0 -0
  30. {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/params.py +0 -0
  31. {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/pool.py +0 -0
  32. {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/postman.py +0 -0
  33. {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/ratelimit.py +0 -0
  34. {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/runners.py +0 -0
  35. {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/schema.py +0 -0
  36. {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/sqltools.py +0 -0
  37. {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate/store.py +0 -0
  38. {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate.egg-info/dependency_links.txt +0 -0
  39. {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate.egg-info/entry_points.txt +0 -0
  40. {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate.egg-info/requires.txt +0 -0
  41. {queryapigate-0.7.0 → queryapigate-0.8.0}/queryapigate.egg-info/top_level.txt +0 -0
  42. {queryapigate-0.7.0 → queryapigate-0.8.0}/setup.cfg +0 -0
  43. {queryapigate-0.7.0 → queryapigate-0.8.0}/tests/test_api_keys.py +0 -0
  44. {queryapigate-0.7.0 → queryapigate-0.8.0}/tests/test_audit_log.py +0 -0
  45. {queryapigate-0.7.0 → queryapigate-0.8.0}/tests/test_cli.py +0 -0
  46. {queryapigate-0.7.0 → queryapigate-0.8.0}/tests/test_collection_bundle.py +0 -0
  47. {queryapigate-0.7.0 → queryapigate-0.8.0}/tests/test_collection_postman.py +0 -0
  48. {queryapigate-0.7.0 → queryapigate-0.8.0}/tests/test_collections.py +0 -0
  49. {queryapigate-0.7.0 → queryapigate-0.8.0}/tests/test_connection_encryption.py +0 -0
  50. {queryapigate-0.7.0 → queryapigate-0.8.0}/tests/test_cors_and_rate_limit.py +0 -0
  51. {queryapigate-0.7.0 → queryapigate-0.8.0}/tests/test_duckdb.py +0 -0
  52. {queryapigate-0.7.0 → queryapigate-0.8.0}/tests/test_integration.py +0 -0
  53. {queryapigate-0.7.0 → queryapigate-0.8.0}/tests/test_jdbc.py +0 -0
  54. {queryapigate-0.7.0 → queryapigate-0.8.0}/tests/test_observability.py +0 -0
  55. {queryapigate-0.7.0 → queryapigate-0.8.0}/tests/test_params.py +0 -0
  56. {queryapigate-0.7.0 → queryapigate-0.8.0}/tests/test_release_check.py +0 -0
  57. {queryapigate-0.7.0 → queryapigate-0.8.0}/tests/test_response_cache.py +0 -0
  58. {queryapigate-0.7.0 → queryapigate-0.8.0}/tests/test_roles.py +0 -0
  59. {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.7.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 tab listing ClickHouse, MySQL, PostgreSQL and SQLite connections with active/inactive status and each connection's live usage: queries run, failures and average latency">
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 tab 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">
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 tab listing reusable permission templates - connections, read/write access, rate limit and IP allowlisting - with a New key from this shortcut">
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, connection, calling API key, rows, duration and request ID, with a status filter and search box">
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 Log tab listing every administrative change - saved queries, roles and API keys created or updated - with an action filter and search box">
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 tab with stat tiles for requests, error rate, active queries, pool and rate limiting, bar charts of requests by status and queries by connection, and a per-connection latency table">
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 "$PWD/data:/data" -e QUERYAPIGATE_API_KEY=change-me ghcr.io/anantharajuc/queryapigate:latest
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 "$PWD/data:/data" ghcr.io/anantharajuc/queryapigate queryapigate init`). It runs as a non-root user under
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 and remain available under it (the
738
- releases on PyPI: [`queryapigate`](https://pypi.org/project/queryapigate/) 0.5.0-0.6.1 and
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 tab listing ClickHouse, MySQL, PostgreSQL and SQLite connections with active/inactive status and each connection's live usage: queries run, failures and average latency">
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 tab 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">
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 tab listing reusable permission templates - connections, read/write access, rate limit and IP allowlisting - with a New key from this shortcut">
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, connection, calling API key, rows, duration and request ID, with a status filter and search box">
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 Log tab listing every administrative change - saved queries, roles and API keys created or updated - with an action filter and search box">
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 tab with stat tiles for requests, error rate, active queries, pool and rate limiting, bar charts of requests by status and queries by connection, and a per-connection latency table">
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 "$PWD/data:/data" -e QUERYAPIGATE_API_KEY=change-me ghcr.io/anantharajuc/queryapigate:latest
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 "$PWD/data:/data" ghcr.io/anantharajuc/queryapigate queryapigate init`). It runs as a non-root user under
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 and remain available under it (the
678
- releases on PyPI: [`queryapigate`](https://pypi.org/project/queryapigate/) 0.5.0-0.6.1 and
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.
@@ -1,5 +1,5 @@
1
1
  """QueryAPIGate - expose SQL databases as a REST API."""
2
- __version__ = '0.7.0'
2
+ __version__ = '0.8.0'
3
3
 
4
4
  from .app import create_app # noqa: E402 (app imports __version__)
5
5
 
@@ -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
- build_database(str(db_path), now)
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'],