rastr 0.4.0__tar.gz → 0.6.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.

Potentially problematic release.


This version of rastr might be problematic. Click here for more details.

Files changed (88) hide show
  1. {rastr-0.4.0 → rastr-0.6.0}/.github/copilot-instructions.md +1 -1
  2. {rastr-0.4.0 → rastr-0.6.0}/.github/workflows/ci.yml +39 -26
  3. rastr-0.6.0/.github/workflows/release.yml +35 -0
  4. {rastr-0.4.0 → rastr-0.6.0}/CONTRIBUTING.md +14 -2
  5. {rastr-0.4.0 → rastr-0.6.0}/PKG-INFO +11 -5
  6. {rastr-0.4.0 → rastr-0.6.0}/README.md +9 -3
  7. {rastr-0.4.0 → rastr-0.6.0}/pyproject.toml +3 -4
  8. {rastr-0.4.0 → rastr-0.6.0}/src/rastr/_version.py +2 -2
  9. {rastr-0.4.0 → rastr-0.6.0}/src/rastr/arr/fill.py +3 -2
  10. {rastr-0.4.0 → rastr-0.6.0}/src/rastr/create.py +32 -18
  11. {rastr-0.4.0 → rastr-0.6.0}/src/rastr/gis/fishnet.py +14 -2
  12. {rastr-0.4.0 → rastr-0.6.0}/src/rastr/gis/smooth.py +4 -4
  13. {rastr-0.4.0 → rastr-0.6.0}/src/rastr/io.py +40 -14
  14. {rastr-0.4.0 → rastr-0.6.0}/src/rastr/raster.py +627 -101
  15. rastr-0.6.0/src/scripts/demo_taper_border.py +4 -0
  16. rastr-0.6.0/tests/conftest.py +54 -0
  17. {rastr-0.4.0 → rastr-0.6.0}/tests/rastr/test_create.py +206 -20
  18. {rastr-0.4.0 → rastr-0.6.0}/tests/rastr/test_io.py +87 -5
  19. {rastr-0.4.0 → rastr-0.6.0}/tests/rastr/test_meta.py +7 -7
  20. rastr-0.6.0/tests/rastr/test_raster.py +3205 -0
  21. {rastr-0.4.0 → rastr-0.6.0}/uv.lock +43 -22
  22. rastr-0.4.0/.github/workflows/release.yml +0 -32
  23. rastr-0.4.0/tests/conftest.py +0 -12
  24. rastr-0.4.0/tests/rastr/test_raster.py +0 -1488
  25. {rastr-0.4.0 → rastr-0.6.0}/.copier-answers.yml +0 -0
  26. {rastr-0.4.0 → rastr-0.6.0}/.github/ISSUE_TEMPLATE/bug-report.md +0 -0
  27. {rastr-0.4.0 → rastr-0.6.0}/.github/ISSUE_TEMPLATE/enhancement.md +0 -0
  28. {rastr-0.4.0 → rastr-0.6.0}/.gitignore +0 -0
  29. {rastr-0.4.0 → rastr-0.6.0}/.pre-commit-config.yaml +0 -0
  30. {rastr-0.4.0 → rastr-0.6.0}/.python-version +0 -0
  31. {rastr-0.4.0 → rastr-0.6.0}/LICENSE +0 -0
  32. {rastr-0.4.0 → rastr-0.6.0}/docs/index.md +0 -0
  33. {rastr-0.4.0 → rastr-0.6.0}/docs/logo.svg +0 -0
  34. {rastr-0.4.0 → rastr-0.6.0}/mkdocs.yml +0 -0
  35. {rastr-0.4.0 → rastr-0.6.0}/pyrightconfig.json +0 -0
  36. {rastr-0.4.0 → rastr-0.6.0}/requirements.txt +0 -0
  37. {rastr-0.4.0 → rastr-0.6.0}/src/archive/.gitkeep +0 -0
  38. {rastr-0.4.0 → rastr-0.6.0}/src/notebooks/.gitkeep +0 -0
  39. {rastr-0.4.0 → rastr-0.6.0}/src/rastr/__init__.py +0 -0
  40. {rastr-0.4.0 → rastr-0.6.0}/src/rastr/arr/__init__.py +0 -0
  41. {rastr-0.4.0 → rastr-0.6.0}/src/rastr/gis/__init__.py +0 -0
  42. {rastr-0.4.0 → rastr-0.6.0}/src/rastr/meta.py +0 -0
  43. {rastr-0.4.0 → rastr-0.6.0}/src/scripts/.gitkeep +0 -0
  44. {rastr-0.4.0 → rastr-0.6.0}/src/scripts/demo_point_cloud.py +0 -0
  45. {rastr-0.4.0 → rastr-0.6.0}/tasks/ABOUT_TASKS.md +0 -0
  46. {rastr-0.4.0 → rastr-0.6.0}/tasks/dev_sync.ps1 +0 -0
  47. {rastr-0.4.0 → rastr-0.6.0}/tasks/scripts/activate_venv.sh +0 -0
  48. {rastr-0.4.0 → rastr-0.6.0}/tasks/scripts/configure_project.sh +0 -0
  49. {rastr-0.4.0 → rastr-0.6.0}/tasks/scripts/dev_sync.sh +0 -0
  50. {rastr-0.4.0 → rastr-0.6.0}/tasks/scripts/install_backend.sh +0 -0
  51. {rastr-0.4.0 → rastr-0.6.0}/tasks/scripts/install_venv.sh +0 -0
  52. {rastr-0.4.0 → rastr-0.6.0}/tasks/scripts/recover_corrupt_venv.sh +0 -0
  53. {rastr-0.4.0 → rastr-0.6.0}/tasks/scripts/sh_runner.ps1 +0 -0
  54. {rastr-0.4.0 → rastr-0.6.0}/tasks/scripts/sync_requirements.sh +0 -0
  55. {rastr-0.4.0 → rastr-0.6.0}/tasks/scripts/sync_template.sh +0 -0
  56. {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/activate_venv +0 -0
  57. {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/activate_venv.cmd +0 -0
  58. {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/activate_venv.ps1 +0 -0
  59. {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/configure_project +0 -0
  60. {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/configure_project.cmd +0 -0
  61. {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/configure_project.ps1 +0 -0
  62. {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/dev_sync +0 -0
  63. {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/dev_sync.cmd +0 -0
  64. {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/dev_sync.ps1 +0 -0
  65. {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/install_backend +0 -0
  66. {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/install_backend.cmd +0 -0
  67. {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/install_backend.ps1 +0 -0
  68. {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/install_venv +0 -0
  69. {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/install_venv.cmd +0 -0
  70. {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/install_venv.ps1 +0 -0
  71. {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/recover_corrupt_venv +0 -0
  72. {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/recover_corrupt_venv.cmd +0 -0
  73. {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/recover_corrupt_venv.ps1 +0 -0
  74. {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/sync_requirements +0 -0
  75. {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/sync_requirements.cmd +0 -0
  76. {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/sync_requirements.ps1 +0 -0
  77. {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/sync_template +0 -0
  78. {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/sync_template.cmd +0 -0
  79. {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/sync_template.ps1 +0 -0
  80. {rastr-0.4.0 → rastr-0.6.0}/tasks/sync_template.ps1 +0 -0
  81. {rastr-0.4.0 → rastr-0.6.0}/tests/assets/.gitkeep +0 -0
  82. {rastr-0.4.0 → rastr-0.6.0}/tests/assets/pga_g_clipped.grd +0 -0
  83. {rastr-0.4.0 → rastr-0.6.0}/tests/assets/pga_g_clipped.tif +0 -0
  84. {rastr-0.4.0 → rastr-0.6.0}/tests/rastr/.gitkeep +0 -0
  85. {rastr-0.4.0 → rastr-0.6.0}/tests/rastr/gis/test_fishnet.py +0 -0
  86. {rastr-0.4.0 → rastr-0.6.0}/tests/rastr/gis/test_smooth.py +0 -0
  87. {rastr-0.4.0 → rastr-0.6.0}/tests/rastr/regression_test_data/test_plot_raster.png +0 -0
  88. {rastr-0.4.0 → rastr-0.6.0}/tests/rastr/regression_test_data/test_write_raster_to_file.tif +0 -0
@@ -10,7 +10,7 @@
10
10
  - Write unit tests using `pytest` inside `tests/`, structured based on `src/`.
11
11
  - Example: `src/x/y/z` → `tests/x/y/test_z.py`
12
12
  - Use test fixtures and group tests into classes when appropriate.
13
- - When testing methods on classes, create nested test classes within the test class associated with the overall tested class (e.g., `TestContour` nested within `TestRasterModel` when testing the `contour` method of `RasterModel`).
13
+ - When testing methods on classes, create nested test classes within the test class associated with the overall tested class (e.g., `TestContour` nested within `TestRaster` when testing the `contour` method of `Raster`).
14
14
  - After modifying a function, run its unit tests using pytest.
15
15
  - Run tests in virtual environment: `.\.venv\Scripts\activate; python -m pytest tests/path/to/test_file.py -v`
16
16
 
@@ -1,32 +1,34 @@
1
1
  name: CI
2
2
  permissions:
3
3
  contents: read
4
- pull-requests: write
5
4
  on:
6
5
  workflow_dispatch:
7
6
  push:
8
- branches: ['master', 'develop']
7
+ branches: ["master", "develop"]
9
8
  paths-ignore:
10
- - 'docs/**'
11
- - '**/*.md'
12
- - 'mkdocs.yml'
9
+ - "docs/**"
10
+ - "**/*.md"
11
+ - "mkdocs.yml"
13
12
  pull_request:
14
- branches: ['master', 'develop']
13
+ branches: ["master", "develop"]
15
14
  paths-ignore:
16
- - 'docs/**'
17
- - '**/*.md'
18
- - 'mkdocs.yml'
15
+ - "docs/**"
16
+ - "**/*.md"
17
+ - "mkdocs.yml"
19
18
  concurrency:
20
19
  group: ${{ github.workflow }}-${{ github.ref }}
21
20
  cancel-in-progress: true
22
21
  jobs:
23
22
  tests:
23
+ name: Run Tests and Checks
24
24
  runs-on: ${{ matrix.os }}
25
25
  env:
26
26
  PYTHONIOENCODING: utf-8
27
27
  steps:
28
28
  - name: Checkout code
29
29
  uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
30
+ with:
31
+ persist-credentials: false
30
32
 
31
33
  - name: Setup git user config
32
34
  run: |
@@ -39,32 +41,38 @@ jobs:
39
41
  version: "0.7.13" # Sync with pyproject.toml
40
42
  enable-cache: true
41
43
 
42
- - name: Set up Python
43
- uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
44
- with:
45
- python-version: ${{ matrix.python-version }}
46
-
47
- - name: Setup dependencies
44
+ - name: Setup Python
48
45
  run: |
49
- uv python pin ${{ matrix.python-version }}
50
- uv export --no-managed-python --no-group doc --resolution ${{ matrix.resolution }} > ci-requirements.txt
51
- uv pip install --system -r ci-requirements.txt
46
+ uv python pin ${PYTHON_VERSION}
47
+ uv python install
48
+ env:
49
+ PYTHON_VERSION: ${{ matrix.python-version }}
52
50
 
53
51
  - name: Run pre-commit
54
52
  if: matrix.checks
55
53
  run: |
56
- uv run --frozen pre-commit run --all-files
54
+ uv run pre-commit run --all-files
57
55
 
58
56
  - name: Run pyright
59
57
  if: matrix.checks
60
58
  run: |
61
59
  uv run pyright
62
60
 
61
+ - name: Run zizmor
62
+ if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13'
63
+ run: |
64
+ uv run zizmor --no-progress --pedantic .github/
65
+ env:
66
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
67
+
63
68
  - name: Run pytest
64
69
  uses: pavelzw/pytest-action@510c5e90c360a185039bea56ce8b3e7e51a16507 # v2.2.0
65
70
  if: matrix.pytest
66
71
  with:
72
+ custom-pytest: uv run pytest
67
73
  custom-arguments: --cov --junitxml=junit.xml -o junit_family=legacy --cov-report=xml
74
+ env:
75
+ MPLBACKEND: Agg # https://github.com/orgs/community/discussions/26434
68
76
 
69
77
  - name: Create test reports directory
70
78
  if: matrix.pytest && matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13'
@@ -95,25 +103,28 @@ jobs:
95
103
  name: Analyse Code Quality
96
104
  runs-on: ubuntu-latest
97
105
  needs: tests
106
+ permissions:
107
+ pull-requests: write # SonarQube needs to post comments on PRs
98
108
  if: always() && needs.tests.result == 'success'
99
-
109
+
100
110
  steps:
101
111
  - name: Checkout code
102
112
  uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
103
113
  with:
104
- fetch-depth: 0 # Shallow clones should be disabled for better relevancy of analysis
105
-
114
+ fetch-depth: 0 # Shallow clones should be disabled for better relevancy of analysis
115
+ persist-credentials: false
116
+
106
117
  - name: Download coverage reports
107
118
  uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
108
119
  with:
109
120
  name: coverage-reports
110
121
  path: test-reports/
111
122
  continue-on-error: true
112
-
123
+
113
124
  - name: Create SonarQube properties
114
125
  run: |
115
126
  cat > sonar-project.properties << EOF
116
- sonar.projectKey=${{ vars.SONAR_PROJECT_KEY }}
127
+ sonar.projectKey=${SONAR_PROJECT_KEY}
117
128
  sonar.language=py
118
129
  sonar.python.version=3.13
119
130
  sonar.sources=./src
@@ -122,9 +133,11 @@ jobs:
122
133
  sonar.exclusions=**/Dockerfile,**/notebooks/**,**/scripts/**
123
134
  sonar.verbose=false
124
135
  EOF
125
-
136
+ env:
137
+ SONAR_PROJECT_KEY: ${{ vars.SONAR_PROJECT_KEY }}
138
+
126
139
  - name: Run SonarQube analysis
127
- uses: SonarSource/sonarqube-scan-action@884b79409bbd464b2a59edc326a4b77dc56b2195 # v3.1.0
140
+ uses: SonarSource/sonarqube-scan-action@fd88b7d7ccbaefd23d8f36f73b59db7a3d246602 # v6.0.0
128
141
  env:
129
142
  SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
130
143
  SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }}
@@ -0,0 +1,35 @@
1
+ name: Release to PyPI
2
+ permissions:
3
+ contents: read
4
+ on:
5
+ push:
6
+ tags:
7
+ - "v*"
8
+ jobs:
9
+ deploy:
10
+ name: Release on PyPI
11
+ runs-on: ubuntu-latest
12
+ environment: release
13
+ permissions:
14
+ id-token: write # This is necessary to use trusted publishing with uv
15
+ steps:
16
+ - name: Checkout code
17
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
18
+ with:
19
+ persist-credentials: false
20
+
21
+ - name: Set up uv
22
+ uses: astral-sh/setup-uv@3b9817b1bf26186f03ab8277bab9b827ea5cc254 # v3.2.0
23
+ with:
24
+ version: "0.7.13" # Sync with pyproject.toml
25
+ enable-cache: false
26
+
27
+ - name: "Set up Python"
28
+ uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
29
+ with:
30
+ python-version: 3.13
31
+
32
+ - name: Release
33
+ run: |
34
+ uv build
35
+ uv publish --trusted-publishing always
@@ -55,9 +55,21 @@ uv run detect-test-pollution --fuzz --tests ./tests
55
55
 
56
56
  and follow the prompts to bisect the tests.
57
57
 
58
- ### Profiling Test collection speed
58
+ ### Profiling the Test Suite
59
59
 
60
- To profile the speed of test collection (which is mostly related to import times), use `pyinstrument`:
60
+ To profile the test suite, use `pyinstrument`:
61
+
62
+ ```shell
63
+ uv run pyinstrument -m pytest
64
+ ```
65
+
66
+ For more in-depth exploration, try the `-r html` option to generate an HTML report:
67
+
68
+ ```shell
69
+ uv run pyinstrument -r html -m pytest
70
+ ```
71
+
72
+ To profile the speed of test _collection_ (which is mostly related to import times), you can also use `pyinstrument`:
61
73
 
62
74
  ```shell
63
75
  uv run pyinstrument -m pytest --collect-only
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rastr
3
- Version: 0.4.0
3
+ Version: 0.6.0
4
4
  Summary: Geospatial Raster datatype library for Python.
5
5
  Project-URL: Source Code, https://github.com/tonkintaylor/rastr
6
6
  Project-URL: Bug Tracker, https://github.com/tonkintaylor/rastr/issues
7
7
  Project-URL: Releases, https://github.com/tonkintaylor/rastr/releases
8
- Project-URL: Source Archive, https://github.com/tonkintaylor/rastr/archive/336916af169603534dd0728c10401667f263d98a.zip
8
+ Project-URL: Source Archive, https://github.com/tonkintaylor/rastr/archive/a1b755ea9cde7f81b2963dcb12917090019f2b47.zip
9
9
  Author-email: Tonkin & Taylor Limited <Sub-DisciplineData+AnalyticsStaff@tonkintaylor.co.nz>, Nathan McDougall <nmcdougall@tonkintaylor.co.nz>, Ben Karl <bkarl@tonkintaylor.co.nz>
10
10
  License-Expression: MIT
11
11
  License-File: LICENSE
@@ -75,10 +75,10 @@ from pyproj.crs.crs import CRS
75
75
  from rasterio.transform import from_origin
76
76
  from rastr.create import full_raster
77
77
  from rastr.meta import RasterMeta
78
- from rastr.raster import RasterModel
78
+ from rastr.raster import Raster
79
79
 
80
- # Create an example raster
81
- raster = RasterModel.example()
80
+ # Read a raster from a file
81
+ raster = Raster.read_file("path/to/raster.tif")
82
82
 
83
83
  # Basic arithmetic operations
84
84
  doubled = raster * 2
@@ -131,6 +131,12 @@ Current version limitations:
131
131
  - Square cells only (rectangular cell support planned).
132
132
  - Only float dtypes (integer support planned).
133
133
 
134
+ ## Similar Projects
135
+
136
+ - [rasters](https://github.com/python-rasters/rasters) is a project with similar goals of providing a dedicated raster datatype in Python with higher-level interfaces for GIS operations. Unlike `rastr`, it has support for multi-band rasters, and has some more advanced functionality for Earth Science applications. Both projects are relatively new and under active development.
137
+ - [rasterio](https://rasterio.readthedocs.io/) is a core dependency of `rastr` and provides low-level raster I/O and processing capabilities.
138
+ - [rioxarray](https://corteva.github.io/rioxarray/stable/getting_started/getting_started.html) extends [`xarray`](https://docs.xarray.dev/en/stable/index.html) for raster data with geospatial support via `rasterio`.
139
+
134
140
  ### Contributing
135
141
 
136
142
  See the
@@ -41,10 +41,10 @@ from pyproj.crs.crs import CRS
41
41
  from rasterio.transform import from_origin
42
42
  from rastr.create import full_raster
43
43
  from rastr.meta import RasterMeta
44
- from rastr.raster import RasterModel
44
+ from rastr.raster import Raster
45
45
 
46
- # Create an example raster
47
- raster = RasterModel.example()
46
+ # Read a raster from a file
47
+ raster = Raster.read_file("path/to/raster.tif")
48
48
 
49
49
  # Basic arithmetic operations
50
50
  doubled = raster * 2
@@ -97,6 +97,12 @@ Current version limitations:
97
97
  - Square cells only (rectangular cell support planned).
98
98
  - Only float dtypes (integer support planned).
99
99
 
100
+ ## Similar Projects
101
+
102
+ - [rasters](https://github.com/python-rasters/rasters) is a project with similar goals of providing a dedicated raster datatype in Python with higher-level interfaces for GIS operations. Unlike `rastr`, it has support for multi-band rasters, and has some more advanced functionality for Earth Science applications. Both projects are relatively new and under active development.
103
+ - [rasterio](https://rasterio.readthedocs.io/) is a core dependency of `rastr` and provides low-level raster I/O and processing capabilities.
104
+ - [rioxarray](https://corteva.github.io/rioxarray/stable/getting_started/getting_started.html) extends [`xarray`](https://docs.xarray.dev/en/stable/index.html) for raster data with geospatial support via `rasterio`.
105
+
100
106
  ### Contributing
101
107
 
102
108
  See the
@@ -63,9 +63,10 @@ dev = [
63
63
  "pre-commit-update>=0.8.0",
64
64
  "pyinstrument>=5.1.1",
65
65
  "pyright[nodejs]>=1.1.403",
66
- "ruff>=0.12.5",
66
+ "ruff>=0.13.2",
67
67
  "tqdm>=4.67.1",
68
68
  "usethis>=0.15.2",
69
+ "zizmor>=1.14.2",
69
70
  ]
70
71
  test = [
71
72
  "coverage[toml]>=7.10.1",
@@ -123,7 +124,6 @@ lint.ignore = [
123
124
  # In some cases where your code is poorly-performing you might want to enable them again.
124
125
  # ##############################
125
126
  "PD101", # Harms readability for a performance optimization.
126
- "PD901", # We often call variables "df" in functions dealing with pandas DataFrames.
127
127
  "PERF203", # Too many false positives.
128
128
  "PERF401", # This can hurt readability; the performance is not always worth it.
129
129
  "PIE804", # This is controversial, some pandas APIs work better with dict approach.
@@ -182,7 +182,6 @@ lint.per-file-ignores."tests/**/*.py" = [
182
182
  "PLR0913", # When we use fixtures, our test functions can have many arguments
183
183
  "S101", # Using assert is fine in tests
184
184
  "SLF001", # Accessing private members is sometimes necessary in tests
185
- "TID253", # Conditional imports are not banned for tests
186
185
  ]
187
186
  lint.allowed-confusables = [ "ℹ", "–", "σ" ]
188
187
  lint.flake8-tidy-imports.banned-api."pytz".msg = "'zoneinfo' should be preferred to 'pytz' when using Python 3.9 and later, see https://tonkintaylor-sonarqube.azurewebsites.net/coding_rules?open=python%3AS6890&rule_key=python%3AS6890"
@@ -265,6 +264,6 @@ root_packages = [ "rastr" ]
265
264
  [[tool.importlinter.contracts]]
266
265
  name = "rastr"
267
266
  type = "layers"
268
- layers = [ "create | io", "raster", "meta", "arr | gis", "_version" ]
267
+ layers = [ "create : io : raster", "meta", "arr | gis", "_version" ]
269
268
  containers = [ "rastr" ]
270
269
  exhaustive = true
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.4.0'
32
- __version_tuple__ = version_tuple = (0, 4, 0)
31
+ __version__ = version = '0.6.0'
32
+ __version_tuple__ = version_tuple = (0, 6, 0)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -8,7 +8,7 @@ if TYPE_CHECKING:
8
8
  from numpy.typing import NDArray
9
9
 
10
10
 
11
- def fillna_nearest_neighbours(arr: NDArray[np.float64]) -> NDArray[np.float64]:
11
+ def fillna_nearest_neighbours(arr: NDArray) -> NDArray:
12
12
  """Fill NaN values in an N-dimensional array with their nearest neighbours' values.
13
13
 
14
14
  The nearest neighbour is determined using the Euclidean distance between array
@@ -28,4 +28,5 @@ def fillna_nearest_neighbours(arr: NDArray[np.float64]) -> NDArray[np.float64]:
28
28
  # Interpolate at the array indices
29
29
  interp = NearestNDInterpolator(nonnan_idxs, arr[nonnan_mask])
30
30
  filled_arr = interp(*np.indices(arr.shape))
31
- return filled_arr
31
+ # Preserve the original dtype
32
+ return filled_arr.astype(arr.dtype)
@@ -13,10 +13,10 @@ from shapely.geometry import Point
13
13
 
14
14
  from rastr.gis.fishnet import create_point_grid, get_point_grid_shape
15
15
  from rastr.meta import RasterMeta
16
- from rastr.raster import RasterModel
16
+ from rastr.raster import Raster
17
17
 
18
18
  if TYPE_CHECKING:
19
- from collections.abc import Iterable
19
+ from collections.abc import Collection, Iterable
20
20
 
21
21
  import geopandas as gpd
22
22
  from numpy.typing import ArrayLike
@@ -49,9 +49,9 @@ def raster_distance_from_polygon(
49
49
  *,
50
50
  raster_meta: RasterMeta,
51
51
  extent_polygon: Polygon | None = None,
52
- snap_raster: RasterModel | None = None,
52
+ snap_raster: Raster | None = None,
53
53
  show_pbar: bool = False,
54
- ) -> RasterModel:
54
+ ) -> Raster:
55
55
  """Make a raster where each cell's value is its centre's distance to a polygon.
56
56
 
57
57
  The raster should use a projected coordinate system.
@@ -116,7 +116,7 @@ def raster_distance_from_polygon(
116
116
  distances = np.where(mask, np.array([polygon.distance(pt) for pt in _pts]), np.nan)
117
117
  distance_raster = distances.reshape(x.shape)
118
118
 
119
- return RasterModel(arr=distance_raster, raster_meta=raster_meta)
119
+ return Raster(arr=distance_raster, raster_meta=raster_meta)
120
120
 
121
121
 
122
122
  def _pbar(iterable: Iterable[_T], *, desc: str | None = None) -> Iterable[_T]:
@@ -130,19 +130,19 @@ def full_raster(
130
130
  *,
131
131
  bounds: tuple[float, float, float, float],
132
132
  fill_value: float = np.nan,
133
- ) -> RasterModel:
133
+ ) -> Raster:
134
134
  """Create a raster with a specified fill value for all cells."""
135
135
  shape = get_point_grid_shape(bounds=bounds, cell_size=raster_meta.cell_size)
136
136
  arr = np.full(shape, fill_value, dtype=np.float32)
137
- return RasterModel(arr=arr, raster_meta=raster_meta)
137
+ return Raster(arr=arr, raster_meta=raster_meta)
138
138
 
139
139
 
140
140
  def rasterize_gdf(
141
141
  gdf: gpd.GeoDataFrame,
142
142
  *,
143
143
  raster_meta: RasterMeta,
144
- target_cols: list[str],
145
- ) -> list[RasterModel]:
144
+ target_cols: Collection[str],
145
+ ) -> list[Raster]:
146
146
  """Rasterize geometries from a GeoDataFrame.
147
147
 
148
148
  Supports polygons, points, linestrings, and other geometry types.
@@ -205,14 +205,16 @@ def rasterize_gdf(
205
205
  dtype=np.float32,
206
206
  )
207
207
 
208
- # Create RasterModel
209
- raster = RasterModel(arr=raster_array, raster_meta=raster_meta)
208
+ # Create Raster
209
+ raster = Raster(arr=raster_array, raster_meta=raster_meta)
210
210
  rasters.append(raster)
211
211
 
212
212
  return rasters
213
213
 
214
214
 
215
- def _validate_columns_exist(gdf: gpd.GeoDataFrame, target_cols: list[str]) -> None:
215
+ def _validate_columns_exist(
216
+ gdf: gpd.GeoDataFrame, target_cols: Collection[str]
217
+ ) -> None:
216
218
  """Validate that all target columns exist in the GeoDataFrame.
217
219
 
218
220
  Args:
@@ -228,7 +230,9 @@ def _validate_columns_exist(gdf: gpd.GeoDataFrame, target_cols: list[str]) -> No
228
230
  raise MissingColumnsError(msg)
229
231
 
230
232
 
231
- def _validate_columns_numeric(gdf: gpd.GeoDataFrame, target_cols: list[str]) -> None:
233
+ def _validate_columns_numeric(
234
+ gdf: gpd.GeoDataFrame, target_cols: Collection[str]
235
+ ) -> None:
232
236
  """Validate that all target columns contain numeric data.
233
237
 
234
238
  Args:
@@ -286,7 +290,7 @@ def raster_from_point_cloud(
286
290
  *,
287
291
  crs: CRS | str,
288
292
  cell_size: float | None = None,
289
- ) -> RasterModel:
293
+ ) -> Raster:
290
294
  """Create a raster from a point cloud via interpolation.
291
295
 
292
296
  Interpolation is only possible within the convex hull of the points. Outside of
@@ -320,8 +324,18 @@ def raster_from_point_cloud(
320
324
  if len(x) != len(y) or len(x) != len(z):
321
325
  msg = "Length of x, y, and z must be equal."
322
326
  raise ValueError(msg)
327
+ xy_finite_mask = np.isfinite(x) & np.isfinite(y)
328
+ if np.any(~xy_finite_mask):
329
+ msg = "Some (x,y) points are NaN-valued or non-finite. These will be ignored."
330
+ warnings.warn(msg, stacklevel=2)
331
+ x = x[xy_finite_mask]
332
+ y = y[xy_finite_mask]
333
+ z = z[xy_finite_mask]
323
334
  if len(x) < 3:
324
- msg = "At least three (x, y, z) points are required to triangulate a surface."
335
+ msg = (
336
+ "At least three valid (x, y, z) points are required to triangulate a "
337
+ "surface."
338
+ )
325
339
  raise ValueError(msg)
326
340
  # Check for duplicate (x, y) points
327
341
  xy_points = np.column_stack((x, y))
@@ -332,8 +346,8 @@ def raster_from_point_cloud(
332
346
  # Heuristic for cell size if not provided
333
347
  if cell_size is None:
334
348
  # Half the 5th percentile of nearest neighbor distances between the (x,y) points
335
- tree = KDTree(np.column_stack((x, y)))
336
- distances, _ = tree.query(np.column_stack((x, y)), k=2)
349
+ tree = KDTree(xy_points)
350
+ distances, _ = tree.query(xy_points, k=2)
337
351
  distances: np.ndarray
338
352
  cell_size = float(np.percentile(distances[distances > 0], 5)) / 2
339
353
 
@@ -378,4 +392,4 @@ def raster_from_point_cloud(
378
392
  crs=crs,
379
393
  transform=transform,
380
394
  )
381
- return RasterModel(arr=arr, raster_meta=raster_meta)
395
+ return Raster(arr=arr, raster_meta=raster_meta)
@@ -41,8 +41,20 @@ def get_point_grid_shape(
41
41
  """Calculate the shape of the point grid based on bounds and cell size."""
42
42
 
43
43
  xmin, ymin, xmax, ymax = bounds
44
- ncols = int(np.ceil((xmax - xmin) / cell_size))
45
- nrows = int(np.ceil((ymax - ymin) / cell_size))
44
+ ncols_exact = (xmax - xmin) / cell_size
45
+ nrows_exact = (ymax - ymin) / cell_size
46
+
47
+ # Use round for values very close to integers to avoid floating-point
48
+ # sensitivity while maintaining ceil behavior for truly fractional values
49
+ if np.isclose(ncols_exact, np.round(ncols_exact)):
50
+ ncols = int(np.round(ncols_exact))
51
+ else:
52
+ ncols = int(np.ceil(ncols_exact))
53
+
54
+ if np.isclose(nrows_exact, np.round(nrows_exact)):
55
+ nrows = int(np.round(nrows_exact))
56
+ else:
57
+ nrows = int(np.ceil(nrows_exact))
46
58
 
47
59
  return nrows, ncols
48
60
 
@@ -5,7 +5,7 @@ Fork + Port of <https://github.com/philipschall/shapelysmooth> (Public domain)
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
- from typing import TYPE_CHECKING, TypeAlias
8
+ from typing import TYPE_CHECKING, TypeVar
9
9
 
10
10
  import numpy as np
11
11
  from shapely.geometry import LineString, Polygon
@@ -14,7 +14,7 @@ from typing_extensions import assert_never
14
14
  if TYPE_CHECKING:
15
15
  from numpy.typing import NDArray
16
16
 
17
- T: TypeAlias = LineString | Polygon
17
+ T = TypeVar("T", bound=LineString | Polygon)
18
18
 
19
19
 
20
20
  class InputeTypeError(TypeError):
@@ -38,12 +38,12 @@ def catmull_rom_smooth(geometry: T, alpha: float = 0.5, subdivs: int = 10) -> T:
38
38
  coords, interior_coords = _get_coords(geometry)
39
39
  coords_smoothed = _catmull_rom(coords, alpha=alpha, subdivs=subdivs)
40
40
  if isinstance(geometry, LineString):
41
- return type(geometry)(coords_smoothed)
41
+ return geometry.__class__(coords_smoothed)
42
42
  elif isinstance(geometry, Polygon):
43
43
  interior_coords_smoothed = [
44
44
  _catmull_rom(c, alpha=alpha, subdivs=subdivs) for c in interior_coords
45
45
  ]
46
- return type(geometry)(coords_smoothed, holes=interior_coords_smoothed)
46
+ return geometry.__class__(coords_smoothed, holes=interior_coords_smoothed)
47
47
  else:
48
48
  assert_never(geometry)
49
49
 
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from pathlib import Path
4
- from typing import TYPE_CHECKING
4
+ from typing import TYPE_CHECKING, TypeVar
5
5
 
6
6
  import numpy as np
7
7
  import rasterio
@@ -9,39 +9,59 @@ import rasterio.merge
9
9
  from pyproj.crs.crs import CRS
10
10
 
11
11
  from rastr.meta import RasterMeta
12
- from rastr.raster import RasterModel
12
+ from rastr.raster import Raster
13
13
 
14
14
  if TYPE_CHECKING:
15
15
  from numpy.typing import NDArray
16
16
 
17
+ R = TypeVar("R", bound=Raster)
18
+
17
19
 
18
20
  def read_raster_inmem(
19
- raster_path: Path | str, *, crs: CRS | str | None = None
20
- ) -> RasterModel:
21
- """Read raster data from a file and return an in-memory Raster object."""
21
+ raster_path: Path | str,
22
+ *,
23
+ crs: CRS | str | None = None,
24
+ cls: type[R] = Raster,
25
+ ) -> R:
26
+ """Read raster data from a file and return an in-memory Raster object.
27
+
28
+ Args:
29
+ raster_path: Path to the raster file.
30
+ crs: Optional CRS to override the raster's native CRS.
31
+ cls: The Raster subclass to instantiate. This is mostly for internal use,
32
+ but can be useful if you have a custom `Raster` subclass.
33
+ """
22
34
  crs = CRS.from_user_input(crs) if crs is not None else None
23
35
 
24
36
  with rasterio.open(raster_path, mode="r") as dst:
25
37
  # Read the entire array
26
- arr: NDArray[np.float64] = dst.read()
27
- arr = arr.squeeze().astype(np.float64)
38
+ raw_arr: NDArray = dst.read()
39
+ raw_arr = raw_arr.squeeze()
40
+
28
41
  # Extract metadata
29
42
  cell_size = dst.res[0]
30
43
  if crs is None:
31
44
  crs = CRS.from_user_input(dst.crs)
32
45
  transform = dst.transform
33
46
  nodata = dst.nodata
47
+
48
+ # Cast integers to float16 to handle NaN values
49
+ if np.issubdtype(raw_arr.dtype, np.integer):
50
+ arr = raw_arr.astype(np.float16)
51
+ else:
52
+ arr = raw_arr
53
+
34
54
  if nodata is not None:
35
- arr[arr == nodata] = np.nan
55
+ arr[raw_arr == nodata] = np.nan
36
56
 
37
57
  raster_meta = RasterMeta(cell_size=cell_size, crs=crs, transform=transform)
38
- raster_obj = RasterModel(arr=arr, raster_meta=raster_meta)
58
+ raster_obj = cls(arr=arr, raster_meta=raster_meta)
39
59
  return raster_obj
40
60
 
41
61
 
42
62
  def read_raster_mosaic_inmem(
43
63
  mosaic_dir: Path | str, *, glob: str = "*.tif", crs: CRS | None = None
44
- ) -> RasterModel:
64
+ ) -> Raster:
45
65
  """Read a raster mosaic from a directory and return an in-memory Raster object.
46
66
 
47
67
  This assumes that all rasters have the same metadata, e.g. coordinate system,
@@ -81,13 +101,19 @@ def read_raster_mosaic_inmem(
81
101
  crs = CRS.from_user_input(sources[0].crs)
82
102
 
83
103
  nodata = sources[0].nodata
84
- if nodata is not None:
85
- arr[arr == nodata] = np.nan
104
+ raw_arr = arr.squeeze()
86
105
 
87
- arr = arr.squeeze().astype(np.float64)
106
+ # Cast integers to float16 to handle NaN values
107
+ if np.issubdtype(raw_arr.dtype, np.integer):
108
+ arr = raw_arr.astype(np.float16)
109
+ else:
110
+ arr = raw_arr
111
+
112
+ if nodata is not None:
113
+ arr[raw_arr == nodata] = np.nan
88
114
 
89
115
  raster_meta = RasterMeta(cell_size=cell_size, crs=crs, transform=transform)
90
- raster_obj = RasterModel(arr=arr, raster_meta=raster_meta)
116
+ raster_obj = Raster(arr=arr, raster_meta=raster_meta)
91
117
  return raster_obj
92
118
  finally:
93
119
  for src in sources: