api-dock 0.7.1__tar.gz → 0.8.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 (38) hide show
  1. {api_dock-0.7.1 → api_dock-0.8.1}/PKG-INFO +312 -31
  2. {api_dock-0.7.1 → api_dock-0.8.1}/README.md +311 -30
  3. api_dock-0.8.1/api_dock/database_config.py +1101 -0
  4. {api_dock-0.7.1 → api_dock-0.8.1}/api_dock/example_api_dock_config/config.yaml +8 -0
  5. api_dock-0.8.1/api_dock/example_api_dock_config/databases/config.yaml +94 -0
  6. {api_dock-0.7.1 → api_dock-0.8.1}/api_dock/fast_api.py +33 -0
  7. {api_dock-0.7.1 → api_dock-0.8.1}/api_dock/flask_api.py +31 -0
  8. api_dock-0.8.1/api_dock/listings.py +346 -0
  9. {api_dock-0.7.1 → api_dock-0.8.1}/api_dock/route_mapper.py +66 -15
  10. {api_dock-0.7.1 → api_dock-0.8.1}/api_dock/sql_builder.py +341 -17
  11. {api_dock-0.7.1 → api_dock-0.8.1}/api_dock/storage_auth.py +133 -36
  12. {api_dock-0.7.1 → api_dock-0.8.1}/api_dock/types.py +82 -2
  13. {api_dock-0.7.1 → api_dock-0.8.1}/api_dock.egg-info/PKG-INFO +312 -31
  14. {api_dock-0.7.1 → api_dock-0.8.1}/api_dock.egg-info/SOURCES.txt +5 -0
  15. {api_dock-0.7.1 → api_dock-0.8.1}/pyproject.toml +1 -1
  16. api_dock-0.8.1/tests/test_listings.py +221 -0
  17. api_dock-0.8.1/tests/test_schema_unions.py +308 -0
  18. api_dock-0.8.1/tests/test_shared_database_config.py +835 -0
  19. api_dock-0.7.1/api_dock/database_config.py +0 -441
  20. {api_dock-0.7.1 → api_dock-0.8.1}/LICENSE.md +0 -0
  21. {api_dock-0.7.1 → api_dock-0.8.1}/api_dock/__init__.py +0 -0
  22. {api_dock-0.7.1 → api_dock-0.8.1}/api_dock/auth.py +0 -0
  23. {api_dock-0.7.1 → api_dock-0.8.1}/api_dock/cli.py +0 -0
  24. {api_dock-0.7.1 → api_dock-0.8.1}/api_dock/config.py +0 -0
  25. {api_dock-0.7.1 → api_dock-0.8.1}/api_dock/config_discovery.py +0 -0
  26. {api_dock-0.7.1 → api_dock-0.8.1}/api_dock/encryption.py +0 -0
  27. {api_dock-0.7.1 → api_dock-0.8.1}/api_dock/example_api_dock_config/databases/example_db.yaml +0 -0
  28. {api_dock-0.7.1 → api_dock-0.8.1}/api_dock/example_api_dock_config/remotes/example_remote.yaml +0 -0
  29. {api_dock-0.7.1 → api_dock-0.8.1}/api_dock.egg-info/dependency_links.txt +0 -0
  30. {api_dock-0.7.1 → api_dock-0.8.1}/api_dock.egg-info/entry_points.txt +0 -0
  31. {api_dock-0.7.1 → api_dock-0.8.1}/api_dock.egg-info/requires.txt +0 -0
  32. {api_dock-0.7.1 → api_dock-0.8.1}/api_dock.egg-info/top_level.txt +0 -0
  33. {api_dock-0.7.1 → api_dock-0.8.1}/setup.cfg +0 -0
  34. {api_dock-0.7.1 → api_dock-0.8.1}/tests/test_inject_cookies.py +0 -0
  35. {api_dock-0.7.1 → api_dock-0.8.1}/tests/test_proxy_pipeline.py +0 -0
  36. {api_dock-0.7.1 → api_dock-0.8.1}/tests/test_sql_builder.py +0 -0
  37. {api_dock-0.7.1 → api_dock-0.8.1}/tests/test_sql_selector.py +0 -0
  38. {api_dock-0.7.1 → api_dock-0.8.1}/tests/test_types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: api_dock
