umbra-py 0.1.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 (209) hide show
  1. umbra_py-0.1.0/.claude/hooks/session-start.sh +33 -0
  2. umbra_py-0.1.0/.claude/settings.json +36 -0
  3. umbra_py-0.1.0/.dockerignore +32 -0
  4. umbra_py-0.1.0/.github/ISSUE_TEMPLATE/bug_report.md +21 -0
  5. umbra_py-0.1.0/.github/ISSUE_TEMPLATE/feature_request.md +16 -0
  6. umbra_py-0.1.0/.github/PULL_REQUEST_TEMPLATE.md +14 -0
  7. umbra_py-0.1.0/.github/dependabot.yml +21 -0
  8. umbra_py-0.1.0/.github/workflows/ci.yml +154 -0
  9. umbra_py-0.1.0/.github/workflows/docker.yml +64 -0
  10. umbra_py-0.1.0/.github/workflows/docs.yml +150 -0
  11. umbra_py-0.1.0/.github/workflows/live-canary.yml +62 -0
  12. umbra_py-0.1.0/.github/workflows/publish-index.yml +213 -0
  13. umbra_py-0.1.0/.github/workflows/release.yml +128 -0
  14. umbra_py-0.1.0/.github/workflows/security-audit.yml +85 -0
  15. umbra_py-0.1.0/.gitignore +240 -0
  16. umbra_py-0.1.0/.pre-commit-config.yaml +16 -0
  17. umbra_py-0.1.0/AGENTS.md +403 -0
  18. umbra_py-0.1.0/CHANGELOG.md +6485 -0
  19. umbra_py-0.1.0/CITATION.cff +28 -0
  20. umbra_py-0.1.0/CLAUDE.md +43 -0
  21. umbra_py-0.1.0/CODE_OF_CONDUCT.md +135 -0
  22. umbra_py-0.1.0/CONTRIBUTING.md +84 -0
  23. umbra_py-0.1.0/Dockerfile +64 -0
  24. umbra_py-0.1.0/LICENSE +201 -0
  25. umbra_py-0.1.0/PKG-INFO +262 -0
  26. umbra_py-0.1.0/README.md +168 -0
  27. umbra_py-0.1.0/SECURITY.md +58 -0
  28. umbra_py-0.1.0/codecov.yml +14 -0
  29. umbra_py-0.1.0/docker-compose.yml +45 -0
  30. umbra_py-0.1.0/docker-entrypoint.sh +62 -0
  31. umbra_py-0.1.0/docs/STRATEGY.md +272 -0
  32. umbra_py-0.1.0/docs/TODO.md +987 -0
  33. umbra_py-0.1.0/docs/schemas/README.md +201 -0
  34. umbra_py-0.1.0/docs/schemas/chip-dataset.schema.json +407 -0
  35. umbra_py-0.1.0/docs/schemas/chip-record.schema.json +179 -0
  36. umbra_py-0.1.0/docs/schemas/chip-skipped.schema.json +49 -0
  37. umbra_py-0.1.0/docs/schemas/download.schema.json +43 -0
  38. umbra_py-0.1.0/docs/schemas/error.schema.json +32 -0
  39. umbra_py-0.1.0/docs/schemas/index-info.schema.json +65 -0
  40. umbra_py-0.1.0/docs/schemas/item-context.schema.json +122 -0
  41. umbra_py-0.1.0/docs/schemas/preflight.schema.json +190 -0
  42. umbra_py-0.1.0/docs/schemas/render-job.schema.json +99 -0
  43. umbra_py-0.1.0/docs/schemas/render-manifest.schema.json +54 -0
  44. umbra_py-0.1.0/docs/schemas/scene-description.schema.json +104 -0
  45. umbra_py-0.1.0/docs/schemas/scene-matches.schema.json +64 -0
  46. umbra_py-0.1.0/docs/schemas/search-plan.schema.json +136 -0
  47. umbra_py-0.1.0/docs/schemas/site-coverage.schema.json +152 -0
  48. umbra_py-0.1.0/docs/schemas/stack-provenance.schema.json +130 -0
  49. umbra_py-0.1.0/docs/schemas/stack-stats.schema.json +447 -0
  50. umbra_py-0.1.0/docs/schemas/task-matches.schema.json +53 -0
  51. umbra_py-0.1.0/docs/schemas/watch-delta.schema.json +66 -0
  52. umbra_py-0.1.0/docs_src/CNAME +1 -0
  53. umbra_py-0.1.0/docs_src/cli.md +15 -0
  54. umbra_py-0.1.0/docs_src/deploy.md +250 -0
  55. umbra_py-0.1.0/docs_src/guides/limitations.md +75 -0
  56. umbra_py-0.1.0/docs_src/guides/notebooks.md +24 -0
  57. umbra_py-0.1.0/docs_src/index.md +56 -0
  58. umbra_py-0.1.0/docs_src/install.md +47 -0
  59. umbra_py-0.1.0/docs_src/quickstart.md +82 -0
  60. umbra_py-0.1.0/docs_src/reference/ai.md +65 -0
  61. umbra_py-0.1.0/docs_src/reference/catalog.md +7 -0
  62. umbra_py-0.1.0/docs_src/reference/chips.md +88 -0
  63. umbra_py-0.1.0/docs_src/reference/convert.md +147 -0
  64. umbra_py-0.1.0/docs_src/reference/download.md +11 -0
  65. umbra_py-0.1.0/docs_src/reference/exceptions.md +32 -0
  66. umbra_py-0.1.0/docs_src/reference/index.md +19 -0
  67. umbra_py-0.1.0/docs_src/reference/load.md +72 -0
  68. umbra_py-0.1.0/docs_src/reference/models.md +25 -0
  69. umbra_py-0.1.0/docs_src/reference/terrain.md +25 -0
  70. umbra_py-0.1.0/docs_src/reference/viz.md +8 -0
  71. umbra_py-0.1.0/examples/01_hello_umbra.ipynb +212 -0
  72. umbra_py-0.1.0/examples/02_download_and_open_gec.ipynb +180 -0
  73. umbra_py-0.1.0/examples/03_change_detection.ipynb +142 -0
  74. umbra_py-0.1.0/examples/04_amplitude_time_series.ipynb +193 -0
  75. umbra_py-0.1.0/examples/05_detection_chips.ipynb +218 -0
  76. umbra_py-0.1.0/examples/06_site_monitoring.ipynb +214 -0
  77. umbra_py-0.1.0/examples/07_sicd_amplitude.ipynb +229 -0
  78. umbra_py-0.1.0/examples/08_time_series_datacube.ipynb +379 -0
  79. umbra_py-0.1.0/examples/README.md +82 -0
  80. umbra_py-0.1.0/examples/baseline/01_search_catalog_pystac.py +98 -0
  81. umbra_py-0.1.0/examples/baseline/02_search_catalog_handrolled.py +170 -0
  82. umbra_py-0.1.0/examples/baseline/03_find_the_geotiff.py +161 -0
  83. umbra_py-0.1.0/examples/baseline/04_download_assets.py +121 -0
  84. umbra_py-0.1.0/examples/baseline/05_open_sicd.py +100 -0
  85. umbra_py-0.1.0/examples/baseline/06_with_umbra_py.py +83 -0
  86. umbra_py-0.1.0/examples/change.md +263 -0
  87. umbra_py-0.1.0/examples/gallery.md +153 -0
  88. umbra_py-0.1.0/examples/load.md +134 -0
  89. umbra_py-0.1.0/examples/maps.md +560 -0
  90. umbra_py-0.1.0/examples/quicklook.md +195 -0
  91. umbra_py-0.1.0/examples/swipe.md +87 -0
  92. umbra_py-0.1.0/llms-full.txt +143 -0
  93. umbra_py-0.1.0/llms.txt +30 -0
  94. umbra_py-0.1.0/mkdocs.yml +99 -0
  95. umbra_py-0.1.0/pyproject.toml +228 -0
  96. umbra_py-0.1.0/server.json +69 -0
  97. umbra_py-0.1.0/src/umbra_py/__init__.py +364 -0
  98. umbra_py-0.1.0/src/umbra_py/_geometry.py +266 -0
  99. umbra_py-0.1.0/src/umbra_py/_html.py +402 -0
  100. umbra_py-0.1.0/src/umbra_py/_http.py +157 -0
  101. umbra_py-0.1.0/src/umbra_py/_lazy_imagery.py +677 -0
  102. umbra_py-0.1.0/src/umbra_py/_specfun.py +149 -0
  103. umbra_py-0.1.0/src/umbra_py/_spinner.py +116 -0
  104. umbra_py-0.1.0/src/umbra_py/catalog.py +697 -0
  105. umbra_py-0.1.0/src/umbra_py/chips.py +2052 -0
  106. umbra_py-0.1.0/src/umbra_py/cli/__init__.py +111 -0
  107. umbra_py-0.1.0/src/umbra_py/cli/__main__.py +10 -0
  108. umbra_py-0.1.0/src/umbra_py/cli/_root.py +73 -0
  109. umbra_py-0.1.0/src/umbra_py/cli/_shared.py +653 -0
  110. umbra_py-0.1.0/src/umbra_py/cli/atlas.py +461 -0
  111. umbra_py-0.1.0/src/umbra_py/cli/composites.py +745 -0
  112. umbra_py-0.1.0/src/umbra_py/cli/discover.py +1069 -0
  113. umbra_py-0.1.0/src/umbra_py/cli/explore.py +1120 -0
  114. umbra_py-0.1.0/src/umbra_py/cli/indexes.py +1172 -0
  115. umbra_py-0.1.0/src/umbra_py/cli/process.py +2130 -0
  116. umbra_py-0.1.0/src/umbra_py/cli/scenes.py +389 -0
  117. umbra_py-0.1.0/src/umbra_py/constants.py +193 -0
  118. umbra_py-0.1.0/src/umbra_py/context.py +148 -0
  119. umbra_py-0.1.0/src/umbra_py/convert.py +3854 -0
  120. umbra_py-0.1.0/src/umbra_py/coverage.py +873 -0
  121. umbra_py-0.1.0/src/umbra_py/dates.py +177 -0
  122. umbra_py-0.1.0/src/umbra_py/dem.py +218 -0
  123. umbra_py-0.1.0/src/umbra_py/demo.py +1465 -0
  124. umbra_py-0.1.0/src/umbra_py/describe.py +864 -0
  125. umbra_py-0.1.0/src/umbra_py/download.py +203 -0
  126. umbra_py-0.1.0/src/umbra_py/embed.py +643 -0
  127. umbra_py-0.1.0/src/umbra_py/exceptions.py +123 -0
  128. umbra_py-0.1.0/src/umbra_py/export.py +117 -0
  129. umbra_py-0.1.0/src/umbra_py/fuzzy.py +85 -0
  130. umbra_py-0.1.0/src/umbra_py/geocode.py +104 -0
  131. umbra_py-0.1.0/src/umbra_py/geoid.py +102 -0
  132. umbra_py-0.1.0/src/umbra_py/index.py +1738 -0
  133. umbra_py-0.1.0/src/umbra_py/langchain.py +238 -0
  134. umbra_py-0.1.0/src/umbra_py/llamaindex.py +264 -0
  135. umbra_py-0.1.0/src/umbra_py/llms_txt.py +377 -0
  136. umbra_py-0.1.0/src/umbra_py/load.py +3007 -0
  137. umbra_py-0.1.0/src/umbra_py/mcp_server.py +1691 -0
  138. umbra_py-0.1.0/src/umbra_py/models.py +623 -0
  139. umbra_py-0.1.0/src/umbra_py/narrate.py +929 -0
  140. umbra_py-0.1.0/src/umbra_py/planner.py +822 -0
  141. umbra_py-0.1.0/src/umbra_py/pmtiles.py +1047 -0
  142. umbra_py-0.1.0/src/umbra_py/preflight.py +880 -0
  143. umbra_py-0.1.0/src/umbra_py/py.typed +0 -0
  144. umbra_py-0.1.0/src/umbra_py/schemas.py +113 -0
  145. umbra_py-0.1.0/src/umbra_py/semantic.py +429 -0
  146. umbra_py-0.1.0/src/umbra_py/serve.py +3927 -0
  147. umbra_py-0.1.0/src/umbra_py/showcase.py +1309 -0
  148. umbra_py-0.1.0/src/umbra_py/viewer.py +521 -0
  149. umbra_py-0.1.0/src/umbra_py/viz/__init__.py +159 -0
  150. umbra_py-0.1.0/src/umbra_py/viz/_deps.py +27 -0
  151. umbra_py-0.1.0/src/umbra_py/viz/composites.py +563 -0
  152. umbra_py-0.1.0/src/umbra_py/viz/contact_sheet.py +172 -0
  153. umbra_py-0.1.0/src/umbra_py/viz/geojson.py +112 -0
  154. umbra_py-0.1.0/src/umbra_py/viz/maps.py +832 -0
  155. umbra_py-0.1.0/src/umbra_py/viz/raster.py +418 -0
  156. umbra_py-0.1.0/src/umbra_py/watch.py +283 -0
  157. umbra_py-0.1.0/tests/__init__.py +0 -0
  158. umbra_py-0.1.0/tests/conftest.py +40 -0
  159. umbra_py-0.1.0/tests/data/sample_item.json +284 -0
  160. umbra_py-0.1.0/tests/test_acquisition_filters.py +352 -0
  161. umbra_py-0.1.0/tests/test_canopy.py +343 -0
  162. umbra_py-0.1.0/tests/test_catalog.py +604 -0
  163. umbra_py-0.1.0/tests/test_chips.py +2750 -0
  164. umbra_py-0.1.0/tests/test_citation.py +47 -0
  165. umbra_py-0.1.0/tests/test_cli_json.py +354 -0
  166. umbra_py-0.1.0/tests/test_cli_option_groups.py +111 -0
  167. umbra_py-0.1.0/tests/test_cli_token.py +212 -0
  168. umbra_py-0.1.0/tests/test_context.py +26 -0
  169. umbra_py-0.1.0/tests/test_convert.py +4541 -0
  170. umbra_py-0.1.0/tests/test_coverage.py +1680 -0
  171. umbra_py-0.1.0/tests/test_dates.py +163 -0
  172. umbra_py-0.1.0/tests/test_dem.py +191 -0
  173. umbra_py-0.1.0/tests/test_demo.py +673 -0
  174. umbra_py-0.1.0/tests/test_describe.py +653 -0
  175. umbra_py-0.1.0/tests/test_docs_snippets.py +74 -0
  176. umbra_py-0.1.0/tests/test_download.py +282 -0
  177. umbra_py-0.1.0/tests/test_embed.py +561 -0
  178. umbra_py-0.1.0/tests/test_errors.py +214 -0
  179. umbra_py-0.1.0/tests/test_examples.py +134 -0
  180. umbra_py-0.1.0/tests/test_export.py +243 -0
  181. umbra_py-0.1.0/tests/test_fuzzy.py +59 -0
  182. umbra_py-0.1.0/tests/test_geocode.py +213 -0
  183. umbra_py-0.1.0/tests/test_geoid.py +68 -0
  184. umbra_py-0.1.0/tests/test_geometry.py +716 -0
  185. umbra_py-0.1.0/tests/test_html.py +246 -0
  186. umbra_py-0.1.0/tests/test_http.py +114 -0
  187. umbra_py-0.1.0/tests/test_index.py +2330 -0
  188. umbra_py-0.1.0/tests/test_langchain.py +315 -0
  189. umbra_py-0.1.0/tests/test_lazy_imagery.py +701 -0
  190. umbra_py-0.1.0/tests/test_live.py +169 -0
  191. umbra_py-0.1.0/tests/test_llamaindex.py +310 -0
  192. umbra_py-0.1.0/tests/test_llms_txt.py +68 -0
  193. umbra_py-0.1.0/tests/test_load.py +3364 -0
  194. umbra_py-0.1.0/tests/test_mcp_registry.py +260 -0
  195. umbra_py-0.1.0/tests/test_mcp_server.py +1703 -0
  196. umbra_py-0.1.0/tests/test_models.py +248 -0
  197. umbra_py-0.1.0/tests/test_narrate.py +703 -0
  198. umbra_py-0.1.0/tests/test_planner.py +690 -0
  199. umbra_py-0.1.0/tests/test_pmtiles.py +926 -0
  200. umbra_py-0.1.0/tests/test_preflight.py +807 -0
  201. umbra_py-0.1.0/tests/test_schemas.py +1423 -0
  202. umbra_py-0.1.0/tests/test_semantic.py +366 -0
  203. umbra_py-0.1.0/tests/test_serve.py +3196 -0
  204. umbra_py-0.1.0/tests/test_showcase.py +1257 -0
  205. umbra_py-0.1.0/tests/test_specfun.py +93 -0
  206. umbra_py-0.1.0/tests/test_viewer.py +234 -0
  207. umbra_py-0.1.0/tests/test_viz.py +1930 -0
  208. umbra_py-0.1.0/tests/test_watch.py +255 -0
  209. umbra_py-0.1.0/tests/test_workflows.py +352 -0
