api-dock 0.8.2__tar.gz → 0.9.1__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 (61) hide show
  1. api_dock-0.9.1/PKG-INFO +333 -0
  2. api_dock-0.9.1/README.md +291 -0
  3. api_dock-0.9.1/api_dock/__init__.py +72 -0
  4. {api_dock-0.8.2 → api_dock-0.9.1}/api_dock/cli.py +111 -1
  5. {api_dock-0.8.2 → api_dock-0.9.1}/api_dock/config.py +315 -37
  6. api_dock-0.9.1/api_dock/database_backends.py +248 -0
  7. {api_dock-0.8.2 → api_dock-0.9.1}/api_dock/database_config.py +670 -112
  8. {api_dock-0.8.2 → api_dock-0.9.1}/api_dock/example_api_dock_config/config.yaml +3 -0
  9. {api_dock-0.8.2 → api_dock-0.9.1}/api_dock/example_api_dock_config/databases/config.yaml +36 -0
  10. {api_dock-0.8.2 → api_dock-0.9.1}/api_dock/example_api_dock_config/databases/example_db.yaml +4 -4
  11. api_dock-0.9.1/api_dock/example_api_dock_config/remotes/config.yaml +49 -0
  12. {api_dock-0.8.2 → api_dock-0.9.1}/api_dock/fast_api.py +77 -4
  13. {api_dock-0.8.2 → api_dock-0.9.1}/api_dock/flask_api.py +75 -2
  14. {api_dock-0.8.2 → api_dock-0.9.1}/api_dock/listings.py +32 -13
  15. api_dock-0.9.1/api_dock/lookups.py +1128 -0
  16. api_dock-0.9.1/api_dock/postgres_backend.py +385 -0
  17. api_dock-0.9.1/api_dock/postgres_config.py +358 -0
  18. {api_dock-0.8.2 → api_dock-0.9.1}/api_dock/route_mapper.py +410 -124
  19. {api_dock-0.8.2 → api_dock-0.9.1}/api_dock/sql_builder.py +617 -222
  20. api_dock-0.9.1/api_dock/sql_template_check.py +317 -0
  21. {api_dock-0.8.2 → api_dock-0.9.1}/api_dock/types.py +16 -1
  22. api_dock-0.9.1/api_dock.egg-info/PKG-INFO +333 -0
  23. {api_dock-0.8.2 → api_dock-0.9.1}/api_dock.egg-info/SOURCES.txt +19 -0
  24. {api_dock-0.8.2 → api_dock-0.9.1}/api_dock.egg-info/requires.txt +4 -0
  25. {api_dock-0.8.2 → api_dock-0.9.1}/pyproject.toml +7 -2
  26. api_dock-0.9.1/tests/test_bound_parameters.py +260 -0
  27. api_dock-0.9.1/tests/test_database_requests.py +335 -0
  28. api_dock-0.9.1/tests/test_json_conversion.py +140 -0
  29. {api_dock-0.8.2 → api_dock-0.9.1}/tests/test_listings.py +75 -0
  30. api_dock-0.9.1/tests/test_lookup_cli.py +96 -0
  31. api_dock-0.9.1/tests/test_lookup_requests.py +343 -0
  32. api_dock-0.9.1/tests/test_lookup_sources.py +189 -0
  33. api_dock-0.9.1/tests/test_lookups.py +486 -0
  34. api_dock-0.9.1/tests/test_postgres_config.py +256 -0
  35. api_dock-0.9.1/tests/test_postgres_mixed.py +247 -0
  36. api_dock-0.9.1/tests/test_postgres_requests.py +177 -0
  37. {api_dock-0.8.2 → api_dock-0.9.1}/tests/test_proxy_pipeline.py +5 -0
  38. api_dock-0.9.1/tests/test_remote_config.py +251 -0
  39. {api_dock-0.8.2 → api_dock-0.9.1}/tests/test_runtime_settings.py +7 -7
  40. {api_dock-0.8.2 → api_dock-0.9.1}/tests/test_schema_unions.py +7 -6
  41. {api_dock-0.8.2 → api_dock-0.9.1}/tests/test_shared_database_config.py +9 -10
  42. {api_dock-0.8.2 → api_dock-0.9.1}/tests/test_sql_builder.py +17 -14
  43. {api_dock-0.8.2 → api_dock-0.9.1}/tests/test_sql_selector.py +10 -5
  44. api_dock-0.9.1/tests/test_sql_template_check.py +147 -0
  45. api_dock-0.9.1/tests/test_startup_check.py +449 -0
  46. api_dock-0.8.2/PKG-INFO +0 -1665
  47. api_dock-0.8.2/README.md +0 -1626
  48. api_dock-0.8.2/api_dock/__init__.py +0 -26
  49. api_dock-0.8.2/api_dock.egg-info/PKG-INFO +0 -1665
  50. {api_dock-0.8.2 → api_dock-0.9.1}/LICENSE.md +0 -0
  51. {api_dock-0.8.2 → api_dock-0.9.1}/api_dock/auth.py +0 -0
  52. {api_dock-0.8.2 → api_dock-0.9.1}/api_dock/config_discovery.py +0 -0
  53. {api_dock-0.8.2 → api_dock-0.9.1}/api_dock/encryption.py +0 -0
  54. {api_dock-0.8.2 → api_dock-0.9.1}/api_dock/example_api_dock_config/remotes/example_remote.yaml +0 -0
  55. {api_dock-0.8.2 → api_dock-0.9.1}/api_dock/storage_auth.py +0 -0
  56. {api_dock-0.8.2 → api_dock-0.9.1}/api_dock.egg-info/dependency_links.txt +0 -0
  57. {api_dock-0.8.2 → api_dock-0.9.1}/api_dock.egg-info/entry_points.txt +0 -0
  58. {api_dock-0.8.2 → api_dock-0.9.1}/api_dock.egg-info/top_level.txt +0 -0
  59. {api_dock-0.8.2 → api_dock-0.9.1}/setup.cfg +0 -0
  60. {api_dock-0.8.2 → api_dock-0.9.1}/tests/test_inject_cookies.py +0 -0
  61. {api_dock-0.8.2 → api_dock-0.9.1}/tests/test_types.py +0 -0
