api-dock 0.5.0__tar.gz → 0.7.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.5.0 → api_dock-0.7.0}/PKG-INFO +139 -11
- {api_dock-0.5.0 → api_dock-0.7.0}/README.md +136 -8
- {api_dock-0.5.0 → api_dock-0.7.0}/api_dock/cli.py +4 -13
- {api_dock-0.5.0 → api_dock-0.7.0}/api_dock/config.py +74 -18
- {api_dock-0.5.0 → api_dock-0.7.0}/api_dock/config_discovery.py +9 -15
- api_dock-0.7.0/api_dock/example_api_dock_config/config.yaml +23 -0
- api_dock-0.7.0/api_dock/example_api_dock_config/databases/example_db.yaml +82 -0
- api_dock-0.7.0/api_dock/example_api_dock_config/remotes/example_remote.yaml +30 -0
- api_dock-0.7.0/api_dock/fast_api.py +265 -0
- api_dock-0.7.0/api_dock/flask_api.py +187 -0
- api_dock-0.7.0/api_dock/route_mapper.py +676 -0
- {api_dock-0.5.0 → api_dock-0.7.0}/api_dock/sql_builder.py +67 -5
- api_dock-0.7.0/api_dock/types.py +89 -0
- {api_dock-0.5.0 → api_dock-0.7.0}/api_dock.egg-info/PKG-INFO +139 -11
- {api_dock-0.5.0 → api_dock-0.7.0}/api_dock.egg-info/SOURCES.txt +8 -7
- {api_dock-0.5.0 → api_dock-0.7.0}/api_dock.egg-info/requires.txt +1 -1
- {api_dock-0.5.0 → api_dock-0.7.0}/api_dock.egg-info/top_level.txt +0 -1
- {api_dock-0.5.0 → api_dock-0.7.0}/pyproject.toml +6 -6
- api_dock-0.7.0/tests/test_inject_cookies.py +121 -0
- api_dock-0.7.0/tests/test_proxy_pipeline.py +731 -0
- api_dock-0.7.0/tests/test_sql_builder.py +137 -0
- api_dock-0.7.0/tests/test_types.py +124 -0
- api_dock-0.5.0/api_dock/fast_api.py +0 -170
- api_dock-0.5.0/api_dock/flask_api.py +0 -218
- api_dock-0.5.0/api_dock/route_mapper.py +0 -576
- api_dock-0.5.0/config/config.yaml +0 -25
- api_dock-0.5.0/config/databases/db_example.yaml +0 -19
- api_dock-0.5.0/config/databases/test_users.yaml +0 -127
- api_dock-0.5.0/config/remotes/remote_with_allowed_routes.yaml +0 -10
- api_dock-0.5.0/config/remotes/remote_with_custom_mapping.yaml +0 -16
- api_dock-0.5.0/config/remotes/remote_with_restrictions.yaml +0 -8
- api_dock-0.5.0/config/remotes/remote_with_wildcards.yaml +0 -18
- {api_dock-0.5.0 → api_dock-0.7.0}/LICENSE.md +0 -0
- {api_dock-0.5.0 → api_dock-0.7.0}/api_dock/__init__.py +0 -0
- {api_dock-0.5.0 → api_dock-0.7.0}/api_dock/auth.py +0 -0
- {api_dock-0.5.0 → api_dock-0.7.0}/api_dock/database_config.py +0 -0
- {api_dock-0.5.0 → api_dock-0.7.0}/api_dock/encryption.py +0 -0
- {api_dock-0.5.0 → api_dock-0.7.0}/api_dock/storage_auth.py +0 -0
- {api_dock-0.5.0 → api_dock-0.7.0}/api_dock.egg-info/dependency_links.txt +0 -0
- {api_dock-0.5.0 → api_dock-0.7.0}/api_dock.egg-info/entry_points.txt +0 -0
- {api_dock-0.5.0 → api_dock-0.7.0}/setup.cfg +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: api_dock
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.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
|
-
License:
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
7
|
Classifier: Development Status :: 4 - Beta
|
|
8
8
|
Classifier: Intended Audience :: Developers
|
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -26,7 +26,7 @@ Requires-Dist: pyyaml<7,>=6.0.3
|
|
|
26
26
|
Requires-Dist: httpx<0.29,>=0.28.1
|
|
27
27
|
Requires-Dist: duckdb<2,>=1.1.3
|
|
28
28
|
Requires-Dist: flask
|
|
29
|
-
Requires-Dist: cryptography<
|
|
29
|
+
Requires-Dist: cryptography<49.0.0,>=48.0.0
|
|
30
30
|
Requires-Dist: boto3<2,>=1.42.59
|
|
31
31
|
Provides-Extra: dev
|
|
32
32
|
Requires-Dist: pycodestyle<3,>=2.14.0; extra == "dev"
|
|
@@ -218,6 +218,7 @@ remotes:
|
|
|
218
218
|
settings:
|
|
219
219
|
add_trailing_slash: true # Auto-add trailing slash to paths (default: true)
|
|
220
220
|
follow_protocol_downgrades: false # Allow HTTPS->HTTP redirects (default: false)
|
|
221
|
+
timeout: 10 # Upstream request timeout in seconds (default: 10)
|
|
221
222
|
```
|
|
222
223
|
|
|
223
224
|
### HTTP behavior Settings
|
|
@@ -228,6 +229,8 @@ The optional `settings` section controls HTTP behavior:
|
|
|
228
229
|
|
|
229
230
|
- **`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).
|
|
230
231
|
|
|
232
|
+
- **`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).
|
|
233
|
+
|
|
231
234
|
---
|
|
232
235
|
|
|
233
236
|
## Remote Configurations
|
|
@@ -349,7 +352,7 @@ For now only parquet support is working but we will be adding other Databases in
|
|
|
349
352
|
|
|
350
353
|
### Database Configuration
|
|
351
354
|
|
|
352
|
-
Database configurations are stored in `
|
|
355
|
+
Database configurations are stored in `api_dock_config/databases/` directory. Each database defines:
|
|
353
356
|
- **tables**: Mapping of table names to file paths (supports S3, GCS, HTTPS, local paths)
|
|
354
357
|
- **queries**: Named SQL queries for reuse
|
|
355
358
|
- **routes**: REST endpoints mapped to SQL queries
|
|
@@ -463,6 +466,34 @@ GET /db/users
|
|
|
463
466
|
# SQL: SELECT * FROM users WHERE height < 200
|
|
464
467
|
```
|
|
465
468
|
|
|
469
|
+
### Repeated Parameters with `multivalue_sql`
|
|
470
|
+
|
|
471
|
+
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, quote-escaped SQL value list suitable for an `IN` clause.
|
|
472
|
+
|
|
473
|
+
Behavior is unchanged when `multivalue_sql` is absent, and when only a single value is passed the normal `sql` template is used.
|
|
474
|
+
|
|
475
|
+
```yaml
|
|
476
|
+
routes:
|
|
477
|
+
- route: detections
|
|
478
|
+
sql: SELECT [[detections]].* FROM [[detections]]
|
|
479
|
+
query_params:
|
|
480
|
+
- recording_id:
|
|
481
|
+
sql: "[[detections]].recording_id = {{recording_id}}" # single value
|
|
482
|
+
multivalue_sql: "[[detections]].recording_id IN {{recording_id}}" # 2+ values
|
|
483
|
+
- scientific_name:
|
|
484
|
+
sql: "[[detections]].scientific_name = '{{scientific_name}}'"
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
```bash
|
|
488
|
+
GET /db/detections?recording_id=4&scientific_name=Gryllus%20fultoni
|
|
489
|
+
# SQL: SELECT detections.* FROM detections
|
|
490
|
+
# WHERE detections.recording_id = '4' AND detections.scientific_name = 'Gryllus fultoni'
|
|
491
|
+
|
|
492
|
+
GET /db/detections?recording_id=4&recording_id=1&scientific_name=Gryllus%20fultoni
|
|
493
|
+
# SQL: SELECT detections.* FROM detections
|
|
494
|
+
# WHERE detections.recording_id IN ('4', '1') AND detections.scientific_name = 'Gryllus fultoni'
|
|
495
|
+
```
|
|
496
|
+
|
|
466
497
|
### Sorting and Pagination with `sql_append`
|
|
467
498
|
|
|
468
499
|
Use `sql_append` to append clauses *after* the WHERE clause — for `ORDER BY`, `LIMIT`, `OFFSET`, etc. Fragments are appended in the order they appear in the YAML config, so **the YAML order must match valid SQL order** (ORDER BY before LIMIT before OFFSET).
|
|
@@ -702,30 +733,79 @@ API Dock supports cookie extraction and authentication for both remote APIs and
|
|
|
702
733
|
|
|
703
734
|
## Cookie Configuration
|
|
704
735
|
|
|
705
|
-
|
|
736
|
+
### Forwarding client cookies
|
|
737
|
+
|
|
738
|
+
Configure which cookies to extract from incoming requests and forward to the upstream API (or make available as template variables in SQL routes):
|
|
706
739
|
|
|
707
740
|
```yaml
|
|
708
|
-
#
|
|
741
|
+
# Forward all cookies from the client request
|
|
709
742
|
cookies: true
|
|
710
743
|
|
|
711
|
-
#
|
|
744
|
+
# Forward only specific cookies
|
|
712
745
|
cookies: [session_id, auth_token, user_preferences]
|
|
713
746
|
|
|
714
|
-
#
|
|
747
|
+
# Forward no cookies (default behavior)
|
|
715
748
|
cookies: false
|
|
716
749
|
```
|
|
717
750
|
|
|
718
|
-
When `cookies: true`, all cookies are accepted and available. When `cookies: false` (default), no cookies are processed except authentication cookies when authentication is configured. When providing a list, only
|
|
751
|
+
When `cookies: true`, all cookies are accepted and available. When `cookies: false` (default), no cookies are processed except authentication cookies when authentication is configured. When providing a list, only the named cookies are forwarded.
|
|
719
752
|
|
|
720
|
-
|
|
753
|
+
Forwarded cookies are accessible in SQL queries using `{{cookies.cookie_name}}`:
|
|
721
754
|
|
|
722
755
|
```yaml
|
|
723
|
-
# Database route using cookies
|
|
724
756
|
routes:
|
|
725
757
|
- route: user/profile
|
|
726
758
|
sql: SELECT * FROM [[users]] WHERE session_id = '{{cookies.session_id}}'
|
|
727
759
|
```
|
|
728
760
|
|
|
761
|
+
### Injecting cookies from the server environment
|
|
762
|
+
|
|
763
|
+
The `cookies` list also accepts dict entries to inject cookies into every outgoing upstream request, regardless of what the client sent. This is useful when the upstream API requires a server-side credential (e.g. a session token stored in an environment variable) rather than a cookie from the end user.
|
|
764
|
+
|
|
765
|
+
Dict entries and string entries can be mixed freely in the same list.
|
|
766
|
+
|
|
767
|
+
```yaml
|
|
768
|
+
cookies:
|
|
769
|
+
- session_id # forward this cookie from the client request
|
|
770
|
+
- key: __Secure-authjs.session-token # inject from environment variable
|
|
771
|
+
value: "env:SOUNDHUB_SESSION_TOKEN"
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
#### Dict entry forms
|
|
775
|
+
|
|
776
|
+
| Form | Behaviour |
|
|
777
|
+
|---|---|
|
|
778
|
+
| `{key: NAME, value: "literal"}` | Inject cookie `NAME` with the literal string `"literal"` |
|
|
779
|
+
| `{key: NAME, value: "env:MY_VAR"}` | Inject cookie `NAME` with the value of env var `MY_VAR`; empty string if unset |
|
|
780
|
+
| `{key: MY_VAR}` | Shorthand — equivalent to `{key: MY_VAR, value: "env:MY_VAR"}` |
|
|
781
|
+
|
|
782
|
+
The shorthand form uses the key as both the cookie name **and** the env var name. Use the explicit `value: "env:..."` form when the cookie name and the env var name differ (which is common — cookie names often contain characters that aren't valid env var names).
|
|
783
|
+
|
|
784
|
+
#### Example: proxying an API that requires a session cookie
|
|
785
|
+
|
|
786
|
+
```yaml
|
|
787
|
+
# api_dock_config/remotes/my_service/1.0.yaml
|
|
788
|
+
name: my_service
|
|
789
|
+
url: https://api.example.com
|
|
790
|
+
|
|
791
|
+
cookies:
|
|
792
|
+
- key: __Secure-authjs.session-token
|
|
793
|
+
value: "env:MY_SERVICE_SESSION_TOKEN"
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
Set the env var before starting api-dock:
|
|
797
|
+
|
|
798
|
+
```bash
|
|
799
|
+
export MY_SERVICE_SESSION_TOKEN="your-session-token-here"
|
|
800
|
+
pixi run api-dock start
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
Every request proxied to `my_service` will include the `__Secure-authjs.session-token` cookie, even if the client did not send one.
|
|
804
|
+
|
|
805
|
+
#### Injection precedence
|
|
806
|
+
|
|
807
|
+
If an injected cookie has the same name as a forwarded client cookie, the injected value takes precedence. This ensures the server-side credential is always used, regardless of what the client sends.
|
|
808
|
+
|
|
729
809
|
## Authentication Configuration
|
|
730
810
|
|
|
731
811
|
Configure authentication to validate requests before processing. Multiple authentication methods are supported:
|
|
@@ -1119,6 +1199,54 @@ pixi run jupyter lab .
|
|
|
1119
1199
|
pixi run python scripts/hello_world.py
|
|
1120
1200
|
```
|
|
1121
1201
|
|
|
1202
|
+
---
|
|
1203
|
+
|
|
1204
|
+
# Development
|
|
1205
|
+
|
|
1206
|
+
## Publishing a Release
|
|
1207
|
+
|
|
1208
|
+
```bash
|
|
1209
|
+
# 0. Make sure you are on `main` and merged with any changes
|
|
1210
|
+
|
|
1211
|
+
# 1. Bump version in pyproject.toml
|
|
1212
|
+
|
|
1213
|
+
# 2. Commit everything
|
|
1214
|
+
git add -A
|
|
1215
|
+
git commit -m "v0.6.1: stream proxy responses (fix large-response 502 + content-encoding)"
|
|
1216
|
+
|
|
1217
|
+
# 3. Tag and push
|
|
1218
|
+
git tag v0.6.1
|
|
1219
|
+
git push origin main v0.6.1
|
|
1220
|
+
|
|
1221
|
+
# 4. Build the wheel (requires the `dev` pixi environment)
|
|
1222
|
+
rm -rf dist/
|
|
1223
|
+
find . -name "__pycache__" -type d -exec rm -rf {} +
|
|
1224
|
+
find . -name "*.pyc" -delete
|
|
1225
|
+
pixi run -e dev python -m build --wheel
|
|
1226
|
+
ls dist/*.whl
|
|
1227
|
+
|
|
1228
|
+
# 5. Create GitHub release with the wheel attached
|
|
1229
|
+
gh release create v0.6.1 dist/api_dock-0.6.1-py3-none-any.whl \
|
|
1230
|
+
--title "v0.6.1" --notes "$(cat <<'EOF'
|
|
1231
|
+
* new features
|
|
1232
|
+
- Remote proxy responses are now streamed (FastAPI) — upstream bytes are piped to the client as they arrive instead of being buffered fully in memory
|
|
1233
|
+
- New `timeout` setting (default 10s) for the upstream request; set to `null`/`false` to disable
|
|
1234
|
+
* bug fixes
|
|
1235
|
+
- Large upstream responses no longer return 502 — streamed via `StreamingResponse` instead of reading the whole body into memory
|
|
1236
|
+
- `Content-Encoding` (gzip/br/deflate) is now preserved on compressed responses — raw bytes are streamed via `aiter_raw()` so the header stays valid and the client can decompress
|
|
1237
|
+
- Slow upstreams (e.g. large aggregation queries) no longer 502 at httpx's hardcoded 5s default — the timeout is now configurable via the `timeout` setting
|
|
1238
|
+
* cleanup / other improvements
|
|
1239
|
+
- Added `PreparedRequest` dataclass and split route validation/resolution into `RouteMapper.prepare_remote_request()`; the FastAPI adapter issues the streaming HTTP call
|
|
1240
|
+
- `map_route()` (buffered) retained for the Flask/sync path
|
|
1241
|
+
- Added streaming test coverage (`TestStreamUpstream`, plus `prepare_remote_request` and streaming-header tests) — 53 tests total
|
|
1242
|
+
EOF
|
|
1243
|
+
)"
|
|
1244
|
+
|
|
1245
|
+
# 6. Publish to PyPI
|
|
1246
|
+
pixi run -e dev python -m twine upload dist/*.whl
|
|
1247
|
+
```
|
|
1248
|
+
|
|
1249
|
+
|
|
1122
1250
|
---
|
|
1123
1251
|
|
|
1124
1252
|
# License
|
|
@@ -179,6 +179,7 @@ remotes:
|
|
|
179
179
|
settings:
|
|
180
180
|
add_trailing_slash: true # Auto-add trailing slash to paths (default: true)
|
|
181
181
|
follow_protocol_downgrades: false # Allow HTTPS->HTTP redirects (default: false)
|
|
182
|
+
timeout: 10 # Upstream request timeout in seconds (default: 10)
|
|
182
183
|
```
|
|
183
184
|
|
|
184
185
|
### HTTP behavior Settings
|
|
@@ -189,6 +190,8 @@ The optional `settings` section controls HTTP behavior:
|
|
|
189
190
|
|
|
190
191
|
- **`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).
|
|
191
192
|
|
|
193
|
+
- **`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).
|
|
194
|
+
|
|
192
195
|
---
|
|
193
196
|
|
|
194
197
|
## Remote Configurations
|
|
@@ -310,7 +313,7 @@ For now only parquet support is working but we will be adding other Databases in
|
|
|
310
313
|
|
|
311
314
|
### Database Configuration
|
|
312
315
|
|
|
313
|
-
Database configurations are stored in `
|
|
316
|
+
Database configurations are stored in `api_dock_config/databases/` directory. Each database defines:
|
|
314
317
|
- **tables**: Mapping of table names to file paths (supports S3, GCS, HTTPS, local paths)
|
|
315
318
|
- **queries**: Named SQL queries for reuse
|
|
316
319
|
- **routes**: REST endpoints mapped to SQL queries
|
|
@@ -424,6 +427,34 @@ GET /db/users
|
|
|
424
427
|
# SQL: SELECT * FROM users WHERE height < 200
|
|
425
428
|
```
|
|
426
429
|
|
|
430
|
+
### Repeated Parameters with `multivalue_sql`
|
|
431
|
+
|
|
432
|
+
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, quote-escaped SQL value list suitable for an `IN` clause.
|
|
433
|
+
|
|
434
|
+
Behavior is unchanged when `multivalue_sql` is absent, and when only a single value is passed the normal `sql` template is used.
|
|
435
|
+
|
|
436
|
+
```yaml
|
|
437
|
+
routes:
|
|
438
|
+
- route: detections
|
|
439
|
+
sql: SELECT [[detections]].* FROM [[detections]]
|
|
440
|
+
query_params:
|
|
441
|
+
- recording_id:
|
|
442
|
+
sql: "[[detections]].recording_id = {{recording_id}}" # single value
|
|
443
|
+
multivalue_sql: "[[detections]].recording_id IN {{recording_id}}" # 2+ values
|
|
444
|
+
- scientific_name:
|
|
445
|
+
sql: "[[detections]].scientific_name = '{{scientific_name}}'"
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
```bash
|
|
449
|
+
GET /db/detections?recording_id=4&scientific_name=Gryllus%20fultoni
|
|
450
|
+
# SQL: SELECT detections.* FROM detections
|
|
451
|
+
# WHERE detections.recording_id = '4' AND detections.scientific_name = 'Gryllus fultoni'
|
|
452
|
+
|
|
453
|
+
GET /db/detections?recording_id=4&recording_id=1&scientific_name=Gryllus%20fultoni
|
|
454
|
+
# SQL: SELECT detections.* FROM detections
|
|
455
|
+
# WHERE detections.recording_id IN ('4', '1') AND detections.scientific_name = 'Gryllus fultoni'
|
|
456
|
+
```
|
|
457
|
+
|
|
427
458
|
### Sorting and Pagination with `sql_append`
|
|
428
459
|
|
|
429
460
|
Use `sql_append` to append clauses *after* the WHERE clause — for `ORDER BY`, `LIMIT`, `OFFSET`, etc. Fragments are appended in the order they appear in the YAML config, so **the YAML order must match valid SQL order** (ORDER BY before LIMIT before OFFSET).
|
|
@@ -663,30 +694,79 @@ API Dock supports cookie extraction and authentication for both remote APIs and
|
|
|
663
694
|
|
|
664
695
|
## Cookie Configuration
|
|
665
696
|
|
|
666
|
-
|
|
697
|
+
### Forwarding client cookies
|
|
698
|
+
|
|
699
|
+
Configure which cookies to extract from incoming requests and forward to the upstream API (or make available as template variables in SQL routes):
|
|
667
700
|
|
|
668
701
|
```yaml
|
|
669
|
-
#
|
|
702
|
+
# Forward all cookies from the client request
|
|
670
703
|
cookies: true
|
|
671
704
|
|
|
672
|
-
#
|
|
705
|
+
# Forward only specific cookies
|
|
673
706
|
cookies: [session_id, auth_token, user_preferences]
|
|
674
707
|
|
|
675
|
-
#
|
|
708
|
+
# Forward no cookies (default behavior)
|
|
676
709
|
cookies: false
|
|
677
710
|
```
|
|
678
711
|
|
|
679
|
-
When `cookies: true`, all cookies are accepted and available. When `cookies: false` (default), no cookies are processed except authentication cookies when authentication is configured. When providing a list, only
|
|
712
|
+
When `cookies: true`, all cookies are accepted and available. When `cookies: false` (default), no cookies are processed except authentication cookies when authentication is configured. When providing a list, only the named cookies are forwarded.
|
|
680
713
|
|
|
681
|
-
|
|
714
|
+
Forwarded cookies are accessible in SQL queries using `{{cookies.cookie_name}}`:
|
|
682
715
|
|
|
683
716
|
```yaml
|
|
684
|
-
# Database route using cookies
|
|
685
717
|
routes:
|
|
686
718
|
- route: user/profile
|
|
687
719
|
sql: SELECT * FROM [[users]] WHERE session_id = '{{cookies.session_id}}'
|
|
688
720
|
```
|
|
689
721
|
|
|
722
|
+
### Injecting cookies from the server environment
|
|
723
|
+
|
|
724
|
+
The `cookies` list also accepts dict entries to inject cookies into every outgoing upstream request, regardless of what the client sent. This is useful when the upstream API requires a server-side credential (e.g. a session token stored in an environment variable) rather than a cookie from the end user.
|
|
725
|
+
|
|
726
|
+
Dict entries and string entries can be mixed freely in the same list.
|
|
727
|
+
|
|
728
|
+
```yaml
|
|
729
|
+
cookies:
|
|
730
|
+
- session_id # forward this cookie from the client request
|
|
731
|
+
- key: __Secure-authjs.session-token # inject from environment variable
|
|
732
|
+
value: "env:SOUNDHUB_SESSION_TOKEN"
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
#### Dict entry forms
|
|
736
|
+
|
|
737
|
+
| Form | Behaviour |
|
|
738
|
+
|---|---|
|
|
739
|
+
| `{key: NAME, value: "literal"}` | Inject cookie `NAME` with the literal string `"literal"` |
|
|
740
|
+
| `{key: NAME, value: "env:MY_VAR"}` | Inject cookie `NAME` with the value of env var `MY_VAR`; empty string if unset |
|
|
741
|
+
| `{key: MY_VAR}` | Shorthand — equivalent to `{key: MY_VAR, value: "env:MY_VAR"}` |
|
|
742
|
+
|
|
743
|
+
The shorthand form uses the key as both the cookie name **and** the env var name. Use the explicit `value: "env:..."` form when the cookie name and the env var name differ (which is common — cookie names often contain characters that aren't valid env var names).
|
|
744
|
+
|
|
745
|
+
#### Example: proxying an API that requires a session cookie
|
|
746
|
+
|
|
747
|
+
```yaml
|
|
748
|
+
# api_dock_config/remotes/my_service/1.0.yaml
|
|
749
|
+
name: my_service
|
|
750
|
+
url: https://api.example.com
|
|
751
|
+
|
|
752
|
+
cookies:
|
|
753
|
+
- key: __Secure-authjs.session-token
|
|
754
|
+
value: "env:MY_SERVICE_SESSION_TOKEN"
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
Set the env var before starting api-dock:
|
|
758
|
+
|
|
759
|
+
```bash
|
|
760
|
+
export MY_SERVICE_SESSION_TOKEN="your-session-token-here"
|
|
761
|
+
pixi run api-dock start
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
Every request proxied to `my_service` will include the `__Secure-authjs.session-token` cookie, even if the client did not send one.
|
|
765
|
+
|
|
766
|
+
#### Injection precedence
|
|
767
|
+
|
|
768
|
+
If an injected cookie has the same name as a forwarded client cookie, the injected value takes precedence. This ensures the server-side credential is always used, regardless of what the client sends.
|
|
769
|
+
|
|
690
770
|
## Authentication Configuration
|
|
691
771
|
|
|
692
772
|
Configure authentication to validate requests before processing. Multiple authentication methods are supported:
|
|
@@ -1080,6 +1160,54 @@ pixi run jupyter lab .
|
|
|
1080
1160
|
pixi run python scripts/hello_world.py
|
|
1081
1161
|
```
|
|
1082
1162
|
|
|
1163
|
+
---
|
|
1164
|
+
|
|
1165
|
+
# Development
|
|
1166
|
+
|
|
1167
|
+
## Publishing a Release
|
|
1168
|
+
|
|
1169
|
+
```bash
|
|
1170
|
+
# 0. Make sure you are on `main` and merged with any changes
|
|
1171
|
+
|
|
1172
|
+
# 1. Bump version in pyproject.toml
|
|
1173
|
+
|
|
1174
|
+
# 2. Commit everything
|
|
1175
|
+
git add -A
|
|
1176
|
+
git commit -m "v0.6.1: stream proxy responses (fix large-response 502 + content-encoding)"
|
|
1177
|
+
|
|
1178
|
+
# 3. Tag and push
|
|
1179
|
+
git tag v0.6.1
|
|
1180
|
+
git push origin main v0.6.1
|
|
1181
|
+
|
|
1182
|
+
# 4. Build the wheel (requires the `dev` pixi environment)
|
|
1183
|
+
rm -rf dist/
|
|
1184
|
+
find . -name "__pycache__" -type d -exec rm -rf {} +
|
|
1185
|
+
find . -name "*.pyc" -delete
|
|
1186
|
+
pixi run -e dev python -m build --wheel
|
|
1187
|
+
ls dist/*.whl
|
|
1188
|
+
|
|
1189
|
+
# 5. Create GitHub release with the wheel attached
|
|
1190
|
+
gh release create v0.6.1 dist/api_dock-0.6.1-py3-none-any.whl \
|
|
1191
|
+
--title "v0.6.1" --notes "$(cat <<'EOF'
|
|
1192
|
+
* new features
|
|
1193
|
+
- Remote proxy responses are now streamed (FastAPI) — upstream bytes are piped to the client as they arrive instead of being buffered fully in memory
|
|
1194
|
+
- New `timeout` setting (default 10s) for the upstream request; set to `null`/`false` to disable
|
|
1195
|
+
* bug fixes
|
|
1196
|
+
- Large upstream responses no longer return 502 — streamed via `StreamingResponse` instead of reading the whole body into memory
|
|
1197
|
+
- `Content-Encoding` (gzip/br/deflate) is now preserved on compressed responses — raw bytes are streamed via `aiter_raw()` so the header stays valid and the client can decompress
|
|
1198
|
+
- Slow upstreams (e.g. large aggregation queries) no longer 502 at httpx's hardcoded 5s default — the timeout is now configurable via the `timeout` setting
|
|
1199
|
+
* cleanup / other improvements
|
|
1200
|
+
- Added `PreparedRequest` dataclass and split route validation/resolution into `RouteMapper.prepare_remote_request()`; the FastAPI adapter issues the streaming HTTP call
|
|
1201
|
+
- `map_route()` (buffered) retained for the Flask/sync path
|
|
1202
|
+
- Added streaming test coverage (`TestStreamUpstream`, plus `prepare_remote_request` and streaming-header tests) — 53 tests total
|
|
1203
|
+
EOF
|
|
1204
|
+
)"
|
|
1205
|
+
|
|
1206
|
+
# 6. Publish to PyPI
|
|
1207
|
+
pixi run -e dev python -m twine upload dist/*.whl
|
|
1208
|
+
```
|
|
1209
|
+
|
|
1210
|
+
|
|
1083
1211
|
---
|
|
1084
1212
|
|
|
1085
1213
|
# License
|
|
@@ -459,15 +459,12 @@ def _list_configs() -> None:
|
|
|
459
459
|
|
|
460
460
|
# Check for local configs
|
|
461
461
|
local_dir = Path("api_dock_config")
|
|
462
|
-
config_dir = Path("config")
|
|
463
|
-
|
|
464
462
|
local_configs = list(local_dir.glob("*.yaml")) if local_dir.exists() else []
|
|
465
|
-
config_configs = list(config_dir.glob("*.yaml")) if config_dir.exists() else []
|
|
466
463
|
|
|
467
|
-
# Check for
|
|
464
|
+
# Check for bundled example configs
|
|
468
465
|
try:
|
|
469
466
|
import importlib.resources as pkg_resources
|
|
470
|
-
package_dir = Path(pkg_resources.files("api_dock") / "
|
|
467
|
+
package_dir = Path(pkg_resources.files("api_dock") / "example_api_dock_config")
|
|
471
468
|
package_configs = list(package_dir.glob("*.yaml")) if package_dir.exists() else []
|
|
472
469
|
except Exception:
|
|
473
470
|
package_configs = []
|
|
@@ -478,17 +475,11 @@ def _list_configs() -> None:
|
|
|
478
475
|
click.echo(f" {config_file.stem}")
|
|
479
476
|
click.echo()
|
|
480
477
|
else:
|
|
481
|
-
click.echo("📁 Local configurations (api_dock_config/): None")
|
|
482
|
-
click.echo()
|
|
483
|
-
|
|
484
|
-
if config_configs:
|
|
485
|
-
click.echo("📁 Project configurations (config/):")
|
|
486
|
-
for config_file in sorted(config_configs):
|
|
487
|
-
click.echo(f" {config_file.stem}")
|
|
478
|
+
click.echo("📁 Local configurations (api_dock_config/): None — run 'api-dock init' to create")
|
|
488
479
|
click.echo()
|
|
489
480
|
|
|
490
481
|
if package_configs:
|
|
491
|
-
click.echo("📦
|
|
482
|
+
click.echo("📦 Example configurations (run 'api-dock init' to copy to api_dock_config/):")
|
|
492
483
|
for config_file in sorted(package_configs):
|
|
493
484
|
click.echo(f" {config_file.stem}")
|
|
494
485
|
click.echo()
|
|
@@ -242,7 +242,10 @@ def get_settings(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
242
242
|
|
|
243
243
|
|
|
244
244
|
def get_cookies_config(config: Dict[str, Any]) -> List[str]:
|
|
245
|
-
"""Extract cookie
|
|
245
|
+
"""Extract cookie names to forward from incoming requests.
|
|
246
|
+
|
|
247
|
+
Only string entries in the ``cookies`` list are returned here. Dict entries
|
|
248
|
+
(cookie injection) are handled separately by ``resolve_inject_cookies``.
|
|
246
249
|
|
|
247
250
|
Args:
|
|
248
251
|
config: Configuration dictionary (main, remote, or database).
|
|
@@ -254,9 +257,7 @@ def get_cookies_config(config: Dict[str, Any]) -> List[str]:
|
|
|
254
257
|
if not isinstance(cookies, list):
|
|
255
258
|
return []
|
|
256
259
|
|
|
257
|
-
|
|
258
|
-
cookie_list = [str(cookie) for cookie in cookies if cookie]
|
|
259
|
-
return cookie_list
|
|
260
|
+
return [str(c) for c in cookies if c and isinstance(c, str)]
|
|
260
261
|
|
|
261
262
|
|
|
262
263
|
def filter_cookies_by_config(cookies: Dict[str, str], config: Dict[str, Any]) -> Dict[str, str]:
|
|
@@ -288,23 +289,26 @@ def filter_cookies_by_config(cookies: Dict[str, str], config: Dict[str, Any]) ->
|
|
|
288
289
|
else:
|
|
289
290
|
return {}
|
|
290
291
|
|
|
291
|
-
# Handle list
|
|
292
|
+
# Handle list — may contain string names (forward from client) and/or
|
|
293
|
+
# dict entries (inject from config/env var).
|
|
292
294
|
elif isinstance(cookies_setting, list):
|
|
295
|
+
injected = resolve_inject_cookies(config)
|
|
293
296
|
allowed_cookies = get_cookies_config(config)
|
|
297
|
+
|
|
294
298
|
if not allowed_cookies:
|
|
295
|
-
#
|
|
299
|
+
# No string entries — only auth key forwarded from client, plus injected.
|
|
300
|
+
result: Dict[str, str] = {}
|
|
296
301
|
if auth_key and auth_key in cookies:
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
return {}
|
|
302
|
+
result[auth_key] = cookies[auth_key]
|
|
303
|
+
result.update(injected)
|
|
304
|
+
return result
|
|
301
305
|
|
|
302
|
-
# Always include authentication key in
|
|
306
|
+
# Always include authentication key in forwarded set.
|
|
303
307
|
if auth_key and auth_key not in allowed_cookies:
|
|
304
308
|
allowed_cookies = allowed_cookies + [auth_key]
|
|
305
309
|
|
|
306
|
-
# Filter cookies to only include allowed ones
|
|
307
310
|
filtered = {k: v for k, v in cookies.items() if k in allowed_cookies}
|
|
311
|
+
filtered.update(injected)
|
|
308
312
|
return filtered
|
|
309
313
|
|
|
310
314
|
# Default: no cookie configuration means only authentication key passed
|
|
@@ -316,6 +320,56 @@ def filter_cookies_by_config(cookies: Dict[str, str], config: Dict[str, Any]) ->
|
|
|
316
320
|
return {}
|
|
317
321
|
|
|
318
322
|
|
|
323
|
+
def resolve_inject_cookies(config: Dict[str, Any]) -> Dict[str, str]:
|
|
324
|
+
"""Resolve dict entries in the ``cookies`` list into ready-to-send cookie values.
|
|
325
|
+
|
|
326
|
+
The ``cookies`` list accepts a mix of strings (names of client cookies to forward)
|
|
327
|
+
and dicts (cookies to inject into the outgoing request regardless of what the
|
|
328
|
+
client sent). This function handles the dict entries only.
|
|
329
|
+
|
|
330
|
+
Dict entry forms:
|
|
331
|
+
|
|
332
|
+
- ``{key: COOKIE_NAME, value: "literal-value"}`` — inject with a literal string.
|
|
333
|
+
- ``{key: COOKIE_NAME, value: "env:MY_ENV_VAR"}`` — inject with the value of
|
|
334
|
+
environment variable ``MY_ENV_VAR``; resolves to empty string if unset.
|
|
335
|
+
- ``{key: MY_ENV_VAR}`` — shorthand: no ``value`` means look up an env var whose
|
|
336
|
+
name equals the key (equivalent to ``{key: MY_ENV_VAR, value: "env:MY_ENV_VAR"}``).
|
|
337
|
+
|
|
338
|
+
Example YAML::
|
|
339
|
+
|
|
340
|
+
cookies:
|
|
341
|
+
- session_id # forward from client request
|
|
342
|
+
- key: __Secure-authjs.session-token
|
|
343
|
+
value: "env:SOUNDHUB_SESSION_TOKEN" # inject from env var
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
config: Configuration dictionary (main, remote, or database).
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
Dictionary mapping cookie names to their resolved string values.
|
|
350
|
+
"""
|
|
351
|
+
cookies_setting = config.get("cookies", [])
|
|
352
|
+
if not isinstance(cookies_setting, list):
|
|
353
|
+
return {}
|
|
354
|
+
|
|
355
|
+
result: Dict[str, str] = {}
|
|
356
|
+
for entry in cookies_setting:
|
|
357
|
+
if not isinstance(entry, dict):
|
|
358
|
+
continue
|
|
359
|
+
key = entry.get("key")
|
|
360
|
+
if not key or not isinstance(key, str):
|
|
361
|
+
continue
|
|
362
|
+
raw_value = entry.get("value")
|
|
363
|
+
if raw_value is None:
|
|
364
|
+
value = os.environ.get(key, "")
|
|
365
|
+
elif isinstance(raw_value, str) and raw_value.startswith("env:"):
|
|
366
|
+
value = os.environ.get(raw_value[4:], "")
|
|
367
|
+
else:
|
|
368
|
+
value = str(raw_value)
|
|
369
|
+
result[key] = value
|
|
370
|
+
return result
|
|
371
|
+
|
|
372
|
+
|
|
319
373
|
def get_authentication_config(config: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
320
374
|
"""Extract authentication configuration from config.
|
|
321
375
|
|
|
@@ -337,8 +391,6 @@ def get_authentication_config(config: Dict[str, Any]) -> Optional[Dict[str, Any]
|
|
|
337
391
|
if "encrypted" not in auth_config:
|
|
338
392
|
auth_config["encrypted"] = DEFAULT_AUTH_SETTINGS["encrypted"]
|
|
339
393
|
|
|
340
|
-
auth_key = auth_config.get("key", "UNKNOWN")
|
|
341
|
-
auth_method = auth_config.get("method", "UNKNOWN")
|
|
342
394
|
return auth_config
|
|
343
395
|
|
|
344
396
|
|
|
@@ -618,25 +670,29 @@ def find_route_mapping(full_route: str, method: str, remote_config: Dict[str, An
|
|
|
618
670
|
|
|
619
671
|
|
|
620
672
|
def filter_remote_query_params(
|
|
621
|
-
query_params: Dict[str,
|
|
673
|
+
query_params: Dict[str, Any],
|
|
622
674
|
route: str,
|
|
623
675
|
method: str,
|
|
624
676
|
remote_config: Dict[str, Any]
|
|
625
|
-
) -> Dict[str,
|
|
677
|
+
) -> Dict[str, Any]:
|
|
626
678
|
"""Filter query parameters based on remote config settings.
|
|
627
679
|
|
|
628
680
|
Checks for a route-level query_params setting first, then falls back
|
|
629
681
|
to the top-level remote config query_params. If neither exists, all
|
|
630
682
|
params are passed through unchanged (backward compatible).
|
|
631
683
|
|
|
684
|
+
Filtering is by key only, so values may be strings or lists of strings
|
|
685
|
+
(the latter forwarding repeated keys such as ?id=1&id=2 intact).
|
|
686
|
+
|
|
632
687
|
Args:
|
|
633
|
-
query_params: Original query parameters from the request.
|
|
688
|
+
query_params: Original query parameters from the request. Values may be
|
|
689
|
+
strings or lists of strings.
|
|
634
690
|
route: The actual route path (e.g., "users/123").
|
|
635
691
|
method: HTTP method (e.g., "GET", "POST").
|
|
636
692
|
remote_config: Remote configuration dictionary.
|
|
637
693
|
|
|
638
694
|
Returns:
|
|
639
|
-
Filtered query parameters dictionary.
|
|
695
|
+
Filtered query parameters dictionary (value types preserved).
|
|
640
696
|
"""
|
|
641
697
|
routes = remote_config.get("routes", [])
|
|
642
698
|
|