@@ -0,0 +1,33 @@
1
+ #!/bin/bash
2
+ # SessionStart hook for Claude Code on the web (and other remote coding agents).
3
+ #
4
+ # A fresh remote container has `uv` and the CLI linters on PATH but does NOT have
5
+ # umbra-py itself installed, so `pytest`, `mypy`, and the `umbra` CLI all fail
6
+ # until the package is installed editable. This hook closes that gap so an agent
7
+ # can run the exact checks CI runs (see .github/workflows/ci.yml) from the first
8
+ # turn — no "command not found", no import-skipped tests.
9
+ #
10
+ # It installs every extra so the FULL offline suite runs rather than
11
+ # import-skipping the viz / serve / convert / load / agent modules — mirroring
12
+ # CI's `test-all-extras` job, the one place every module actually executes and
13
+ # where the coverage gate is measured.
14
+ #
15
+ # Runs synchronously (no `{"async": true}` line) so the environment is ready
16
+ # before the first agent turn — preventing a race where a check runs before its
17
+ # dependencies exist. Idempotent and non-interactive: safe to re-run on
18
+ # resume/clear/compact, and `uv` no-ops when nothing changed.
19
+ set -euo pipefail
20
+
21
+ # Only bother in a remote environment (Claude Code on the web). Local sessions
22
+ # manage their own venv per AGENTS.md §3.
23
+ if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then
24
+ exit 0
25
+ fi
26
+
27
+ cd "${CLAUDE_PROJECT_DIR:-.}"
28
+
29
+ # `--system` matches CI: install into the container's Python rather than a venv.
30
+ # The extras list mirrors ci.yml's `test-all-extras` job so nothing import-skips.
31
+ uv pip install --system -e ".[dev,all,dask,mcp,serve,ai,langchain,llamaindex]"
32
+
33
+ echo "umbra-py installed editable with all extras — ruff / mypy / pytest ready." >&2
@@ -0,0 +1,36 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/claude-code-settings.json",
3
+ "hooks": {
4
+ "SessionStart": [
5
+ {
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh"
10
+ }
11
+ ]
12
+ }
13
+ ]
14
+ },
15
+ "permissions": {
16
+ "allow": [
17
+ "Bash(uv pip install:*)",
18
+ "Bash(uv venv:*)",
19
+ "Bash(ruff check:*)",
20
+ "Bash(ruff format:*)",
21
+ "Bash(mypy:*)",
22
+ "Bash(pytest:*)",
23
+ "Bash(python -m pytest:*)",
24
+ "Bash(python3 -m pytest:*)",
25
+ "Bash(pre-commit run:*)",
26
+ "Bash(umbra:*)",
27
+ "Bash(umbra-py:*)",
28
+ "Bash(git status:*)",
29
+ "Bash(git diff:*)",
30
+ "Bash(git log:*)",
31
+ "Bash(git show:*)",
32
+ "Bash(git branch:*)",
33
+ "Bash(git add:*)"
34
+ ]
35
+ }
36
+ }
@@ -0,0 +1,32 @@
1
+ # Keep the build context small and the image reproducible: ship only the
2
+ # package source + metadata the wheel build needs, never local caches, VCS
3
+ # history, tests, docs, or notebooks.
4
+ .git
5
+ .github
6
+ .gitignore
7
+ .pre-commit-config.yaml
8
+ .ruff_cache
9
+ .mypy_cache
10
+ .pytest_cache
11
+ **/__pycache__
12
+ *.pyc
13
+ .venv
14
+ venv
15
+ build
16
+ dist
17
+ *.egg-info
18
+ tests
19
+ examples
20
+ # The planning docs, but *not* `docs/schemas/`: the wheel force-includes those
21
+ # as package data (`umbra_py/_schemas/`) so `umbra serve` can put the published
22
+ # contracts in its generated OpenAPI document, and an installed package has no
23
+ # checkout to fall back to. Excluding them fails the build outright, which is
24
+ # the honest failure for an image whose /openapi.json would otherwise raise.
25
+ docs/*.md
26
+ docs_src
27
+ mkdocs.yml
28
+ site
29
+ *.db
30
+ *.parquet
31
+ *.pmtiles
32
+ CHANGELOG.md
@@ -0,0 +1,21 @@
1
+ ---
2
+ name: Bug report
3
+ about: Report a problem with umbra-py
4
+ labels: bug
5
+ ---
6
+
7
+ **What happened?**
8
+ A clear description of the bug.
9
+
10
+ **To reproduce**
11
+ Minimal code (or `umbra ...` command) that triggers it. If it involves a
12
+ specific Umbra item, please include the STAC item URL.
13
+
14
+ **Expected behavior**
15
+
16
+ **Environment**
17
+ - umbra-py version:
18
+ - Python version:
19
+ - OS:
20
+
21
+ **Additional context / traceback**
@@ -0,0 +1,16 @@
1
+ ---
2
+ name: Feature request
3
+ about: Suggest a new feature, workflow, or integration
4
+ labels: enhancement
5
+ ---
6
+
7
+ **Problem you're trying to solve**
8
+ What's hard or impossible to do with umbra-py today?
9
+
10
+ **Proposed solution**
11
+ A sketch of the API / CLI / workflow you'd like.
12
+
13
+ **Alternatives considered**
14
+
15
+ **Additional context**
16
+ Related libraries, Umbra products involved, sample item URLs, etc.
@@ -0,0 +1,14 @@
1
+ ## Summary
2
+
3
+ <!-- What does this PR change and why? Link related issues. -->
4
+
5
+ ## Changes
6
+
7
+ -
8
+
9
+ ## Checklist
10
+
11
+ - [ ] Tests added or updated (`pytest`)
12
+ - [ ] `ruff check .` and `ruff format --check .` pass
13
+ - [ ] `CHANGELOG.md` updated under **Unreleased** for user-visible changes
14
+ - [ ] Docs / docstrings updated where relevant
@@ -0,0 +1,21 @@
1
+ version: 2
2
+ updates:
3
+ # Keep workflow actions fresh (checkout, setup-python, setup-uv, ...).
4
+ # Grouped so a week's bumps arrive as one PR, not five.
5
+ - package-ecosystem: github-actions
6
+ directory: "/"
7
+ schedule:
8
+ interval: weekly
9
+ groups:
10
+ actions:
11
+ patterns: ["*"]
12
+
13
+ # Python deps are open-ended ranges in pyproject.toml, so this mostly
14
+ # surfaces new majors that fall outside a range — low traffic by design.
15
+ - package-ecosystem: pip
16
+ directory: "/"
17
+ schedule:
18
+ interval: weekly
19
+ groups:
20
+ python:
21
+ patterns: ["*"]
@@ -0,0 +1,154 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ # A superseded push's runs are dead feedback; cancel them.
9
+ concurrency:
10
+ group: ci-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ jobs:
14
+ test:
15
+ runs-on: ubuntu-latest
16
+ strategy:
17
+ fail-fast: false
18
+ matrix:
19
+ python-version: ["3.10", "3.11", "3.12"]
20
+ steps:
21
+ - uses: actions/checkout@v7
22
+
23
+ - name: Set up Python ${{ matrix.python-version }}
24
+ uses: actions/setup-python@v7
25
+ with:
26
+ python-version: ${{ matrix.python-version }}
27
+
28
+ - name: Install uv
29
+ uses: astral-sh/setup-uv@v7
30
+ with:
31
+ enable-cache: true
32
+ cache-dependency-glob: pyproject.toml
33
+
34
+ - name: Install package
35
+ run: uv pip install --system -e ".[dev]"
36
+
37
+ - name: Lint
38
+ run: ruff check .
39
+
40
+ - name: Format check
41
+ run: ruff format --check .
42
+
43
+ - name: Test (offline)
44
+ run: pytest -q
45
+
46
+ # The package ships a py.typed marker, so downstream type checkers trust its
47
+ # inline annotations. This job verifies they are actually consistent instead
48
+ # of shipping an unchecked promise. It runs against a core [dev] install (no
49
+ # optional extras), matching the [tool.mypy] config: the untyped optional
50
+ # libraries (rasterio, fastapi, sarpy, ...) are import-ignored, so only
51
+ # umbra-py's own types are checked. This is the surface a bare `pip install
52
+ # umbra-py` gets; the all-extras surface is checked by the job below.
53
+ type-check:
54
+ runs-on: ubuntu-latest
55
+ steps:
56
+ - uses: actions/checkout@v7
57
+
58
+ - name: Set up Python
59
+ uses: actions/setup-python@v7
60
+ with:
61
+ python-version: "3.12"
62
+
63
+ - name: Install uv
64
+ uses: astral-sh/setup-uv@v7
65
+ with:
66
+ enable-cache: true
67
+ cache-dependency-glob: pyproject.toml
68
+
69
+ - name: Install package
70
+ run: uv pip install --system -e ".[dev]"
71
+
72
+ - name: Type check
73
+ run: mypy
74
+
75
+ # The core type-check above import-ignores every optional library, so a
76
+ # misuse of one that *does* ship stubs (PIL, click, numpy) is invisible to it
77
+ # -- exactly how `Image.ADAPTIVE`, wrong under Pillow's stubs, reached main
78
+ # and greeted every stubs-present dev/agent session with a failing mypy line.
79
+ # This job installs all extras and runs the same mypy, so that surface is
80
+ # checked on every PR -- the type-check mirror of `test-all-extras`.
81
+ type-check-all-extras:
82
+ runs-on: ubuntu-latest
83
+ steps:
84
+ - uses: actions/checkout@v7
85
+
86
+ - name: Set up Python
87
+ uses: actions/setup-python@v7
88
+ with:
89
+ python-version: "3.12"
90
+
91
+ - name: Install uv
92
+ uses: astral-sh/setup-uv@v7
93
+ with:
94
+ enable-cache: true
95
+ cache-dependency-glob: pyproject.toml
96
+
97
+ - name: Install package with all extras
98
+ run: uv pip install --system -e ".[dev,all,dask,mcp,serve,ai,langchain,llamaindex]"
99
+
100
+ # `--python-version 3.12` overrides the config's 3.10 target for this job
101
+ # only. numpy's shipped stubs use PEP 695 `type` statements, which mypy
102
+ # refuses to parse below a 3.12 target -- so checking the all-extras
103
+ # surface at 3.10 dies in numpy's `.pyi`, not in our code. The 3.10 floor
104
+ # that the py.typed promise is really about stays enforced on umbra-py's
105
+ # own annotations by the core `type-check` job above (which has no numpy);
106
+ # the `[attr-defined]`-class misuse this job exists to catch is
107
+ # target-independent, so nothing is lost by checking the stub surface at
108
+ # 3.12.
109
+ - name: Type check (all extras)
110
+ run: mypy --python-version 3.12
111
+
112
+ # The core matrix installs only [dev], so every test needing an optional
113
+ # extra (viz / load / convert / export) importorskips there — silently.
114
+ # This job installs everything so those tests actually run on every PR.
115
+ test-all-extras:
116
+ runs-on: ubuntu-latest
117
+ steps:
118
+ - uses: actions/checkout@v7
119
+
120
+ - name: Set up Python
121
+ uses: actions/setup-python@v7
122
+ with:
123
+ python-version: "3.12"
124
+
125
+ - name: Install uv
126
+ uses: astral-sh/setup-uv@v7
127
+ with:
128
+ enable-cache: true
129
+ cache-dependency-glob: pyproject.toml
130
+
131
+ - name: Install package with all extras
132
+ run: uv pip install --system -e ".[dev,all,dask,mcp,serve,ai,langchain,llamaindex]"
133
+
134
+ # This is the one job where every module actually runs, so it is also
135
+ # where coverage is meaningful. The floor guards against regressions; it
136
+ # sits a couple of points below the current figure so ordinary branch
137
+ # fluctuation across Python patch releases doesn't turn CI red. The XML
138
+ # report feeds the Codecov badge in the README.
139
+ - name: Test (offline, all extras) with coverage
140
+ run: >-
141
+ pytest -q
142
+ --cov=umbra_py
143
+ --cov-report=term-missing
144
+ --cov-report=xml
145
+ --cov-fail-under=88
146
+
147
+ - name: Upload coverage to Codecov
148
+ uses: codecov/codecov-action@v7
149
+ with:
150
+ files: ./coverage.xml
151
+ # Never let a coverage-upload hiccup (or an unconfigured Codecov
152
+ # integration) fail the build — the gate above is the real check.
153
+ fail_ci_if_error: false
154
+ token: ${{ secrets.CODECOV_TOKEN }}
@@ -0,0 +1,64 @@
1
+ name: Docker
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ # A superseded push's build is dead feedback; cancel it.
9
+ concurrency:
10
+ group: docker-image-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ jobs:
14
+ # Build the self-hosting `umbra serve` image and smoke-test it end to end,
15
+ # so a broken Dockerfile / entrypoint fails review instead of a user's first
16
+ # `docker run`. Everything here is offline: the smoke test serves in live
17
+ # mode (no index, no S3 walk until an actual /search), so `/` and `/healthz`
18
+ # answer without any external network.
19
+ build:
20
+ runs-on: ubuntu-latest
21
+ steps:
22
+ - uses: actions/checkout@v7
23
+
24
+ - name: Build image (serve extra)
25
+ run: docker build --build-arg UMBRA_EXTRAS=serve -t umbra-py:ci .
26
+
27
+ - name: CLI passthrough works
28
+ run: docker run --rm umbra-py:ci --version
29
+
30
+ - name: Validate compose file
31
+ run: docker compose config >/dev/null
32
+
33
+ - name: Start the server (live mode -- no external network)
34
+ run: docker run -d --name umbra-serve -e UMBRA_SERVE_LIVE=1 -p 8000:8000 umbra-py:ci
35
+
36
+ - name: Wait for /healthz
37
+ run: |
38
+ for _ in $(seq 1 30); do
39
+ if curl -fsS http://127.0.0.1:8000/healthz >/dev/null; then
40
+ echo "healthy"; exit 0
41
+ fi
42
+ sleep 2
43
+ done
44
+ echo "server did not become healthy in time" >&2
45
+ docker logs umbra-serve || true
46
+ exit 1
47
+
48
+ - name: Landing page is a STAC Catalog
49
+ run: |
50
+ curl -fsS http://127.0.0.1:8000/ \
51
+ | python -c "import sys,json; d=json.load(sys.stdin); assert d['type']=='Catalog'; print('landing OK')"
52
+
53
+ - name: /healthz reports a ready live backend
54
+ run: |
55
+ curl -fsS http://127.0.0.1:8000/healthz \
56
+ | python -c "import sys,json; d=json.load(sys.stdin); assert d['ready'] is True and d['backend']=='live', d; print('healthz OK')"
57
+
58
+ - name: Server logs
59
+ if: always()
60
+ run: docker logs umbra-serve || true
61
+
62
+ - name: Cleanup
63
+ if: always()
64
+ run: docker rm -f umbra-serve || true
@@ -0,0 +1,150 @@
1
+ name: Docs
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ # The showcase is built from the `catalog-index` release, not from the repo,
8
+ # so a freshly published index changes what the site should show while the
9
+ # commit it deploys from stays identical. Without this the showcase only
10
+ # refreshes when something unrelated lands on main — and a *first* index
11
+ # publish leaves site/showcase/ missing (a 404 on a link the docs advertise)
12
+ # until the next push happens to come along. Rebuild when the index does.
13
+ workflow_run:
14
+ workflows: ["Publish catalog index"]
15
+ types: [completed]
16
+
17
+ # Only one concurrent Pages deployment; a superseded build is dead feedback.
18
+ concurrency:
19
+ group: docs-${{ github.ref }}
20
+ cancel-in-progress: true
21
+
22
+ jobs:
23
+ # Build on every PR (and on main) so a broken docstring cross-reference or
24
+ # nav entry fails review, not the deploy. `--strict` turns warnings into
25
+ # errors. The package itself is installed with every extra so mkdocstrings
26
+ # can import each module it documents.
27
+ build:
28
+ runs-on: ubuntu-latest
29
+ # A failed index publish has no new snapshot to show, so don't redeploy for
30
+ # it. (Push and pull_request runs carry no workflow_run context and always
31
+ # proceed.)
32
+ if: >-
33
+ github.event_name != 'workflow_run'
34
+ || github.event.workflow_run.conclusion == 'success'
35
+ steps:
36
+ - uses: actions/checkout@v7
37
+
38
+ - name: Set up Python
39
+ uses: actions/setup-python@v7
40
+ with:
41
+ python-version: "3.12"
42
+
43
+ - name: Install uv
44
+ uses: astral-sh/setup-uv@v7
45
+ with:
46
+ enable-cache: true
47
+ cache-dependency-glob: pyproject.toml
48
+
49
+ - name: Install package + docs toolchain
50
+ run: uv pip install --system -e ".[docs,all,mcp,serve,ai,langchain,llamaindex]"
51
+
52
+ - name: Build (strict)
53
+ run: mkdocs build --strict
54
+
55
+ # Build the static, zero-install showcase (a whole-catalog interactive
56
+ # explorer over the published snapshot plus a gallery of precomputed
57
+ # change composites) into site/showcase/ so it deploys to GitHub Pages
58
+ # beside the docs. Main-only (it deploys only from main) and non-blocking:
59
+ # if the catalog-index release isn't published yet, or the fetch fails, the
60
+ # docs deploy must still go out, so this step never fails the job
61
+ # (STRATEGY.md §8 demo/hosting).
62
+ #
63
+ # Non-blocking, but never *silent*. This was `continue-on-error`, which
64
+ # reports the step as having succeeded, so a run that shipped no showcase
65
+ # at all looked exactly like one that shipped a good one: the docs went
66
+ # green while the showcase link they advertise served a 404. The failure
67
+ # is swallowed here instead — deliberately, and with a warning annotation
68
+ # plus a job-summary note — so the deploy still goes out and the run that
69
+ # dropped the showcase says so.
70
+ #
71
+ # --unified builds ONE explorer page reading catalog.pmtiles directly, so
72
+ # the landing page leads to the whole catalog with the filters, rather than
73
+ # to a click-only map and a separate explorer over a gathered slice. It
74
+ # searches nothing, so no --local/--limit/--max-per-task here.
75
+ #
76
+ # --featured 6 renders six change composites, streaming only a downsampled
77
+ # overview of each scene (the `viz` extra arrives with `[all]` above); a
78
+ # site whose asset won't render is dropped with a warning, so a bad scene
79
+ # costs one tile rather than the gallery. That selection *does* read the
80
+ # index, hence the fetch below.
81
+ - name: Build catalog showcase
82
+ if: github.ref == 'refs/heads/main'
83
+ # Mode A: the model key is held here, at build time, and never reaches
84
+ # the static site (a secret shipped to a browser is a published secret).
85
+ # `umbra showcase --narrate` bakes a cached reading per featured site; it
86
+ # is gated on a key's presence inside the command, so a fork PR (no
87
+ # secrets) or an unset secret just skips the narrations and still ships
88
+ # the gallery. Any one of the three keys works — whichever the maintainer
89
+ # set as a repo secret.
90
+ env:
91
+ OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
92
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
93
+ OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
94
+ run: |
95
+ set +e
96
+ umbra index fetch &&
97
+ umbra showcase \
98
+ --unified \
99
+ --local \
100
+ --fetch-pmtiles \
101
+ --featured 6 \
102
+ --narrate \
103
+ --out site/showcase \
104
+ --updated "$(date -u +%Y-%m-%d)"
105
+ status=$?
106
+ set -e
107
+
108
+ # A half-written directory would deploy as a broken showcase, which is
109
+ # worse than none: check for the landing page the build promises, not
110
+ # just the exit status.
111
+ if [ "$status" -eq 0 ] && [ -f site/showcase/index.html ]; then
112
+ echo "Showcase built into site/showcase/."
113
+ exit 0
114
+ fi
115
+ rm -rf site/showcase
116
+ note="The docs deployed without site/showcase/, so \
117
+ https://umbra-py.space/showcase/ will 404 until a run \
118
+ rebuilds it. Usual cause: the rolling 'catalog-index' release has no \
119
+ catalog.db yet, so 'umbra index fetch' 404s — check the 'Publish \
120
+ catalog index' workflow."
121
+ echo "::warning title=Showcase not built::${note}"
122
+ {
123
+ echo "### :warning: Showcase not built"
124
+ echo
125
+ echo "${note}"
126
+ } >> "$GITHUB_STEP_SUMMARY"
127
+
128
+ - name: Upload Pages artifact
129
+ if: github.ref == 'refs/heads/main'
130
+ uses: actions/upload-pages-artifact@v5
131
+ with:
132
+ path: site
133
+
134
+ # Publish to GitHub Pages, but only from main — never from a PR. Requires
135
+ # Pages to be enabled for the repository (Settings → Pages → Source: GitHub
136
+ # Actions); until then this job is skipped/red without affecting PR CI.
137
+ deploy:
138
+ if: github.ref == 'refs/heads/main'
139
+ needs: build
140
+ runs-on: ubuntu-latest
141
+ permissions:
142
+ pages: write
143
+ id-token: write
144
+ environment:
145
+ name: github-pages
146
+ url: ${{ steps.deployment.outputs.page_url }}
147
+ steps:
148
+ - name: Deploy to GitHub Pages
149
+ id: deployment
150
+ uses: actions/deploy-pages@v5
@@ -0,0 +1,62 @@
1
+ name: Live catalog canary
2
+
3
+ # Umbra's open-data catalog is an external dependency that drifts: bucket
4
+ # layout, sidecar naming, asset keys (models.py already handles two naming
5
+ # generations). The offline suite can't see that drift, so this runs the
6
+ # `-m network` integration tests against the live catalog on a schedule.
7
+ # A failure here usually means "Umbra changed something", not "a PR broke
8
+ # it" — it opens (or comments on) a tracking issue instead of failing PRs.
9
+
10
+ on:
11
+ schedule:
12
+ - cron: "43 5 * * 1" # Mondays 05:43 UTC, ahead of the index publish
13
+ workflow_dispatch:
14
+
15
+ permissions:
16
+ contents: read
17
+ issues: write
18
+
19
+ jobs:
20
+ live-tests:
21
+ runs-on: ubuntu-latest
22
+ # Don't hit Umbra's bucket from forks; schedules there stay off anyway.
23
+ if: github.repository == 'reesehammer/umbra-py'
24
+ timeout-minutes: 30
25
+ steps:
26
+ - uses: actions/checkout@v7
27
+
28
+ - name: Set up Python
29
+ uses: actions/setup-python@v7
30
+ with:
31
+ python-version: "3.12"
32
+
33
+ - name: Install uv
34
+ uses: astral-sh/setup-uv@v7
35
+ with:
36
+ enable-cache: true
37
+ cache-dependency-glob: pyproject.toml
38
+
39
+ - name: Install package with all extras
40
+ run: uv pip install --system -e ".[dev,all]"
41
+
42
+ - name: Run live tests against Umbra's public catalog
43
+ run: pytest -m network -q
44
+
45
+ - name: Open or update a tracking issue on failure
46
+ if: failure()
47
+ env:
48
+ GH_TOKEN: ${{ github.token }}
49
+ run: |
50
+ run_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
51
+ body="The scheduled \`pytest -m network\` run against Umbra's public
52
+ catalog failed on $(date -u +%Y-%m-%d) — likely catalog drift
53
+ (bucket layout, sidecar naming, asset keys) rather than a code
54
+ regression. See the [failed run]($run_url)."
55
+ existing=$(gh issue list --state open \
56
+ --search 'Live catalog canary failed in:title' \
57
+ --json number --jq '.[0].number')
58
+ if [ -n "$existing" ]; then
59
+ gh issue comment "$existing" --body "$body"
60
+ else
61
+ gh issue create --title "Live catalog canary failed" --body "$body"
62
+ fi