Skip to content

Commit 0c1e011

Browse files
author
Ben Cleary
committed
Tidied up and fixed linting, licece and some minor issues see ticket for
full details
1 parent de110fb commit 0c1e011

File tree

19 files changed

+270
-148
lines changed

19 files changed

+270
-148
lines changed

.github/workflows/ci.yml

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,23 @@ env:
1717
MPLCONFIGDIR: ${{ github.workspace }}/.mplconfig
1818

1919
jobs:
20+
lint:
21+
runs-on: ubuntu-latest
22+
steps:
23+
- uses: actions/checkout@v4
24+
- uses: actions/setup-python@v5
25+
with:
26+
python-version: "3.13"
27+
- uses: astral-sh/setup-uv@v5
28+
with:
29+
enable-cache: true
30+
- name: Install dependencies
31+
run: uv sync --frozen --all-extras --group dev
32+
- name: Run Ruff linter
33+
run: uv run ruff check src/
34+
- name: Run Ruff formatter check
35+
run: uv run ruff format --check src/
36+
2037
test:
2138
runs-on: ubuntu-latest
2239
strategy:
@@ -38,7 +55,7 @@ jobs:
3855

3956
build:
4057
runs-on: ubuntu-latest
41-
needs: [test]
58+
needs: [lint, test]
4259
steps:
4360
- uses: actions/checkout@v4
4461
- uses: actions/setup-python@v5

