api-dock 0.9.0__tar.gz → 0.10.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 (67) hide show
  1. api_dock-0.10.0/PKG-INFO +347 -0
  2. api_dock-0.10.0/README.md +304 -0
  3. {api_dock-0.9.0 → api_dock-0.10.0}/api_dock/auth.py +186 -58
  4. {api_dock-0.9.0 → api_dock-0.10.0}/api_dock/cli.py +259 -98
  5. {api_dock-0.9.0 → api_dock-0.10.0}/api_dock/config.py +419 -146
  6. {api_dock-0.9.0 → api_dock-0.10.0}/api_dock/config_discovery.py +30 -47
  7. api_dock-0.10.0/api_dock/database_backends.py +247 -0
  8. {api_dock-0.9.0 → api_dock-0.10.0}/api_dock/database_config.py +401 -158
  9. {api_dock-0.9.0 → api_dock-0.10.0}/api_dock/encryption.py +49 -23
  10. {api_dock-0.9.0 → api_dock-0.10.0}/api_dock/example_api_dock_config/config.yaml +3 -0
  11. {api_dock-0.9.0 → api_dock-0.10.0}/api_dock/example_api_dock_config/databases/config.yaml +36 -0
  12. api_dock-0.10.0/api_dock/example_api_dock_config/remotes/config.yaml +49 -0
  13. {api_dock-0.9.0 → api_dock-0.10.0}/api_dock/example_api_dock_config/remotes/example_remote.yaml +3 -1
  14. {api_dock-0.9.0 → api_dock-0.10.0}/api_dock/fast_api.py +67 -8
  15. {api_dock-0.9.0 → api_dock-0.10.0}/api_dock/flask_api.py +42 -1
  16. {api_dock-0.9.0 → api_dock-0.10.0}/api_dock/listings.py +7 -27
  17. api_dock-0.10.0/api_dock/lookups.py +1128 -0
  18. api_dock-0.10.0/api_dock/postgres_backend.py +385 -0
  19. api_dock-0.10.0/api_dock/postgres_config.py +358 -0
  20. api_dock-0.10.0/api_dock/route_mapper.py +1446 -0
  21. {api_dock-0.9.0 → api_dock-0.10.0}/api_dock/sql_builder.py +472 -111
  22. {api_dock-0.9.0 → api_dock-0.10.0}/api_dock/storage_auth.py +37 -75
  23. {api_dock-0.9.0 → api_dock-0.10.0}/api_dock/types.py +21 -2
  24. api_dock-0.10.0/api_dock.egg-info/PKG-INFO +347 -0
  25. {api_dock-0.9.0 → api_dock-0.10.0}/api_dock.egg-info/SOURCES.txt +20 -0
  26. {api_dock-0.9.0 → api_dock-0.10.0}/api_dock.egg-info/requires.txt +7 -0
  27. {api_dock-0.9.0 → api_dock-0.10.0}/pyproject.toml +12 -5
  28. api_dock-0.10.0/tests/test_auth.py +249 -0
  29. {api_dock-0.9.0 → api_dock-0.10.0}/tests/test_bound_parameters.py +8 -8
  30. api_dock-0.10.0/tests/test_encryption_and_discovery.py +137 -0
  31. api_dock-0.10.0/tests/test_json_conversion.py +140 -0
  32. api_dock-0.10.0/tests/test_lookup_cli.py +96 -0
  33. api_dock-0.10.0/tests/test_lookup_requests.py +343 -0
  34. api_dock-0.10.0/tests/test_lookup_sources.py +189 -0
  35. api_dock-0.10.0/tests/test_lookups.py +486 -0
  36. api_dock-0.10.0/tests/test_postgres_config.py +256 -0
  37. api_dock-0.10.0/tests/test_postgres_mixed.py +247 -0
  38. api_dock-0.10.0/tests/test_postgres_requests.py +177 -0
  39. {api_dock-0.9.0 → api_dock-0.10.0}/tests/test_proxy_pipeline.py +4 -4
  40. api_dock-0.10.0/tests/test_remote_config.py +264 -0
  41. api_dock-0.10.0/tests/test_remote_cookies.py +151 -0
  42. api_dock-0.10.0/tests/test_review_fixes.py +209 -0
  43. {api_dock-0.9.0 → api_dock-0.10.0}/tests/test_runtime_settings.py +7 -7
  44. {api_dock-0.9.0 → api_dock-0.10.0}/tests/test_sql_builder.py +3 -3
  45. api_dock-0.10.0/tests/test_sql_placement.py +176 -0
  46. {api_dock-0.9.0 → api_dock-0.10.0}/tests/test_sql_selector.py +2 -2
  47. {api_dock-0.9.0 → api_dock-0.10.0}/tests/test_startup_check.py +48 -4
  48. api_dock-0.10.0/tests/test_storage_auth.py +77 -0
  49. api_dock-0.9.0/PKG-INFO +0 -1702
  50. api_dock-0.9.0/README.md +0 -1663
  51. api_dock-0.9.0/api_dock/route_mapper.py +0 -991
  52. api_dock-0.9.0/api_dock.egg-info/PKG-INFO +0 -1702
  53. {api_dock-0.9.0 → api_dock-0.10.0}/LICENSE.md +0 -0
  54. {api_dock-0.9.0 → api_dock-0.10.0}/api_dock/__init__.py +0 -0
  55. {api_dock-0.9.0 → api_dock-0.10.0}/api_dock/example_api_dock_config/databases/example_db.yaml +0 -0
  56. {api_dock-0.9.0 → api_dock-0.10.0}/api_dock/sql_template_check.py +0 -0
  57. {api_dock-0.9.0 → api_dock-0.10.0}/api_dock.egg-info/dependency_links.txt +0 -0
  58. {api_dock-0.9.0 → api_dock-0.10.0}/api_dock.egg-info/entry_points.txt +0 -0
  59. {api_dock-0.9.0 → api_dock-0.10.0}/api_dock.egg-info/top_level.txt +0 -0
  60. {api_dock-0.9.0 → api_dock-0.10.0}/setup.cfg +0 -0
  61. {api_dock-0.9.0 → api_dock-0.10.0}/tests/test_database_requests.py +0 -0
  62. {api_dock-0.9.0 → api_dock-0.10.0}/tests/test_inject_cookies.py +0 -0
  63. {api_dock-0.9.0 → api_dock-0.10.0}/tests/test_listings.py +0 -0
  64. {api_dock-0.9.0 → api_dock-0.10.0}/tests/test_schema_unions.py +0 -0
  65. {api_dock-0.9.0 → api_dock-0.10.0}/tests/test_shared_database_config.py +0 -0
  66. {api_dock-0.9.0 → api_dock-0.10.0}/tests/test_sql_template_check.py +0 -0
  67. {api_dock-0.9.0 → api_dock-0.10.0}/tests/test_types.py +0 -0