@@ -0,0 +1,333 @@
1
+ Metadata-Version: 2.4
2
+ Name: api_dock
3
+ Version: 0.9.1
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.8
11
+ Classifier: Programming Language :: Python :: 3.9
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Database
16
+ Classifier: Topic :: Internet :: WWW/HTTP
17
+ Classifier: Topic :: Software Development :: Libraries
18
+ Classifier: Topic :: Scientific/Engineering
19
+ Requires-Python: >=3.11
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE.md
22
+ Requires-Dist: click
23
+ Requires-Dist: fastapi<0.118,>=0.117.1
24
+ Requires-Dist: uvicorn<0.38,>=0.37.0
25
+ Requires-Dist: pyyaml<7,>=6.0.3
26
+ Requires-Dist: httpx<0.29,>=0.28.1
27
+ Requires-Dist: duckdb<2,>=1.1.3
28
+ Requires-Dist: flask
29
+ Requires-Dist: cryptography<49.0.0,>=48.0.0
30
+ Requires-Dist: boto3<2,>=1.42.59
31
+ Provides-Extra: dev
32
+ Requires-Dist: pycodestyle<3,>=2.14.0; extra == "dev"
33
+ Requires-Dist: ipykernel<7,>=6.30.1; extra == "dev"
34
+ Requires-Dist: jupyterlab<5,>=4.4.9; extra == "dev"
35
+ Requires-Dist: numpy<3,>=2.3.3; extra == "dev"
36
+ Requires-Dist: pandas<3,>=2.3.2; extra == "dev"
37
+ Requires-Dist: pytest<10,>=9.0.2; extra == "dev"
38
+ Provides-Extra: postgres
39
+ Requires-Dist: psycopg[binary]<4,>=3.3.6; extra == "postgres"
40
+ Requires-Dist: psycopg_pool<4,>=3.3.2; extra == "postgres"
41
+ Dynamic: license-file
42
+
43
+ # API Dock
44
+
45
+ API Dock builds an API from YAML files. Behind one endpoint it proxies requests to **remote HTTP
46
+ APIs** and serves **SQL routes** over Parquet/CSV files (local disk, S3, GCS, Azure or HTTPS) and
47
+ **PostgreSQL** tables. Run it with the `api-dock` CLI as a FastAPI or Flask app, or embed its
48
+ `RouteMapper` in an existing Python service.
49
+
50
+ | source | served at | how |
51
+ |---|---|---|
52
+ | remote HTTP API | `/<remote>/<path>` | forwarded upstream and streamed back, with optional allow-lists and restrictions |
53
+ | file tables (Parquet, CSV, ...) | `/<database>/<version>/<route>` | SQL run by DuckDB |
54
+ | PostgreSQL tables | `/<database>/<version>/<route>` | SQL run natively, or by DuckDB when a route mixes sources |
55
+
56
+ Request values reach the database as bound parameters, never pasted into SQL, and every database
57
+ config is checked when the server starts.
58
+
59
+ **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
60
+ [Concepts](https://github.com/SchmidtDSE/api_dock/wiki/Concepts).
61
+
62
+ ---
63
+
64
+ ## Install
65
+
66
+ ```bash
67
+ pip install api-dock # core
68
+ pip install 'api-dock[postgres]' # adds the PostgreSQL driver and pool
69
+ conda install -c conda-forge api_dock # or from conda-forge
70
+ ```
71
+
72
+ API Dock needs Python 3.11 or later.
73
+
74
+ ---
75
+
76
+ ## Quick start
77
+
78
+ ```bash
79
+ api-dock init # creates api_dock_config/ with commented example files
80
+ ```
81
+
82
+ Replace the examples with a main config, one remote and one database:
83
+
84
+ ```
85
+ api_dock_config/
86
+ ├── config.yaml
87
+ ├── remotes/
88
+ │ └── httpbin.yaml
89
+ └── databases/
90
+ └── places.yaml
91
+ ```
92
+
93
+ ```yaml
94
+ # api_dock_config/config.yaml
95
+ name: my-api
96
+ description: My first API Dock
97
+ remotes:
98
+ - httpbin
99
+ databases:
100
+ - places
101
+ settings:
102
+ add_trailing_slash: false # httpbin doesn't want /get/
103
+ ```
104
+
105
+ ```yaml
106
+ # api_dock_config/remotes/httpbin.yaml
107
+ name: httpbin
108
+ url: https://httpbin.org
109
+ ```
110
+
111
+ ```yaml
112
+ # api_dock_config/databases/places.yaml
113
+ tables:
114
+ places: data/places.parquet # or s3://bucket/places/**/*.parquet, a PostgreSQL table, ...
115
+ routes:
116
+ - route: places
117
+ sql: SELECT * FROM [[places]]
118
+ query_params:
119
+ - country:
120
+ sql: "country = {{country}}"
121
+ - route: places/{{id}}
122
+ sql: SELECT * FROM [[places]] WHERE id = {{id}}
123
+ ```
124
+
125
+ `[[places]]` is replaced by the table's source; `{{id}}` and `{{country}}` are request values,
126
+ sent to the database as bound parameters.
127
+
128
+ ```bash
129
+ api-dock start # FastAPI on port 8000
130
+ curl http://localhost:8000/httpbin/get # proxied to https://httpbin.org/get
131
+ curl http://localhost:8000/places/places?country=FR
132
+ # [{"id": 2, "name": "Lyon", "country": "FR"}]
133
+ curl http://localhost:8000/places/places/1
134
+ ```
135
+
136
+ [Getting Started](https://github.com/SchmidtDSE/api_dock/wiki/Getting-Started) walks through this example, including making the Parquet
137
+ file.
138
+
139
+ ---
140
+
141
+ ## What you can configure
142
+
143
+ | topic | wiki page |
144
+ |---|---|
145
+ | main config, settings (`timeout`, `base_path`, `duckdb`, ...), multiple configs | [Configuration](https://github.com/SchmidtDSE/api_dock/wiki/Configuration) |
146
+ | versioned remotes and databases, inline versions, `latest` | [Versioning](https://github.com/SchmidtDSE/api_dock/wiki/Versioning) |
147
+ | 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) |
148
+ | cookies, database authentication, encrypted values | [Authentication and Cookies](https://github.com/SchmidtDSE/api_dock/wiki/Authentication-and-Cookies) |
149
+ | tables, `[[table]]` references, routes, startup checks | [SQL Database Support](https://github.com/SchmidtDSE/api_dock/wiki/SQL-Database-Support) |
150
+ | filtering, sorting, pagination, required params | [Query Parameters](https://github.com/SchmidtDSE/api_dock/wiki/Query-Parameters) |
151
+ | picking SQL from the request | [Conditional SQL](https://github.com/SchmidtDSE/api_dock/wiki/Conditional-SQL) |
152
+ | shared tables, schemas, slugs, shared routes | [Shared Database Config](https://github.com/SchmidtDSE/api_dock/wiki/Shared-Database-Config) |
153
+ | unions across schemas (`[[*.table]]`, schema groups) | [Cross-Schema Queries](https://github.com/SchmidtDSE/api_dock/wiki/Cross-Schema-Queries) |
154
+ | PostgreSQL connections, engines, safety | [PostgreSQL](https://github.com/SchmidtDSE/api_dock/wiki/PostgreSQL) |
155
+ | databases, versions and values generated from a query, refreshed on a schedule | [Lookups](https://github.com/SchmidtDSE/api_dock/wiki/Lookups) |
156
+ | `/databases`, `/remotes`, `/sources` listings | [Catalog Endpoints](https://github.com/SchmidtDSE/api_dock/wiki/Catalog-Endpoints) |
157
+ | `RouteMapper` in your own app, production deployment | [Python API and Deployment](https://github.com/SchmidtDSE/api_dock/wiki/Python-API-and-Deployment) |
158
+
159
+ ---
160
+
161
+ ## Example: versions from a catalog (lookups)
162
+
163
+ When the list of databases/versions lives somewhere else (a table of model runs, a
164
+ deployments API), a lookup turns its rows into config. api_dock runs it at startup, every
165
+ `refresh` and on demand:
166
+
167
+ ```yaml
168
+ # api_dock_config/databases/config.yaml
169
+ database:
170
+ connections:
171
+ core: {host: db.example.com, dbname: catalog, user: readonly, password: env:DB_PASSWORD}
172
+ runs_catalog: {connection: core, table: public.model_runs} # or {uri: s3://.../runs.parquet}
173
+
174
+ lookups:
175
+ model_runs:
176
+ sql: |
177
+ SELECT name, version, detections_uri,
178
+ replace(name, '-', '_') || '_' || replace(version, '.', 'p') AS schema
179
+ FROM [[runs_catalog]] WHERE published
180
+ refresh: 7d
181
+ allow: ["s3://my-bucket/runs/"] # URIs from rows must start with this
182
+
183
+ slugs:
184
+ - from: model_runs # one database/version per row
185
+ name: "{{row.name}}"
186
+ version: "{{row.version}}"
187
+ schema:
188
+ name: "{{row.schema}}"
189
+ tables:
190
+ detections: {uri: "{{row.detections_uri}}"}
191
+ ```
192
+
193
+ ```yaml
194
+ # api_dock_config/config.yaml
195
+ databases:
196
+ - from: model_runs # serve every database the lookup generates
197
+ settings:
198
+ lookups: # optional: GET status / POST refresh
199
+ refresh_route: /admin/lookups
200
+ token: env:API_DOCK_ADMIN_TOKEN
201
+ ```
202
+
203
+ Lookups can read PostgreSQL tables, Parquet/CSV files or an HTTP API, and can also
204
+ generate remote versions or fill single values. See [Lookups](https://github.com/SchmidtDSE/api_dock/wiki/Lookups).
205
+
206
+ ---
207
+
208
+ ## CLI
209
+
210
+ ```bash
211
+ api-dock # list configs and commands
212
+ api-dock init [--force] # create api_dock_config/
213
+ api-dock start [config_name] # serve api_dock_config/<config_name>.yaml (default: config)
214
+ api-dock start --backbone flask --host 127.0.0.1 --port 9000 --log-level debug
215
+ api-dock describe [config_name] # print the config
216
+ api-dock generate-key # local encryption key
217
+ api-dock encrypt "secret" # also --method env_key|aws_kms
218
+ api-dock decrypt "gAAAAA..."
219
+ api-dock lookups # run the config's lookups and print their rows
220
+ ```
221
+
222
+ Flask responses are buffered and Flask refuses configs with PostgreSQL connections; use the default
223
+ FastAPI backbone for those. Full reference: [Getting Started](https://github.com/SchmidtDSE/api_dock/wiki/Getting-Started#cli-reference).
224
+
225
+ ---
226
+
227
+ ## How it works (in brief)
228
+
229
+ - **Configs.** The main `config.yaml` lists remotes and databases and is read at startup. Remote
230
+ files, database files and the shared `remotes/config.yaml` and `databases/config.yaml` are read
231
+ again on each request, so
232
+ route edits don't need a restart.
233
+ - **Remotes.** A request is checked against the remote's allow/block lists, then forwarded with
234
+ httpx. The FastAPI app streams the upstream response back.
235
+ - **Databases.** The version is resolved (`latest`, version files, `slugs`), the route matched and
236
+ its SQL built: `[[table]]` references expanded, query-param fragments appended, values bound.
237
+ - **Engines.** A route whose tables are all on one PostgreSQL connection runs natively through
238
+ that connection's pool. Anything else runs on an in-memory DuckDB in a worker thread, with
239
+ PostgreSQL attached read-only when needed.
240
+ - **Lookups.** Named queries (SQL over the configured tables, or an HTTP API) run at startup
241
+ and on a schedule; `from:` entries turn their rows into database versions or remote versions.
242
+ - **Startup checks.** Every database and version is checked as requests will see it (table
243
+ references, quoted variables, unions, connections, engines); a bad config stops startup with a
244
+ message naming the database, version and route.
245
+
246
+ ---
247
+
248
+ ## Repo layout
249
+
250
+ ```
251
+ api_dock/ the package
252
+ cli.py api-dock commands
253
+ config.py, config_discovery.py main/remote config loading, settings, route rules
254
+ route_mapper.py RouteMapper: request handling, startup checks, PostgreSQL lifecycle
255
+ fast_api.py, flask_api.py the two app backbones
256
+ database_config.py database configs, shared config, versions and slugs
257
+ lookups.py lookups: templates, SQL/HTTP runners, refresh
258
+ sql_builder.py SQL building, table references, unions, engine choice
259
+ database_backends.py DuckDB backend postgres_backend.py, postgres_config.py PostgreSQL
260
+ storage_auth.py, auth.py, encryption.py, listings.py, sql_template_check.py, types.py
261
+ example_api_dock_config/ copied by `api-dock init`
262
+ tests/ pixi run -e dev pytest -q
263
+ ```
264
+
265
+ More in the [Developer Guide](https://github.com/SchmidtDSE/api_dock/wiki/Developer-Guide).
266
+
267
+ ---
268
+
269
+ # Development
270
+
271
+ ```bash
272
+ pixi install -e dev # includes psycopg and a local PostgreSQL for the tests
273
+ pixi run -e dev pytest -q # PostgreSQL tests skip if PostgreSQL/psycopg are missing
274
+ ```
275
+
276
+ ## Publishing a Release
277
+
278
+ 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.
279
+
280
+ ```bash
281
+ # 0. Start from a clean, up-to-date main
282
+ export VERSION=0.9.1 # the NEW version, no leading "v"
283
+ git checkout main
284
+ git pull origin main
285
+ git status
286
+
287
+ # 1. Set `version` in pyproject.toml to $VERSION
288
+
289
+ # 2. Run the tests
290
+ pixi run -e dev pytest -q
291
+
292
+ # 3. Commit, tag, push (the commit command adds the "v$VERSION: " prefix)
293
+ export COMMIT_MESSAGE='PostgreSQL, lookups, remotes/config.yaml, numeric latest'
294
+ git add -A
295
+ git commit -m "v$VERSION: $COMMIT_MESSAGE"
296
+ git tag "v$VERSION"
297
+ git push origin main "v$VERSION"
298
+
299
+ # 4. Publish the GitHub Release; this triggers the PyPI upload.
300
+ # Don't use --draft (the workflow only runs on a published release); no wheel needs attaching.
301
+ gh release create "v$VERSION" \
302
+ --title "v$VERSION" \
303
+ --notes "$(cat <<'EOF'
304
+ * new features
305
+ - PostgreSQL tables (`pip install 'api_dock[postgres]'`): named `connections` in `databases/config.yaml` and `{connection, table}` tables. A route runs natively on PostgreSQL when all its tables are on one connection (one pool per connection, read-only transactions, statement timeout), otherwise on DuckDB with PostgreSQL attached read-only, so unions and joins can mix PostgreSQL and Parquet. Route `engine: postgres|duckdb` checks or forces the choice; 503 when a database is unavailable
306
+ - Lookups: named queries (SQL over any configured table, or an HTTP API) run at startup, every `refresh` and on demand. `from:` entries turn rows into database/versions (with inline schemas) or remote versions, `{{lookup.<name>.<column>}}` fills single values, and `databases: [- from: <lookup>]` serves what they generate. New rows are checked before use; values used as URIs must match `allow:` prefixes. Optional token-protected refresh endpoint (`settings.lookups`) and `api-dock lookups` CLI
307
+ - `remotes/config.yaml`: define remotes inline (`version`, `versions` or unversioned); mixes with remote files, files win
308
+ - Slugs can define their schema inline: `schema: {name, tables}`
309
+ - Database values convert to JSON recursively, including UUIDs, network addresses, intervals and arrays
310
+ * bug fixes
311
+ - `latest` and version lists compare versions numerically (`0.10` > `0.9`, `0.10.0` > `0.9.0`) for remotes and databases; they used to compare as floats or text
312
+ * cleanup / other improvements
313
+ - Database queries go through a `DatabaseBackend` interface (`DuckDBBackend`, `PostgresBackend`); the SQL builder takes the backend's bound-value marker
314
+ - Startup checks also cover PostgreSQL connections, every route's engine, `remotes/config.yaml` and lookup-generated config
315
+ - Slim README; detailed docs moved to the wiki (https://github.com/SchmidtDSE/api_dock/wiki), including new Concepts, PostgreSQL, Lookups and Developer Guide pages
316
+ - Test suite grew from 373 to 626 tests (a throwaway PostgreSQL server runs the PostgreSQL tests when available)
317
+ EOF
318
+ )"
319
+
320
+ # 5. Watch the publish workflow, then confirm PyPI has the new version
321
+ gh run watch "$(gh run list --workflow=publish_to_pypi.yml -L1 --json databaseId -q '.[0].databaseId')" --repo SchmidtDSE/api_dock
322
+ curl -s https://pypi.org/pypi/api-dock/json | python3 -c "import sys,json; print('PyPI latest:', json.load(sys.stdin)['info']['version'])"
323
+
324
+ # 6. conda-forge: once the bot opens the v$VERSION PR (usually within hours), check that the recipe's
325
+ # run requirements match pyproject.toml dependencies (the bot only bumps version + sha256), then merge it
326
+ gh pr list --repo conda-forge/api_dock-feedstock --state open
327
+ ```
328
+
329
+ ---
330
+
331
+ # License
332
+
333
+ BSD 3-Clause
@@ -0,0 +1,291 @@
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.9.1 # 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='PostgreSQL, lookups, remotes/config.yaml, numeric latest'
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
+ * new features
263
+ - PostgreSQL tables (`pip install 'api_dock[postgres]'`): named `connections` in `databases/config.yaml` and `{connection, table}` tables. A route runs natively on PostgreSQL when all its tables are on one connection (one pool per connection, read-only transactions, statement timeout), otherwise on DuckDB with PostgreSQL attached read-only, so unions and joins can mix PostgreSQL and Parquet. Route `engine: postgres|duckdb` checks or forces the choice; 503 when a database is unavailable
264
+ - Lookups: named queries (SQL over any configured table, or an HTTP API) run at startup, every `refresh` and on demand. `from:` entries turn rows into database/versions (with inline schemas) or remote versions, `{{lookup.<name>.<column>}}` fills single values, and `databases: [- from: <lookup>]` serves what they generate. New rows are checked before use; values used as URIs must match `allow:` prefixes. Optional token-protected refresh endpoint (`settings.lookups`) and `api-dock lookups` CLI
265
+ - `remotes/config.yaml`: define remotes inline (`version`, `versions` or unversioned); mixes with remote files, files win
266
+ - Slugs can define their schema inline: `schema: {name, tables}`
267
+ - Database values convert to JSON recursively, including UUIDs, network addresses, intervals and arrays
268
+ * bug fixes
269
+ - `latest` and version lists compare versions numerically (`0.10` > `0.9`, `0.10.0` > `0.9.0`) for remotes and databases; they used to compare as floats or text
270
+ * cleanup / other improvements
271
+ - Database queries go through a `DatabaseBackend` interface (`DuckDBBackend`, `PostgresBackend`); the SQL builder takes the backend's bound-value marker
272
+ - Startup checks also cover PostgreSQL connections, every route's engine, `remotes/config.yaml` and lookup-generated config
273
+ - Slim README; detailed docs moved to the wiki (https://github.com/SchmidtDSE/api_dock/wiki), including new Concepts, PostgreSQL, Lookups and Developer Guide pages
274
+ - Test suite grew from 373 to 626 tests (a throwaway PostgreSQL server runs the PostgreSQL tests when available)
275
+ EOF
276
+ )"
277
+
278
+ # 5. Watch the publish workflow, then confirm PyPI has the new version
279
+ gh run watch "$(gh run list --workflow=publish_to_pypi.yml -L1 --json databaseId -q '.[0].databaseId')" --repo SchmidtDSE/api_dock
280
+ curl -s https://pypi.org/pypi/api-dock/json | python3 -c "import sys,json; print('PyPI latest:', json.load(sys.stdin)['info']['version'])"
281
+
282
+ # 6. conda-forge: once the bot opens the v$VERSION PR (usually within hours), check that the recipe's
283
+ # run requirements match pyproject.toml dependencies (the bot only bumps version + sha256), then merge it
284
+ gh pr list --repo conda-forge/api_dock-feedstock --state open
285
+ ```
286
+
287
+ ---
288
+
289
+ # License
290
+
291
+ BSD 3-Clause