api-dock 0.5.0__tar.gz → 0.6.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.6.0}/PKG-INFO +112 -11
- {api_dock-0.5.0 → api_dock-0.6.0}/README.md +109 -8
- {api_dock-0.5.0 → api_dock-0.6.0}/api_dock/cli.py +4 -13
- {api_dock-0.5.0 → api_dock-0.6.0}/api_dock/config.py +66 -14
- {api_dock-0.5.0 → api_dock-0.6.0}/api_dock/config_discovery.py +9 -15
- api_dock-0.6.0/api_dock/example_api_dock_config/config.yaml +22 -0
- api_dock-0.6.0/api_dock/example_api_dock_config/databases/example_db.yaml +75 -0
- api_dock-0.6.0/api_dock/example_api_dock_config/remotes/example_remote.yaml +30 -0
- {api_dock-0.5.0 → api_dock-0.6.0}/api_dock/fast_api.py +20 -35
- api_dock-0.6.0/api_dock/flask_api.py +181 -0
- {api_dock-0.5.0 → api_dock-0.6.0}/api_dock/route_mapper.py +158 -191
- api_dock-0.6.0/api_dock/types.py +57 -0
- {api_dock-0.5.0 → api_dock-0.6.0}/api_dock.egg-info/PKG-INFO +112 -11
- {api_dock-0.5.0 → api_dock-0.6.0}/api_dock.egg-info/SOURCES.txt +7 -7
- {api_dock-0.5.0 → api_dock-0.6.0}/api_dock.egg-info/requires.txt +1 -1
- {api_dock-0.5.0 → api_dock-0.6.0}/api_dock.egg-info/top_level.txt +0 -1
- {api_dock-0.5.0 → api_dock-0.6.0}/pyproject.toml +6 -6
- api_dock-0.6.0/tests/test_inject_cookies.py +121 -0
- api_dock-0.6.0/tests/test_proxy_pipeline.py +400 -0
- api_dock-0.6.0/tests/test_types.py +75 -0
- api_dock-0.5.0/api_dock/flask_api.py +0 -218
- 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.6.0}/LICENSE.md +0 -0
- {api_dock-0.5.0 → api_dock-0.6.0}/api_dock/__init__.py +0 -0
- {api_dock-0.5.0 → api_dock-0.6.0}/api_dock/auth.py +0 -0
- {api_dock-0.5.0 → api_dock-0.6.0}/api_dock/database_config.py +0 -0
- {api_dock-0.5.0 → api_dock-0.6.0}/api_dock/encryption.py +0 -0
- {api_dock-0.5.0 → api_dock-0.6.0}/api_dock/sql_builder.py +0 -0
- {api_dock-0.5.0 → api_dock-0.6.0}/api_dock/storage_auth.py +0 -0
- {api_dock-0.5.0 → api_dock-0.6.0}/api_dock.egg-info/dependency_links.txt +0 -0
- {api_dock-0.5.0 → api_dock-0.6.0}/api_dock.egg-info/entry_points.txt +0 -0
- {api_dock-0.5.0 → api_dock-0.6.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.6.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"
|
|
@@ -349,7 +349,7 @@ For now only parquet support is working but we will be adding other Databases in
|
|
|
349
349
|
|
|
350
350
|
### Database Configuration
|
|
351
351
|
|
|
352
|
-
Database configurations are stored in `
|
|
352
|
+
Database configurations are stored in `api_dock_config/databases/` directory. Each database defines:
|
|
353
353
|
- **tables**: Mapping of table names to file paths (supports S3, GCS, HTTPS, local paths)
|
|
354
354
|
- **queries**: Named SQL queries for reuse
|
|
355
355
|
- **routes**: REST endpoints mapped to SQL queries
|
|
@@ -702,30 +702,79 @@ API Dock supports cookie extraction and authentication for both remote APIs and
|
|
|
702
702
|
|
|
703
703
|
## Cookie Configuration
|
|
704
704
|
|
|
705
|
-
|
|
705
|
+
### Forwarding client cookies
|
|
706
|
+
|
|
707
|
+
Configure which cookies to extract from incoming requests and forward to the upstream API (or make available as template variables in SQL routes):
|
|
706
708
|
|
|
707
709
|
```yaml
|
|
708
|
-
#
|
|
710
|
+
# Forward all cookies from the client request
|
|
709
711
|
cookies: true
|
|
710
712
|
|
|
711
|
-
#
|
|
713
|
+
# Forward only specific cookies
|
|
712
714
|
cookies: [session_id, auth_token, user_preferences]
|
|
713
715
|
|
|
714
|
-
#
|
|
716
|
+
# Forward no cookies (default behavior)
|
|
715
717
|
cookies: false
|
|
716
718
|
```
|
|
717
719
|
|
|
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
|
|
720
|
+
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
721
|
|
|
720
|
-
|
|
722
|
+
Forwarded cookies are accessible in SQL queries using `{{cookies.cookie_name}}`:
|
|
721
723
|
|
|
722
724
|
```yaml
|
|
723
|
-
# Database route using cookies
|
|
724
725
|
routes:
|
|
725
726
|
- route: user/profile
|
|
726
727
|
sql: SELECT * FROM [[users]] WHERE session_id = '{{cookies.session_id}}'
|
|
727
728
|
```
|
|
728
729
|
|
|
730
|
+
### Injecting cookies from the server environment
|
|
731
|
+
|
|
732
|
+
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.
|
|
733
|
+
|
|
734
|
+
Dict entries and string entries can be mixed freely in the same list.
|
|
735
|
+
|
|
736
|
+
```yaml
|
|
737
|
+
cookies:
|
|
738
|
+
- session_id # forward this cookie from the client request
|
|
739
|
+
- key: __Secure-authjs.session-token # inject from environment variable
|
|
740
|
+
value: "env:SOUNDHUB_SESSION_TOKEN"
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
#### Dict entry forms
|
|
744
|
+
|
|
745
|
+
| Form | Behaviour |
|
|
746
|
+
|---|---|
|
|
747
|
+
| `{key: NAME, value: "literal"}` | Inject cookie `NAME` with the literal string `"literal"` |
|
|
748
|
+
| `{key: NAME, value: "env:MY_VAR"}` | Inject cookie `NAME` with the value of env var `MY_VAR`; empty string if unset |
|
|
749
|
+
| `{key: MY_VAR}` | Shorthand — equivalent to `{key: MY_VAR, value: "env:MY_VAR"}` |
|
|
750
|
+
|
|
751
|
+
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).
|
|
752
|
+
|
|
753
|
+
#### Example: proxying an API that requires a session cookie
|
|
754
|
+
|
|
755
|
+
```yaml
|
|
756
|
+
# api_dock_config/remotes/my_service/1.0.yaml
|
|
757
|
+
name: my_service
|
|
758
|
+
url: https://api.example.com
|
|
759
|
+
|
|
760
|
+
cookies:
|
|
761
|
+
- key: __Secure-authjs.session-token
|
|
762
|
+
value: "env:MY_SERVICE_SESSION_TOKEN"
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
Set the env var before starting api-dock:
|
|
766
|
+
|
|
767
|
+
```bash
|
|
768
|
+
export MY_SERVICE_SESSION_TOKEN="your-session-token-here"
|
|
769
|
+
pixi run api-dock start
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
Every request proxied to `my_service` will include the `__Secure-authjs.session-token` cookie, even if the client did not send one.
|
|
773
|
+
|
|
774
|
+
#### Injection precedence
|
|
775
|
+
|
|
776
|
+
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.
|
|
777
|
+
|
|
729
778
|
## Authentication Configuration
|
|
730
779
|
|
|
731
780
|
Configure authentication to validate requests before processing. Multiple authentication methods are supported:
|
|
@@ -1119,6 +1168,58 @@ pixi run jupyter lab .
|
|
|
1119
1168
|
pixi run python scripts/hello_world.py
|
|
1120
1169
|
```
|
|
1121
1170
|
|
|
1171
|
+
---
|
|
1172
|
+
|
|
1173
|
+
---
|
|
1174
|
+
|
|
1175
|
+
# Development
|
|
1176
|
+
|
|
1177
|
+
## Publishing a Release
|
|
1178
|
+
|
|
1179
|
+
```bash
|
|
1180
|
+
# 0. Make sure you are on `main` and merged with any changes
|
|
1181
|
+
|
|
1182
|
+
# 1. Bump version in pyproject.toml
|
|
1183
|
+
|
|
1184
|
+
# 2. Commit everything
|
|
1185
|
+
git add -A
|
|
1186
|
+
git commit -m "v0.6.0: proxy passthrough fixes, cookie injection"
|
|
1187
|
+
|
|
1188
|
+
# 3. Tag and push
|
|
1189
|
+
git tag v0.6.0
|
|
1190
|
+
git push origin main v0.6.0
|
|
1191
|
+
|
|
1192
|
+
# 4. Build the wheel (requires the `dev` pixi environment)
|
|
1193
|
+
rm -rf dist/
|
|
1194
|
+
find . -name "__pycache__" -type d -exec rm -rf {} +
|
|
1195
|
+
find . -name "*.pyc" -delete
|
|
1196
|
+
pixi run -e dev python -m build --wheel
|
|
1197
|
+
ls dist/*.whl
|
|
1198
|
+
|
|
1199
|
+
# 5. Create GitHub release with the wheel attached
|
|
1200
|
+
gh release create v0.6.0 dist/api_dock-0.6.0-py3-none-any.whl \
|
|
1201
|
+
--title "v0.6.0" --notes "$(cat <<'EOF'
|
|
1202
|
+
* new features
|
|
1203
|
+
- Cookie injection: dict entries in the `cookies` list inject server-side cookies into upstream requests, with `env:MY_VAR` env var support and literal value support
|
|
1204
|
+
* bug fixes
|
|
1205
|
+
- Binary responses (image, audio) no longer corrupted — raw bytes passed through with correct content-type
|
|
1206
|
+
- 3xx redirects returned to client when `follow_redirects: false` (previously followed internally, doubling egress)
|
|
1207
|
+
- Upstream response headers (Cache-Control, ETag, Last-Modified, etc.) now forwarded to client
|
|
1208
|
+
- Upstream 4xx/5xx error bodies passed through verbatim (previously wrapped/swallowed)
|
|
1209
|
+
- JSON responses no longer re-serialized — raw bytes returned as-is, preserving exact upstream payload
|
|
1210
|
+
* cleanup / other improvements
|
|
1211
|
+
- Added `ProxyResponse` typed dataclass as the return contract for `map_route()` and `map_database_route()`
|
|
1212
|
+
- Added test suite (38 tests covering proxy pipeline and cookie injection)
|
|
1213
|
+
- Removed broken root `__init__.py` that caused pytest import conflicts
|
|
1214
|
+
- Bumped cryptography dependency to >=48.0.0,<49.0.0
|
|
1215
|
+
EOF
|
|
1216
|
+
)"
|
|
1217
|
+
|
|
1218
|
+
# 6. Publish to PyPI
|
|
1219
|
+
pixi run -e dev python -m twine upload dist/*.whl
|
|
1220
|
+
```
|
|
1221
|
+
|
|
1222
|
+
|
|
1122
1223
|
---
|
|
1123
1224
|
|
|
1124
1225
|
# License
|
|
@@ -310,7 +310,7 @@ For now only parquet support is working but we will be adding other Databases in
|
|
|
310
310
|
|
|
311
311
|
### Database Configuration
|
|
312
312
|
|
|
313
|
-
Database configurations are stored in `
|
|
313
|
+
Database configurations are stored in `api_dock_config/databases/` directory. Each database defines:
|
|
314
314
|
- **tables**: Mapping of table names to file paths (supports S3, GCS, HTTPS, local paths)
|
|
315
315
|
- **queries**: Named SQL queries for reuse
|
|
316
316
|
- **routes**: REST endpoints mapped to SQL queries
|
|
@@ -663,30 +663,79 @@ API Dock supports cookie extraction and authentication for both remote APIs and
|
|
|
663
663
|
|
|
664
664
|
## Cookie Configuration
|
|
665
665
|
|
|
666
|
-
|
|
666
|
+
### Forwarding client cookies
|
|
667
|
+
|
|
668
|
+
Configure which cookies to extract from incoming requests and forward to the upstream API (or make available as template variables in SQL routes):
|
|
667
669
|
|
|
668
670
|
```yaml
|
|
669
|
-
#
|
|
671
|
+
# Forward all cookies from the client request
|
|
670
672
|
cookies: true
|
|
671
673
|
|
|
672
|
-
#
|
|
674
|
+
# Forward only specific cookies
|
|
673
675
|
cookies: [session_id, auth_token, user_preferences]
|
|
674
676
|
|
|
675
|
-
#
|
|
677
|
+
# Forward no cookies (default behavior)
|
|
676
678
|
cookies: false
|
|
677
679
|
```
|
|
678
680
|
|
|
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
|
|
681
|
+
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
682
|
|
|
681
|
-
|
|
683
|
+
Forwarded cookies are accessible in SQL queries using `{{cookies.cookie_name}}`:
|
|
682
684
|
|
|
683
685
|
```yaml
|
|
684
|
-
# Database route using cookies
|
|
685
686
|
routes:
|
|
686
687
|
- route: user/profile
|
|
687
688
|
sql: SELECT * FROM [[users]] WHERE session_id = '{{cookies.session_id}}'
|
|
688
689
|
```
|
|
689
690
|
|
|
691
|
+
### Injecting cookies from the server environment
|
|
692
|
+
|
|
693
|
+
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.
|
|
694
|
+
|
|
695
|
+
Dict entries and string entries can be mixed freely in the same list.
|
|
696
|
+
|
|
697
|
+
```yaml
|
|
698
|
+
cookies:
|
|
699
|
+
- session_id # forward this cookie from the client request
|
|
700
|
+
- key: __Secure-authjs.session-token # inject from environment variable
|
|
701
|
+
value: "env:SOUNDHUB_SESSION_TOKEN"
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
#### Dict entry forms
|
|
705
|
+
|
|
706
|
+
| Form | Behaviour |
|
|
707
|
+
|---|---|
|
|
708
|
+
| `{key: NAME, value: "literal"}` | Inject cookie `NAME` with the literal string `"literal"` |
|
|
709
|
+
| `{key: NAME, value: "env:MY_VAR"}` | Inject cookie `NAME` with the value of env var `MY_VAR`; empty string if unset |
|
|
710
|
+
| `{key: MY_VAR}` | Shorthand — equivalent to `{key: MY_VAR, value: "env:MY_VAR"}` |
|
|
711
|
+
|
|
712
|
+
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).
|
|
713
|
+
|
|
714
|
+
#### Example: proxying an API that requires a session cookie
|
|
715
|
+
|
|
716
|
+
```yaml
|
|
717
|
+
# api_dock_config/remotes/my_service/1.0.yaml
|
|
718
|
+
name: my_service
|
|
719
|
+
url: https://api.example.com
|
|
720
|
+
|
|
721
|
+
cookies:
|
|
722
|
+
- key: __Secure-authjs.session-token
|
|
723
|
+
value: "env:MY_SERVICE_SESSION_TOKEN"
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
Set the env var before starting api-dock:
|
|
727
|
+
|
|
728
|
+
```bash
|
|
729
|
+
export MY_SERVICE_SESSION_TOKEN="your-session-token-here"
|
|
730
|
+
pixi run api-dock start
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
Every request proxied to `my_service` will include the `__Secure-authjs.session-token` cookie, even if the client did not send one.
|
|
734
|
+
|
|
735
|
+
#### Injection precedence
|
|
736
|
+
|
|
737
|
+
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.
|
|
738
|
+
|
|
690
739
|
## Authentication Configuration
|
|
691
740
|
|
|
692
741
|
Configure authentication to validate requests before processing. Multiple authentication methods are supported:
|
|
@@ -1080,6 +1129,58 @@ pixi run jupyter lab .
|
|
|
1080
1129
|
pixi run python scripts/hello_world.py
|
|
1081
1130
|
```
|
|
1082
1131
|
|
|
1132
|
+
---
|
|
1133
|
+
|
|
1134
|
+
---
|
|
1135
|
+
|
|
1136
|
+
# Development
|
|
1137
|
+
|
|
1138
|
+
## Publishing a Release
|
|
1139
|
+
|
|
1140
|
+
```bash
|
|
1141
|
+
# 0. Make sure you are on `main` and merged with any changes
|
|
1142
|
+
|
|
1143
|
+
# 1. Bump version in pyproject.toml
|
|
1144
|
+
|
|
1145
|
+
# 2. Commit everything
|
|
1146
|
+
git add -A
|
|
1147
|
+
git commit -m "v0.6.0: proxy passthrough fixes, cookie injection"
|
|
1148
|
+
|
|
1149
|
+
# 3. Tag and push
|
|
1150
|
+
git tag v0.6.0
|
|
1151
|
+
git push origin main v0.6.0
|
|
1152
|
+
|
|
1153
|
+
# 4. Build the wheel (requires the `dev` pixi environment)
|
|
1154
|
+
rm -rf dist/
|
|
1155
|
+
find . -name "__pycache__" -type d -exec rm -rf {} +
|
|
1156
|
+
find . -name "*.pyc" -delete
|
|
1157
|
+
pixi run -e dev python -m build --wheel
|
|
1158
|
+
ls dist/*.whl
|
|
1159
|
+
|
|
1160
|
+
# 5. Create GitHub release with the wheel attached
|
|
1161
|
+
gh release create v0.6.0 dist/api_dock-0.6.0-py3-none-any.whl \
|
|
1162
|
+
--title "v0.6.0" --notes "$(cat <<'EOF'
|
|
1163
|
+
* new features
|
|
1164
|
+
- Cookie injection: dict entries in the `cookies` list inject server-side cookies into upstream requests, with `env:MY_VAR` env var support and literal value support
|
|
1165
|
+
* bug fixes
|
|
1166
|
+
- Binary responses (image, audio) no longer corrupted — raw bytes passed through with correct content-type
|
|
1167
|
+
- 3xx redirects returned to client when `follow_redirects: false` (previously followed internally, doubling egress)
|
|
1168
|
+
- Upstream response headers (Cache-Control, ETag, Last-Modified, etc.) now forwarded to client
|
|
1169
|
+
- Upstream 4xx/5xx error bodies passed through verbatim (previously wrapped/swallowed)
|
|
1170
|
+
- JSON responses no longer re-serialized — raw bytes returned as-is, preserving exact upstream payload
|
|
1171
|
+
* cleanup / other improvements
|
|
1172
|
+
- Added `ProxyResponse` typed dataclass as the return contract for `map_route()` and `map_database_route()`
|
|
1173
|
+
- Added test suite (38 tests covering proxy pipeline and cookie injection)
|
|
1174
|
+
- Removed broken root `__init__.py` that caused pytest import conflicts
|
|
1175
|
+
- Bumped cryptography dependency to >=48.0.0,<49.0.0
|
|
1176
|
+
EOF
|
|
1177
|
+
)"
|
|
1178
|
+
|
|
1179
|
+
# 6. Publish to PyPI
|
|
1180
|
+
pixi run -e dev python -m twine upload dist/*.whl
|
|
1181
|
+
```
|
|
1182
|
+
|
|
1183
|
+
|
|
1083
1184
|
---
|
|
1084
1185
|
|
|
1085
1186
|
# 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
|
|
|
@@ -31,9 +31,8 @@ def find_config(config_name: Optional[str] = None) -> Optional[str]:
|
|
|
31
31
|
"""Find configuration file by name.
|
|
32
32
|
|
|
33
33
|
Search order:
|
|
34
|
-
1. api_dock_config/<config_name>.yaml
|
|
35
|
-
2.
|
|
36
|
-
3. api_dock/config/<config_name>.yaml (package default)
|
|
34
|
+
1. api_dock_config/<config_name>.yaml (local user config)
|
|
35
|
+
2. api_dock/example_api_dock_config/<config_name>.yaml (bundled package default)
|
|
37
36
|
|
|
38
37
|
Args:
|
|
39
38
|
config_name: Config name without .yaml extension (default: "config").
|
|
@@ -48,20 +47,15 @@ def find_config(config_name: Optional[str] = None) -> Optional[str]:
|
|
|
48
47
|
if config_name.endswith(".yaml"):
|
|
49
48
|
config_name = config_name[:-5]
|
|
50
49
|
|
|
51
|
-
# Check local config directory
|
|
50
|
+
# Check local config directory first
|
|
52
51
|
local_path = Path(f"{LOCAL_CONFIG_DIR}/{config_name}.yaml")
|
|
53
52
|
if local_path.exists():
|
|
54
53
|
return str(local_path)
|
|
55
54
|
|
|
56
|
-
#
|
|
57
|
-
config_path = Path(f"config/{config_name}.yaml")
|
|
58
|
-
if config_path.exists():
|
|
59
|
-
return str(config_path)
|
|
60
|
-
|
|
61
|
-
# Check package config directory
|
|
55
|
+
# Fall back to bundled package examples
|
|
62
56
|
try:
|
|
63
57
|
import importlib.resources as pkg_resources
|
|
64
|
-
package_config = Path(pkg_resources.files("api_dock") / "
|
|
58
|
+
package_config = Path(pkg_resources.files("api_dock") / "example_api_dock_config" / f"{config_name}.yaml")
|
|
65
59
|
if package_config.exists():
|
|
66
60
|
return str(package_config)
|
|
67
61
|
except Exception:
|
|
@@ -91,7 +85,7 @@ def init_config() -> bool:
|
|
|
91
85
|
(local_dir / "remotes").mkdir(exist_ok=True)
|
|
92
86
|
(local_dir / "databases").mkdir(exist_ok=True)
|
|
93
87
|
|
|
94
|
-
# Step 3: Copy
|
|
88
|
+
# Step 3: Copy example configs from package
|
|
95
89
|
package_dir = _get_package_config_dir()
|
|
96
90
|
if not package_dir:
|
|
97
91
|
return False
|
|
@@ -130,14 +124,14 @@ def init_config() -> bool:
|
|
|
130
124
|
# INTERNAL
|
|
131
125
|
#
|
|
132
126
|
def _get_package_config_dir() -> Optional[Path]:
|
|
133
|
-
"""Get the path to the
|
|
127
|
+
"""Get the path to the bundled example config directory inside the package.
|
|
134
128
|
|
|
135
129
|
Returns:
|
|
136
|
-
Path to
|
|
130
|
+
Path to example_api_dock_config directory, or None if not found.
|
|
137
131
|
"""
|
|
138
132
|
try:
|
|
139
133
|
import importlib.resources as pkg_resources
|
|
140
|
-
config_dir = Path(pkg_resources.files("api_dock") / "
|
|
134
|
+
config_dir = Path(pkg_resources.files("api_dock") / "example_api_dock_config")
|
|
141
135
|
return config_dir if config_dir.exists() else None
|
|
142
136
|
except Exception:
|
|
143
137
|
return None
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: my-api
|
|
2
|
+
description: My API Dock instance
|
|
3
|
+
authors:
|
|
4
|
+
- Your Name
|
|
5
|
+
|
|
6
|
+
# Remote APIs to proxy (add a file per remote in remotes/)
|
|
7
|
+
remotes:
|
|
8
|
+
- example_remote
|
|
9
|
+
|
|
10
|
+
# SQL databases to query (add a file per database in databases/)
|
|
11
|
+
databases:
|
|
12
|
+
- example_db
|
|
13
|
+
|
|
14
|
+
settings:
|
|
15
|
+
add_trailing_slash: false # Set true to auto-append trailing slash to proxied paths
|
|
16
|
+
follow_redirects: true # Set false to pass 3xx redirects through to the client
|
|
17
|
+
|
|
18
|
+
# Global route restrictions — applied to all remotes unless overridden per-remote.
|
|
19
|
+
# Uncomment to block DELETE on every remote:
|
|
20
|
+
# restricted:
|
|
21
|
+
# - route: "*"
|
|
22
|
+
# method: delete
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
name: example_db
|
|
2
|
+
description: Example database
|
|
3
|
+
authors:
|
|
4
|
+
- API Team
|
|
5
|
+
|
|
6
|
+
# Table definitions — supports S3, GCS, HTTPS, and local paths.
|
|
7
|
+
# Use **/* for partitioned Parquet (e.g. Hive-partitioned directories).
|
|
8
|
+
tables:
|
|
9
|
+
items: s3://your-bucket/items.parquet
|
|
10
|
+
# items: gs://your-bucket/items.parquet # Google Cloud Storage
|
|
11
|
+
# items: https://host/path/items.parquet # HTTPS
|
|
12
|
+
# items: data/items.parquet # local file
|
|
13
|
+
|
|
14
|
+
routes:
|
|
15
|
+
# List all items with optional filtering, sorting, and pagination.
|
|
16
|
+
- route: items
|
|
17
|
+
sql: SELECT [[items]].* FROM [[items]]
|
|
18
|
+
query_params:
|
|
19
|
+
# WHERE clause filter — only applied when the param is present in the URL.
|
|
20
|
+
- category:
|
|
21
|
+
sql: "[[items]].category = '{{category}}'"
|
|
22
|
+
|
|
23
|
+
# Sorting — sql_append clauses are appended after the WHERE clause in YAML order.
|
|
24
|
+
- sort:
|
|
25
|
+
sql_append: ORDER BY {{sort}} {{direction}}
|
|
26
|
+
default: id # used when ?sort= is not provided
|
|
27
|
+
- direction:
|
|
28
|
+
default: ASC # value-only param — feeds into sort's template
|
|
29
|
+
|
|
30
|
+
# Pagination
|
|
31
|
+
- limit:
|
|
32
|
+
sql_append: LIMIT {{limit}}
|
|
33
|
+
default: 50
|
|
34
|
+
- offset:
|
|
35
|
+
sql_append: OFFSET {{offset}}
|
|
36
|
+
|
|
37
|
+
# Fetch a single item by ID.
|
|
38
|
+
- route: items/{{id}}
|
|
39
|
+
sql: SELECT [[items]].* FROM [[items]] WHERE [[items]].id = {{id}}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ── Advanced features (uncomment to use) ─────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
# Named queries — define reusable SQL fragments and reference them with [[name]].
|
|
45
|
+
# queries:
|
|
46
|
+
# items_with_meta: |
|
|
47
|
+
# SELECT [[items]].*, [[meta]].label
|
|
48
|
+
# FROM [[items]]
|
|
49
|
+
# JOIN [[meta]] ON [[items]].id = [[meta]].item_id
|
|
50
|
+
|
|
51
|
+
# Required parameter with a custom error response (returns 400 if missing).
|
|
52
|
+
# - route: items/search
|
|
53
|
+
# sql: SELECT [[items]].* FROM [[items]]
|
|
54
|
+
# query_params:
|
|
55
|
+
# - q:
|
|
56
|
+
# sql: "[[items]].name ILIKE '%{{q}}%'"
|
|
57
|
+
# required: true
|
|
58
|
+
# missing_response:
|
|
59
|
+
# error: "q is required"
|
|
60
|
+
# http_status: 400
|
|
61
|
+
|
|
62
|
+
# Conditional parameter — branch on the parameter's value.
|
|
63
|
+
# - status:
|
|
64
|
+
# conditional:
|
|
65
|
+
# active:
|
|
66
|
+
# sql: "[[items]].active = true"
|
|
67
|
+
# inactive:
|
|
68
|
+
# sql: "[[items]].active = false"
|
|
69
|
+
# default:
|
|
70
|
+
# response: "Unknown status — use active or inactive"
|
|
71
|
+
|
|
72
|
+
# Direct response — return a fixed JSON response without executing SQL.
|
|
73
|
+
# - debug:
|
|
74
|
+
# response:
|
|
75
|
+
# message: "Debug mode — no query executed"
|