api-dock 0.8.1__tar.gz → 0.9.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {api_dock-0.8.1 → api_dock-0.9.0}/PKG-INFO +74 -26
- {api_dock-0.8.1 → api_dock-0.9.0}/README.md +73 -25
- api_dock-0.9.0/api_dock/__init__.py +72 -0
- {api_dock-0.8.1 → api_dock-0.9.0}/api_dock/config.py +4 -3
- {api_dock-0.8.1 → api_dock-0.9.0}/api_dock/database_config.py +260 -58
- {api_dock-0.8.1 → api_dock-0.9.0}/api_dock/example_api_dock_config/config.yaml +5 -0
- {api_dock-0.8.1 → api_dock-0.9.0}/api_dock/example_api_dock_config/databases/example_db.yaml +4 -4
- {api_dock-0.8.1 → api_dock-0.9.0}/api_dock/fast_api.py +68 -5
- {api_dock-0.8.1 → api_dock-0.9.0}/api_dock/flask_api.py +60 -4
- {api_dock-0.8.1 → api_dock-0.9.0}/api_dock/listings.py +28 -11
- {api_dock-0.8.1 → api_dock-0.9.0}/api_dock/route_mapper.py +302 -45
- {api_dock-0.8.1 → api_dock-0.9.0}/api_dock/sql_builder.py +325 -214
- api_dock-0.9.0/api_dock/sql_template_check.py +317 -0
- {api_dock-0.8.1 → api_dock-0.9.0}/api_dock.egg-info/PKG-INFO +74 -26
- {api_dock-0.8.1 → api_dock-0.9.0}/api_dock.egg-info/SOURCES.txt +6 -0
- {api_dock-0.8.1 → api_dock-0.9.0}/pyproject.toml +1 -1
- api_dock-0.9.0/tests/test_bound_parameters.py +260 -0
- api_dock-0.9.0/tests/test_database_requests.py +335 -0
- {api_dock-0.8.1 → api_dock-0.9.0}/tests/test_listings.py +75 -0
- {api_dock-0.8.1 → api_dock-0.9.0}/tests/test_proxy_pipeline.py +5 -0
- api_dock-0.9.0/tests/test_runtime_settings.py +249 -0
- {api_dock-0.8.1 → api_dock-0.9.0}/tests/test_schema_unions.py +7 -6
- {api_dock-0.8.1 → api_dock-0.9.0}/tests/test_shared_database_config.py +9 -10
- {api_dock-0.8.1 → api_dock-0.9.0}/tests/test_sql_builder.py +17 -14
- {api_dock-0.8.1 → api_dock-0.9.0}/tests/test_sql_selector.py +10 -5
- api_dock-0.9.0/tests/test_sql_template_check.py +147 -0
- api_dock-0.9.0/tests/test_startup_check.py +449 -0
- api_dock-0.8.1/api_dock/__init__.py +0 -26
- {api_dock-0.8.1 → api_dock-0.9.0}/LICENSE.md +0 -0
- {api_dock-0.8.1 → api_dock-0.9.0}/api_dock/auth.py +0 -0
- {api_dock-0.8.1 → api_dock-0.9.0}/api_dock/cli.py +0 -0
- {api_dock-0.8.1 → api_dock-0.9.0}/api_dock/config_discovery.py +0 -0
- {api_dock-0.8.1 → api_dock-0.9.0}/api_dock/encryption.py +0 -0
- {api_dock-0.8.1 → api_dock-0.9.0}/api_dock/example_api_dock_config/databases/config.yaml +0 -0
- {api_dock-0.8.1 → api_dock-0.9.0}/api_dock/example_api_dock_config/remotes/example_remote.yaml +0 -0
- {api_dock-0.8.1 → api_dock-0.9.0}/api_dock/storage_auth.py +0 -0
- {api_dock-0.8.1 → api_dock-0.9.0}/api_dock/types.py +0 -0
- {api_dock-0.8.1 → api_dock-0.9.0}/api_dock.egg-info/dependency_links.txt +0 -0
- {api_dock-0.8.1 → api_dock-0.9.0}/api_dock.egg-info/entry_points.txt +0 -0
- {api_dock-0.8.1 → api_dock-0.9.0}/api_dock.egg-info/requires.txt +0 -0
- {api_dock-0.8.1 → api_dock-0.9.0}/api_dock.egg-info/top_level.txt +0 -0
- {api_dock-0.8.1 → api_dock-0.9.0}/setup.cfg +0 -0
- {api_dock-0.8.1 → api_dock-0.9.0}/tests/test_inject_cookies.py +0 -0
- {api_dock-0.8.1 → api_dock-0.9.0}/tests/test_types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: api_dock
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: A flexible API gateway that allows you to proxy requests to multiple remote APIs and Databases
|
|
5
5
|
Author-email: Brookie Guzder-Williams <bguzder-williams@berkeley.edu>
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -219,19 +219,33 @@ remotes:
|
|
|
219
219
|
settings:
|
|
220
220
|
add_trailing_slash: true # Auto-add trailing slash to paths (default: true)
|
|
221
221
|
follow_protocol_downgrades: false # Allow HTTPS->HTTP redirects (default: false)
|
|
222
|
+
follow_redirects: true # Follow remote redirects (default: true)
|
|
222
223
|
timeout: 10 # Upstream request timeout in seconds (default: 10)
|
|
224
|
+
base_path: /dock # Also serve the API under this prefix (default: none)
|
|
225
|
+
duckdb: # Options for database queries (default: none)
|
|
226
|
+
memory_limit: 700MB
|
|
227
|
+
threads: 2
|
|
228
|
+
max_concurrent_queries: 2
|
|
223
229
|
```
|
|
224
230
|
|
|
225
|
-
###
|
|
231
|
+
### Settings
|
|
226
232
|
|
|
227
|
-
The optional `settings` section controls HTTP behavior:
|
|
233
|
+
The optional `settings` section controls HTTP and query behavior:
|
|
228
234
|
|
|
229
235
|
- **`add_trailing_slash`** (default: `true`): Automatically append a trailing slash to all proxied paths. This prevents 307/301 redirects from remote APIs that require trailing slashes (e.g., `/projects` → `/projects/`). Set to `false` to disable this behavior.
|
|
230
236
|
|
|
231
237
|
- **`follow_protocol_downgrades`** (default: `false`): Control how HTTP redirects are handled. When `false` (recommended), HTTPS→HTTP redirects are blocked for security. When `true`, allows following redirects that downgrade from HTTPS to HTTP (not recommended for production).
|
|
232
238
|
|
|
239
|
+
- **`follow_redirects`** (default: `true`): Whether redirects from a remote are followed by API Dock (`true`) or passed through to the client with their `Location` header (`false`). Set it to `false` when a remote answers with redirects the client should follow itself, such as presigned S3 URLs for large files.
|
|
240
|
+
|
|
233
241
|
- **`timeout`** (default: `10`): Upstream request timeout in seconds, applied to both the streaming and buffered proxy paths. Raise it for slow upstreams (e.g. large aggregation queries) that would otherwise return a 502 on timeout. Set to `null` or `false` to disable the timeout entirely (not recommended — a stalled upstream can hold the connection open indefinitely).
|
|
234
242
|
|
|
243
|
+
- **`base_path`** (default: none): An extra URL prefix the API is also served under, e.g. `/dock`. Use it when a proxy or CDN forwards a path on another domain without stripping it (say CloudFront routes `https://app.example.org/dock/*` to API Dock): `/dock/birdnet/latest/detections/` is then handled as `/birdnet/latest/detections/`. Paths without the prefix keep working, so direct calls and health checks are unaffected.
|
|
244
|
+
|
|
245
|
+
- **`duckdb`** (default: none): Options for the DuckDB connection each database query runs on. Every key except `max_concurrent_queries` is applied as `SET <key> = <value>`, so any [DuckDB setting](https://duckdb.org/docs/configuration/overview) works; the useful ones on small servers are `memory_limit` (DuckDB spills to disk or fails the query instead of exceeding it), `threads`, and `temp_directory`. `max_concurrent_queries` caps how many database queries run at once in the process; further queries wait their turn. Memory limits apply per query, so on a small instance set `memory_limit × max_concurrent_queries` below the instance's memory.
|
|
246
|
+
|
|
247
|
+
Database queries run in worker threads, so a slow query doesn't hold up other requests (including health checks on `/`).
|
|
248
|
+
|
|
235
249
|
### Catalog Endpoints (`expose`)
|
|
236
250
|
|
|
237
251
|
The optional `expose` section adds read-only endpoints that list the models and versions of your configured databases, remotes, or both ("sources"). Listings are **opt-in** — with no `expose` key nothing is added.
|
|
@@ -411,6 +425,22 @@ Database configurations are stored in `api_dock_config/databases/` directory. Ea
|
|
|
411
425
|
- **routes**: REST endpoints mapped to SQL queries
|
|
412
426
|
- **schema** (optional): the shared schema (from `databases/config.yaml`) this config's `[[table]]` references fall back to. See [Shared Tables and Schemas](#shared-tables-and-schemas-databasesconfigyaml)
|
|
413
427
|
|
|
428
|
+
### Startup checks
|
|
429
|
+
|
|
430
|
+
When API Dock starts, it checks every database and version listed in the main config, from config files and from the shared `databases/config.yaml` (`slugs`), exactly as requests will see them: merged with the main config and with the shared `routes`/`query_params` that apply to that version (after `include`/`exclude`). If anything is wrong it refuses to start and names the database, version, route and template, for example:
|
|
431
|
+
|
|
432
|
+
```
|
|
433
|
+
ValueError: Database 'owl' version '4.0', route '/detections/{{id}}/overlaps': sql: Table 'all_modelz.detections' not found in database configuration
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
It checks that:
|
|
437
|
+
- the shared `databases/config.yaml` is valid (`slugs`, `schema_groups`, and the types of its sections)
|
|
438
|
+
- every route and query param has a valid shape
|
|
439
|
+
- no `{{variable}}` is inside a quoted string (other than a string that is exactly one variable, `'{{x}}'`), a quoted identifier, or a SQL comment, and no template ends inside a comment (see [How values reach the database](#how-values-reach-the-database))
|
|
440
|
+
- every `[[table]]`, `[[schema.table]]`, `[[*.table]]` and `[[group.table]]` reference resolves, unions are only used after `FROM`/`JOIN`, and `source_columns` is valid
|
|
441
|
+
|
|
442
|
+
Database and remote config files are read from the folder that holds the main config file.
|
|
443
|
+
|
|
414
444
|
### Syntax
|
|
415
445
|
|
|
416
446
|
As with the remote-apis, the routes to databases use double-curly-brackets {{}} to reference url variable placeholders.
|
|
@@ -713,6 +743,24 @@ Rules:
|
|
|
713
743
|
## URL Query Parameters
|
|
714
744
|
|
|
715
745
|
|
|
746
|
+
### How values reach the database
|
|
747
|
+
|
|
748
|
+
api_dock does not paste request values into SQL. Each `{{variable}}` in `sql`, `multivalue_sql`, conditional `sql` and `queries:` becomes a placeholder, and its value (from the path, query string, a `default`, or a cookie) is sent to DuckDB separately. DuckDB converts the value to the column's type, so number, date and boolean filters work as written. A value that is not a valid number, date or boolean for its column, such as `?age=25 OR true`, is rejected with an error instead of being run as SQL.
|
|
749
|
+
|
|
750
|
+
Because the value is sent separately, **write variables without quotes**:
|
|
751
|
+
|
|
752
|
+
| Write | Not |
|
|
753
|
+
|---|---|
|
|
754
|
+
| `department = {{department}}` | `department = '{{department}}'` |
|
|
755
|
+
| `UPPER(name) = UPPER({{name}})` | `UPPER(name) = UPPER('{{name}}')` |
|
|
756
|
+
| `name ILIKE '%' \|\| {{name}} \|\| '%'` | `name ILIKE '%{{name}}%'` |
|
|
757
|
+
|
|
758
|
+
For compatibility with 0.8.x and earlier configs, a string that is exactly one variable (`'{{department}}'`) is read as `{{department}}`. Any other quoted variable, like `'%{{name}}%'`, would be read as literal text, so api_dock refuses to start and prints the route with the fixed form (rewrite it with `||` as in the last row above). A variable can't be used as a column name in double quotes (`"{{column}}"`) either. A variable inside a SQL comment (`-- {{x}}` or `/* {{x}} */`) would be sent with nothing in the query to use it, so api_dock refuses to start and asks you to remove it. A template also can't end inside a comment, because api_dock adds WHERE conditions and `sql_append` clauses after it on the same line: put the comment on its own line or use `/* */`.
|
|
759
|
+
|
|
760
|
+
`sql_append` works differently. Its values are column names, `ASC`/`DESC` or numbers, which a database can't accept as separate values, so they are written into the SQL text. Each one must contain only letters, digits, spaces and `_ . , ( ) -`, and must not contain `--`. The same applies to the `sql_append` of a [conditional SQL selection](#conditional-sql-selection) branch.
|
|
761
|
+
|
|
762
|
+
The `# SQL:` comments in the examples below show values in place so the queries are easier to read.
|
|
763
|
+
|
|
716
764
|
### Basic Filtering with `sql`
|
|
717
765
|
|
|
718
766
|
Use `sql` to add WHERE clause fragments. Each fragment is joined with `AND`. Optional by default — only included if the parameter is in the URL.
|
|
@@ -725,7 +773,7 @@ routes:
|
|
|
725
773
|
- age:
|
|
726
774
|
sql: age = {{age}} # optional — only if ?age= provided
|
|
727
775
|
- department:
|
|
728
|
-
sql: department =
|
|
776
|
+
sql: department = {{department}}
|
|
729
777
|
- height:
|
|
730
778
|
sql: height < {{height}}
|
|
731
779
|
default: 200 # always included (uses 200 if not in URL)
|
|
@@ -741,7 +789,7 @@ GET /db/users
|
|
|
741
789
|
|
|
742
790
|
### Repeated Parameters with `multivalue_sql`
|
|
743
791
|
|
|
744
|
-
A query parameter key can appear more than once in the URL (e.g. `?recording_id=1&recording_id=4`). Add a `multivalue_sql` template alongside `sql` to handle this: when **more than one** value is passed for the key, `multivalue_sql` is used instead of `sql`, and `{{param}}` expands to a parenthesized
|
|
792
|
+
A query parameter key can appear more than once in the URL (e.g. `?recording_id=1&recording_id=4`). Add a `multivalue_sql` template alongside `sql` to handle this: when **more than one** value is passed for the key, `multivalue_sql` is used instead of `sql`, and `{{param}}` expands to a parenthesized list with one value per URL entry, suitable for an `IN` clause. Each value is sent to the database separately, like any other variable.
|
|
745
793
|
|
|
746
794
|
Behavior is unchanged when `multivalue_sql` is absent, and when only a single value is passed the normal `sql` template is used.
|
|
747
795
|
|
|
@@ -754,7 +802,7 @@ routes:
|
|
|
754
802
|
sql: "[[detections]].recording_id = {{recording_id}}" # single value
|
|
755
803
|
multivalue_sql: "[[detections]].recording_id IN {{recording_id}}" # 2+ values
|
|
756
804
|
- scientific_name:
|
|
757
|
-
sql: "[[detections]].scientific_name =
|
|
805
|
+
sql: "[[detections]].scientific_name = {{scientific_name}}"
|
|
758
806
|
```
|
|
759
807
|
|
|
760
808
|
```bash
|
|
@@ -780,7 +828,7 @@ routes:
|
|
|
780
828
|
query_params:
|
|
781
829
|
# WHERE clause params
|
|
782
830
|
- department:
|
|
783
|
-
sql: department =
|
|
831
|
+
sql: department = {{department}}
|
|
784
832
|
# Post-WHERE params
|
|
785
833
|
- sort:
|
|
786
834
|
sql_append: ORDER BY {{sort}} {{sort_direction}}
|
|
@@ -893,13 +941,13 @@ routes:
|
|
|
893
941
|
query_params:
|
|
894
942
|
# WHERE clause filters
|
|
895
943
|
- name:
|
|
896
|
-
sql: name ILIKE '%{{name}}%'
|
|
944
|
+
sql: name ILIKE '%' || {{name}} || '%'
|
|
897
945
|
- age_min:
|
|
898
946
|
sql: age >= {{age_min}}
|
|
899
947
|
- age_max:
|
|
900
948
|
sql: age <= {{age_max}}
|
|
901
949
|
- department:
|
|
902
|
-
sql: department =
|
|
950
|
+
sql: department = {{department}}
|
|
903
951
|
# Sorting and pagination (sql_append)
|
|
904
952
|
- sort:
|
|
905
953
|
sql_append: ORDER BY {{sort}} {{sort_direction}}
|
|
@@ -1147,7 +1195,7 @@ Forwarded cookies are accessible in SQL queries using `{{cookies.cookie_name}}`:
|
|
|
1147
1195
|
```yaml
|
|
1148
1196
|
routes:
|
|
1149
1197
|
- route: user/profile
|
|
1150
|
-
sql: SELECT * FROM [[users]] WHERE session_id =
|
|
1198
|
+
sql: SELECT * FROM [[users]] WHERE session_id = {{cookies.session_id}}
|
|
1151
1199
|
```
|
|
1152
1200
|
|
|
1153
1201
|
### Injecting cookies from the server environment
|
|
@@ -1490,9 +1538,9 @@ routes:
|
|
|
1490
1538
|
sql: SELECT * FROM [[events]]
|
|
1491
1539
|
query_params:
|
|
1492
1540
|
- date_from:
|
|
1493
|
-
sql: event_date >=
|
|
1541
|
+
sql: event_date >= {{date_from}}
|
|
1494
1542
|
- event_type:
|
|
1495
|
-
sql: type =
|
|
1543
|
+
sql: type = {{event_type}}
|
|
1496
1544
|
- user_id:
|
|
1497
1545
|
sql: user_id = {{user_id}}
|
|
1498
1546
|
required: true
|
|
@@ -1568,13 +1616,13 @@ tables:
|
|
|
1568
1616
|
|
|
1569
1617
|
routes:
|
|
1570
1618
|
- route: my-activity
|
|
1571
|
-
sql: SELECT * FROM [[user_activity]] WHERE user_id =
|
|
1619
|
+
sql: SELECT * FROM [[user_activity]] WHERE user_id = {{cookies.user_id}}
|
|
1572
1620
|
|
|
1573
1621
|
- route: user-settings
|
|
1574
1622
|
sql: |
|
|
1575
1623
|
SELECT * FROM [[user_activity]]
|
|
1576
|
-
WHERE user_id =
|
|
1577
|
-
AND session_token =
|
|
1624
|
+
WHERE user_id = {{cookies.user_id}}
|
|
1625
|
+
AND session_token = {{cookies.session_token}}
|
|
1578
1626
|
```
|
|
1579
1627
|
|
|
1580
1628
|
---
|
|
@@ -1601,7 +1649,7 @@ Publishing a GitHub Release is what publishes to PyPI: `.github/workflows/publis
|
|
|
1601
1649
|
|
|
1602
1650
|
```bash
|
|
1603
1651
|
# 0. Start from a clean, up-to-date main
|
|
1604
|
-
export VERSION=0.
|
|
1652
|
+
export VERSION=0.9.0 # the NEW version, no leading "v"
|
|
1605
1653
|
git checkout main
|
|
1606
1654
|
git pull origin main
|
|
1607
1655
|
git status
|
|
@@ -1612,7 +1660,7 @@ git status
|
|
|
1612
1660
|
pixi run -e dev pytest -q
|
|
1613
1661
|
|
|
1614
1662
|
# 3. Commit, tag, push (the commit command adds the "v$VERSION: " prefix)
|
|
1615
|
-
export COMMIT_MESSAGE='
|
|
1663
|
+
export COMMIT_MESSAGE='bound SQL parameters (fix SQL injection) and startup config checks'
|
|
1616
1664
|
git add -A
|
|
1617
1665
|
git commit -m "v$VERSION: $COMMIT_MESSAGE"
|
|
1618
1666
|
git tag "v$VERSION"
|
|
@@ -1624,17 +1672,17 @@ gh release create "v$VERSION" \
|
|
|
1624
1672
|
--title "v$VERSION" \
|
|
1625
1673
|
--notes "$(cat <<'EOF'
|
|
1626
1674
|
* new features
|
|
1627
|
-
-
|
|
1628
|
-
-
|
|
1629
|
-
-
|
|
1630
|
-
- `{{self.schema}}`, `{{self.name}}`, and `{{self.version}}` placeholders for the database/version being queried
|
|
1631
|
-
- Together these support an "overlaps" route shared by every database/version (every detection overlapping a given one across all schemas, except that detection itself); shared query params such as `confidence`, `sort`, and `limit` apply to its rows
|
|
1675
|
+
- Request values are sent to DuckDB as bound parameters, separately from the SQL, so they can never run as SQL. Write variables without quotes (`UPPER(name) = UPPER({{name}})`); a string that is exactly one variable (`'{{name}}'`) still works, and patterns like `'%{{name}}%'` become `'%' || {{name}} || '%'`
|
|
1676
|
+
- Startup checks: API Dock refuses to start, naming the database, version, route and template, if a config has a quoted or commented variable, a template ending in a comment, a malformed route, an invalid shared `databases/config.yaml`, a `[[table]]` / `[[schema.table]]` / union reference that doesn't resolve, or invalid `source_columns`. Each version is checked as requests see it (shared routes/query_params, include/exclude, slugs)
|
|
1677
|
+
- Database and remote configs are read from the folder that holds the main config file
|
|
1632
1678
|
* bug fixes
|
|
1633
|
-
-
|
|
1679
|
+
- SQL injection: query/path/cookie values were pasted into SQL and only quoted when the SQL fragment happened to contain words like SELECT/WHERE/AND/OR, so filters like `confidence >= {{confidence}}` accepted raw SQL (`?confidence=0 OR 1=1`, `UNION SELECT ... read_csv(...)`)
|
|
1680
|
+
- `response:` bodies are filled in as plain text (they no longer get SQL quotes)
|
|
1681
|
+
- Importing api_dock (or running `api-dock --help`) no longer builds an app from the current directory's config; the default `app`/`fastapi_app`/`flask_app` are built on first use
|
|
1634
1682
|
* cleanup / other improvements
|
|
1635
|
-
-
|
|
1636
|
-
-
|
|
1637
|
-
- Test suite grew from
|
|
1683
|
+
- Removed unused `build_sql_query_legacy` / `_substitute_parameters`; `build_sql_query` returns `(sql, values)` and `build_sql_query_with_tables` returns `(sql, values, tables)`
|
|
1684
|
+
- New `sql_template_check` module and `check_table_references`; README sections "How values reach the database" and "Startup checks"
|
|
1685
|
+
- Test suite grew from 253 to 373 tests
|
|
1638
1686
|
EOF
|
|
1639
1687
|
)"
|
|
1640
1688
|
|
|
@@ -180,19 +180,33 @@ remotes:
|
|
|
180
180
|
settings:
|
|
181
181
|
add_trailing_slash: true # Auto-add trailing slash to paths (default: true)
|
|
182
182
|
follow_protocol_downgrades: false # Allow HTTPS->HTTP redirects (default: false)
|
|
183
|
+
follow_redirects: true # Follow remote redirects (default: true)
|
|
183
184
|
timeout: 10 # Upstream request timeout in seconds (default: 10)
|
|
185
|
+
base_path: /dock # Also serve the API under this prefix (default: none)
|
|
186
|
+
duckdb: # Options for database queries (default: none)
|
|
187
|
+
memory_limit: 700MB
|
|
188
|
+
threads: 2
|
|
189
|
+
max_concurrent_queries: 2
|
|
184
190
|
```
|
|
185
191
|
|
|
186
|
-
###
|
|
192
|
+
### Settings
|
|
187
193
|
|
|
188
|
-
The optional `settings` section controls HTTP behavior:
|
|
194
|
+
The optional `settings` section controls HTTP and query behavior:
|
|
189
195
|
|
|
190
196
|
- **`add_trailing_slash`** (default: `true`): Automatically append a trailing slash to all proxied paths. This prevents 307/301 redirects from remote APIs that require trailing slashes (e.g., `/projects` → `/projects/`). Set to `false` to disable this behavior.
|
|
191
197
|
|
|
192
198
|
- **`follow_protocol_downgrades`** (default: `false`): Control how HTTP redirects are handled. When `false` (recommended), HTTPS→HTTP redirects are blocked for security. When `true`, allows following redirects that downgrade from HTTPS to HTTP (not recommended for production).
|
|
193
199
|
|
|
200
|
+
- **`follow_redirects`** (default: `true`): Whether redirects from a remote are followed by API Dock (`true`) or passed through to the client with their `Location` header (`false`). Set it to `false` when a remote answers with redirects the client should follow itself, such as presigned S3 URLs for large files.
|
|
201
|
+
|
|
194
202
|
- **`timeout`** (default: `10`): Upstream request timeout in seconds, applied to both the streaming and buffered proxy paths. Raise it for slow upstreams (e.g. large aggregation queries) that would otherwise return a 502 on timeout. Set to `null` or `false` to disable the timeout entirely (not recommended — a stalled upstream can hold the connection open indefinitely).
|
|
195
203
|
|
|
204
|
+
- **`base_path`** (default: none): An extra URL prefix the API is also served under, e.g. `/dock`. Use it when a proxy or CDN forwards a path on another domain without stripping it (say CloudFront routes `https://app.example.org/dock/*` to API Dock): `/dock/birdnet/latest/detections/` is then handled as `/birdnet/latest/detections/`. Paths without the prefix keep working, so direct calls and health checks are unaffected.
|
|
205
|
+
|
|
206
|
+
- **`duckdb`** (default: none): Options for the DuckDB connection each database query runs on. Every key except `max_concurrent_queries` is applied as `SET <key> = <value>`, so any [DuckDB setting](https://duckdb.org/docs/configuration/overview) works; the useful ones on small servers are `memory_limit` (DuckDB spills to disk or fails the query instead of exceeding it), `threads`, and `temp_directory`. `max_concurrent_queries` caps how many database queries run at once in the process; further queries wait their turn. Memory limits apply per query, so on a small instance set `memory_limit × max_concurrent_queries` below the instance's memory.
|
|
207
|
+
|
|
208
|
+
Database queries run in worker threads, so a slow query doesn't hold up other requests (including health checks on `/`).
|
|
209
|
+
|
|
196
210
|
### Catalog Endpoints (`expose`)
|
|
197
211
|
|
|
198
212
|
The optional `expose` section adds read-only endpoints that list the models and versions of your configured databases, remotes, or both ("sources"). Listings are **opt-in** — with no `expose` key nothing is added.
|
|
@@ -372,6 +386,22 @@ Database configurations are stored in `api_dock_config/databases/` directory. Ea
|
|
|
372
386
|
- **routes**: REST endpoints mapped to SQL queries
|
|
373
387
|
- **schema** (optional): the shared schema (from `databases/config.yaml`) this config's `[[table]]` references fall back to. See [Shared Tables and Schemas](#shared-tables-and-schemas-databasesconfigyaml)
|
|
374
388
|
|
|
389
|
+
### Startup checks
|
|
390
|
+
|
|
391
|
+
When API Dock starts, it checks every database and version listed in the main config, from config files and from the shared `databases/config.yaml` (`slugs`), exactly as requests will see them: merged with the main config and with the shared `routes`/`query_params` that apply to that version (after `include`/`exclude`). If anything is wrong it refuses to start and names the database, version, route and template, for example:
|
|
392
|
+
|
|
393
|
+
```
|
|
394
|
+
ValueError: Database 'owl' version '4.0', route '/detections/{{id}}/overlaps': sql: Table 'all_modelz.detections' not found in database configuration
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
It checks that:
|
|
398
|
+
- the shared `databases/config.yaml` is valid (`slugs`, `schema_groups`, and the types of its sections)
|
|
399
|
+
- every route and query param has a valid shape
|
|
400
|
+
- no `{{variable}}` is inside a quoted string (other than a string that is exactly one variable, `'{{x}}'`), a quoted identifier, or a SQL comment, and no template ends inside a comment (see [How values reach the database](#how-values-reach-the-database))
|
|
401
|
+
- every `[[table]]`, `[[schema.table]]`, `[[*.table]]` and `[[group.table]]` reference resolves, unions are only used after `FROM`/`JOIN`, and `source_columns` is valid
|
|
402
|
+
|
|
403
|
+
Database and remote config files are read from the folder that holds the main config file.
|
|
404
|
+
|
|
375
405
|
### Syntax
|
|
376
406
|
|
|
377
407
|
As with the remote-apis, the routes to databases use double-curly-brackets {{}} to reference url variable placeholders.
|
|
@@ -674,6 +704,24 @@ Rules:
|
|
|
674
704
|
## URL Query Parameters
|
|
675
705
|
|
|
676
706
|
|
|
707
|
+
### How values reach the database
|
|
708
|
+
|
|
709
|
+
api_dock does not paste request values into SQL. Each `{{variable}}` in `sql`, `multivalue_sql`, conditional `sql` and `queries:` becomes a placeholder, and its value (from the path, query string, a `default`, or a cookie) is sent to DuckDB separately. DuckDB converts the value to the column's type, so number, date and boolean filters work as written. A value that is not a valid number, date or boolean for its column, such as `?age=25 OR true`, is rejected with an error instead of being run as SQL.
|
|
710
|
+
|
|
711
|
+
Because the value is sent separately, **write variables without quotes**:
|
|
712
|
+
|
|
713
|
+
| Write | Not |
|
|
714
|
+
|---|---|
|
|
715
|
+
| `department = {{department}}` | `department = '{{department}}'` |
|
|
716
|
+
| `UPPER(name) = UPPER({{name}})` | `UPPER(name) = UPPER('{{name}}')` |
|
|
717
|
+
| `name ILIKE '%' \|\| {{name}} \|\| '%'` | `name ILIKE '%{{name}}%'` |
|
|
718
|
+
|
|
719
|
+
For compatibility with 0.8.x and earlier configs, a string that is exactly one variable (`'{{department}}'`) is read as `{{department}}`. Any other quoted variable, like `'%{{name}}%'`, would be read as literal text, so api_dock refuses to start and prints the route with the fixed form (rewrite it with `||` as in the last row above). A variable can't be used as a column name in double quotes (`"{{column}}"`) either. A variable inside a SQL comment (`-- {{x}}` or `/* {{x}} */`) would be sent with nothing in the query to use it, so api_dock refuses to start and asks you to remove it. A template also can't end inside a comment, because api_dock adds WHERE conditions and `sql_append` clauses after it on the same line: put the comment on its own line or use `/* */`.
|
|
720
|
+
|
|
721
|
+
`sql_append` works differently. Its values are column names, `ASC`/`DESC` or numbers, which a database can't accept as separate values, so they are written into the SQL text. Each one must contain only letters, digits, spaces and `_ . , ( ) -`, and must not contain `--`. The same applies to the `sql_append` of a [conditional SQL selection](#conditional-sql-selection) branch.
|
|
722
|
+
|
|
723
|
+
The `# SQL:` comments in the examples below show values in place so the queries are easier to read.
|
|
724
|
+
|
|
677
725
|
### Basic Filtering with `sql`
|
|
678
726
|
|
|
679
727
|
Use `sql` to add WHERE clause fragments. Each fragment is joined with `AND`. Optional by default — only included if the parameter is in the URL.
|
|
@@ -686,7 +734,7 @@ routes:
|
|
|
686
734
|
- age:
|
|
687
735
|
sql: age = {{age}} # optional — only if ?age= provided
|
|
688
736
|
- department:
|
|
689
|
-
sql: department =
|
|
737
|
+
sql: department = {{department}}
|
|
690
738
|
- height:
|
|
691
739
|
sql: height < {{height}}
|
|
692
740
|
default: 200 # always included (uses 200 if not in URL)
|
|
@@ -702,7 +750,7 @@ GET /db/users
|
|
|
702
750
|
|
|
703
751
|
### Repeated Parameters with `multivalue_sql`
|
|
704
752
|
|
|
705
|
-
A query parameter key can appear more than once in the URL (e.g. `?recording_id=1&recording_id=4`). Add a `multivalue_sql` template alongside `sql` to handle this: when **more than one** value is passed for the key, `multivalue_sql` is used instead of `sql`, and `{{param}}` expands to a parenthesized
|
|
753
|
+
A query parameter key can appear more than once in the URL (e.g. `?recording_id=1&recording_id=4`). Add a `multivalue_sql` template alongside `sql` to handle this: when **more than one** value is passed for the key, `multivalue_sql` is used instead of `sql`, and `{{param}}` expands to a parenthesized list with one value per URL entry, suitable for an `IN` clause. Each value is sent to the database separately, like any other variable.
|
|
706
754
|
|
|
707
755
|
Behavior is unchanged when `multivalue_sql` is absent, and when only a single value is passed the normal `sql` template is used.
|
|
708
756
|
|
|
@@ -715,7 +763,7 @@ routes:
|
|
|
715
763
|
sql: "[[detections]].recording_id = {{recording_id}}" # single value
|
|
716
764
|
multivalue_sql: "[[detections]].recording_id IN {{recording_id}}" # 2+ values
|
|
717
765
|
- scientific_name:
|
|
718
|
-
sql: "[[detections]].scientific_name =
|
|
766
|
+
sql: "[[detections]].scientific_name = {{scientific_name}}"
|
|
719
767
|
```
|
|
720
768
|
|
|
721
769
|
```bash
|
|
@@ -741,7 +789,7 @@ routes:
|
|
|
741
789
|
query_params:
|
|
742
790
|
# WHERE clause params
|
|
743
791
|
- department:
|
|
744
|
-
sql: department =
|
|
792
|
+
sql: department = {{department}}
|
|
745
793
|
# Post-WHERE params
|
|
746
794
|
- sort:
|
|
747
795
|
sql_append: ORDER BY {{sort}} {{sort_direction}}
|
|
@@ -854,13 +902,13 @@ routes:
|
|
|
854
902
|
query_params:
|
|
855
903
|
# WHERE clause filters
|
|
856
904
|
- name:
|
|
857
|
-
sql: name ILIKE '%{{name}}%'
|
|
905
|
+
sql: name ILIKE '%' || {{name}} || '%'
|
|
858
906
|
- age_min:
|
|
859
907
|
sql: age >= {{age_min}}
|
|
860
908
|
- age_max:
|
|
861
909
|
sql: age <= {{age_max}}
|
|
862
910
|
- department:
|
|
863
|
-
sql: department =
|
|
911
|
+
sql: department = {{department}}
|
|
864
912
|
# Sorting and pagination (sql_append)
|
|
865
913
|
- sort:
|
|
866
914
|
sql_append: ORDER BY {{sort}} {{sort_direction}}
|
|
@@ -1108,7 +1156,7 @@ Forwarded cookies are accessible in SQL queries using `{{cookies.cookie_name}}`:
|
|
|
1108
1156
|
```yaml
|
|
1109
1157
|
routes:
|
|
1110
1158
|
- route: user/profile
|
|
1111
|
-
sql: SELECT * FROM [[users]] WHERE session_id =
|
|
1159
|
+
sql: SELECT * FROM [[users]] WHERE session_id = {{cookies.session_id}}
|
|
1112
1160
|
```
|
|
1113
1161
|
|
|
1114
1162
|
### Injecting cookies from the server environment
|
|
@@ -1451,9 +1499,9 @@ routes:
|
|
|
1451
1499
|
sql: SELECT * FROM [[events]]
|
|
1452
1500
|
query_params:
|
|
1453
1501
|
- date_from:
|
|
1454
|
-
sql: event_date >=
|
|
1502
|
+
sql: event_date >= {{date_from}}
|
|
1455
1503
|
- event_type:
|
|
1456
|
-
sql: type =
|
|
1504
|
+
sql: type = {{event_type}}
|
|
1457
1505
|
- user_id:
|
|
1458
1506
|
sql: user_id = {{user_id}}
|
|
1459
1507
|
required: true
|
|
@@ -1529,13 +1577,13 @@ tables:
|
|
|
1529
1577
|
|
|
1530
1578
|
routes:
|
|
1531
1579
|
- route: my-activity
|
|
1532
|
-
sql: SELECT * FROM [[user_activity]] WHERE user_id =
|
|
1580
|
+
sql: SELECT * FROM [[user_activity]] WHERE user_id = {{cookies.user_id}}
|
|
1533
1581
|
|
|
1534
1582
|
- route: user-settings
|
|
1535
1583
|
sql: |
|
|
1536
1584
|
SELECT * FROM [[user_activity]]
|
|
1537
|
-
WHERE user_id =
|
|
1538
|
-
AND session_token =
|
|
1585
|
+
WHERE user_id = {{cookies.user_id}}
|
|
1586
|
+
AND session_token = {{cookies.session_token}}
|
|
1539
1587
|
```
|
|
1540
1588
|
|
|
1541
1589
|
---
|
|
@@ -1562,7 +1610,7 @@ Publishing a GitHub Release is what publishes to PyPI: `.github/workflows/publis
|
|
|
1562
1610
|
|
|
1563
1611
|
```bash
|
|
1564
1612
|
# 0. Start from a clean, up-to-date main
|
|
1565
|
-
export VERSION=0.
|
|
1613
|
+
export VERSION=0.9.0 # the NEW version, no leading "v"
|
|
1566
1614
|
git checkout main
|
|
1567
1615
|
git pull origin main
|
|
1568
1616
|
git status
|
|
@@ -1573,7 +1621,7 @@ git status
|
|
|
1573
1621
|
pixi run -e dev pytest -q
|
|
1574
1622
|
|
|
1575
1623
|
# 3. Commit, tag, push (the commit command adds the "v$VERSION: " prefix)
|
|
1576
|
-
export COMMIT_MESSAGE='
|
|
1624
|
+
export COMMIT_MESSAGE='bound SQL parameters (fix SQL injection) and startup config checks'
|
|
1577
1625
|
git add -A
|
|
1578
1626
|
git commit -m "v$VERSION: $COMMIT_MESSAGE"
|
|
1579
1627
|
git tag "v$VERSION"
|
|
@@ -1585,17 +1633,17 @@ gh release create "v$VERSION" \
|
|
|
1585
1633
|
--title "v$VERSION" \
|
|
1586
1634
|
--notes "$(cat <<'EOF'
|
|
1587
1635
|
* new features
|
|
1588
|
-
-
|
|
1589
|
-
-
|
|
1590
|
-
-
|
|
1591
|
-
- `{{self.schema}}`, `{{self.name}}`, and `{{self.version}}` placeholders for the database/version being queried
|
|
1592
|
-
- Together these support an "overlaps" route shared by every database/version (every detection overlapping a given one across all schemas, except that detection itself); shared query params such as `confidence`, `sort`, and `limit` apply to its rows
|
|
1636
|
+
- Request values are sent to DuckDB as bound parameters, separately from the SQL, so they can never run as SQL. Write variables without quotes (`UPPER(name) = UPPER({{name}})`); a string that is exactly one variable (`'{{name}}'`) still works, and patterns like `'%{{name}}%'` become `'%' || {{name}} || '%'`
|
|
1637
|
+
- Startup checks: API Dock refuses to start, naming the database, version, route and template, if a config has a quoted or commented variable, a template ending in a comment, a malformed route, an invalid shared `databases/config.yaml`, a `[[table]]` / `[[schema.table]]` / union reference that doesn't resolve, or invalid `source_columns`. Each version is checked as requests see it (shared routes/query_params, include/exclude, slugs)
|
|
1638
|
+
- Database and remote configs are read from the folder that holds the main config file
|
|
1593
1639
|
* bug fixes
|
|
1594
|
-
-
|
|
1640
|
+
- SQL injection: query/path/cookie values were pasted into SQL and only quoted when the SQL fragment happened to contain words like SELECT/WHERE/AND/OR, so filters like `confidence >= {{confidence}}` accepted raw SQL (`?confidence=0 OR 1=1`, `UNION SELECT ... read_csv(...)`)
|
|
1641
|
+
- `response:` bodies are filled in as plain text (they no longer get SQL quotes)
|
|
1642
|
+
- Importing api_dock (or running `api-dock --help`) no longer builds an app from the current directory's config; the default `app`/`fastapi_app`/`flask_app` are built on first use
|
|
1595
1643
|
* cleanup / other improvements
|
|
1596
|
-
-
|
|
1597
|
-
-
|
|
1598
|
-
- Test suite grew from
|
|
1644
|
+
- Removed unused `build_sql_query_legacy` / `_substitute_parameters`; `build_sql_query` returns `(sql, values)` and `build_sql_query_with_tables` returns `(sql, values, tables)`
|
|
1645
|
+
- New `sql_template_check` module and `check_table_references`; README sections "How values reach the database" and "Startup checks"
|
|
1646
|
+
- Test suite grew from 253 to 373 tests
|
|
1599
1647
|
EOF
|
|
1600
1648
|
)"
|
|
1601
1649
|
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""
|
|
2
|
+
|
|
3
|
+
API Dock Core Module
|
|
4
|
+
|
|
5
|
+
Core functionality for API Dock wrapper.
|
|
6
|
+
|
|
7
|
+
License: BSD 3-Clause
|
|
8
|
+
|
|
9
|
+
"""
|
|
10
|
+
#
|
|
11
|
+
# IMPORTS
|
|
12
|
+
#
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from api_dock.config import (
|
|
16
|
+
filter_cookies_by_config,
|
|
17
|
+
find_remote_config,
|
|
18
|
+
get_authentication_config,
|
|
19
|
+
get_cookies_config,
|
|
20
|
+
get_remote_names,
|
|
21
|
+
load_main_config,
|
|
22
|
+
merge_inherited_config,
|
|
23
|
+
validate_authentication_config,
|
|
24
|
+
validate_cookies_config,
|
|
25
|
+
)
|
|
26
|
+
from api_dock.fast_api import create_app as create_fastapi_app
|
|
27
|
+
from api_dock.flask_api import create_app as create_flask_app
|
|
28
|
+
from api_dock.route_mapper import RouteMapper
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
#
|
|
32
|
+
# CONSTANTS
|
|
33
|
+
#
|
|
34
|
+
# For backward compatibility, default to FastAPI
|
|
35
|
+
create_app = create_fastapi_app
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"app", "create_app",
|
|
39
|
+
"fastapi_app", "create_fastapi_app",
|
|
40
|
+
"flask_app", "create_flask_app",
|
|
41
|
+
"load_main_config", "find_remote_config", "get_remote_names",
|
|
42
|
+
"RouteMapper"
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
#
|
|
47
|
+
# PUBLIC
|
|
48
|
+
#
|
|
49
|
+
def __getattr__(name: str) -> Any:
|
|
50
|
+
"""Return the default apps on first use instead of building them at import.
|
|
51
|
+
|
|
52
|
+
``api_dock.app`` / ``api_dock.fastapi_app`` (FastAPI) and
|
|
53
|
+
``api_dock.flask_app`` are built from the default config the first time
|
|
54
|
+
they are accessed (see ``fast_api.__getattr__``), so importing api_dock no
|
|
55
|
+
longer requires a valid config in the current directory.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
name: The attribute being looked up.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
The requested default app.
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
AttributeError: For any other missing attribute.
|
|
65
|
+
"""
|
|
66
|
+
if name in ("app", "fastapi_app"):
|
|
67
|
+
from api_dock import fast_api
|
|
68
|
+
return fast_api.app
|
|
69
|
+
if name == "flask_app":
|
|
70
|
+
from api_dock import flask_api
|
|
71
|
+
return flask_api.app
|
|
72
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -553,7 +553,7 @@ def resolve_latest_version(versions: List[str]) -> Optional[str]:
|
|
|
553
553
|
return sorted_versions[0]
|
|
554
554
|
|
|
555
555
|
|
|
556
|
-
def is_route_allowed(route: str, config: Dict[str, Any], remote_name: Optional[str] = None, version: Optional[str] = None, method: Optional[str] = None) -> bool:
|
|
556
|
+
def is_route_allowed(route: str, config: Dict[str, Any], remote_name: Optional[str] = None, version: Optional[str] = None, method: Optional[str] = None, config_dir: Optional[str] = None) -> bool:
|
|
557
557
|
"""Check if a route is allowed based on configuration restrictions.
|
|
558
558
|
|
|
559
559
|
Args:
|
|
@@ -562,6 +562,8 @@ def is_route_allowed(route: str, config: Dict[str, Any], remote_name: Optional[s
|
|
|
562
562
|
remote_name: Name of the remote API (for remote-specific restrictions).
|
|
563
563
|
version: Version string for versioned remotes.
|
|
564
564
|
method: HTTP method (e.g., "GET", "POST", "DELETE").
|
|
565
|
+
config_dir: Directory holding the remote config files. If omitted, it
|
|
566
|
+
is guessed from a few common locations.
|
|
565
567
|
|
|
566
568
|
Returns:
|
|
567
569
|
True if route is allowed, False otherwise.
|
|
@@ -581,9 +583,8 @@ def is_route_allowed(route: str, config: Dict[str, Any], remote_name: Optional[s
|
|
|
581
583
|
try:
|
|
582
584
|
# Try to infer config_dir by checking which directory exists
|
|
583
585
|
# This is a heuristic to support both default and custom config locations
|
|
584
|
-
config_dir = None
|
|
585
586
|
remotes = config.get("remotes", [])
|
|
586
|
-
if remotes:
|
|
587
|
+
if config_dir is None and remotes:
|
|
587
588
|
first_remote = remotes[0] if isinstance(remotes[0], str) else remotes[0].get("name") if isinstance(remotes[0], dict) else None
|
|
588
589
|
if first_remote:
|
|
589
590
|
# Try common locations
|