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.
Files changed (38) hide show
  1. {api_dock-0.5.0 → api_dock-0.6.0}/PKG-INFO +112 -11
  2. {api_dock-0.5.0 → api_dock-0.6.0}/README.md +109 -8
  3. {api_dock-0.5.0 → api_dock-0.6.0}/api_dock/cli.py +4 -13
  4. {api_dock-0.5.0 → api_dock-0.6.0}/api_dock/config.py +66 -14
  5. {api_dock-0.5.0 → api_dock-0.6.0}/api_dock/config_discovery.py +9 -15
  6. api_dock-0.6.0/api_dock/example_api_dock_config/config.yaml +22 -0
  7. api_dock-0.6.0/api_dock/example_api_dock_config/databases/example_db.yaml +75 -0
  8. api_dock-0.6.0/api_dock/example_api_dock_config/remotes/example_remote.yaml +30 -0
  9. {api_dock-0.5.0 → api_dock-0.6.0}/api_dock/fast_api.py +20 -35
  10. api_dock-0.6.0/api_dock/flask_api.py +181 -0
  11. {api_dock-0.5.0 → api_dock-0.6.0}/api_dock/route_mapper.py +158 -191
  12. api_dock-0.6.0/api_dock/types.py +57 -0
  13. {api_dock-0.5.0 → api_dock-0.6.0}/api_dock.egg-info/PKG-INFO +112 -11
  14. {api_dock-0.5.0 → api_dock-0.6.0}/api_dock.egg-info/SOURCES.txt +7 -7
  15. {api_dock-0.5.0 → api_dock-0.6.0}/api_dock.egg-info/requires.txt +1 -1
  16. {api_dock-0.5.0 → api_dock-0.6.0}/api_dock.egg-info/top_level.txt +0 -1
  17. {api_dock-0.5.0 → api_dock-0.6.0}/pyproject.toml +6 -6
  18. api_dock-0.6.0/tests/test_inject_cookies.py +121 -0
  19. api_dock-0.6.0/tests/test_proxy_pipeline.py +400 -0
  20. api_dock-0.6.0/tests/test_types.py +75 -0
  21. api_dock-0.5.0/api_dock/flask_api.py +0 -218
  22. api_dock-0.5.0/config/config.yaml +0 -25
  23. api_dock-0.5.0/config/databases/db_example.yaml +0 -19
  24. api_dock-0.5.0/config/databases/test_users.yaml +0 -127
  25. api_dock-0.5.0/config/remotes/remote_with_allowed_routes.yaml +0 -10
  26. api_dock-0.5.0/config/remotes/remote_with_custom_mapping.yaml +0 -16
  27. api_dock-0.5.0/config/remotes/remote_with_restrictions.yaml +0 -8
  28. api_dock-0.5.0/config/remotes/remote_with_wildcards.yaml +0 -18
  29. {api_dock-0.5.0 → api_dock-0.6.0}/LICENSE.md +0 -0
  30. {api_dock-0.5.0 → api_dock-0.6.0}/api_dock/__init__.py +0 -0
  31. {api_dock-0.5.0 → api_dock-0.6.0}/api_dock/auth.py +0 -0
  32. {api_dock-0.5.0 → api_dock-0.6.0}/api_dock/database_config.py +0 -0
  33. {api_dock-0.5.0 → api_dock-0.6.0}/api_dock/encryption.py +0 -0
  34. {api_dock-0.5.0 → api_dock-0.6.0}/api_dock/sql_builder.py +0 -0
  35. {api_dock-0.5.0 → api_dock-0.6.0}/api_dock/storage_auth.py +0 -0
  36. {api_dock-0.5.0 → api_dock-0.6.0}/api_dock.egg-info/dependency_links.txt +0 -0
  37. {api_dock-0.5.0 → api_dock-0.6.0}/api_dock.egg-info/entry_points.txt +0 -0
  38. {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.5.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: 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"
@@ -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 `config/databases/` directory. Each database defines:
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
- Configure cookies to extract from incoming requests and make them available as template variables:
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
- # Enable all cookies (default: false)
710
+ # Forward all cookies from the client request
709
711
  cookies: true
710
712
 
711
- # Or specify specific cookies to extract
713
+ # Forward only specific cookies
712
714
  cookies: [session_id, auth_token, user_preferences]
713
715
 
714
- # Disable all cookies (default behavior)
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 specified cookies are extracted.
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
- Cookies can then be accessed in SQL queries using `{{cookies.cookie_name}}`:
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 `config/databases/` directory. Each database defines:
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
- Configure cookies to extract from incoming requests and make them available as template variables:
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
- # Enable all cookies (default: false)
671
+ # Forward all cookies from the client request
670
672
  cookies: true
671
673
 
672
- # Or specify specific cookies to extract
674
+ # Forward only specific cookies
673
675
  cookies: [session_id, auth_token, user_preferences]
674
676
 
675
- # Disable all cookies (default behavior)
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 specified cookies are extracted.
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
- Cookies can then be accessed in SQL queries using `{{cookies.cookie_name}}`:
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 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
 
@@ -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. config/<config_name>.yaml
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
- # Check config/ directory
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") / "config" / f"{config_name}.yaml")
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 default configs from package
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 package's config directory.
127
+ """Get the path to the bundled example config directory inside the package.
134
128
 
135
129
  Returns:
136
- Path to package config directory, or None if not found.
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") / "config")
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"