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.
Files changed (41) hide show
  1. {api_dock-0.5.0 → api_dock-0.7.0}/PKG-INFO +139 -11
  2. {api_dock-0.5.0 → api_dock-0.7.0}/README.md +136 -8
  3. {api_dock-0.5.0 → api_dock-0.7.0}/api_dock/cli.py +4 -13
  4. {api_dock-0.5.0 → api_dock-0.7.0}/api_dock/config.py +74 -18
  5. {api_dock-0.5.0 → api_dock-0.7.0}/api_dock/config_discovery.py +9 -15
  6. api_dock-0.7.0/api_dock/example_api_dock_config/config.yaml +23 -0
  7. api_dock-0.7.0/api_dock/example_api_dock_config/databases/example_db.yaml +82 -0
  8. api_dock-0.7.0/api_dock/example_api_dock_config/remotes/example_remote.yaml +30 -0
  9. api_dock-0.7.0/api_dock/fast_api.py +265 -0
  10. api_dock-0.7.0/api_dock/flask_api.py +187 -0
  11. api_dock-0.7.0/api_dock/route_mapper.py +676 -0
  12. {api_dock-0.5.0 → api_dock-0.7.0}/api_dock/sql_builder.py +67 -5
  13. api_dock-0.7.0/api_dock/types.py +89 -0
  14. {api_dock-0.5.0 → api_dock-0.7.0}/api_dock.egg-info/PKG-INFO +139 -11
  15. {api_dock-0.5.0 → api_dock-0.7.0}/api_dock.egg-info/SOURCES.txt +8 -7
  16. {api_dock-0.5.0 → api_dock-0.7.0}/api_dock.egg-info/requires.txt +1 -1
  17. {api_dock-0.5.0 → api_dock-0.7.0}/api_dock.egg-info/top_level.txt +0 -1
  18. {api_dock-0.5.0 → api_dock-0.7.0}/pyproject.toml +6 -6
  19. api_dock-0.7.0/tests/test_inject_cookies.py +121 -0
  20. api_dock-0.7.0/tests/test_proxy_pipeline.py +731 -0
  21. api_dock-0.7.0/tests/test_sql_builder.py +137 -0
  22. api_dock-0.7.0/tests/test_types.py +124 -0
  23. api_dock-0.5.0/api_dock/fast_api.py +0 -170
  24. api_dock-0.5.0/api_dock/flask_api.py +0 -218
  25. api_dock-0.5.0/api_dock/route_mapper.py +0 -576
  26. api_dock-0.5.0/config/config.yaml +0 -25
  27. api_dock-0.5.0/config/databases/db_example.yaml +0 -19
  28. api_dock-0.5.0/config/databases/test_users.yaml +0 -127
  29. api_dock-0.5.0/config/remotes/remote_with_allowed_routes.yaml +0 -10
  30. api_dock-0.5.0/config/remotes/remote_with_custom_mapping.yaml +0 -16
  31. api_dock-0.5.0/config/remotes/remote_with_restrictions.yaml +0 -8
  32. api_dock-0.5.0/config/remotes/remote_with_wildcards.yaml +0 -18
  33. {api_dock-0.5.0 → api_dock-0.7.0}/LICENSE.md +0 -0
  34. {api_dock-0.5.0 → api_dock-0.7.0}/api_dock/__init__.py +0 -0
  35. {api_dock-0.5.0 → api_dock-0.7.0}/api_dock/auth.py +0 -0
  36. {api_dock-0.5.0 → api_dock-0.7.0}/api_dock/database_config.py +0 -0
  37. {api_dock-0.5.0 → api_dock-0.7.0}/api_dock/encryption.py +0 -0
  38. {api_dock-0.5.0 → api_dock-0.7.0}/api_dock/storage_auth.py +0 -0
  39. {api_dock-0.5.0 → api_dock-0.7.0}/api_dock.egg-info/dependency_links.txt +0 -0
  40. {api_dock-0.5.0 → api_dock-0.7.0}/api_dock.egg-info/entry_points.txt +0 -0
  41. {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.5.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: BSd 3-clause
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<47,>=46.0.5
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 `config/databases/` directory. Each database defines:
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
- Configure cookies to extract from incoming requests and make them available as template variables:
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
- # Enable all cookies (default: false)
741
+ # Forward all cookies from the client request
709
742
  cookies: true
710
743
 
711
- # Or specify specific cookies to extract
744
+ # Forward only specific cookies
712
745
  cookies: [session_id, auth_token, user_preferences]
713
746
 
714
- # Disable all cookies (default behavior)
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 specified cookies are extracted.
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
- Cookies can then be accessed in SQL queries using `{{cookies.cookie_name}}`:
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 `config/databases/` directory. Each database defines:
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
- Configure cookies to extract from incoming requests and make them available as template variables:
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
- # Enable all cookies (default: false)
702
+ # Forward all cookies from the client request
670
703
  cookies: true
671
704
 
672
- # Or specify specific cookies to extract
705
+ # Forward only specific cookies
673
706
  cookies: [session_id, auth_token, user_preferences]
674
707
 
675
- # Disable all cookies (default behavior)
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 specified cookies are extracted.
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
- Cookies can then be accessed in SQL queries using `{{cookies.cookie_name}}`:
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 package configs
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") / "config")
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("📦 Package configurations:")
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 configuration from config.
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
- # Ensure all cookie names are strings
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 of cookie names (existing behavior)
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
- # Empty list means no cookies allowed except authentication key
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
- auth_only = {auth_key: cookies[auth_key]}
298
- return auth_only
299
- else:
300
- return {}
302
+ result[auth_key] = cookies[auth_key]
303
+ result.update(injected)
304
+ return result
301
305
 
302
- # Always include authentication key in allowed cookies
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, 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, 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