From 3ece3389b61167d9dde60f2f54e99a9863b2daf5 Mon Sep 17 00:00:00 2001 From: Scott Staniewicz Date: Sun, 3 Dec 2023 16:03:48 -0500 Subject: [PATCH 1/3] fix dataset release hash --- src/opera_utils/datasets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/opera_utils/datasets.py b/src/opera_utils/datasets.py index 62347ff..2cc9f25 100644 --- a/src/opera_utils/datasets.py +++ b/src/opera_utils/datasets.py @@ -43,8 +43,8 @@ # are their respective SHA256 hashes. Files will be downloaded # automatically when needed. registry={ - f"frame-geometries-simple-{BURST_DB_VERSION}.geojson.zip": "f0094f4cdc287d56d7a126a42f1e3075e50309afe8a431f49df1ecd8d8b26c8b", - f"burst-id-geometries-simple-{BURST_DB_VERSION}.geojson.zip": "d9cfe71ec836facd5a782ea82625c30a824b78f2b2689106c4d6808bbfce0898", + f"frame-geometries-simple-{BURST_DB_VERSION}.geojson.zip": "963f63577221a3baa20f3a2101c7a01eefb0cc853f6f111708a5bb35bebfc0ed", + f"burst-id-geometries-simple-{BURST_DB_VERSION}.geojson.zip": "e75cc27809448d7ace2164879626fb0b5616b16981a6b2d6d234e3b17cb615fa", f"opera-s1-disp-burst-to-frame-{BURST_DB_VERSION}.json.zip": "436cce345378dc31e81ed661497bab2e744217a5d63c0bb92817dc837786cd22", f"opera-s1-disp-frame-to-burst-{BURST_DB_VERSION}.json.zip": "a48382afcb89f0ff681982b0fc24476ec9c6c1b8a67ae1a26cf380a450ffadc0", }, From 4ec6ce98c65a23b61ff8264d2f165154be713599 Mon Sep 17 00:00:00 2001 From: Scott Staniewicz Date: Sun, 3 Dec 2023 16:20:26 -0500 Subject: [PATCH 2/3] add the cli for looking up frame bounds --- environment.yml | 2 ++ pyproject.toml | 2 +- src/opera_utils/_helpers.py | 12 +++++++ src/opera_utils/burst_frame_db.py | 4 +-- src/opera_utils/cli.py | 54 +++++++++++++++++++++++++++++++ 5 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 src/opera_utils/cli.py diff --git a/environment.yml b/environment.yml index 04f0a0e..9843854 100644 --- a/environment.yml +++ b/environment.yml @@ -5,7 +5,9 @@ dependencies: - python>=3.8 - pip>=21.3 # https://pip.pypa.io/en/stable/reference/build-system/pyproject-toml/#editable-installation - git # for pip install, due to setuptools_scm + - click>=7.0 - h5py>=1.10 - numpy>=1.20 - pooch>=1.7 + - pyproj>=3.3 - shapely>=1.8 diff --git a/pyproject.toml b/pyproject.toml index 8b59a8a..d26c16b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ Changelog = "https://github.com/opera-adt/opera-utils/releases" # Entry points for the command line interface [project.scripts] -opera_utils = "opera_utils.cli:main" +opera-utils = "opera_utils.cli:cli_app" [tool.setuptools.dynamic] dependencies = { file = ["requirements.txt"] } diff --git a/src/opera_utils/_helpers.py b/src/opera_utils/_helpers.py index 9ac5c7a..fed2a93 100644 --- a/src/opera_utils/_helpers.py +++ b/src/opera_utils/_helpers.py @@ -3,6 +3,8 @@ from itertools import chain, combinations from typing import Any, Iterable, Mapping +from ._types import Bbox + def flatten(list_of_lists: Iterable[Iterable[Any]]) -> chain[Any]: """Flatten one level of nesting.""" @@ -25,3 +27,13 @@ def powerset(iterable: Iterable[Any]) -> chain[tuple[Any, ...]]: """ s = list(iterable) return flatten(combinations(s, r) for r in range(len(s) + 1)) + + +def reproject_bounds(bounds: Bbox, src_epsg: int, dst_epsg: int) -> Bbox: + """Reproject the (left, bottom, right top) from `src_epsg to `dst_epsg`.""" + from pyproj import Transformer + + t = Transformer.from_crs(src_epsg, dst_epsg, always_xy=True) + left, bottom, right, top = bounds + bbox: Bbox = (*t.transform(left, bottom), *t.transform(right, top)) # type: ignore + return bbox diff --git a/src/opera_utils/burst_frame_db.py b/src/opera_utils/burst_frame_db.py index 05f0582..d8b8a24 100644 --- a/src/opera_utils/burst_frame_db.py +++ b/src/opera_utils/burst_frame_db.py @@ -87,8 +87,8 @@ def get_burst_id_geojson( def _form_where_in_query(values: Sequence[str], column_name): # Example: # "burst_id_jpl in ('t005_009471_iw2','t007_013706_iw2','t008_015794_iw1')" - burst_str = ",".join(f"'{b}'" for b in values) - return f"{column_name} IN ({burst_str})" + where_in_str = ",".join(f"'{b}'" for b in values) + return f"{column_name} IN ({where_in_str})" def _get_geojson( diff --git a/src/opera_utils/cli.py b/src/opera_utils/cli.py new file mode 100644 index 0000000..62ce671 --- /dev/null +++ b/src/opera_utils/cli.py @@ -0,0 +1,54 @@ +"""Command-line interface for opera-utils.""" +from __future__ import annotations + +import json +import logging + +import click + +from .burst_frame_db import get_frame_bbox + + +@click.group() +@click.version_option() +@click.option("--debug", is_flag=True, default=False) +@click.pass_context +def cli_app(ctx, debug): + """Orca command-line interface.""" + level = logging.DEBUG if debug else logging.INFO + handler = logging.StreamHandler() + logging.basicConfig(level=level, handlers=[handler]) + + +@cli_app.command() +@click.argument("frame_id") +@click.option( + "--latlon", + "-l", + is_flag=True, + help="Output the bounding box in latitude/longitude (EPSG:4326)", +) +@click.option( + "--bounds-only", + "-b", + is_flag=True, + help="Output only (left, bottom, right, top) and omit EPSG", +) +def frame_bbox(frame_id, latlon: bool, bounds_only: bool): + """Look up the EPSG/bounding box for FRAME_ID. + + Outputs as JSON string to stdout like + {"epsg": 32618, "bbox": [157140.0, 4145220.0, 440520.0, 4375770.0]} + + Unless `--bounds-only` is given + """ + epsg, bounds = get_frame_bbox(frame_id=frame_id) + if latlon: + from opera_utils._helpers import reproject_bounds + + bounds = reproject_bounds(bounds, epsg, 4326) + if bounds_only: + click.echo(list(bounds)) + else: + obj = dict(epsg=epsg, bbox=bounds) + click.echo(json.dumps(obj)) From e9ab93bb3808e2c7a8c7ee599a61a1e7b33ab6a5 Mon Sep 17 00:00:00 2001 From: Scott Staniewicz Date: Sun, 3 Dec 2023 16:25:17 -0500 Subject: [PATCH 3/3] add basic cli test --- tests/test_cli.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 tests/test_cli.py diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..eebe540 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,16 @@ +from click.testing import CliRunner + +from opera_utils.cli import cli_app + + +def test_cli_help(): + runner = CliRunner() + result = runner.invoke(cli_app, ["--help"]) + assert result.exit_code == 0 + assert result.output.startswith("Usage:") + + +def test_frame_bbox_help(): + runner = CliRunner() + result = runner.invoke(cli_app, ["frame-bbox", "--help"]) + assert result.exit_code == 0