@@ -0,0 +1,347 @@
1
+ Metadata-Version: 2.4
2
+ Name: api_dock
3
+ Version: 0.10.0
4
+ Summary: A flexible API gateway that allows you to proxy requests to multiple remote APIs and Databases
5
+ Author-email: Brookie Guzder-Williams <bguzder-williams@berkeley.edu>
6
+ License-Expression: BSD-3-Clause
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Programming Language :: Python :: 3.14
14
+ Classifier: Topic :: Database
15
+ Classifier: Topic :: Internet :: WWW/HTTP
16
+ Classifier: Topic :: Software Development :: Libraries
17
+ Classifier: Topic :: Scientific/Engineering
18
+ Requires-Python: >=3.11
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE.md
21
+ Requires-Dist: click
22
+ Requires-Dist: fastapi<0.118,>=0.117.1
23
+ Requires-Dist: uvicorn<0.38,>=0.37.0
24
+ Requires-Dist: pyyaml<7,>=6.0.3
25
+ Requires-Dist: httpx<0.29,>=0.28.1
26
+ Requires-Dist: duckdb<2,>=1.1.3
27
+ Requires-Dist: flask
28
+ Requires-Dist: cryptography<49.0.0,>=48.0.0
29
+ Requires-Dist: boto3<2,>=1.42.59
30
+ Provides-Extra: dev
31
+ Requires-Dist: pycodestyle<3,>=2.14.0; extra == "dev"
32
+ Requires-Dist: ipykernel<7,>=6.30.1; extra == "dev"
33
+ Requires-Dist: jupyterlab<5,>=4.4.9; extra == "dev"
34
+ Requires-Dist: numpy<3,>=2.3.3; extra == "dev"
35
+ Requires-Dist: pandas<3,>=2.3.2; extra == "dev"
36
+ Requires-Dist: pytest<10,>=9.0.2; extra == "dev"
37
+ Provides-Extra: postgres
38
+ Requires-Dist: psycopg[binary]<4,>=3.3.6; extra == "postgres"
39
+ Requires-Dist: psycopg_pool<4,>=3.3.2; extra == "postgres"
40
+ Provides-Extra: gcp
41
+ Requires-Dist: google-cloud-secret-manager<3,>=2; extra == "gcp"
42
+ Dynamic: license-file
43
+
44
+ # API Dock
45
+
46
+ API Dock builds an API from YAML files. Behind one endpoint it proxies requests to **remote HTTP
47
+ APIs** and serves **SQL routes** over Parquet/CSV files (local disk, S3, GCS, Azure or HTTPS) and
48
+ **PostgreSQL** tables. Run it with the `api-dock` CLI as a FastAPI or Flask app, or embed its
49
+ `RouteMapper` in an existing Python service.
50
+
51
+ | source | served at | how |
52
+ |---|---|---|
53
+ | remote HTTP API | `/<remote>/<path>` | forwarded upstream and streamed back, with optional allow-lists and restrictions |
54
+ | file tables (Parquet, CSV, ...) | `/<database>/<version>/<route>` | SQL run by DuckDB |
55
+ | PostgreSQL tables | `/<database>/<version>/<route>` | SQL run natively, or by DuckDB when a route mixes sources |
56
+
57
+ Request values reach the database as bound parameters, never pasted into SQL, and every database
58
+ config is checked when the server starts.
59
+
60
+ **Documentation lives in the [wiki](https://github.com/SchmidtDSE/api_dock/wiki/).** Start with [Getting Started](https://github.com/SchmidtDSE/api_dock/wiki/Getting-Started) and
61
+ [Concepts](https://github.com/SchmidtDSE/api_dock/wiki/Concepts).
62
+
63
+ ---
64
+
65
+ ## Install
66
+
67
+ ```bash
68
+ pip install api-dock # core
69
+ pip install 'api-dock[postgres]' # adds the PostgreSQL driver and pool
70
+ conda install -c conda-forge api_dock # or from conda-forge
71
+ ```
72
+
73
+ API Dock needs Python 3.11 or later.
74
+
75
+ ---
76
+
77
+ ## Quick start
78
+
79
+ ```bash
80
+ api-dock init # creates api_dock_config/ with commented example files
81
+ ```
82
+
83
+ Replace the examples with a main config, one remote and one database:
84
+
85
+ ```
86
+ api_dock_config/
87
+ ├── config.yaml
88
+ ├── remotes/
89
+ │ └── httpbin.yaml
90
+ └── databases/
91
+ └── places.yaml
92
+ ```
93
+
94
+ ```yaml
95
+ # api_dock_config/config.yaml
96
+ name: my-api
97
+ description: My first API Dock
98
+ remotes:
99
+ - httpbin
100
+ databases:
101
+ - places
102
+ settings:
103
+ add_trailing_slash: false # httpbin doesn't want /get/
104
+ ```
105
+
106
+ ```yaml
107
+ # api_dock_config/remotes/httpbin.yaml
108
+ name: httpbin
109
+ url: https://httpbin.org
110
+ ```
111
+
112
+ ```yaml
113
+ # api_dock_config/databases/places.yaml
114
+ tables:
115
+ places: data/places.parquet # or s3://bucket/places/**/*.parquet, a PostgreSQL table, ...
116
+ routes:
117
+ - route: places
118
+ sql: SELECT * FROM [[places]]
119
+ query_params:
120
+ - country:
121
+ sql: "country = {{country}}"
122
+ - route: places/{{id}}
123
+ sql: SELECT * FROM [[places]] WHERE id = {{id}}
124
+ ```
125
+
126
+ `[[places]]` is replaced by the table's source; `{{id}}` and `{{country}}` are request values,
127
+ sent to the database as bound parameters.
128
+
129
+ ```bash
130
+ api-dock start # FastAPI on port 8000
131
+ curl http://localhost:8000/httpbin/get # proxied to https://httpbin.org/get
132
+ curl http://localhost:8000/places/places?country=FR
133
+ # [{"id": 2, "name": "Lyon", "country": "FR"}]
134
+ curl http://localhost:8000/places/places/1
135
+ ```
136
+
137
+ [Getting Started](https://github.com/SchmidtDSE/api_dock/wiki/Getting-Started) walks through this example, including making the Parquet
138
+ file.
139
+
140
+ ---
141
+
142
+ ## What you can configure
143
+
144
+ | topic | wiki page |
145
+ |---|---|
146
+ | main config, settings (`timeout`, `base_path`, `duckdb`, ...), multiple configs | [Configuration](https://github.com/SchmidtDSE/api_dock/wiki/Configuration) |
147
+ | versioned remotes and databases, inline versions, `latest` | [Versioning](https://github.com/SchmidtDSE/api_dock/wiki/Versioning) |
148
+ | remote configs (files or `remotes/config.yaml`), allow-lists, `restricted` patterns, route mapping | [Routing and Restrictions](https://github.com/SchmidtDSE/api_dock/wiki/Routing-and-Restrictions) |
149
+ | cookies, database authentication, encrypted values | [Authentication and Cookies](https://github.com/SchmidtDSE/api_dock/wiki/Authentication-and-Cookies) |
150
+ | tables, `[[table]]` references, routes, startup checks | [SQL Database Support](https://github.com/SchmidtDSE/api_dock/wiki/SQL-Database-Support) |
151
+ | filtering, sorting, pagination, required params | [Query Parameters](https://github.com/SchmidtDSE/api_dock/wiki/Query-Parameters) |
152
+ | picking SQL from the request | [Conditional SQL](https://github.com/SchmidtDSE/api_dock/wiki/Conditional-SQL) |
153
+ | shared tables, schemas, slugs, shared routes | [Shared Database Config](https://github.com/SchmidtDSE/api_dock/wiki/Shared-Database-Config) |
154
+ | unions across schemas (`[[*.table]]`, schema groups) | [Cross-Schema Queries](https://github.com/SchmidtDSE/api_dock/wiki/Cross-Schema-Queries) |
155
+ | PostgreSQL connections, engines, safety | [PostgreSQL](https://github.com/SchmidtDSE/api_dock/wiki/PostgreSQL) |
156
+ | databases, versions and values generated from a query, refreshed on a schedule | [Lookups](https://github.com/SchmidtDSE/api_dock/wiki/Lookups) |
157
+ | `/databases`, `/remotes`, `/sources` listings | [Catalog Endpoints](https://github.com/SchmidtDSE/api_dock/wiki/Catalog-Endpoints) |
158
+ | `RouteMapper` in your own app, production deployment | [Python API and Deployment](https://github.com/SchmidtDSE/api_dock/wiki/Python-API-and-Deployment) |
159
+
160
+ ---
161
+
162
+ ## Example: versions from a catalog (lookups)
163
+
164
+ When the list of databases/versions lives somewhere else (a table of model runs, a
165
+ deployments API), a lookup turns its rows into config. api_dock runs it at startup, every
166
+ `refresh` and on demand:
167
+
168
+ ```yaml
169
+ # api_dock_config/databases/config.yaml
170
+ database:
171
+ connections:
172
+ core: {host: db.example.com, dbname: catalog, user: readonly, password: env:DB_PASSWORD}
173
+ runs_catalog: {connection: core, table: public.model_runs} # or {uri: s3://.../runs.parquet}
174
+
175
+ lookups:
176
+ model_runs:
177
+ sql: |
178
+ SELECT name, version, detections_uri,
179
+ replace(name, '-', '_') || '_' || replace(version, '.', 'p') AS schema
180
+ FROM [[runs_catalog]] WHERE published
181
+ refresh: 7d
182
+ allow: ["s3://my-bucket/runs/"] # URIs from rows must start with this
183
+
184
+ slugs:
185
+ - from: model_runs # one database/version per row
186
+ name: "{{row.name}}"
187
+ version: "{{row.version}}"
188
+ schema:
189
+ name: "{{row.schema}}"
190
+ tables:
191
+ detections: {uri: "{{row.detections_uri}}"}
192
+ ```
193
+
194
+ ```yaml
195
+ # api_dock_config/config.yaml
196
+ databases:
197
+ - from: model_runs # serve every database the lookup generates
198
+ settings:
199
+ lookups: # optional: GET status / POST refresh
200
+ refresh_route: /admin/lookups
201
+ token: env:API_DOCK_ADMIN_TOKEN
202
+ ```
203
+
204
+ Lookups can read PostgreSQL tables, Parquet/CSV files or an HTTP API, and can also
205
+ generate remote versions or fill single values. See [Lookups](https://github.com/SchmidtDSE/api_dock/wiki/Lookups).
206
+
207
+ ---
208
+
209
+ ## CLI
210
+
211
+ ```bash
212
+ api-dock # list configs and commands
213
+ api-dock init [--force] # create api_dock_config/
214
+ api-dock start [config_name] # serve api_dock_config/<config_name>.yaml (default: config)
215
+ api-dock start --backbone flask --host 127.0.0.1 --port 9000 --log-level debug
216
+ api-dock describe [config_name] # print the config
217
+ api-dock generate-key # local encryption key
218
+ api-dock encrypt "secret" # also --method env_key|aws_kms
219
+ api-dock decrypt "gAAAAA..."
220
+ api-dock lookups # run the config's lookups and print their rows
221
+ ```
222
+
223
+ Flask responses are buffered and Flask refuses configs with PostgreSQL connections; use the default
224
+ FastAPI backbone for those. Full reference: [Getting Started](https://github.com/SchmidtDSE/api_dock/wiki/Getting-Started#cli-reference).
225
+
226
+ ---
227
+
228
+ ## How it works (in brief)
229
+
230
+ - **Configs.** The main `config.yaml` lists remotes and databases and is read at startup. Remote
231
+ files, database files and the shared `remotes/config.yaml` and `databases/config.yaml` are read
232
+ again on each request, so
233
+ route edits don't need a restart.
234
+ - **Remotes.** A request is checked against the remote's allow/block lists, then forwarded with
235
+ httpx. The FastAPI app streams the upstream response back.
236
+ - **Databases.** The version is resolved (`latest`, version files, `slugs`), the route matched and
237
+ its SQL built: `[[table]]` references expanded, query-param fragments appended, values bound.
238
+ - **Engines.** A route whose tables are all on one PostgreSQL connection runs natively through
239
+ that connection's pool. Anything else runs on an in-memory DuckDB in a worker thread, with
240
+ PostgreSQL attached read-only when needed.
241
+ - **Lookups.** Named queries (SQL over the configured tables, or an HTTP API) run at startup
242
+ and on a schedule; `from:` entries turn their rows into database versions or remote versions.
243
+ - **Startup checks.** Every database and version is checked as requests will see it (table
244
+ references, quoted variables, unions, connections, engines); a bad config stops startup with a
245
+ message naming the database, version and route.
246
+
247
+ ---
248
+
249
+ ## Repo layout
250
+
251
+ ```
252
+ api_dock/ the package
253
+ cli.py api-dock commands
254
+ config.py, config_discovery.py main/remote config loading, settings, route rules
255
+ route_mapper.py RouteMapper: request handling, startup checks, PostgreSQL lifecycle
256
+ fast_api.py, flask_api.py the two app backbones
257
+ database_config.py database configs, shared config, versions and slugs
258
+ lookups.py lookups: templates, SQL/HTTP runners, refresh
259
+ sql_builder.py SQL building, table references, unions, engine choice
260
+ database_backends.py DuckDB backend postgres_backend.py, postgres_config.py PostgreSQL
261
+ storage_auth.py, auth.py, encryption.py, listings.py, sql_template_check.py, types.py
262
+ example_api_dock_config/ copied by `api-dock init`
263
+ tests/ pixi run -e dev pytest -q
264
+ ```
265
+
266
+ More in the [Developer Guide](https://github.com/SchmidtDSE/api_dock/wiki/Developer-Guide).
267
+
268
+ ---
269
+
270
+ # Development
271
+
272
+ ```bash
273
+ pixi install -e dev # includes psycopg and a local PostgreSQL for the tests
274
+ pixi run -e dev pytest -q # PostgreSQL tests skip if PostgreSQL/psycopg are missing
275
+ ```
276
+
277
+ ## Publishing a Release
278
+
279
+ Publishing a GitHub Release is what publishes to PyPI: `.github/workflows/publish_to_pypi.yml` runs on `release: published`, builds the sdist and wheel with `uv build`, and uploads them using PyPI trusted publishing (OIDC). There's no local build, no API token, and no `twine`. conda-forge follows automatically: its bot opens a version PR on [conda-forge/api_dock-feedstock](https://github.com/conda-forge/api_dock-feedstock), which a maintainer merges.
280
+
281
+ ```bash
282
+ # 0. Start from a clean, up-to-date main
283
+ export VERSION=0.10.0 # the NEW version, no leading "v"
284
+ git checkout main
285
+ git pull origin main
286
+ git status
287
+
288
+ # 1. Set `version` in pyproject.toml to $VERSION
289
+
290
+ # 2. Run the tests
291
+ pixi run -e dev pytest -q
292
+
293
+ # 3. Commit, tag, push (the commit command adds the "v$VERSION: " prefix)
294
+ export COMMIT_MESSAGE='code review fixes: remote cookies, safer SQL building, auth caching'
295
+ git add -A
296
+ git commit -m "v$VERSION: $COMMIT_MESSAGE"
297
+ git tag "v$VERSION"
298
+ git push origin main "v$VERSION"
299
+
300
+ # 4. Publish the GitHub Release; this triggers the PyPI upload.
301
+ # Don't use --draft (the workflow only runs on a published release); no wheel needs attaching.
302
+ gh release create "v$VERSION" \
303
+ --title "v$VERSION" \
304
+ --notes "$(cat <<'EOF'
305
+ **Upgrading:** a remote now receives only the cookies its `cookies:` setting allows (none without it). A remote that relied on the browser's cookies being passed through needs `cookies: [<name>]`, or `cookies: true` for all of them. Configs that set `action`, an unknown version `schema:`, non-boolean `add_trailing_slash`/`follow_redirects`, or a bad `timeout` now stop at startup.
306
+
307
+ * new features
308
+ - `api-dock describe` builds the API like `start` (lookups, startup checks) and prints every database/version with expanded route SQL, plus each remote's url
309
+ - `api-dock init --force` replaces existing files; the whole example tree is copied
310
+ - `gcp` extra (`pip install 'api_dock[gcp]'`) for GCP Secret Manager authentication
311
+ - Startup warning when an `authentication` block would be ignored by remote routes
312
+ * bug fixes
313
+ - Remote cookies: the client's raw `Cookie` header is no longer forwarded; the upstream `Cookie` header is built from the remote's `cookies` setting, so filtering and injected cookies work even when the client sends cookies
314
+ - Several upstream `Set-Cookie` headers reach the client separately instead of merged into one (new `ProxyResponse.set_cookies`)
315
+ - Query-param filters join the base query's top-level `WHERE` (strings, comments, CTEs and subqueries ignored), parenthesized and before a trailing `GROUP BY`/`ORDER BY`/`LIMIT`: fixes `WHERE a OR b` letting rows past filters, CTE-only `WHERE`s and base queries ending in `ORDER BY`
316
+ - `[[table]]` is a table source only right after `FROM`/`JOIN` or a `FROM`-list comma (was any `FROM`/`JOIN` in the previous 20 characters, which broke `ON [[a]].id = [[b]].id`)
317
+ - `sql_append` values are limited to integers and column names (with `ASC`/`DESC`, `NULLS FIRST/LAST`); the unimplemented `action` (which echoed request values, including cookies) is refused
318
+ - Authentication providers are built once per config instead of on every request (no secret-store/KMS call per request); `refresh_interval` now refreshes, keeping the last values if a refresh fails; tokens compared in constant time; `aws_tokens_file` works (`aws_key_id` optional)
319
+ - Remote route mapping fills `{{route_name}}` and `{{cookies.x}}` in `remote_route`, and keeps a query string written there
320
+ - Include/exclude and listing filters compare versions part by part (`1.10` no longer matches `1.1`)
321
+ - Values in DuckDB secret SQL (S3 region, GCS keys, HTTP headers) are quoted; a second GCS `service_account` no longer overwrites the process-wide one
322
+ - A main config that is missing (explicit path), invalid YAML or not a mapping stops startup instead of serving an empty API; settings are validated at startup
323
+ - An explicit encryption `key_file` must exist (only the default key file falls back to `API_DOCK_ENCRYPTION_KEY`); `env_key` reads only its variable
324
+ - Remote error responses no longer include internal error text (it's logged); `map_route_sync` no longer leaks event loops
325
+ * cleanup / other improvements
326
+ - `map_database_route` split into named steps; one version-resolution helper for remotes and databases
327
+ - A remote request reads only that remote's config file (was every remote file, twice)
328
+ - Removed unused internal functions; shared YAML-loading and version-matching helpers; `follow_protocol_downgrades` (never implemented) removed
329
+ - FastAPI app reports the package version; classifiers match Python 3.11+; style fixes
330
+ - Tests grew from 626 to 760, including the first tests for authentication, encryption, config discovery and storage credentials
331
+ EOF
332
+ )"
333
+
334
+ # 5. Watch the publish workflow, then confirm PyPI has the new version
335
+ gh run watch "$(gh run list --workflow=publish_to_pypi.yml -L1 --json databaseId -q '.[0].databaseId')" --repo SchmidtDSE/api_dock
336
+ curl -s https://pypi.org/pypi/api-dock/json | python3 -c "import sys,json; print('PyPI latest:', json.load(sys.stdin)['info']['version'])"
337
+
338
+ # 6. conda-forge: once the bot opens the v$VERSION PR (usually within hours), check that the recipe's
339
+ # run requirements match pyproject.toml dependencies (the bot only bumps version + sha256), then merge it
340
+ gh pr list --repo conda-forge/api_dock-feedstock --state open
341
+ ```
342
+
343
+ ---
344
+
345
+ # License
346
+
347
+ BSD 3-Clause
@@ -0,0 +1,304 @@
1
+ # API Dock
2
+
3
+ API Dock builds an API from YAML files. Behind one endpoint it proxies requests to **remote HTTP
4
+ APIs** and serves **SQL routes** over Parquet/CSV files (local disk, S3, GCS, Azure or HTTPS) and
5
+ **PostgreSQL** tables. Run it with the `api-dock` CLI as a FastAPI or Flask app, or embed its
6
+ `RouteMapper` in an existing Python service.
7
+
8
+ | source | served at | how |
9
+ |---|---|---|
10
+ | remote HTTP API | `/<remote>/<path>` | forwarded upstream and streamed back, with optional allow-lists and restrictions |
11
+ | file tables (Parquet, CSV, ...) | `/<database>/<version>/<route>` | SQL run by DuckDB |
12
+ | PostgreSQL tables | `/<database>/<version>/<route>` | SQL run natively, or by DuckDB when a route mixes sources |
13
+
14
+ Request values reach the database as bound parameters, never pasted into SQL, and every database
15
+ config is checked when the server starts.
16
+
17
+ **Documentation lives in the [wiki](https://github.com/SchmidtDSE/api_dock/wiki/).** Start with [Getting Started](https://github.com/SchmidtDSE/api_dock/wiki/Getting-Started) and
18
+ [Concepts](https://github.com/SchmidtDSE/api_dock/wiki/Concepts).
19
+
20
+ ---
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ pip install api-dock # core
26
+ pip install 'api-dock[postgres]' # adds the PostgreSQL driver and pool
27
+ conda install -c conda-forge api_dock # or from conda-forge
28
+ ```
29
+
30
+ API Dock needs Python 3.11 or later.
31
+
32
+ ---
33
+
34
+ ## Quick start
35
+
36
+ ```bash
37
+ api-dock init # creates api_dock_config/ with commented example files
38
+ ```
39
+
40
+ Replace the examples with a main config, one remote and one database:
41
+
42
+ ```
43
+ api_dock_config/
44
+ ├── config.yaml
45
+ ├── remotes/
46
+ │ └── httpbin.yaml
47
+ └── databases/
48
+ └── places.yaml
49
+ ```
50
+
51
+ ```yaml
52
+ # api_dock_config/config.yaml
53
+ name: my-api
54
+ description: My first API Dock
55
+ remotes:
56
+ - httpbin
57
+ databases:
58
+ - places
59
+ settings:
60
+ add_trailing_slash: false # httpbin doesn't want /get/
61
+ ```
62
+
63
+ ```yaml
64
+ # api_dock_config/remotes/httpbin.yaml
65
+ name: httpbin
66
+ url: https://httpbin.org
67
+ ```
68
+
69
+ ```yaml
70
+ # api_dock_config/databases/places.yaml
71
+ tables:
72
+ places: data/places.parquet # or s3://bucket/places/**/*.parquet, a PostgreSQL table, ...
73
+ routes:
74
+ - route: places
75
+ sql: SELECT * FROM [[places]]
76
+ query_params:
77
+ - country:
78
+ sql: "country = {{country}}"
79
+ - route: places/{{id}}
80
+ sql: SELECT * FROM [[places]] WHERE id = {{id}}
81
+ ```
82
+
83
+ `[[places]]` is replaced by the table's source; `{{id}}` and `{{country}}` are request values,
84
+ sent to the database as bound parameters.
85
+
86
+ ```bash
87
+ api-dock start # FastAPI on port 8000
88
+ curl http://localhost:8000/httpbin/get # proxied to https://httpbin.org/get
89
+ curl http://localhost:8000/places/places?country=FR
90
+ # [{"id": 2, "name": "Lyon", "country": "FR"}]
91
+ curl http://localhost:8000/places/places/1
92
+ ```
93
+
94
+ [Getting Started](https://github.com/SchmidtDSE/api_dock/wiki/Getting-Started) walks through this example, including making the Parquet
95
+ file.
96
+
97
+ ---
98
+
99
+ ## What you can configure
100
+
101
+ | topic | wiki page |
102
+ |---|---|
103
+ | main config, settings (`timeout`, `base_path`, `duckdb`, ...), multiple configs | [Configuration](https://github.com/SchmidtDSE/api_dock/wiki/Configuration) |
104
+ | versioned remotes and databases, inline versions, `latest` | [Versioning](https://github.com/SchmidtDSE/api_dock/wiki/Versioning) |
105
+ | remote configs (files or `remotes/config.yaml`), allow-lists, `restricted` patterns, route mapping | [Routing and Restrictions](https://github.com/SchmidtDSE/api_dock/wiki/Routing-and-Restrictions) |
106
+ | cookies, database authentication, encrypted values | [Authentication and Cookies](https://github.com/SchmidtDSE/api_dock/wiki/Authentication-and-Cookies) |
107
+ | tables, `[[table]]` references, routes, startup checks | [SQL Database Support](https://github.com/SchmidtDSE/api_dock/wiki/SQL-Database-Support) |
108
+ | filtering, sorting, pagination, required params | [Query Parameters](https://github.com/SchmidtDSE/api_dock/wiki/Query-Parameters) |
109
+ | picking SQL from the request | [Conditional SQL](https://github.com/SchmidtDSE/api_dock/wiki/Conditional-SQL) |
110
+ | shared tables, schemas, slugs, shared routes | [Shared Database Config](https://github.com/SchmidtDSE/api_dock/wiki/Shared-Database-Config) |
111
+ | unions across schemas (`[[*.table]]`, schema groups) | [Cross-Schema Queries](https://github.com/SchmidtDSE/api_dock/wiki/Cross-Schema-Queries) |
112
+ | PostgreSQL connections, engines, safety | [PostgreSQL](https://github.com/SchmidtDSE/api_dock/wiki/PostgreSQL) |
113
+ | databases, versions and values generated from a query, refreshed on a schedule | [Lookups](https://github.com/SchmidtDSE/api_dock/wiki/Lookups) |
114
+ | `/databases`, `/remotes`, `/sources` listings | [Catalog Endpoints](https://github.com/SchmidtDSE/api_dock/wiki/Catalog-Endpoints) |
115
+ | `RouteMapper` in your own app, production deployment | [Python API and Deployment](https://github.com/SchmidtDSE/api_dock/wiki/Python-API-and-Deployment) |
116
+
117
+ ---
118
+
119
+ ## Example: versions from a catalog (lookups)
120
+
121
+ When the list of databases/versions lives somewhere else (a table of model runs, a
122
+ deployments API), a lookup turns its rows into config. api_dock runs it at startup, every
123
+ `refresh` and on demand:
124
+
125
+ ```yaml
126
+ # api_dock_config/databases/config.yaml
127
+ database:
128
+ connections:
129
+ core: {host: db.example.com, dbname: catalog, user: readonly, password: env:DB_PASSWORD}
130
+ runs_catalog: {connection: core, table: public.model_runs} # or {uri: s3://.../runs.parquet}
131
+
132
+ lookups:
133
+ model_runs:
134
+ sql: |
135
+ SELECT name, version, detections_uri,
136
+ replace(name, '-', '_') || '_' || replace(version, '.', 'p') AS schema
137
+ FROM [[runs_catalog]] WHERE published
138
+ refresh: 7d
139
+ allow: ["s3://my-bucket/runs/"] # URIs from rows must start with this
140
+
141
+ slugs:
142
+ - from: model_runs # one database/version per row
143
+ name: "{{row.name}}"
144
+ version: "{{row.version}}"
145
+ schema:
146
+ name: "{{row.schema}}"
147
+ tables:
148
+ detections: {uri: "{{row.detections_uri}}"}
149
+ ```
150
+
151
+ ```yaml
152
+ # api_dock_config/config.yaml
153
+ databases:
154
+ - from: model_runs # serve every database the lookup generates
155
+ settings:
156
+ lookups: # optional: GET status / POST refresh
157
+ refresh_route: /admin/lookups
158
+ token: env:API_DOCK_ADMIN_TOKEN
159
+ ```
160
+
161
+ Lookups can read PostgreSQL tables, Parquet/CSV files or an HTTP API, and can also
162
+ generate remote versions or fill single values. See [Lookups](https://github.com/SchmidtDSE/api_dock/wiki/Lookups).
163
+
164
+ ---
165
+
166
+ ## CLI
167
+
168
+ ```bash
169
+ api-dock # list configs and commands
170
+ api-dock init [--force] # create api_dock_config/
171
+ api-dock start [config_name] # serve api_dock_config/<config_name>.yaml (default: config)
172
+ api-dock start --backbone flask --host 127.0.0.1 --port 9000 --log-level debug
173
+ api-dock describe [config_name] # print the config
174
+ api-dock generate-key # local encryption key
175
+ api-dock encrypt "secret" # also --method env_key|aws_kms
176
+ api-dock decrypt "gAAAAA..."
177
+ api-dock lookups # run the config's lookups and print their rows
178
+ ```
179
+
180
+ Flask responses are buffered and Flask refuses configs with PostgreSQL connections; use the default
181
+ FastAPI backbone for those. Full reference: [Getting Started](https://github.com/SchmidtDSE/api_dock/wiki/Getting-Started#cli-reference).
182
+
183
+ ---
184
+
185
+ ## How it works (in brief)
186
+
187
+ - **Configs.** The main `config.yaml` lists remotes and databases and is read at startup. Remote
188
+ files, database files and the shared `remotes/config.yaml` and `databases/config.yaml` are read
189
+ again on each request, so
190
+ route edits don't need a restart.
191
+ - **Remotes.** A request is checked against the remote's allow/block lists, then forwarded with
192
+ httpx. The FastAPI app streams the upstream response back.
193
+ - **Databases.** The version is resolved (`latest`, version files, `slugs`), the route matched and
194
+ its SQL built: `[[table]]` references expanded, query-param fragments appended, values bound.
195
+ - **Engines.** A route whose tables are all on one PostgreSQL connection runs natively through
196
+ that connection's pool. Anything else runs on an in-memory DuckDB in a worker thread, with
197
+ PostgreSQL attached read-only when needed.
198
+ - **Lookups.** Named queries (SQL over the configured tables, or an HTTP API) run at startup
199
+ and on a schedule; `from:` entries turn their rows into database versions or remote versions.
200
+ - **Startup checks.** Every database and version is checked as requests will see it (table
201
+ references, quoted variables, unions, connections, engines); a bad config stops startup with a
202
+ message naming the database, version and route.
203
+
204
+ ---
205
+
206
+ ## Repo layout
207
+
208
+ ```
209
+ api_dock/ the package
210
+ cli.py api-dock commands
211
+ config.py, config_discovery.py main/remote config loading, settings, route rules
212
+ route_mapper.py RouteMapper: request handling, startup checks, PostgreSQL lifecycle
213
+ fast_api.py, flask_api.py the two app backbones
214
+ database_config.py database configs, shared config, versions and slugs
215
+ lookups.py lookups: templates, SQL/HTTP runners, refresh
216
+ sql_builder.py SQL building, table references, unions, engine choice
217
+ database_backends.py DuckDB backend postgres_backend.py, postgres_config.py PostgreSQL
218
+ storage_auth.py, auth.py, encryption.py, listings.py, sql_template_check.py, types.py
219
+ example_api_dock_config/ copied by `api-dock init`
220
+ tests/ pixi run -e dev pytest -q
221
+ ```
222
+
223
+ More in the [Developer Guide](https://github.com/SchmidtDSE/api_dock/wiki/Developer-Guide).
224
+
225
+ ---
226
+
227
+ # Development
228
+
229
+ ```bash
230
+ pixi install -e dev # includes psycopg and a local PostgreSQL for the tests
231
+ pixi run -e dev pytest -q # PostgreSQL tests skip if PostgreSQL/psycopg are missing
232
+ ```
233
+
234
+ ## Publishing a Release
235
+
236
+ Publishing a GitHub Release is what publishes to PyPI: `.github/workflows/publish_to_pypi.yml` runs on `release: published`, builds the sdist and wheel with `uv build`, and uploads them using PyPI trusted publishing (OIDC). There's no local build, no API token, and no `twine`. conda-forge follows automatically: its bot opens a version PR on [conda-forge/api_dock-feedstock](https://github.com/conda-forge/api_dock-feedstock), which a maintainer merges.
237
+
238
+ ```bash
239
+ # 0. Start from a clean, up-to-date main
240
+ export VERSION=0.10.0 # the NEW version, no leading "v"
241
+ git checkout main
242
+ git pull origin main
243
+ git status
244
+
245
+ # 1. Set `version` in pyproject.toml to $VERSION
246
+
247
+ # 2. Run the tests
248
+ pixi run -e dev pytest -q
249
+
250
+ # 3. Commit, tag, push (the commit command adds the "v$VERSION: " prefix)
251
+ export COMMIT_MESSAGE='code review fixes: remote cookies, safer SQL building, auth caching'
252
+ git add -A
253
+ git commit -m "v$VERSION: $COMMIT_MESSAGE"
254
+ git tag "v$VERSION"
255
+ git push origin main "v$VERSION"
256
+
257
+ # 4. Publish the GitHub Release; this triggers the PyPI upload.
258
+ # Don't use --draft (the workflow only runs on a published release); no wheel needs attaching.
259
+ gh release create "v$VERSION" \
260
+ --title "v$VERSION" \
261
+ --notes "$(cat <<'EOF'
262
+ **Upgrading:** a remote now receives only the cookies its `cookies:` setting allows (none without it). A remote that relied on the browser's cookies being passed through needs `cookies: [<name>]`, or `cookies: true` for all of them. Configs that set `action`, an unknown version `schema:`, non-boolean `add_trailing_slash`/`follow_redirects`, or a bad `timeout` now stop at startup.
263
+
264
+ * new features
265
+ - `api-dock describe` builds the API like `start` (lookups, startup checks) and prints every database/version with expanded route SQL, plus each remote's url
266
+ - `api-dock init --force` replaces existing files; the whole example tree is copied
267
+ - `gcp` extra (`pip install 'api_dock[gcp]'`) for GCP Secret Manager authentication
268
+ - Startup warning when an `authentication` block would be ignored by remote routes
269
+ * bug fixes
270
+ - Remote cookies: the client's raw `Cookie` header is no longer forwarded; the upstream `Cookie` header is built from the remote's `cookies` setting, so filtering and injected cookies work even when the client sends cookies
271
+ - Several upstream `Set-Cookie` headers reach the client separately instead of merged into one (new `ProxyResponse.set_cookies`)
272
+ - Query-param filters join the base query's top-level `WHERE` (strings, comments, CTEs and subqueries ignored), parenthesized and before a trailing `GROUP BY`/`ORDER BY`/`LIMIT`: fixes `WHERE a OR b` letting rows past filters, CTE-only `WHERE`s and base queries ending in `ORDER BY`
273
+ - `[[table]]` is a table source only right after `FROM`/`JOIN` or a `FROM`-list comma (was any `FROM`/`JOIN` in the previous 20 characters, which broke `ON [[a]].id = [[b]].id`)
274
+ - `sql_append` values are limited to integers and column names (with `ASC`/`DESC`, `NULLS FIRST/LAST`); the unimplemented `action` (which echoed request values, including cookies) is refused
275
+ - Authentication providers are built once per config instead of on every request (no secret-store/KMS call per request); `refresh_interval` now refreshes, keeping the last values if a refresh fails; tokens compared in constant time; `aws_tokens_file` works (`aws_key_id` optional)
276
+ - Remote route mapping fills `{{route_name}}` and `{{cookies.x}}` in `remote_route`, and keeps a query string written there
277
+ - Include/exclude and listing filters compare versions part by part (`1.10` no longer matches `1.1`)
278
+ - Values in DuckDB secret SQL (S3 region, GCS keys, HTTP headers) are quoted; a second GCS `service_account` no longer overwrites the process-wide one
279
+ - A main config that is missing (explicit path), invalid YAML or not a mapping stops startup instead of serving an empty API; settings are validated at startup
280
+ - An explicit encryption `key_file` must exist (only the default key file falls back to `API_DOCK_ENCRYPTION_KEY`); `env_key` reads only its variable
281
+ - Remote error responses no longer include internal error text (it's logged); `map_route_sync` no longer leaks event loops
282
+ * cleanup / other improvements
283
+ - `map_database_route` split into named steps; one version-resolution helper for remotes and databases
284
+ - A remote request reads only that remote's config file (was every remote file, twice)
285
+ - Removed unused internal functions; shared YAML-loading and version-matching helpers; `follow_protocol_downgrades` (never implemented) removed
286
+ - FastAPI app reports the package version; classifiers match Python 3.11+; style fixes
287
+ - Tests grew from 626 to 760, including the first tests for authentication, encryption, config discovery and storage credentials
288
+ EOF
289
+ )"
290
+
291
+ # 5. Watch the publish workflow, then confirm PyPI has the new version
292
+ gh run watch "$(gh run list --workflow=publish_to_pypi.yml -L1 --json databaseId -q '.[0].databaseId')" --repo SchmidtDSE/api_dock
293
+ curl -s https://pypi.org/pypi/api-dock/json | python3 -c "import sys,json; print('PyPI latest:', json.load(sys.stdin)['info']['version'])"
294
+
295
+ # 6. conda-forge: once the bot opens the v$VERSION PR (usually within hours), check that the recipe's
296
+ # run requirements match pyproject.toml dependencies (the bot only bumps version + sha256), then merge it
297
+ gh pr list --repo conda-forge/api_dock-feedstock --state open
298
+ ```
299
+
300
+ ---
301
+
302
+ # License
303
+
304
+ BSD 3-Clause