3
- Version: 0.7.1
3
+ Version: 0.8.1
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
6
  License-Expression: BSD-3-Clause
@@ -95,6 +95,7 @@ Here is an example:
95
95
  api_dock_config
96
96
  ├── config.yaml # The default main-config file
97
97
  ├── databases
98
+ │ ├── config.yaml # (optional) shared tables/schemas for all databases
98
99
  │ ├── unversioned_db.yaml # Database config without versioning
99
100
  │ └── versioned_db # Folder containing database configs for different versions
100
101
  │ ├── 0.1.yaml
@@ -231,6 +232,58 @@ The optional `settings` section controls HTTP behavior:
231
232
 
232
233
  - **`timeout`** (default: `10`): Upstream request timeout in seconds, applied to both the streaming and buffered proxy paths. Raise it for slow upstreams (e.g. large aggregation queries) that would otherwise return a 502 on timeout. Set to `null` or `false` to disable the timeout entirely (not recommended — a stalled upstream can hold the connection open indefinitely).
233
234
 
235
+ ### Catalog Endpoints (`expose`)
236
+
237
+ The optional `expose` section adds read-only endpoints that list the models and versions of your configured databases, remotes, or both ("sources"). Listings are **opt-in** — with no `expose` key nothing is added.
238
+
239
+ ```yaml
240
+ # Enable all three defaults: /databases, /remotes, /sources
241
+ expose: true
242
+ ```
243
+
244
+ ```yaml
245
+ GET /databases
246
+ # [{"model": "birdnet", "version": "2.4"},
247
+ # {"model": "birdnet", "version": "3.0"},
248
+ # {"model": "owl", "version": "0.5"}]
249
+
250
+ GET /sources # databases + remotes, combined
251
+ # [{"model": "birdnet", "version": "2.4"}, ..., {"model": "core", "version": "0.5.0"}]
252
+ ```
253
+
254
+ Versions are the config filename stems, so semver like `0.5.0` is preserved; unversioned sources report `version: null`. Enable only what you want, and control the output shape with `dict`:
255
+
256
+ ```yaml
257
+ expose:
258
+ dict: false # return "model/version" strings instead of {model, version} dicts
259
+ databases: true # add /databases
260
+ remotes: true # add /remotes
261
+ # sources omitted → not added
262
+
263
+ # GET /databases -> ["birdnet/2.4", "birdnet/3.0", "owl/0.5"]
264
+ ```
265
+
266
+ Each of `databases` / `remotes` / `sources` accepts several forms:
267
+
268
+ ```yaml
269
+ expose:
270
+ databases: false # do not add the endpoint
271
+
272
+ remotes: "list/remotes" # custom route path (same as true, but at /list/remotes)
273
+
274
+ databases: # explicit list of models (optionally version-filtered)
275
+ - birdnet
276
+ - owl:
277
+ versions: [4.0, 5.0] # ints/floats/strings all match the "4.0"/"5.0" stems
278
+
279
+ sources: # most explicit form
280
+ route: "list/sources"
281
+ include: [birdnet, core] # true (default) | false | list of models
282
+ dict: true # per-endpoint override of the top-level `dict`
283
+ ```
284
+
285
+ Each listing route is served both with and without a trailing slash (e.g. `/sources` and `/sources/` both work), so it doesn't matter which convention your clients use. Custom multi-segment routes (e.g. `list/databases`) take precedence over the `/{remote}/{path}` proxy. If a listing route would shadow a configured remote/database, or an `include` names something that doesn't exist, API Dock emits a startup warning. The exposed routes are also reflected in the root (`/`) metadata's `endpoints`.
286
+
234
287
  ---
235
288
 
236
289
  ## Remote Configurations
