queryapigate 0.7.0__tar.gz → 0.7.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {queryapigate-0.7.0/queryapigate.egg-info → queryapigate-0.7.1}/PKG-INFO +14 -6
- {queryapigate-0.7.0 → queryapigate-0.7.1}/README.md +13 -5
- {queryapigate-0.7.0 → queryapigate-0.7.1}/queryapigate/__init__.py +1 -1
- {queryapigate-0.7.0 → queryapigate-0.7.1}/queryapigate/app.py +4 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/queryapigate/examples.py +6 -2
- {queryapigate-0.7.0 → queryapigate-0.7.1/queryapigate.egg-info}/PKG-INFO +14 -6
- {queryapigate-0.7.0 → queryapigate-0.7.1}/tests/test_examples.py +40 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/LICENSE +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/pyproject.toml +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/queryapigate/__main__.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/queryapigate/apikeys.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/queryapigate/bundle.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/queryapigate/cache.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/queryapigate/cli.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/queryapigate/collection_admin.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/queryapigate/config.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/queryapigate/cors.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/queryapigate/definitions.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/queryapigate/engine.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/queryapigate/errors.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/queryapigate/formats.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/queryapigate/lib/h2-2.2.224.jar +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/queryapigate/logging_setup.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/queryapigate/metrics.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/queryapigate/openapi.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/queryapigate/params.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/queryapigate/pool.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/queryapigate/postman.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/queryapigate/ratelimit.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/queryapigate/runners.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/queryapigate/schema.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/queryapigate/sqltools.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/queryapigate/store.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/queryapigate/ui.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/queryapigate.egg-info/SOURCES.txt +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/queryapigate.egg-info/dependency_links.txt +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/queryapigate.egg-info/entry_points.txt +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/queryapigate.egg-info/requires.txt +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/queryapigate.egg-info/top_level.txt +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/setup.cfg +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/tests/test_api_keys.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/tests/test_audit_log.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/tests/test_cli.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/tests/test_collection_bundle.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/tests/test_collection_postman.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/tests/test_collections.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/tests/test_connection_encryption.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/tests/test_cors_and_rate_limit.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/tests/test_duckdb.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/tests/test_integration.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/tests/test_jdbc.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/tests/test_observability.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/tests/test_params.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/tests/test_queryapigate.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/tests/test_release_check.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/tests/test_response_cache.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/tests/test_roles.py +0 -0
- {queryapigate-0.7.0 → queryapigate-0.7.1}/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.
|
|
3
|
+
Version: 0.7.1
|
|
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=aiVbAkA2LS8">
|
|
78
|
+
<img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/demo-thumbnail.png" alt="Watch the 60-second 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
|
|
@@ -610,7 +616,7 @@ instead of it - a request has to pass both. See [Per-key rate limiting](document
|
|
|
610
616
|
Every release is published to GitHub Container Registry for `linux/amd64` and `linux/arm64`:
|
|
611
617
|
|
|
612
618
|
~~~bash
|
|
613
|
-
docker run -p 5000:5000 -v
|
|
619
|
+
docker run -p 5000:5000 -v queryapigate-data:/data -e QUERYAPIGATE_API_KEY=change-me ghcr.io/anantharajuc/queryapigate:latest
|
|
614
620
|
~~~
|
|
615
621
|
|
|
616
622
|
| Tag | Contents |
|
|
@@ -619,7 +625,10 @@ docker run -p 5000:5000 -v "$PWD/data:/data" -e QUERYAPIGATE_API_KEY=change-me g
|
|
|
619
625
|
| `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
626
|
|
|
621
627
|
The container keeps `db_connections.json` and `saved_sql/` in `/data` (create a starter with
|
|
622
|
-
`docker run --rm -v
|
|
628
|
+
`docker run --rm -v queryapigate-data:/data ghcr.io/anantharajuc/queryapigate queryapigate init`). The named volume above works
|
|
629
|
+
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
|
|
630
|
+
it is writable by uid 1000, the container's user - a folder that Docker creates for you is owned by root, which the container
|
|
631
|
+
cannot write to (or run with `--user "$(id -u):$(id -g)"`). It runs as a non-root user under
|
|
623
632
|
gunicorn with one worker (the files are protected by an in-process lock) and a health check on `/health`. Behind a
|
|
624
633
|
reverse proxy or load balancer, set `QUERYAPIGATE_TRUST_PROXY=1`. To build it yourself:
|
|
625
634
|
`docker build -t queryapigate .` (add `--build-arg WITH_H2=true` for H2).
|
|
@@ -734,9 +743,8 @@ In plain terms - the [LICENSE](https://github.com/AnanthaRajuC/QueryAPIGate/blob
|
|
|
734
743
|
- **You may not** make it available to others in a commercial product or service that substitutes for QueryAPIGate or
|
|
735
744
|
offers the same or substantially similar functionality (for example, selling it, or hosting it as a paid service).
|
|
736
745
|
- **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).
|
|
746
|
+
- **Versions up to and including 0.6.1** were released under the MIT license. They are no longer distributed (and the
|
|
747
|
+
repository history was reset in 0.7.0), but a copy you already obtained under MIT remains yours under those terms.
|
|
740
748
|
|
|
741
749
|
Need something the license does not cover, such as offering it as part of a hosted service? Get in touch - commercial
|
|
742
750
|
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=aiVbAkA2LS8">
|
|
18
|
+
<img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/demo-thumbnail.png" alt="Watch the 60-second 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
|
|
@@ -550,7 +556,7 @@ instead of it - a request has to pass both. See [Per-key rate limiting](document
|
|
|
550
556
|
Every release is published to GitHub Container Registry for `linux/amd64` and `linux/arm64`:
|
|
551
557
|
|
|
552
558
|
~~~bash
|
|
553
|
-
docker run -p 5000:5000 -v
|
|
559
|
+
docker run -p 5000:5000 -v queryapigate-data:/data -e QUERYAPIGATE_API_KEY=change-me ghcr.io/anantharajuc/queryapigate:latest
|
|
554
560
|
~~~
|
|
555
561
|
|
|
556
562
|
| Tag | Contents |
|
|
@@ -559,7 +565,10 @@ docker run -p 5000:5000 -v "$PWD/data:/data" -e QUERYAPIGATE_API_KEY=change-me g
|
|
|
559
565
|
| `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
566
|
|
|
561
567
|
The container keeps `db_connections.json` and `saved_sql/` in `/data` (create a starter with
|
|
562
|
-
`docker run --rm -v
|
|
568
|
+
`docker run --rm -v queryapigate-data:/data ghcr.io/anantharajuc/queryapigate queryapigate init`). The named volume above works
|
|
569
|
+
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
|
|
570
|
+
it is writable by uid 1000, the container's user - a folder that Docker creates for you is owned by root, which the container
|
|
571
|
+
cannot write to (or run with `--user "$(id -u):$(id -g)"`). It runs as a non-root user under
|
|
563
572
|
gunicorn with one worker (the files are protected by an in-process lock) and a health check on `/health`. Behind a
|
|
564
573
|
reverse proxy or load balancer, set `QUERYAPIGATE_TRUST_PROXY=1`. To build it yourself:
|
|
565
574
|
`docker build -t queryapigate .` (add `--build-arg WITH_H2=true` for H2).
|
|
@@ -674,9 +683,8 @@ In plain terms - the [LICENSE](https://github.com/AnanthaRajuC/QueryAPIGate/blob
|
|
|
674
683
|
- **You may not** make it available to others in a commercial product or service that substitutes for QueryAPIGate or
|
|
675
684
|
offers the same or substantially similar functionality (for example, selling it, or hosting it as a paid service).
|
|
676
685
|
- **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).
|
|
686
|
+
- **Versions up to and including 0.6.1** were released under the MIT license. They are no longer distributed (and the
|
|
687
|
+
repository history was reset in 0.7.0), but a copy you already obtained under MIT remains yours under those terms.
|
|
680
688
|
|
|
681
689
|
Need something the license does not cover, such as offering it as part of a hosted service? Get in touch - commercial
|
|
682
690
|
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']))
|
|
@@ -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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: queryapigate
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.1
|
|
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=aiVbAkA2LS8">
|
|
78
|
+
<img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/demo-thumbnail.png" alt="Watch the 60-second 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
|
|
@@ -610,7 +616,7 @@ instead of it - a request has to pass both. See [Per-key rate limiting](document
|
|
|
610
616
|
Every release is published to GitHub Container Registry for `linux/amd64` and `linux/arm64`:
|
|
611
617
|
|
|
612
618
|
~~~bash
|
|
613
|
-
docker run -p 5000:5000 -v
|
|
619
|
+
docker run -p 5000:5000 -v queryapigate-data:/data -e QUERYAPIGATE_API_KEY=change-me ghcr.io/anantharajuc/queryapigate:latest
|
|
614
620
|
~~~
|
|
615
621
|
|
|
616
622
|
| Tag | Contents |
|
|
@@ -619,7 +625,10 @@ docker run -p 5000:5000 -v "$PWD/data:/data" -e QUERYAPIGATE_API_KEY=change-me g
|
|
|
619
625
|
| `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
626
|
|
|
621
627
|
The container keeps `db_connections.json` and `saved_sql/` in `/data` (create a starter with
|
|
622
|
-
`docker run --rm -v
|
|
628
|
+
`docker run --rm -v queryapigate-data:/data ghcr.io/anantharajuc/queryapigate queryapigate init`). The named volume above works
|
|
629
|
+
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
|
|
630
|
+
it is writable by uid 1000, the container's user - a folder that Docker creates for you is owned by root, which the container
|
|
631
|
+
cannot write to (or run with `--user "$(id -u):$(id -g)"`). It runs as a non-root user under
|
|
623
632
|
gunicorn with one worker (the files are protected by an in-process lock) and a health check on `/health`. Behind a
|
|
624
633
|
reverse proxy or load balancer, set `QUERYAPIGATE_TRUST_PROXY=1`. To build it yourself:
|
|
625
634
|
`docker build -t queryapigate .` (add `--build-arg WITH_H2=true` for H2).
|
|
@@ -734,9 +743,8 @@ In plain terms - the [LICENSE](https://github.com/AnanthaRajuC/QueryAPIGate/blob
|
|
|
734
743
|
- **You may not** make it available to others in a commercial product or service that substitutes for QueryAPIGate or
|
|
735
744
|
offers the same or substantially similar functionality (for example, selling it, or hosting it as a paid service).
|
|
736
745
|
- **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).
|
|
746
|
+
- **Versions up to and including 0.6.1** were released under the MIT license. They are no longer distributed (and the
|
|
747
|
+
repository history was reset in 0.7.0), but a copy you already obtained under MIT remains yours under those terms.
|
|
740
748
|
|
|
741
749
|
Need something the license does not cover, such as offering it as part of a hosted service? Get in touch - commercial
|
|
742
750
|
licensing may be available. This is a summary, not legal advice.
|
|
@@ -39,6 +39,14 @@ class ExamplesTestCase(unittest.TestCase):
|
|
|
39
39
|
store.save_version(name, {'sql_query': 'SELECT 1', 'author': 'me', 'description': 'mine', 'tags': [],
|
|
40
40
|
'query_parameters': {}}, collection)
|
|
41
41
|
|
|
42
|
+
def make_home_unwritable(self):
|
|
43
|
+
"""A real read-only data folder - what a Docker bind mount created by root looks like to the container's
|
|
44
|
+
non-root user. (Mocking an OSError would not do: SQLite raises its own error type here.)"""
|
|
45
|
+
if os.geteuid() == 0:
|
|
46
|
+
self.skipTest('root ignores directory permissions')
|
|
47
|
+
os.chmod(self.home, 0o555)
|
|
48
|
+
self.addCleanup(os.chmod, self.home, 0o755)
|
|
49
|
+
|
|
42
50
|
def run_cli(self, *argv):
|
|
43
51
|
out, err = io.StringIO(), io.StringIO()
|
|
44
52
|
with redirect_stdout(out), redirect_stderr(err):
|
|
@@ -47,6 +55,13 @@ class ExamplesTestCase(unittest.TestCase):
|
|
|
47
55
|
|
|
48
56
|
|
|
49
57
|
class DatabaseTests(ExamplesTestCase):
|
|
58
|
+
def test_an_unwritable_folder_is_an_apierror_with_a_hint(self):
|
|
59
|
+
self.make_home_unwritable()
|
|
60
|
+
with self.assertRaises(ApiError) as caught:
|
|
61
|
+
examples.load()
|
|
62
|
+
self.assertEqual(caught.exception.status, 500)
|
|
63
|
+
self.assertIn('writable', caught.exception.message)
|
|
64
|
+
|
|
50
65
|
def build(self, **kwargs):
|
|
51
66
|
path = os.path.join(self.home, 'x.db')
|
|
52
67
|
examples.build_database(path, FIXED_NOW, **kwargs)
|
|
@@ -450,6 +465,24 @@ class StartupTests(ExamplesTestCase):
|
|
|
450
465
|
self.assertEqual(app.test_client().get('/health').status_code, 200)
|
|
451
466
|
self.assertEqual(self.saved_files(), ['example_top_films.json'])
|
|
452
467
|
|
|
468
|
+
def test_a_really_unwritable_home_is_a_warning_not_a_crash(self):
|
|
469
|
+
# This crashed the container: examples.load() let SQLite's own OperationalError escape, which the startup hook
|
|
470
|
+
# (catching only ApiError/OSError) did not handle, so the worker died and gunicorn kept restarting it.
|
|
471
|
+
self.make_home_unwritable()
|
|
472
|
+
os.environ['QUERYAPIGATE_LOAD_EXAMPLES'] = 'yes'
|
|
473
|
+
with self.assertLogs('queryapigate', level='WARNING') as captured:
|
|
474
|
+
app = create_app()
|
|
475
|
+
self.assertTrue(any('could not be loaded' in line for line in captured.output), captured.output)
|
|
476
|
+
self.assertEqual(app.test_client().get('/health').status_code, 200)
|
|
477
|
+
|
|
478
|
+
def test_an_unexpected_failure_while_loading_is_still_only_a_warning(self):
|
|
479
|
+
os.environ['QUERYAPIGATE_LOAD_EXAMPLES'] = 'yes'
|
|
480
|
+
with mock.patch.object(examples, 'load', side_effect=RuntimeError('something new')):
|
|
481
|
+
with self.assertLogs('queryapigate', level='WARNING') as captured:
|
|
482
|
+
app = create_app()
|
|
483
|
+
self.assertTrue(any('something new' in line for line in captured.output), captured.output)
|
|
484
|
+
self.assertEqual(app.test_client().get('/health').status_code, 200)
|
|
485
|
+
|
|
453
486
|
def test_a_read_only_home_is_a_warning_not_a_startup_failure(self):
|
|
454
487
|
os.environ['QUERYAPIGATE_LOAD_EXAMPLES'] = 'yes'
|
|
455
488
|
with mock.patch.object(examples, 'load', side_effect=OSError('read-only file system')):
|
|
@@ -489,6 +522,13 @@ class CliTests(ExamplesTestCase):
|
|
|
489
522
|
_, out, _ = self.run_cli('examples', 'unload')
|
|
490
523
|
self.assertIn('partner-key', out)
|
|
491
524
|
|
|
525
|
+
def test_an_unwritable_home_is_a_clean_message_not_a_traceback(self):
|
|
526
|
+
self.make_home_unwritable()
|
|
527
|
+
code, _, err = self.run_cli('examples', 'load')
|
|
528
|
+
self.assertEqual(code, 1)
|
|
529
|
+
self.assertIn('queryapigate examples load:', err)
|
|
530
|
+
self.assertNotIn('Traceback', err)
|
|
531
|
+
|
|
492
532
|
def test_bare_command_shows_help_and_a_leftover_old_setting_still_stops_it(self):
|
|
493
533
|
code, out, _ = self.run_cli('examples')
|
|
494
534
|
self.assertEqual((code, 'load' in out), (2, True))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|