.github/workflows/release.yml

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
8+
permissions:
9+
contents: write
10+
11+
env:
12+
UV_CACHE_DIR: ${{ github.workspace }}/.uv-cache
13+
14+
jobs:
15+
build:
16+
runs-on: ubuntu-latest
17+
steps:
18+
- uses: actions/checkout@v4
19+
20+
- uses: actions/setup-python@v5
21+
with:
22+
python-version: "3.13"
23+
24+
- uses: astral-sh/setup-uv@v5
25+
with:
26+
enable-cache: true
27+
28+
- name: Build sdist and wheel
29+
run: uv build --sdist --wheel
30+
31+
- name: Upload dist artifacts
32+
uses: actions/upload-artifact@v4
33+
with:
34+
name: dist
35+
path: dist/*
36+
37+
github-release:
38+
runs-on: ubuntu-latest
39+
needs: [build]
40+
steps:
41+
- uses: actions/checkout@v4
42+
43+
- name: Download dist artifacts
44+
uses: actions/download-artifact@v4
45+
with:
46+
name: dist
47+
path: dist/
48+
49+
- name: Create GitHub Release
50+
uses: softprops/action-gh-release@v2
51+
with:
52+
files: dist/*
53+
generate_release_notes: true
54+
draft: false
55+
prerelease: false
56+
env:
57+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
58+
59+
# Uncomment this job if you want to publish to PyPI
60+
# pypi-publish:
61+
# runs-on: ubuntu-latest
62+
# needs: [build]
63+
# environment:
64+
# name: pypi
65+
# url: https://pypi.org/project/dtm-differ
66+
# permissions:
67+
# id-token: write # For trusted publishing
68+
# steps:
69+
# - uses: actions/download-artifact@v4
70+
# with:
71+
# name: dist
72+
# path: dist/
73+
#
74+
# - name: Publish to PyPI
75+
# uses: pypa/gh-action-pypi-publish@release/v1

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ dist/
66
wheels/
77
*.egg-info
88

9+
# macOS
10+
.DS_Store
11+
912
# Virtual environments
1013
.venv
1114

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 Ben Cleary
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

pyproject.toml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,38 @@
1+
[build-system]
2+
requires = ["hatchling"]
3+
build-backend = "hatchling.build"
4+
15
[project]
26
name = "dtm-differ"
37
version = "0.1.0"
48
description = "DTM Differ is a Python package for comparing Digital Terrain Models (DTMs). It provides a command-line interface for generating test DTMs and comparing them."
59
readme = "README.md"
610
requires-python = ">=3.13"
11+
license = {text = "MIT"}
12+
authors = [
13+
{name = "Ben Cleary"}
14+
]
15+
keywords = ["geospatial", "dtm", "dem", "terrain", "change-detection", "gis"]
16+
classifiers = [
17+
"Development Status :: 4 - Beta",
18+
"Intended Audience :: Science/Research",
19+
"License :: OSI Approved :: MIT License",
20+
"Programming Language :: Python :: 3",
21+
"Programming Language :: Python :: 3.13",
22+
"Topic :: Scientific/Engineering :: GIS",
23+
]
724
dependencies = [
825
"jinja2>=3.1.6",
926
"rasterio>=1.4.4",
1027
"tqdm>=4.67.1",
1128
"xdem>=0.1.6",
1229
]
1330

31+
[project.urls]
32+
Homepage = "https://github.com/bencleary/dtm-differ"
33+
Repository = "https://github.com/bencleary/dtm-differ"
34+
Issues = "https://github.com/bencleary/dtm-differ/issues"
35+
1436
[project.optional-dependencies]
1537
test = ["pytest>=7.0", "pytest-cov>=4.0"]
1638

@@ -21,4 +43,31 @@ dtm-differ-generate-test-dtms = "dtm_differ.generate_test_dtms:generate_all_scen
2143
[dependency-groups]
2244
dev = [
2345
"pytest>=9.0.2",
46+
"ruff>=0.8.0",
2447
]
48+
49+
[tool.ruff]
50+
line-length = 100
51+
target-version = "py313"
52+
53+
[tool.ruff.lint]
54+
select = [
55+
"E", # pycodestyle errors
56+
"W", # pycodestyle warnings
57+
"F", # pyflakes
58+
"I", # isort
59+
"UP", # pyupgrade
60+
"B", # flake8-bugbear
61+
"SIM", # flake8-simplify
62+
]
63+
ignore = [
64+
"E501", # line too long (handled by formatter)
65+
"SIM108", # ternary operators can reduce readability
66+
"SIM102", # nested ifs sometimes clearer than combined conditions
67+
"SIM103", # explicit return True/False can be clearer
68+
"SIM117", # nested with can be clearer for error handling
69+
]
70+
71+
[tool.ruff.format]
72+
quote-style = "double"
73+
indent-style = "space"

src/dtm_differ/cli.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import xdem
77

8+
from dtm_differ import __version__
89
from dtm_differ.db import Database
910
from dtm_differ.pipeline import run_pipeline
1011
from dtm_differ.pipeline.types import ProcessingConfig
@@ -17,6 +18,7 @@ def build_parser() -> ArgumentParser:
1718
parser = ArgumentParser(
1819
prog="dtm-differ", description="Simple DTM differencing tool for GeoTIFFs"
1920
)
21+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
2022

2123
subparsers = parser.add_subparsers(dest="command", required=True)
2224

@@ -33,9 +35,7 @@ def build_parser() -> ArgumentParser:
3335
"⚠️ WARNING: Assumes input DEMs use meter-based vertical units. "
3436
"Verify your DEM vertical units match this assumption.",
3537
)
36-
run.add_argument(
37-
"--style", choices=["diverging", "terrain", "greyscale"], default="diverging"
38-
)
38+
run.add_argument("--style", choices=["diverging", "terrain", "greyscale"], default="diverging")
3939

4040
run.add_argument(
4141
"--uncertainty",
@@ -121,9 +121,7 @@ def parse_thresholds(raw: str | None) -> tuple[float, float, float]:
121121
return 1.0, values[0], values[1]
122122
if len(values) == 3:
123123
return values[0], values[1], values[2]
124-
raise ValueError(
125-
"Invalid --thresholds; expected 'amber,red' or 'green,amber,red'"
126-
)
124+
raise ValueError("Invalid --thresholds; expected 'amber,red' or 'green,amber,red'")
127125

128126
db = Database(DATABASE_PATH)
129127
db.initialise()

src/dtm_differ/constants.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,3 @@
66
NODATA_DIRECTION: int = 127
77
NODATA_RANK: int = 0
88
NODATA_MASK: int = 255
9-

src/dtm_differ/generate_test_dtms.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def create_test_dem(
3131

3232
# Prepare data: convert nodata to NaN to avoid warnings
3333
prepared_data = data.copy().astype(np.float32)
34-
34+
3535
# Handle non-finite values first (NaN, inf)
3636
non_finite_mask = ~np.isfinite(prepared_data)
3737
if np.any(non_finite_mask):
@@ -40,7 +40,7 @@ def create_test_dem(
4040
prepared_data[non_finite_mask] = nodata
4141
else:
4242
prepared_data[non_finite_mask] = np.nan
43-
43+
4444
# Convert nodata values to NaN before passing to xdem
4545
if nodata is not None:
4646
# Use np.isclose for floating point comparison to handle precision issues
@@ -317,11 +317,11 @@ def generate_scenario_7_mostly_nodata(out_dir: Path) -> None:
317317
def generate_scenario_8_no_nodata_attribute(out_dir: Path) -> None:
318318
"""
319319
Scenario 8: DEMs without nodata attribute but with NaN/inf.
320-
320+
321321
Tests:
322322
- DEMs with NaN values but no nodata attribute
323323
- DEMs with infinite values
324-
324+
325325
Note: NaN/inf will be converted to NaN by create_test_dem, which is fine
326326
for testing how the pipeline handles non-finite values.
327327
"""

src/dtm_differ/geotiff.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,10 @@
33

44
import numpy as np
55
import rasterio
6-
from rasterio.errors import CRSError
7-
from rasterio.errors import RasterioIOError
6+
import xdem
7+
from rasterio.errors import CRSError, RasterioIOError
88

99
from dtm_differ.types import GeoTiffBounds, GeotiffInformation, RasterCompatability
10-
import xdem
1110

1211

1312
def _normalize_linear_units_factor(value: object) -> float | None:
@@ -58,7 +57,7 @@ def validate_geotiff(geotiff_path: str) -> None:
5857
if src.crs is None:
5958
raise ValueError("No CRS")
6059
except RasterioIOError as e:
61-
raise ValueError(f"Invalid GeoTIFF file: {geotiff_path} - {e}")
60+
raise ValueError(f"Invalid GeoTIFF file: {geotiff_path} - {e}") from e
6261

6362

6463
def get_geotiff_metadata(geotiff_path: str) -> tuple[GeotiffInformation, xdem.DEM]:
@@ -141,7 +140,7 @@ def check_raster_compatability(a: GeotiffInformation, b: GeotiffInformation) ->
141140
142141
Returns:
143142
RasterCompatability: The compatibility of the two rasters
144-
143+
145144
Throws:
146145
ValueError: If the two rasters are not compatible
147146
"""
@@ -184,7 +183,7 @@ def reproject_raster(
184183
185184
Throws:
186185
ValueError: If the direction is invalid
187-
"""
186+
"""
188187
match direction:
189188
case "to-a":
190189
return b.reproject(ref=a, resampling=resampling)

src/dtm_differ/pipeline/__init__.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,7 @@ def run_pipeline(
3232
if progress is True:
3333
progress_enabled = True
3434
elif progress is None:
35-
progress_enabled = sys.stderr.isatty() and (
36-
"PYTEST_CURRENT_TEST" not in os.environ
37-
)
35+
progress_enabled = sys.stderr.isatty() and ("PYTEST_CURRENT_TEST" not in os.environ)
3836

3937
if defer_output is True:
4038
defer_output_enabled = True
@@ -57,9 +55,7 @@ def run_pipeline(
5755

5856
# Stage-level progress; some stages (reprojection/polygonize/report) can take a long time.
5957
total_steps = 8
60-
pbar = tqdm(
61-
total=total_steps, desc="dtm-differ", unit="step", file=original_stderr
62-
)
58+
pbar = tqdm(total=total_steps, desc="dtm-differ", unit="step", file=original_stderr)
6359

6460
def step(label: str) -> None:
6561
if pbar is None:
@@ -134,6 +130,8 @@ def step(label: str) -> None:
134130

135131
if exc is None:
136132
db.update_job_status(job_id, status="completed")
133+
else:
134+
db.update_job_status(job_id, status="completed")
137135

138136
return ProcessingResult(
139137
job_id=job_id,
@@ -186,9 +184,7 @@ def _run_pipeline_steps(
186184

187185
if config.generate_report:
188186
try:
189-
stages.generate_report(
190-
rasters, ws, config, a_info, b_info, job_id=ws.job_id
191-
)
187+
stages.generate_report(rasters, ws, config, a_info, b_info, job_id=ws.job_id)
192188
except Exception as e:
193189
warnings.warn(
194190
f"Report generation failed: {e}. Check matplotlib installation and dependencies.",

0 commit comments

Comments
 (0)