@@ -356,6 +409,7 @@ Database configurations are stored in `api_dock_config/databases/` directory. Ea
356
409
  - **tables**: Mapping of table names to file paths (supports S3, GCS, HTTPS, local paths)
357
410
  - **queries**: Named SQL queries for reuse
358
411
  - **routes**: REST endpoints mapped to SQL queries
412
+ - **schema** (optional): the shared schema (from `databases/config.yaml`) this config's `[[table]]` references fall back to. See [Shared Tables and Schemas](#shared-tables-and-schemas-databasesconfigyaml)
359
413
 
360
414
  ### Syntax
361
415
 
@@ -433,6 +487,225 @@ routes:
433
487
  sql: "[[get_permissions]]"
434
488
  ```
435
489
 
490
+ ### Shared Tables and Schemas (`databases/config.yaml`)
491
+
492
+ When several databases or versions read the same tables, define them once in the optional `api_dock_config/databases/config.yaml`. Everything lives under a `database` key: `meta` holds default table metadata, `schema` holds named groups of tables, and every other key is a global table.
493
+
494
+ ```yaml
495
+ # api_dock_config/databases/config.yaml
496
+ database:
497
+ # global tables, available as [[table1]] in any database config
498
+ table1:
499
+ uri: s3://your-bucket/table1.parquet
500
+ table3:
501
+ uri: s3://your-other-bucket/table3.parquet
502
+ region: us-west-1 # a table's own keys override `meta`
503
+ public: false
504
+
505
+ # defaults applied to every table (shared tables and the tables in each version config)
506
+ meta:
507
+ region: us-west-2
508
+ public: true
509
+
510
+ # schemas, available as [[birdnet_2p4.detections]] in any route
511
+ schema:
512
+ birdnet_2p4:
513
+ detections:
514
+ uri: s3://your-bucket/birdnet/2.4/detections.parquet
515
+ birdnet_3p0:
516
+ detections:
517
+ uri: s3://your-bucket/birdnet/3.0/detections.parquet
518
+ public: false
519
+ ```
520
+
521
+ A version config can name the schema it uses with `schema:`. An unqualified `[[name]]` is then looked up in order, first match wins:
522
+
523
+ 1. the version config's own `tables`
524
+ 2. its `schema:` in the shared config
525
+ 3. the shared config's global tables
526
+
527
+ ```yaml
528
+ # api_dock_config/databases/birdnet/2.4.yaml
529
+ name: birdnet
530
+ schema: birdnet_2p4
531
+ tables:
532
+ revisions: s3://your-bucket/birdnet/2.4/revisions.parquet # local to this version
533
+
534
+ routes:
535
+ # [[detections]] isn't in `tables`, so it comes from the birdnet_2p4 schema
536
+ - route: recordings/{{recording_id}}/detections
537
+ sql: SELECT [[detections]].* FROM [[detections]] WHERE [[detections]].recording_id = {{recording_id}}
538
+ ```
539
+
540
+ Any route can reference any schema directly as `[[schema.table]]`, so a different database (e.g. `owl/5.0`) can query `[[birdnet_2p4.detections]]`. To keep a table private to one database/version, define it in that version's `tables` instead. Qualified references are exposed to DuckDB as real views, so you can also use the full name or your own alias in plain SQL:
541
+
542
+ ```yaml
543
+ - route: detections/
544
+ sql: >
545
+ SELECT detections.common_name, COUNT(revisions.id) AS revcount
546
+ FROM [[birdnet_2p4.detections]]
547
+ LEFT JOIN [[revisions]] ON revisions.observation_id = birdnet_2p4.detections.id
548
+ GROUP BY birdnet_2p4.detections.common_name
549
+ ```
550
+
551
+ expands to
552
+
553
+ ```sql
554
+ SELECT detections.common_name, COUNT(revisions.id) AS revcount
555
+ FROM birdnet_2p4.detections
556
+ LEFT JOIN 's3://your-bucket/birdnet/2.4/revisions.parquet' AS revisions ON revisions.observation_id = birdnet_2p4.detections.id
557
+ GROUP BY birdnet_2p4.detections.common_name
558
+ ```
559
+
560
+ Notes:
561
+ - After `FROM`/`JOIN`, `[[schema.table]]` becomes `schema.table` with no alias, so `FROM [[birdnet_2p4.detections]] o` works. Elsewhere it becomes the bare table name (`detections`), because DuckDB doesn't accept `schema.table.*`.
562
+ - Schema and table names used as `[[schema.table]]` must be plain identifiers (letters, digits, underscores).
563
+ - Storage credentials are set per table. Tables whose `region`/`public` differ from the rest get their own S3 secret scoped to their path, so one query can mix regions and public/private buckets.
564
+ - Views are created only for the `[[schema.table]]` tables a query actually references.
565
+
566
+ #### Querying across schemas (`[[*.table]]`, schema groups)
567
+
568
+ Union references read the same table from several schemas at once:
569
+
570
+ | Reference | Reads |
571
+ |---|---|
572
+ | `[[*.detections]]` | every shared schema that has a `detections` table, including the current one |
573
+ | `[[*!.detections]]` | the same, minus the current database/version's `schema:` |
574
+ | `[[group1.detections]]` | the schemas listed in `schema_groups.group1` |
575
+ | `[[group1!.detections]]` | that group, minus the current schema |
576
+
577
+ ```yaml
578
+ # api_dock_config/databases/config.yaml
579
+ schema_groups: # named lists of shared schemas
580
+ birdnet_models:
581
+ - birdnet_2p4
582
+ - birdnet_bullfrog_2p4v0p5
583
+ ```
584
+
585
+ - A union expands, after `FROM`/`JOIN` only, to a parenthesized `UNION ALL BY NAME` over the member schemas, so give it an alias: `FROM [[*.detections]] detections`. Columns missing from some members come back as `NULL`.
586
+ - `*` skips schemas without the table. A group whose member lacks the table, a group naming an unknown schema, and a group sharing a name with a schema are all errors. `!` applies only to `*` and groups; with no `schema:`, it removes nothing.
587
+ - Every member gets its own S3 credentials (see above), so a union can mix regions and public/private buckets.
588
+
589
+ **Source columns.** Union rows carry only the tables' real columns unless the route asks for more with `source_columns`. The available facts are `schema` (the member schema), and `name` and `version` (the database/version whose `schema:` is that schema, or `NULL` if none or several use it):
590
+
591
+ ```yaml
592
+ source_columns: [schema, name, version] # adds schema_name, name, version
593
+ source_columns: {schema: _schema, name: model} # pick a subset and rename
594
+ ```
595
+
596
+ A source column that clashes with a real column raises an error. To use one for filtering without returning it, use DuckDB's `EXCLUDE`: `SELECT detections.* EXCLUDE (schema_name) ...`.
597
+
598
+ **`{{self.*}}` placeholders.** `{{self.schema}}`, `{{self.name}}` and `{{self.version}}` are the current database/version's schema, name and version, as SQL literals (`NULL` when unknown).
599
+
600
+ Together they make an "overlaps" route that every database/version can share. It returns every detection overlapping the given one, across all schemas, except that detection itself; other overlapping rows in the same schema are kept:
601
+
602
+ ```yaml
603
+ routes:
604
+ - route: detections/{{id}}/overlaps
605
+ source_columns: [schema, name, version]
606
+ sql: |
607
+ WITH src AS (
608
+ SELECT recording_id, start_time, end_time FROM [[detections]] WHERE id = {{id}}
609
+ )
610
+ SELECT detections.*
611
+ FROM [[*.detections]] detections
612
+ JOIN src ON detections.recording_id = src.recording_id
613
+ AND detections.start_time < src.end_time
614
+ AND detections.end_time > src.start_time
615
+ WHERE NOT (detections.schema_name = {{self.schema}} AND detections.id = {{id}})
616
+ ```
617
+
618
+ Aliasing the union as `detections` also lets shared filters such as `[[detections]].confidence >= {{confidence}}` apply to the overlapping rows.
619
+
620
+ #### Inline database configs (`slugs`)
621
+
622
+ Simple database/version configs (often just a description and a `schema`) can live in the shared file instead of in their own files. Config files keep working, and the two can be mixed, even for the same database:
623
+
624
+ ```yaml
625
+ # api_dock_config/databases/config.yaml
626
+ slugs:
627
+ - name: birdnet-bullfrog # the database slug in the URL
628
+ version: "2.5" # one version...
629
+ description: American Bullfrog Classifier from Birdnet 2.4
630
+ schema: birdnet_bullfrog_2p5v0p5
631
+ - name: birdnet-apple
632
+ authors: [API Team] # ...or several; keys here are defaults for each version
633
+ versions:
634
+ - version: "1.0"
635
+ description: Apple Classifier 1.0
636
+ schema: birdnet_apple_1p0
637
+ - version: "12.0"
638
+ description: Apple Classifier 12.0
639
+ schema: birdnet_apple_12p0
640
+ - name: notes # no version/versions = an unversioned database
641
+ tables:
642
+ notes: s3://your-bucket/notes.parquet
643
+ ```
644
+
645
+ - Each entry (or each `versions` item) takes the same keys as a database config file: `description`, `authors`, `schema`, `tables`, `routes`, `query_params`, and so on. Shared `routes`/`query_params` (below), including `include`/`exclude`, apply to them like any other database/version.
646
+ - Like file-based databases, a slug is only served if it's listed under `databases:` in the main `config.yaml`.
647
+ - A database's versions are the union of its version files and its `slugs` versions, so `latest`, the `/{database}` versions listing, and the `expose` catalog endpoints all see both. If a file and a slug define the same database/version, the file wins.
648
+ - Quote versions (`version: "2.10"`). Unquoted YAML numbers are floats, so `2.10` would become `"2.1"`.
649
+ - A malformed `slugs` section (missing `name`, both `version` and `versions`, a duplicate version, or a mix of versioned and unversioned entries for one name) returns a 500 "Shared database configuration error".
650
+
651
+ #### Shared routes and query params
652
+
653
+ The shared file can also define top-level `routes` and `query_params`. These are added to **every** database/version, which is handy when each model/version serves the same endpoints over its own `schema`:
654
+
655
+ ```yaml
656
+ # api_dock_config/databases/config.yaml
657
+ database:
658
+ ...
659
+
660
+ routes:
661
+ - route: recordings/{{recording_id}}/detections/
662
+ sql: SELECT [[detections]].* FROM [[detections]] WHERE [[detections]].recording_id = {{recording_id}}
663
+ - route: detections/{{id}}
664
+ sql: SELECT [[detections]].* FROM [[detections]] WHERE [[detections]].id = {{id}}
665
+ - route: not_for_everyone/{{id}}
666
+ sql: SELECT [[other]].* FROM [[other]] WHERE [[other]].id = {{id}}
667
+ exclude: # don't add this route to these slug/versions
668
+ - 'slug1/3.0'
669
+ - slug: slug2
670
+ version: 2.3
671
+ - slug: slug3
672
+ version: '*' # '*' = every version
673
+
674
+ # the same route defined twice: one for everything except birdnet/2.4, one only for it
675
+ - route: detections/
676
+ exclude: ['birdnet/2.4']
677
+ sql: SELECT [[detections]].* FROM [[detections]]
678
+ - route: detections/
679
+ include: ['birdnet/2.4'] # ONLY add this route to these slug/versions
680
+ sql: SELECT [[detections]].*, [[revisions]].id AS revision_id FROM [[detections]] LEFT JOIN [[revisions]] ON [[revisions]].observation_id = [[detections]].id
681
+
682
+ query_params:
683
+ - confidence:
684
+ sql: "[[detections]].confidence >= {{confidence}}"
685
+ - start_time:
686
+ sql: "[[detections]].start_time >= {{start_time}}"
687
+ exclude: ['slug1/3.9']
688
+ - limit:
689
+ sql_append: LIMIT {{limit}}
690
+
691
+ # limit ALL shared routes / query params to these slug/versions
692
+ route_inclusions: ['birdnet', 'owl/5.0']
693
+ query_inclusions: [] # empty or missing = no restriction
694
+
695
+ # opt slug/versions out of ALL shared routes / query params
696
+ route_exclusions: ['legacy_db']
697
+ query_exclusions:
698
+ - slug: slug4
699
+ version: 1.0
700
+ ```
701
+
702
+ Rules:
703
+ - **The version config wins.** Its own routes come first and replace any shared route with the same shape. Shape means the same path segments; `{{param}}` names and leading/trailing slashes are ignored, so `detections/{{id}}` and `/detections/{{detection_id}}/` are the same route. Routes the version config adds on top are kept.
704
+ - Shared `query_params` behave like a version config's top-level `query_params`. They apply to every route, and a param with the same name in the version config (or on a route) overrides the shared one.
705
+ - `include` and `route_inclusions`/`query_inclusions` are the opposite of `exclude` and `route_exclusions`/`query_exclusions`. When given (non-empty), the route or query param is added **only** to the listed slug/versions. A shared item is added only if it passes both the top-level lists and its own `include`/`exclude`.
706
+ - The same route (by shape) or query param (by name) can appear more than once in the shared file. Each database/version gets the first one whose `include`/`exclude` select it, so complementary `include`/`exclude` lists give different databases different versions of an endpoint.
707
+ - `include`, `exclude` and the four top-level lists take a list of `'<slug>/<version>'` strings or `{slug: <slug>, version: <version>}` mappings. `'<slug>'`, `'<slug>/*'` or a missing/`'*'` version match every version, including unversioned databases. Versions compare numerically when possible (`2.3`, `"2.3"`), and `latest` is resolved before matching.
708
+
436
709
  **For more details**, see the [SQL Database Support Wiki](https://github.com/SchmidtDSE/api_dock/wiki/SQL-Database-Support).
437
710
 
438
711
  ---
@@ -1324,47 +1597,55 @@ pixi run python scripts/hello_world.py
1324
1597
 
1325
1598
  ## Publishing a Release
1326
1599
 
1600
+ 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.
1601
+
1327
1602
  ```bash
