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.
- umbra_py-0.1.0/.claude/hooks/session-start.sh +33 -0
- umbra_py-0.1.0/.claude/settings.json +36 -0
- umbra_py-0.1.0/.dockerignore +32 -0
- umbra_py-0.1.0/.github/ISSUE_TEMPLATE/bug_report.md +21 -0
- umbra_py-0.1.0/.github/ISSUE_TEMPLATE/feature_request.md +16 -0
- umbra_py-0.1.0/.github/PULL_REQUEST_TEMPLATE.md +14 -0
- umbra_py-0.1.0/.github/dependabot.yml +21 -0
- umbra_py-0.1.0/.github/workflows/ci.yml +154 -0
- umbra_py-0.1.0/.github/workflows/docker.yml +64 -0
- umbra_py-0.1.0/.github/workflows/docs.yml +150 -0
- umbra_py-0.1.0/.github/workflows/live-canary.yml +62 -0
- umbra_py-0.1.0/.github/workflows/publish-index.yml +213 -0
- umbra_py-0.1.0/.github/workflows/release.yml +128 -0
- umbra_py-0.1.0/.github/workflows/security-audit.yml +85 -0
- umbra_py-0.1.0/.gitignore +240 -0
- umbra_py-0.1.0/.pre-commit-config.yaml +16 -0
- umbra_py-0.1.0/AGENTS.md +403 -0
- umbra_py-0.1.0/CHANGELOG.md +6485 -0
- umbra_py-0.1.0/CITATION.cff +28 -0
- umbra_py-0.1.0/CLAUDE.md +43 -0
- umbra_py-0.1.0/CODE_OF_CONDUCT.md +135 -0
- umbra_py-0.1.0/CONTRIBUTING.md +84 -0
- umbra_py-0.1.0/Dockerfile +64 -0
- umbra_py-0.1.0/LICENSE +201 -0
- umbra_py-0.1.0/PKG-INFO +262 -0
- umbra_py-0.1.0/README.md +168 -0
- umbra_py-0.1.0/SECURITY.md +58 -0
- umbra_py-0.1.0/codecov.yml +14 -0
- umbra_py-0.1.0/docker-compose.yml +45 -0
- umbra_py-0.1.0/docker-entrypoint.sh +62 -0
- umbra_py-0.1.0/docs/STRATEGY.md +272 -0
- umbra_py-0.1.0/docs/TODO.md +987 -0
- umbra_py-0.1.0/docs/schemas/README.md +201 -0
- umbra_py-0.1.0/docs/schemas/chip-dataset.schema.json +407 -0
- umbra_py-0.1.0/docs/schemas/chip-record.schema.json +179 -0
- umbra_py-0.1.0/docs/schemas/chip-skipped.schema.json +49 -0
- umbra_py-0.1.0/docs/schemas/download.schema.json +43 -0
- umbra_py-0.1.0/docs/schemas/error.schema.json +32 -0
- umbra_py-0.1.0/docs/schemas/index-info.schema.json +65 -0
- umbra_py-0.1.0/docs/schemas/item-context.schema.json +122 -0
- umbra_py-0.1.0/docs/schemas/preflight.schema.json +190 -0
- umbra_py-0.1.0/docs/schemas/render-job.schema.json +99 -0
- umbra_py-0.1.0/docs/schemas/render-manifest.schema.json +54 -0
- umbra_py-0.1.0/docs/schemas/scene-description.schema.json +104 -0
- umbra_py-0.1.0/docs/schemas/scene-matches.schema.json +64 -0
- umbra_py-0.1.0/docs/schemas/search-plan.schema.json +136 -0
- umbra_py-0.1.0/docs/schemas/site-coverage.schema.json +152 -0
- umbra_py-0.1.0/docs/schemas/stack-provenance.schema.json +130 -0
- umbra_py-0.1.0/docs/schemas/stack-stats.schema.json +447 -0
- umbra_py-0.1.0/docs/schemas/task-matches.schema.json +53 -0
- umbra_py-0.1.0/docs/schemas/watch-delta.schema.json +66 -0
- umbra_py-0.1.0/docs_src/CNAME +1 -0
- umbra_py-0.1.0/docs_src/cli.md +15 -0
- umbra_py-0.1.0/docs_src/deploy.md +250 -0
- umbra_py-0.1.0/docs_src/guides/limitations.md +75 -0
- umbra_py-0.1.0/docs_src/guides/notebooks.md +24 -0
- umbra_py-0.1.0/docs_src/index.md +56 -0
- umbra_py-0.1.0/docs_src/install.md +47 -0
- umbra_py-0.1.0/docs_src/quickstart.md +82 -0
- umbra_py-0.1.0/docs_src/reference/ai.md +65 -0
- umbra_py-0.1.0/docs_src/reference/catalog.md +7 -0
- umbra_py-0.1.0/docs_src/reference/chips.md +88 -0
- umbra_py-0.1.0/docs_src/reference/convert.md +147 -0
- umbra_py-0.1.0/docs_src/reference/download.md +11 -0
- umbra_py-0.1.0/docs_src/reference/exceptions.md +32 -0
- umbra_py-0.1.0/docs_src/reference/index.md +19 -0
- umbra_py-0.1.0/docs_src/reference/load.md +72 -0
- umbra_py-0.1.0/docs_src/reference/models.md +25 -0
- umbra_py-0.1.0/docs_src/reference/terrain.md +25 -0
- umbra_py-0.1.0/docs_src/reference/viz.md +8 -0
- umbra_py-0.1.0/examples/01_hello_umbra.ipynb +212 -0
- umbra_py-0.1.0/examples/02_download_and_open_gec.ipynb +180 -0
- umbra_py-0.1.0/examples/03_change_detection.ipynb +142 -0
- umbra_py-0.1.0/examples/04_amplitude_time_series.ipynb +193 -0
- umbra_py-0.1.0/examples/05_detection_chips.ipynb +218 -0
- umbra_py-0.1.0/examples/06_site_monitoring.ipynb +214 -0
- umbra_py-0.1.0/examples/07_sicd_amplitude.ipynb +229 -0
- umbra_py-0.1.0/examples/08_time_series_datacube.ipynb +379 -0
- umbra_py-0.1.0/examples/README.md +82 -0
- umbra_py-0.1.0/examples/baseline/01_search_catalog_pystac.py +98 -0
- umbra_py-0.1.0/examples/baseline/02_search_catalog_handrolled.py +170 -0
- umbra_py-0.1.0/examples/baseline/03_find_the_geotiff.py +161 -0
- umbra_py-0.1.0/examples/baseline/04_download_assets.py +121 -0
- umbra_py-0.1.0/examples/baseline/05_open_sicd.py +100 -0
- umbra_py-0.1.0/examples/baseline/06_with_umbra_py.py +83 -0
- umbra_py-0.1.0/examples/change.md +263 -0
- umbra_py-0.1.0/examples/gallery.md +153 -0
- umbra_py-0.1.0/examples/load.md +134 -0
- umbra_py-0.1.0/examples/maps.md +560 -0
- umbra_py-0.1.0/examples/quicklook.md +195 -0
- umbra_py-0.1.0/examples/swipe.md +87 -0
- umbra_py-0.1.0/llms-full.txt +143 -0
- umbra_py-0.1.0/llms.txt +30 -0
- umbra_py-0.1.0/mkdocs.yml +99 -0
- umbra_py-0.1.0/pyproject.toml +228 -0
- umbra_py-0.1.0/server.json +69 -0
- umbra_py-0.1.0/src/umbra_py/__init__.py +364 -0
- umbra_py-0.1.0/src/umbra_py/_geometry.py +266 -0
- umbra_py-0.1.0/src/umbra_py/_html.py +402 -0
- umbra_py-0.1.0/src/umbra_py/_http.py +157 -0
- umbra_py-0.1.0/src/umbra_py/_lazy_imagery.py +677 -0
- umbra_py-0.1.0/src/umbra_py/_specfun.py +149 -0
- umbra_py-0.1.0/src/umbra_py/_spinner.py +116 -0
- umbra_py-0.1.0/src/umbra_py/catalog.py +697 -0
- umbra_py-0.1.0/src/umbra_py/chips.py +2052 -0
- umbra_py-0.1.0/src/umbra_py/cli/__init__.py +111 -0
- umbra_py-0.1.0/src/umbra_py/cli/__main__.py +10 -0
- umbra_py-0.1.0/src/umbra_py/cli/_root.py +73 -0
- umbra_py-0.1.0/src/umbra_py/cli/_shared.py +653 -0
- umbra_py-0.1.0/src/umbra_py/cli/atlas.py +461 -0
- umbra_py-0.1.0/src/umbra_py/cli/composites.py +745 -0
- umbra_py-0.1.0/src/umbra_py/cli/discover.py +1069 -0
- umbra_py-0.1.0/src/umbra_py/cli/explore.py +1120 -0
- umbra_py-0.1.0/src/umbra_py/cli/indexes.py +1172 -0
- umbra_py-0.1.0/src/umbra_py/cli/process.py +2130 -0
- umbra_py-0.1.0/src/umbra_py/cli/scenes.py +389 -0
- umbra_py-0.1.0/src/umbra_py/constants.py +193 -0
- umbra_py-0.1.0/src/umbra_py/context.py +148 -0
- umbra_py-0.1.0/src/umbra_py/convert.py +3854 -0
- umbra_py-0.1.0/src/umbra_py/coverage.py +873 -0
- umbra_py-0.1.0/src/umbra_py/dates.py +177 -0
- umbra_py-0.1.0/src/umbra_py/dem.py +218 -0
- umbra_py-0.1.0/src/umbra_py/demo.py +1465 -0
- umbra_py-0.1.0/src/umbra_py/describe.py +864 -0
- umbra_py-0.1.0/src/umbra_py/download.py +203 -0
- umbra_py-0.1.0/src/umbra_py/embed.py +643 -0
- umbra_py-0.1.0/src/umbra_py/exceptions.py +123 -0
- umbra_py-0.1.0/src/umbra_py/export.py +117 -0
- umbra_py-0.1.0/src/umbra_py/fuzzy.py +85 -0
- umbra_py-0.1.0/src/umbra_py/geocode.py +104 -0
- umbra_py-0.1.0/src/umbra_py/geoid.py +102 -0
- umbra_py-0.1.0/src/umbra_py/index.py +1738 -0
- umbra_py-0.1.0/src/umbra_py/langchain.py +238 -0
- umbra_py-0.1.0/src/umbra_py/llamaindex.py +264 -0
- umbra_py-0.1.0/src/umbra_py/llms_txt.py +377 -0
- umbra_py-0.1.0/src/umbra_py/load.py +3007 -0
- umbra_py-0.1.0/src/umbra_py/mcp_server.py +1691 -0
- umbra_py-0.1.0/src/umbra_py/models.py +623 -0
- umbra_py-0.1.0/src/umbra_py/narrate.py +929 -0
- umbra_py-0.1.0/src/umbra_py/planner.py +822 -0
- umbra_py-0.1.0/src/umbra_py/pmtiles.py +1047 -0
- umbra_py-0.1.0/src/umbra_py/preflight.py +880 -0
- umbra_py-0.1.0/src/umbra_py/py.typed +0 -0
- umbra_py-0.1.0/src/umbra_py/schemas.py +113 -0
- umbra_py-0.1.0/src/umbra_py/semantic.py +429 -0
- umbra_py-0.1.0/src/umbra_py/serve.py +3927 -0
- umbra_py-0.1.0/src/umbra_py/showcase.py +1309 -0
- umbra_py-0.1.0/src/umbra_py/viewer.py +521 -0
- umbra_py-0.1.0/src/umbra_py/viz/__init__.py +159 -0
- umbra_py-0.1.0/src/umbra_py/viz/_deps.py +27 -0
- umbra_py-0.1.0/src/umbra_py/viz/composites.py +563 -0
- umbra_py-0.1.0/src/umbra_py/viz/contact_sheet.py +172 -0
- umbra_py-0.1.0/src/umbra_py/viz/geojson.py +112 -0
- umbra_py-0.1.0/src/umbra_py/viz/maps.py +832 -0
- umbra_py-0.1.0/src/umbra_py/viz/raster.py +418 -0
- umbra_py-0.1.0/src/umbra_py/watch.py +283 -0
- umbra_py-0.1.0/tests/__init__.py +0 -0
- umbra_py-0.1.0/tests/conftest.py +40 -0
- umbra_py-0.1.0/tests/data/sample_item.json +284 -0
- umbra_py-0.1.0/tests/test_acquisition_filters.py +352 -0
- umbra_py-0.1.0/tests/test_canopy.py +343 -0
- umbra_py-0.1.0/tests/test_catalog.py +604 -0
- umbra_py-0.1.0/tests/test_chips.py +2750 -0
- umbra_py-0.1.0/tests/test_citation.py +47 -0
- umbra_py-0.1.0/tests/test_cli_json.py +354 -0
- umbra_py-0.1.0/tests/test_cli_option_groups.py +111 -0
- umbra_py-0.1.0/tests/test_cli_token.py +212 -0
- umbra_py-0.1.0/tests/test_context.py +26 -0
- umbra_py-0.1.0/tests/test_convert.py +4541 -0
- umbra_py-0.1.0/tests/test_coverage.py +1680 -0
- umbra_py-0.1.0/tests/test_dates.py +163 -0
- umbra_py-0.1.0/tests/test_dem.py +191 -0
- umbra_py-0.1.0/tests/test_demo.py +673 -0
- umbra_py-0.1.0/tests/test_describe.py +653 -0
- umbra_py-0.1.0/tests/test_docs_snippets.py +74 -0
- umbra_py-0.1.0/tests/test_download.py +282 -0
- umbra_py-0.1.0/tests/test_embed.py +561 -0
- umbra_py-0.1.0/tests/test_errors.py +214 -0
- umbra_py-0.1.0/tests/test_examples.py +134 -0
- umbra_py-0.1.0/tests/test_export.py +243 -0
- umbra_py-0.1.0/tests/test_fuzzy.py +59 -0
- umbra_py-0.1.0/tests/test_geocode.py +213 -0
- umbra_py-0.1.0/tests/test_geoid.py +68 -0
- umbra_py-0.1.0/tests/test_geometry.py +716 -0
- umbra_py-0.1.0/tests/test_html.py +246 -0
- umbra_py-0.1.0/tests/test_http.py +114 -0
- umbra_py-0.1.0/tests/test_index.py +2330 -0
- umbra_py-0.1.0/tests/test_langchain.py +315 -0
- umbra_py-0.1.0/tests/test_lazy_imagery.py +701 -0
- umbra_py-0.1.0/tests/test_live.py +169 -0
- umbra_py-0.1.0/tests/test_llamaindex.py +310 -0
- umbra_py-0.1.0/tests/test_llms_txt.py +68 -0
- umbra_py-0.1.0/tests/test_load.py +3364 -0
- umbra_py-0.1.0/tests/test_mcp_registry.py +260 -0
- umbra_py-0.1.0/tests/test_mcp_server.py +1703 -0
- umbra_py-0.1.0/tests/test_models.py +248 -0
- umbra_py-0.1.0/tests/test_narrate.py +703 -0
- umbra_py-0.1.0/tests/test_planner.py +690 -0
- umbra_py-0.1.0/tests/test_pmtiles.py +926 -0
- umbra_py-0.1.0/tests/test_preflight.py +807 -0
- umbra_py-0.1.0/tests/test_schemas.py +1423 -0
- umbra_py-0.1.0/tests/test_semantic.py +366 -0
- umbra_py-0.1.0/tests/test_serve.py +3196 -0
- umbra_py-0.1.0/tests/test_showcase.py +1257 -0
- umbra_py-0.1.0/tests/test_specfun.py +93 -0
- umbra_py-0.1.0/tests/test_viewer.py +234 -0
- umbra_py-0.1.0/tests/test_viz.py +1930 -0
- umbra_py-0.1.0/tests/test_watch.py +255 -0
- 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
|