1328
- # 0. Make sure you are on `main` and merged with any changes
1603
+ # 0. Start from a clean, up-to-date main
1604
+ export VERSION=0.8.1 # the NEW version, no leading "v"
1605
+ git checkout main
1606
+ git pull origin main
1607
+ git status
1608
+
1609
+ # 1. Set `version` in pyproject.toml to $VERSION
1329
1610
 
1330
- # 1. Bump version in pyproject.toml
1611
+ # 2. Run the tests
1612
+ pixi run -e dev pytest -q
1331
1613
 
1332
- # 2. Commit everything
1614
+ # 3. Commit, tag, push (the commit command adds the "v$VERSION: " prefix)
1615
+ export COMMIT_MESSAGE='cross-schema unions, schema groups, source columns'
1333
1616
  git add -A
1334
- git commit -m "v0.6.1: stream proxy responses (fix large-response 502 + content-encoding)"
1335
-
1336
- # 3. Tag and push
1337
- git tag v0.6.1
1338
- git push origin main v0.6.1
1339
-
1340
- # 4. Build the wheel (requires the `dev` pixi environment)
1341
- rm -rf dist/
1342
- find . -name "__pycache__" -type d -exec rm -rf {} +
1343
- find . -name "*.pyc" -delete
1344
- pixi run -e dev python -m build --wheel
1345
- ls dist/*.whl
1346
-
1347
- # 5. Create GitHub release with the wheel attached
1348
- gh release create v0.6.1 dist/api_dock-0.6.1-py3-none-any.whl \
1349
- --title "v0.6.1" --notes "$(cat <<'EOF'
1617
+ git commit -m "v$VERSION: $COMMIT_MESSAGE"
1618
+ git tag "v$VERSION"
1619
+ git push origin main "v$VERSION"
1620
+
1621
+ # 4. Publish the GitHub Release; this triggers the PyPI upload.
1622
+ # Don't use --draft (the workflow only runs on a published release); no wheel needs attaching.
1623
+ gh release create "v$VERSION" \
1624
+ --title "v$VERSION" \
1625
+ --notes "$(cat <<'EOF'
1350
1626
  * new features
1351
- - Remote proxy responses are now streamed (FastAPI) — upstream bytes are piped to the client as they arrive instead of being buffered fully in memory
1352
- - New `timeout` setting (default 10s) for the upstream request; set to `null`/`false` to disable
1627
+ - Cross-schema unions: `[[*.table]]` reads a table from every shared schema that has it, and `[[*!.table]]` does the same minus the current database/version's schema
1628
+ - Named `schema_groups` in `databases/config.yaml`, used as `[[group.table]]` / `[[group!.table]]` (validated: known schemas only, no group/schema name clashes)
1629
+ - Route `source_columns` adds where each union row came from (`schema`, `name`, `version`), with default names `schema_name`/`name`/`version` or your own; nothing is added by default
1630
+ - `{{self.schema}}`, `{{self.name}}`, and `{{self.version}}` placeholders for the database/version being queried
1631
+ - Together these support an "overlaps" route shared by every database/version (every detection overlapping a given one across all schemas, except that detection itself); shared query params such as `confidence`, `sort`, and `limit` apply to its rows
1353
1632
  * bug fixes
1354
- - Large upstream responses no longer return 502 — streamed via `StreamingResponse` instead of reading the whole body into memory
1355
- - `Content-Encoding` (gzip/br/deflate) is now preserved on compressed responses — raw bytes are streamed via `aiter_raw()` so the header stays valid and the client can decompress
1356
- - Slow upstreams (e.g. large aggregation queries) no longer 502 at httpx's hardcoded 5s default — the timeout is now configurable via the `timeout` setting
1633
+ - A `!` union that removes every member returns no rows (with the right columns) instead of failing, and only the schemas a union actually reads get views and storage credentials
1357
1634
  * cleanup / other improvements
1358
- - Added `PreparedRequest` dataclass and split route validation/resolution into `RouteMapper.prepare_remote_request()`; the FastAPI adapter issues the streaming HTTP call
1359
- - `map_route()` (buffered) retained for the Flask/sync path
1360
- - Added streaming test coverage (`TestStreamUpstream`, plus `prepare_remote_request` and streaming-header tests) — 53 tests total
1635
+ - Added `SqlContext`; `build_sql_query()` / `build_sql_query_with_tables()` accept an optional `context`
1636
+ - README: new "Querying across schemas" section with an overlaps example; example `databases/config.yaml` shows `schema_groups` and a union route
1637
+ - Test suite grew from 198 to 227 tests (`test_schema_unions.py`, including a real DuckDB end-to-end overlaps test)
1361
1638
  EOF
1362
1639
  )"
1363
1640
 
1364
- # 6. Publish to PyPI
1365
- pixi run -e dev python -m twine upload dist/*.whl
1366
- ```
1641
+ # 5. Watch the publish workflow, then confirm PyPI has the new version
1642
+ gh run watch "$(gh run list --workflow=publish_to_pypi.yml -L1 --json databaseId -q '.[0].databaseId')" --repo SchmidtDSE/api_dock
1643
+ curl -s https://pypi.org/pypi/api-dock/json | python3 -c "import sys,json; print('PyPI latest:', json.load(sys.stdin)['info']['version'])"
1367
1644
 
1645
+ # 6. conda-forge: once the bot opens the v$VERSION PR (usually within hours), check that the recipe's
1646
+ # run requirements match pyproject.toml dependencies (the bot only bumps version + sha256), then merge it
1647
+ gh pr list --repo conda-forge/api_dock-feedstock --state open
1648
+ ```
1368
1649
 
1369
1650
  ---
1370
1651