[Git][debian-gis-team/xradarsat2][upstream] New upstream version 2025.10.15+ds

Antonio Valentino (@antonio.valentino) gitlab at salsa.debian.org
Thu Oct 23 07:14:59 BST 2025



Antonio Valentino pushed to branch upstream at Debian GIS Project / xradarsat2


Commits:
9c5bd253 by Antonio Valentino at 2025-10-23T06:11:30+00:00
New upstream version 2025.10.15+ds
- - - - -


18 changed files:

- .git_archival.txt
- + .github/workflows/ci.yaml
- .github/workflows/publish.yml
- + .github/workflows/upstream-dev.yaml
- .gitignore
- + ci/install-upstream-dev.sh
- + ci/requirements/docs.yaml
- + ci/requirements/environment.yaml
- docs/doc_xradarsat2.ipynb
- + end2end/check_opening_datatree_radarsat2.py
- pyproject.toml
- src/xradarsat2/__init__.py
- src/xradarsat2/radarSat2_tiff_reader.py
- src/xradarsat2/radarSat2_xarray_reader.py
- + src/xradarsat2/tests/test_radarSat2_xarray_reader.py
- src/xradarsat2/utils.py
- src/xradarsat2/xradarsat2-config.yaml
- test/test_opening_datatree_radarsat2.py


Changes:

=====================================
.git_archival.txt
=====================================
@@ -1,4 +1,4 @@
-node: de1141391ab0b197ade12e6427804c6e89b95d80
-node-date: 2024-11-12T14:26:55+01:00
-describe-name: 2024.11.12
-ref-names: HEAD -> main, tag: 2024.11.12
+node: 4391409af1c60cb29670a1c3ff849b40dd88fe0b
+node-date: 2025-10-15T12:08:45+02:00
+describe-name: 2025.10.15
+ref-names: tag: 2025.10.15


=====================================
.github/workflows/ci.yaml
=====================================
@@ -0,0 +1,85 @@
+name: CI
+
+on:
+  push:
+    branches: [main]
+  pull_request:
+    branches: [main]
+  workflow_dispatch:
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+jobs:
+  detect-skip-ci-trigger:
+    name: "Detect CI Trigger: [skip-ci]"
+    if: |
+      github.repository == 'umr-lops/xradarsat2'
+      && (
+          github.event_name == 'push' || github.event_name == 'pull_request'
+      )
+    runs-on: ubuntu-latest
+    outputs:
+      triggered: ${{ steps.detect-trigger.outputs.trigger-found }}
+    steps:
+      - uses: actions/checkout at v4
+        with:
+          fetch-depth: 2
+      - uses: xarray-contrib/ci-trigger at v1
+        id: detect-trigger
+        with:
+          keyword: "[skip-ci]"
+
+  ci:
+    name: ${{ matrix.os }} py${{ matrix.python-version }}
+    runs-on: ${{ matrix.os }}
+    needs: detect-skip-ci-trigger
+
+    if: needs.detect-skip-ci-trigger.outputs.triggered == 'false'
+
+    defaults:
+      run:
+        shell: bash -l {0}
+
+    strategy:
+      fail-fast: false
+      matrix:
+        python-version: ["3.10", "3.11", "3.12"]
+        os: ["ubuntu-latest", "macos-latest", "windows-latest"]
+
+    steps:
+      - name: Checkout the repository
+        uses: actions/checkout at v4
+        with:
+          # need to fetch all tags to get a correct version
+          fetch-depth: 0 # fetch all branches and tags
+
+      - name: Setup environment variables
+        run: |
+          echo "TODAY=$(date +'%Y-%m-%d')" >> $GITHUB_ENV
+
+          echo "CONDA_ENV_FILE=ci/requirements/environment.yaml" >> $GITHUB_ENV
+
+      - name: Setup micromamba
+        uses: mamba-org/setup-micromamba at v2
+        with:
+          environment-file: ${{ env.CONDA_ENV_FILE }}
+          environment-name: xradarsat2-tests
+          cache-environment: true
+          generate-run-shell: true
+          cache-environment-key: "${{runner.os}}-${{runner.arch}}-py${{matrix.python-version}}-${{env.TODAY}}-${{hashFiles(env.CONDA_ENV_FILE)}}"
+          create-args: >-
+            python=${{matrix.python-version}}
+
+      - name: Install xradarsat2
+        run: |
+          python -m pip install --no-deps -e .
+
+      - name: Import xradarsat2
+        run: |
+          python -c "import xradarsat2"
+
+      - name: Run tests
+        run: |
+          python -m pytest --cov=xradarsat2


=====================================
.github/workflows/publish.yml
=====================================
@@ -12,9 +12,9 @@ jobs:
       contents: 'read'
       id-token: 'write'
     steps:
-      - uses: actions/checkout at v4
+      - uses: actions/checkout at v5
       - name: Set up Python
-        uses: actions/setup-python at v5
+        uses: actions/setup-python at v6
         with:
           python-version: '3.x'
       - name: Install dependencies
@@ -28,7 +28,7 @@ jobs:
           twine check dist/*
           pip install dist/*.whl
       - name: Publish to PyPI
-        uses: pypa/gh-action-pypi-publish at 15c56dba361d8335944d31a2ecd17d700fc7bcbc
+        uses: pypa/gh-action-pypi-publish at ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e
         with:
           password: ${{ secrets.pypi_token }}
           repository_url: https://upload.pypi.org/legacy/


=====================================
.github/workflows/upstream-dev.yaml
=====================================
@@ -0,0 +1,98 @@
+name: upstream-dev CI
+
+on:
+  push:
+    branches: [main]
+  pull_request:
+    branches: [main]
+  schedule:
+    - cron: "0 18 * * 0" # Weekly "On Sundays at 18:00" UTC
+  workflow_dispatch:
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+jobs:
+  detect-test-upstream-trigger:
+    name: "Detect CI Trigger: [test-upstream]"
+    if: github.event_name == 'push' || github.event_name == 'pull_request'
+    runs-on: ubuntu-latest
+    outputs:
+      triggered: ${{ steps.detect-trigger.outputs.trigger-found }}
+    steps:
+      - uses: actions/checkout at v4
+        with:
+          fetch-depth: 2
+      - uses: xarray-contrib/ci-trigger at v1.2
+        id: detect-trigger
+        with:
+          keyword: "[test-upstream]"
+
+  upstream-dev:
+    name: upstream-dev
+    runs-on: ubuntu-latest
+    needs: detect-test-upstream-trigger
+
+    if: |
+      always()
+      && github.repository == 'umr-lops/xradarsat2'
+      && (
+        github.event_name == 'schedule'
+        || github.event_name == 'workflow_dispatch'
+        || needs.detect-test-upstream-trigger.outputs.triggered == 'true'
+        || contains(github.event.pull_request.labels.*.name, 'run-upstream')
+      )
+
+    defaults:
+      run:
+        shell: bash -l {0}
+
+    strategy:
+      fail-fast: false
+      matrix:
+        python-version: ["3.12"]
+
+    steps:
+      - name: checkout the repository
+        uses: actions/checkout at v4
+        with:
+          # need to fetch all tags to get a correct version
+          fetch-depth: 0 # fetch all branches and tags
+
+      - name: set up conda environment
+        uses: mamba-org/setup-micromamba at v1
+        with:
+          environment-file: ci/requirements/environment.yaml
+          environment-name: tests
+          create-args: >-
+            python=${{ matrix.python-version }}
+            pytest-reportlog
+
+      - name: install upstream-dev dependencies
+        run: bash ci/install-upstream-dev.sh
+
+      - name: install the package
+        run: python -m pip install --no-deps -e .
+
+      - name: show versions
+        run: python -m pip list
+
+      - name: import
+        run: |
+          python -c 'import xradarsat2'
+
+      - name: run tests
+        if: success()
+        id: status
+        run: |
+          python -m pytest -rf --report-log=pytest-log.jsonl
+
+      - name: report failures
+        if: |
+          failure()
+          && steps.tests.outcome == 'failure'
+          && github.event_name == 'schedule'
+        uses: xarray-contrib/issue-from-pytest-log at v1
+        with:
+          log-path: pytest-log.jsonl


=====================================
.gitignore
=====================================
@@ -129,4 +129,4 @@ tmp/
 /.idea/
 
 dask-worker-space/
-localxradarsat2-config.yaml
\ No newline at end of file
+localxradarsat2-config.yaml


=====================================
ci/install-upstream-dev.sh
=====================================
@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+
+conda remove -y --force cytoolz numpy xarray toolz python-dateutil rioxarray
+python -m pip install \
+  -i https://pypi.anaconda.org/scientific-python-nightly-wheels/simple \
+  --no-deps \
+  --pre \
+  --upgrade \
+  numpy \
+  rioxarray \
+  xarray
+python -m pip install --upgrade \
+  git+https://github.com/pytoolz/toolz \
+  git+https://github.com/dateutil/dateutil


=====================================
ci/requirements/docs.yaml
=====================================
@@ -0,0 +1,8 @@
+name: xradarsat2-docs
+channels:
+  - conda-forge
+dependencies:
+  - python=3.11
+  - sphinx>=4
+  - sphinx_book_theme
+  - ipython


=====================================
ci/requirements/environment.yaml
=====================================
@@ -0,0 +1,26 @@
+name: xradarsat2-tests
+channels:
+  - conda-forge
+dependencies:
+  - python
+  - ipython
+  - pre-commit
+  - pytest
+  - pytest-reportlog
+  - pytest-cov
+  - numpy
+  - toolz
+  - cytoolz
+  - python-dateutil
+  - construct
+  - xarray
+  - rioxarray>=0.17.0
+  - aiohttp
+  - fsspec
+  - dask
+  - opencv
+  - pyyaml
+  - affine
+  - h5netcdf
+  - scipy
+  - xmltodict


=====================================
docs/doc_xradarsat2.ipynb
=====================================
@@ -2,7 +2,7 @@
  "cells": [
   {
    "cell_type": "markdown",
-   "id": "03cfc89b-5ee1-49c2-8bc9-ca88c00eb63b",
+   "id": "0",
    "metadata": {},
    "source": [
     "# examples"
@@ -11,7 +11,7 @@
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "9f901d61-adf5-4ad2-a721-b92fb8555642",
+   "id": "1",
    "metadata": {},
    "outputs": [],
    "source": [
@@ -23,7 +23,7 @@
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "84c1a31d-c27c-4614-8a39-f4dca2801bf6",
+   "id": "2",
    "metadata": {},
    "outputs": [],
    "source": [
@@ -36,7 +36,7 @@
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "90fb68ff-2c19-4432-8d95-98159ee546a8",
+   "id": "3",
    "metadata": {},
    "outputs": [],
    "source": [
@@ -48,7 +48,7 @@
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "ff1730a0-caeb-4c53-8fa0-85c79adfb5a6",
+   "id": "4",
    "metadata": {},
    "outputs": [],
    "source": [
@@ -59,7 +59,7 @@
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "6f74bb35-b8d0-4a17-82d2-4b151e3044fb",
+   "id": "5",
    "metadata": {},
    "outputs": [],
    "source": [
@@ -74,11 +74,6 @@
   }
  ],
  "metadata": {
-  "kernelspec": {
-   "display_name": "Python 3 (ipykernel)",
-   "language": "python",
-   "name": "python3"
-  },
   "language_info": {
    "codemirror_mode": {
     "name": "ipython",
@@ -88,8 +83,7 @@
    "mimetype": "text/x-python",
    "name": "python",
    "nbconvert_exporter": "python",
-   "pygments_lexer": "ipython3",
-   "version": "3.11.0"
+   "pygments_lexer": "ipython3"
   }
  },
  "nbformat": 4,


=====================================
end2end/check_opening_datatree_radarsat2.py
=====================================
@@ -0,0 +1,25 @@
+from xradarsat2.utils import load_config
+import xradarsat2
+import time
+import logging
+
+logging.basicConfig(level=logging.DEBUG)
+logging.debug("start opening RadarSAT-2 product")
+# conf = getconfig.get_config()
+# subswath = conf['product_paths'][0]
+
+t0 = time.time()
+conf = load_config()
+folder_path = conf["folder_path"]
+dt = xradarsat2.rs2_reader(folder_path)
+elapse_t = time.time() - t0
+
+print(type(dt), dt)
+print("out of the reader")
+print(dt)
+print("time to read the SAFE through nfs: %1.2f sec" % elapse_t)
+dt = xradarsat2.load_digital_number(
+    dt, chunks={"pol": "VV", "line": 6000, "sample": 8000}
+)
+print("DN", dt["/digital_numbers"])
+# pdb.set_trace()


=====================================
pyproject.toml
=====================================
@@ -15,31 +15,65 @@ fallback_version = "9999"
 
 [project]
 name = "xradarsat2"
-authors = [
-  { name="Yann Reynaud", email="Yann.Reynaud.2 at ifremer.fr" },
-]
-license = {text = "MIT"}
-description = "xarray/dask distributed L1 sar file reader for radarSat2"
+authors = [{ name = "Yann Reynaud", email = "Yann.Reynaud.2 at ifremer.fr" }]
+license = { text = "MIT" }
+description = "xarray Level-1 SAR file reader for radarSat2"
 readme = "README.md"
 requires-python = ">=3.9"
 classifiers = [
-    "Programming Language :: Python :: 3",
-    "License :: OSI Approved :: MIT License",
-    "Operating System :: OS Independent",
+  "Programming Language :: Python :: 3",
+  "License :: OSI Approved :: MIT License",
+  "Operating System :: OS Independent",
 ]
 dependencies = [
-    'xmltodict',
-    'numpy',
-    "xarray>=2024.10.0",
-    'rasterio',
-    'rioxarray',
-    'dask',
-    'affine',
-    'regex',
-    'pyyaml'
+  'xmltodict',
+  'numpy',
+  "xarray>=2024.10.0",
+  'rasterio',
+  'rioxarray>=0.18.1',
+  'dask',
+  'affine',
+  'regex',
+  'pyyaml',
+  'fsspec',
+  'aiohttp',
+
 ]
 dynamic = ["version"]
 
 [project.urls]
 "Homepage" = "https://github.com/umr-lops/xradarsat2"
 "Bug Tracker" = "https://github.com/umr-lops/xradarsat2/issues"
+
+[tool.coverage.report]
+show_missing = true
+exclude_lines = ["pragma: no cover", "if TYPE_CHECKING"]
+
+[tool.ruff.lint]
+ignore = [
+  "E402",  # module level import not at top of file
+  "E501",  # line too long - let black worry about that
+  "E731",  # do not assign a lambda expression, use a def
+  "UP038", # type union instead of tuple for isinstance etc
+]
+select = [
+  "F",   # Pyflakes
+  "E",   # Pycodestyle
+  "I",   # isort
+  "UP",  # Pyupgrade
+  "TID", # flake8-tidy-imports
+  "W",
+]
+extend-safe-fixes = [
+  "TID252", # absolute imports
+  "UP031",  # percent string interpolation
+]
+fixable = ["I", "TID252", "UP"]
+
+[tool.ruff.lint.isort]
+known-first-party = ["xradarsat2"]
+known-third-party = ["xarray", "toolz", "construct"]
+
+[tool.ruff.lint.flake8-tidy-imports]
+# Disallow all relative imports.
+ban-relative-imports = "all"


=====================================
src/xradarsat2/__init__.py
=====================================
@@ -1,7 +1,10 @@
-from xradarsat2.radarSat2_xarray_reader import load_digital_number  # noqa: F401
-from xradarsat2.radarSat2_xarray_reader import rs2_reader  # noqa: F401
 from importlib.metadata import version
 
+from xradarsat2.radarSat2_xarray_reader import (
+    load_digital_number,  # noqa: F401
+    rs2_reader,  # noqa: F401
+)
+
 try:
     __version__ = version("xradarsat2")
 except Exception:


=====================================
src/xradarsat2/radarSat2_tiff_reader.py
=====================================
@@ -8,6 +8,7 @@ import rioxarray
 import xarray as xr
 import yaml
 from affine import Affine
+
 from xradarsat2.utils import get_glob, load_config
 
 # folder_path = "/home/datawork-cersat-public/cache/project/sarwing/data/RS2/L1/VV/2010/288/" \
@@ -43,12 +44,7 @@ def _load_digital_number(
     tiff_files = list_tiff_files(root_path)
     map_dims = {"pol": "band", "line": "y", "sample": "x"}
     if resolution is not None:
-        comment = 'resampled at "%s" with %s.%s.%s' % (
-            resolution,
-            resampling.__module__,
-            resampling.__class__.__name__,
-            resampling.name,
-        )
+        comment = f'resampled at "{resolution}" with {resampling.__module__}.{resampling.__class__.__name__}.{resampling.name}'
     else:
         comment = "read at full resolution"
 
@@ -158,7 +154,7 @@ def _load_digital_number(
     var_name = "digital_number"
 
     dn.attrs = {
-        "comment": "%s digital number, %s" % (descr, comment),
+        "comment": f"{descr} digital number, {comment}",
         "history": yaml.safe_dump(
             {var_name: get_glob([p.replace(root_path + "/", "") for p in tiff_files])}
         ),


=====================================
src/xradarsat2/radarSat2_xarray_reader.py
=====================================
@@ -4,6 +4,7 @@ import glob
 import os
 import re
 import traceback
+from datetime import datetime
 
 import dask
 import numpy as np
@@ -13,7 +14,6 @@ import xarray as xr
 import xmltodict
 import yaml
 from affine import Affine
-from datetime import datetime
 
 xpath_dict = {
     "geolocation_grid": {
@@ -1896,12 +1896,7 @@ def load_digital_number(
     tiff_files, pols = sort_list_files_and_get_pols(tiff_files)
     map_dims = {"pol": "band", "line": "y", "sample": "x"}
     if resolution is not None:
-        comment = 'resampled at "%s" with %s.%s.%s' % (
-            resolution,
-            resampling.__module__,
-            resampling.__class__.__name__,
-            resampling.name,
-        )
+        comment = f'resampled at "{resolution}" with {resampling.__module__}.{resampling.__class__.__name__}.{resampling.name}'
     else:
         comment = "read at full resolution"
 
@@ -2024,7 +2019,7 @@ def load_digital_number(
     var_name = "digital_number"
 
     dn.attrs = {
-        "comment": "%s digital number, %s" % (descr, comment),
+        "comment": f"{descr} digital number, {comment}",
         "history": yaml.safe_dump(
             {
                 var_name: get_glob(


=====================================
src/xradarsat2/tests/test_radarSat2_xarray_reader.py
=====================================
@@ -0,0 +1,442 @@
+import unittest
+from unittest.mock import Mock, patch, mock_open, MagicMock
+import numpy as np
+import xarray as xr
+from datetime import datetime
+import os
+import tempfile
+
+# Import functions from the module
+from xradarsat2.radarSat2_xarray_reader import (
+    xpath_get,
+    parse_value,
+    create_2d_matrix,
+    get_line_and_pix_info,
+    fill_image_attribute,
+    sort_list_files_and_get_pols,
+    get_glob,
+    get_product_attributes,
+    get_satellite_height,
+    get_satellite_pass_direction,
+    create_dic_geolocation_grid,
+    get_dic_orbit_information,
+    get_dic_attitude_info,
+    get_dict_doppler_centroid,
+    get_dic_doppler_rate_values,
+    get_dict_chirp,
+    get_dict_radar_parameters,
+)
+
+
+class TestXpathGet(unittest.TestCase):
+    """Test cases for xpath_get function"""
+    
+    def test_xpath_get_valid_path(self):
+        test_dict = {"product": {"sourceAttributes": {"satellite": "RS2"}}}
+        result = xpath_get(test_dict, "/product/sourceAttributes/satellite")
+        self.assertEqual(result, "RS2")
+    
+    def test_xpath_get_nested_dict(self):
+        test_dict = {"a": {"b": {"c": "value"}}}
+        result = xpath_get(test_dict, "/a/b/c")
+        self.assertEqual(result, "value")
+    
+    def test_xpath_get_invalid_path(self):
+        test_dict = {"product": {"sourceAttributes": {"satellite": "RS2"}}}
+        result = xpath_get(test_dict, "/product/invalid/path")
+        self.assertIsNone(result)
+    
+    def test_xpath_get_empty_dict(self):
+        result = xpath_get({}, "/some/path")
+        self.assertIsNone(result)
+
+
+class TestParseValue(unittest.TestCase):
+    """Test cases for parse_value function"""
+    
+    def test_parse_integer(self):
+        self.assertEqual(parse_value("123"), 123)
+    
+    def test_parse_float(self):
+        self.assertEqual(parse_value("123.456"), 123.456)
+    
+    def test_parse_boolean(self):
+        self.assertTrue(parse_value("True"))
+        self.assertFalse(parse_value("False"))
+    
+    def test_parse_list(self):
+        self.assertEqual(parse_value("[1, 2, 3]"), [1, 2, 3])
+    
+    def test_parse_string(self):
+        self.assertEqual(parse_value("hello world"), "hello world")
+    
+    def test_parse_dict(self):
+        result = parse_value("{'key': 'value'}")
+        self.assertEqual(result, {'key': 'value'})
+
+
+class TestCreate2DMatrix(unittest.TestCase):
+    """Test cases for create_2d_matrix function"""
+    
+    def test_create_simple_matrix(self):
+        lines = [0, 0, 1, 1]
+        cols = [0, 1, 0, 1]
+        vals = [1.0, 2.0, 3.0, 4.0]
+        result = create_2d_matrix(lines, cols, vals)
+        expected = np.array([[1.0, 2.0], [3.0, 4.0]])
+        np.testing.assert_array_equal(result, expected)
+    
+    def test_create_matrix_with_gaps(self):
+        lines = [0, 0, 2, 2]
+        cols = [0, 1, 0, 1]
+        vals = [1.0, 2.0, 3.0, 4.0]
+        result = create_2d_matrix(lines, cols, vals)
+        self.assertEqual(result.shape, (2, 2))
+    
+    def test_create_matrix_unsorted(self):
+        lines = [1, 0, 1, 0]
+        cols = [1, 0, 0, 1]
+        vals = [4.0, 1.0, 3.0, 2.0]
+        result = create_2d_matrix(lines, cols, vals)
+        expected = np.array([[1.0, 2.0], [3.0, 4.0]])
+        np.testing.assert_array_equal(result, expected)
+
+
+class TestGetLineAndPixInfo(unittest.TestCase):
+    """Test cases for get_line_and_pix_info function"""
+    
+    def test_get_line_and_pix_info(self):
+        dictio = {
+            "PixelSpacing": 12.5,
+            "LineSpacing": 10.0,
+            "someOtherKey": "value",
+            "anotherPixelSpacing": 15.0
+        }
+        result = get_line_and_pix_info(dictio)
+        self.assertIn("line", result)
+        self.assertIn("pixel", result)
+        self.assertIn("LineSpacing", result["line"])
+        self.assertIn("PixelSpacing", result["pixel"])
+        self.assertIn("anotherPixelSpacing", result["pixel"])
+
+
+class TestFillImageAttribute(unittest.TestCase):
+    """Test cases for fill_image_attribute function"""
+    
+    def test_fill_image_attribute_basic(self):
+        dictio = {
+            "product": {
+                "imageAttributes": {
+                    "someKey": "someValue",
+                    "rasterAttributes": {
+                        "numberOfLines": "1000",
+                        "numberOfSamplesPerLine": "2000"
+                    }
+                }
+            }
+        }
+        result = fill_image_attribute(dictio)
+        self.assertIn("rasterAttributes_numberOfLines", result)
+        self.assertIn("rasterAttributes_numberOfSamplesPerLine", result)
+
+
+class TestSortListFilesAndGetPols(unittest.TestCase):
+    """Test cases for sort_list_files_and_get_pols function"""
+    
+    def test_sort_cross_pol_first(self):
+        list_tiff = ["/path/to/imagery_HV.tif", "/path/to/imagery_HH.tif"]
+        sorted_files, pols = sort_list_files_and_get_pols(list_tiff)
+        self.assertEqual(pols, ["HH", "HV"])
+        self.assertTrue(sorted_files[0].endswith("HH.tif"))
+    
+    def test_single_pol(self):
+        list_tiff = ["/path/to/imagery_VV.tif"]
+        sorted_files, pols = sort_list_files_and_get_pols(list_tiff)
+        self.assertEqual(pols, ["VV"])
+        self.assertEqual(len(sorted_files), 1)
+    
+    def test_dual_pol_already_sorted(self):
+        list_tiff = ["/path/to/imagery_HH.tif", "/path/to/imagery_HV.tif"]
+        sorted_files, pols = sort_list_files_and_get_pols(list_tiff)
+        self.assertEqual(pols, ["HH", "HV"])
+
+
+class TestGetGlob(unittest.TestCase):
+    """Test cases for get_glob function"""
+    
+    def test_get_glob_identical(self):
+        strlist = ["file.txt", "file.txt"]
+        result = get_glob(strlist)
+        self.assertEqual(result, "file.txt")
+    
+    def test_get_glob_different_chars(self):
+        strlist = ["file1.txt", "file2.txt", "file3.txt"]
+        result = get_glob(strlist)
+        self.assertIn("*", result)
+    
+    def test_get_glob_prefix_suffix(self):
+        strlist = ["prefix_001_suffix", "prefix_002_suffix", "prefix_003_suffix"]
+        result = get_glob(strlist)
+        self.assertTrue(result.startswith("prefix"))
+        self.assertTrue(result.endswith("suffix"))
+
+
+class TestGetProductAttributes(unittest.TestCase):
+    """Test cases for get_product_attributes function"""
+    
+    def test_get_product_attributes(self):
+        dic = {
+            "product": {
+                "sourceAttributes": {
+                    "satellite": "RADARSAT-2",
+                    "inputDatasetId": "12345",
+                    "rawDataStartTime": "2020-01-01T12:00:00.000000Z",
+                    "radarParameters": {}
+                }
+            }
+        }
+        result = get_product_attributes(dic)
+        self.assertEqual(result["satellite"], "RADARSAT-2")
+        self.assertEqual(result["inputDatasetId"], "12345")
+        self.assertIn("rawDataStartTime", result)
+        # Check if it's a numpy datetime64 (can be array or scalar)
+        self.assertTrue(isinstance(result["rawDataStartTime"], (np.datetime64, np.ndarray)))
+        if isinstance(result["rawDataStartTime"], np.ndarray):
+            self.assertEqual(result["rawDataStartTime"].dtype, np.dtype('datetime64[ns]'))
+
+
+class TestGetSatelliteHeight(unittest.TestCase):
+    """Test cases for get_satellite_height function"""
+    
+    def test_get_satellite_height(self):
+        dic = {
+            "product": {
+                "imageGenerationParameters": {
+                    "sarProcessingInformation": {
+                        "satelliteHeight": {
+                            "#text": "798000.0",
+                            "@units": "m"
+                        }
+                    }
+                }
+            }
+        }
+        result = get_satellite_height(dic)
+        self.assertIn("satelliteHeight", result)
+        self.assertEqual(result["satelliteHeight"], 798000.0)
+        self.assertIn("satelliteHeight_units", result)
+        self.assertEqual(result["satelliteHeight_units"], "m")
+
+
+class TestGetSatellitePassDirection(unittest.TestCase):
+    """Test cases for get_satellite_pass_direction function"""
+    
+    def test_get_pass_direction_ascending(self):
+        dic = {
+            "product": {
+                "sourceAttributes": {
+                    "orbitAndAttitude": {
+                        "orbitInformation": {
+                            "passDirection": "Ascending"
+                        }
+                    }
+                }
+            }
+        }
+        result = get_satellite_pass_direction(dic)
+        self.assertEqual(result["passDirection"], "Ascending")
+    
+    def test_get_pass_direction_descending(self):
+        dic = {
+            "product": {
+                "sourceAttributes": {
+                    "orbitAndAttitude": {
+                        "orbitInformation": {
+                            "passDirection": "Descending"
+                        }
+                    }
+                }
+            }
+        }
+        result = get_satellite_pass_direction(dic)
+        self.assertEqual(result["passDirection"], "Descending")
+
+
+class TestCreateDicGeolocationGrid(unittest.TestCase):
+    """Test cases for create_dic_geolocation_grid function"""
+    
+    def test_create_dic_geolocation_grid(self):
+        dictio = {
+            "product": {
+                "imageAttributes": {
+                    "geographicInformation": {
+                        "geolocationGrid": {
+                            "imageTiePoint": [
+                                {
+                                    "imageCoordinate": {
+                                        "line": "0",
+                                        "pixel": "0"
+                                    },
+                                    "geodeticCoordinate": {
+                                        "latitude": {"#text": "45.0", "@units": "deg"},
+                                        "longitude": {"#text": "-75.0", "@units": "deg"},
+                                        "height": {"#text": "0.0", "@units": "m"}
+                                    }
+                                }
+                            ]
+                        }
+                    },
+                    "rasterAttributes": {
+                        "numberOfLines": "100",
+                        "numberOfSamplesPerLine": "100"
+                    }
+                }
+            }
+        }
+        result = create_dic_geolocation_grid(dictio)
+        self.assertIn("latitude", result)
+        self.assertIn("longitude", result)
+        self.assertIn("height", result)
+        self.assertIn("coords", result)
+        self.assertEqual(len(result["latitude"]["values"]), 1)
+        self.assertEqual(result["latitude"]["values"][0], 45.0)
+
+
+class TestGetDicOrbitInformation(unittest.TestCase):
+    """Test cases for get_dic_orbit_information function"""
+    
+    def test_get_dic_orbit_information(self):
+        dictio = {
+            "product": {
+                "sourceAttributes": {
+                    "orbitAndAttitude": {
+                        "orbitInformation": {
+                            "passDirection": "Ascending",
+                            "stateVector": [
+                                {
+                                    "timeStamp": "2020-01-01T12:00:00.000000Z",
+                                    "xPosition": {"#text": "1000000.0", "@units": "m"},
+                                    "yPosition": {"#text": "2000000.0", "@units": "m"},
+                                    "zPosition": {"#text": "3000000.0", "@units": "m"},
+                                    "xVelocity": {"#text": "100.0", "@units": "m/s"},
+                                    "yVelocity": {"#text": "200.0", "@units": "m/s"},
+                                    "zVelocity": {"#text": "300.0", "@units": "m/s"}
+                                }
+                            ]
+                        }
+                    }
+                }
+            }
+        }
+        result = get_dic_orbit_information(dictio)
+        self.assertIn("ds_attr", result)
+        self.assertIn("timestamp", result)
+        self.assertIn("xPosition", result)
+        self.assertEqual(len(result["xPosition"]["values"]), 1)
+        self.assertEqual(result["xPosition"]["values"][0], 1000000.0)
+
+
+class TestGetDicAttitudeInfo(unittest.TestCase):
+    """Test cases for get_dic_attitude_info function"""
+    
+    def test_get_dic_attitude_info(self):
+        dictio = {
+            "product": {
+                "sourceAttributes": {
+                    "orbitAndAttitude": {
+                        "attitudeInformation": {
+                            "attitudeAngles": [
+                                {
+                                    "timeStamp": "2020-01-01T12:00:00.000000Z",
+                                    "yaw": {"#text": "0.5", "@units": "deg"},
+                                    "roll": {"#text": "0.3", "@units": "deg"},
+                                    "pitch": {"#text": "0.2", "@units": "deg"}
+                                }
+                            ]
+                        }
+                    }
+                }
+            }
+        }
+        result = get_dic_attitude_info(dictio)
+        self.assertIn("ds_attr", result)
+        self.assertIn("timestamp", result)
+        self.assertIn("yaw", result)
+        self.assertIn("roll", result)
+        self.assertIn("pitch", result)
+        self.assertEqual(len(result["yaw"]["values"]), 1)
+
+
+class TestGetDictDopplerCentroid(unittest.TestCase):
+    """Test cases for get_dict_doppler_centroid function"""
+    
+    def test_get_dict_doppler_centroid(self):
+        dictio = {
+            "product": {
+                "imageGenerationParameters": {
+                    "dopplerCentroid": [
+                        {
+                            "timeOfDopplerCentroidEstimate": "2020-01-01T12:00:00.000000Z",
+                            "dopplerAmbiguity": "0",
+                            "dopplerAmbiguityConfidence": "1.0",
+                            "dopplerCentroidReferenceTime": {"#text": "0.5", "@units": "s"},
+                            "dopplerCentroidPolynomialPeriod": {"#text": "1.0", "@units": "s"},
+                            "dopplerCentroidCoefficients": "1.0 2.0 3.0",
+                            "dopplerCentroidConfidence": "0.95"
+                        }
+                    ]
+                }
+            }
+        }
+        result = get_dict_doppler_centroid(dictio)
+        self.assertIn("timeOfDopplerCentroidEstimate", result)
+        self.assertIn("dopplerAmbiguity", result)
+        self.assertEqual(len(result["dopplerCentroidCoefficients"]["values"]), 1)
+        self.assertEqual(len(result["dopplerCentroidCoefficients"]["values"][0]), 3)
+
+
+class TestGetDicDopplerRateValues(unittest.TestCase):
+    """Test cases for get_dic_doppler_rate_values function"""
+    
+    def test_get_dic_doppler_rate_values_dict(self):
+        dictio = {
+            "product": {
+                "imageGenerationParameters": {
+                    "dopplerRateValues": {
+                        "dopplerRateReferenceTime": {"#text": "0.5", "@units": "s"},
+                        "dopplerRateValuesCoefficients": "1.0 2.0 3.0"
+                    }
+                }
+            }
+        }
+        result = get_dic_doppler_rate_values(dictio)
+        self.assertIn("dopplerRateReferenceTime", result)
+        self.assertIn("dopplerRateValuesCoefficients", result)
+        self.assertEqual(len(result["dopplerRateValuesCoefficients"]["values"]), 1)
+
+
+class TestGetDictRadarParameters(unittest.TestCase):
+    """Test cases for get_dict_radar_parameters function"""
+    
+    def test_get_dict_radar_parameters(self):
+        dictio = {
+            "product": {
+                "sourceAttributes": {
+                    "radarParameters": {
+                        "acquisitionType": "Wide",
+                        "beams": "W1 W2",
+                        "polarizations": "HH HV",
+                        "radarCenterFrequency": "5405000000.0"
+                    }
+                }
+            }
+        }
+        result = get_dict_radar_parameters(dictio)
+        self.assertIn("ds_attr", result)
+        self.assertEqual(result["ds_attr"]["acquisitionType"], "Wide")
+        self.assertEqual(result["ds_attr"]["beams"], ["W1", "W2"])
+        self.assertEqual(result["ds_attr"]["polarizations"], ["HH", "HV"])
+
+
+if __name__ == "__main__":
+    unittest.main()


=====================================
src/xradarsat2/utils.py
=====================================
@@ -1,8 +1,14 @@
-import xradarsat2
 import logging
 import os
-import yaml
 import re
+import warnings
+import zipfile
+
+import aiohttp
+import fsspec
+import yaml
+
+import xradarsat2
 
 
 def get_glob(strlist):
@@ -42,6 +48,76 @@ def load_config():
         )
 
     logging.info("config path: %s", config_path)
-    stream = open(config_path, "r")
+    stream = open(config_path)
     conf = yaml.load(stream, Loader=yaml.CLoader)
     return conf
+
+
+def get_test_file(fname):
+    """
+    get test file from  https://cyclobs.ifremer.fr/static/sarwing_datarmor/xsardata/
+    file is unzipped and extracted to `config['data_dir']`
+
+    Parameters
+    ----------
+    fname: str
+        file name to get (without '.zip' extension)
+
+    Returns
+    -------
+    str
+        path to file, relative to `config['data_dir']`
+
+    """
+    config = {"data_dir": "/tmp"}
+
+    def url_get(url, cache_dir=os.path.join(config["data_dir"], "fsspec_cache")):
+        """
+        Get fil from url, using caching.
+
+        Parameters
+        ----------
+        url: str
+        cache_dir: str
+            Cache dir to use. default to `os.path.join(config['data_dir'], 'fsspec_cache')`
+
+        Raises
+        ------
+        FileNotFoundError
+
+        Returns
+        -------
+        filename: str
+            The local file name
+
+        Notes
+        -----
+        Due to fsspec, the returned filename won't match the remote one.
+        """
+
+        if "://" in url:
+            with fsspec.open(
+                f"filecache::{url}",
+                https={"client_kwargs": {"timeout": aiohttp.ClientTimeout(total=3600)}},
+                filecache={
+                    "cache_storage": os.path.join(
+                        os.path.join(config["data_dir"], "fsspec_cache")
+                    )
+                },
+            ) as f:
+                fname = f.name
+        else:
+            fname = url
+
+        return fname
+
+    res_path = config["data_dir"]
+    base_url = "https://cyclobs.ifremer.fr/static/sarwing_datarmor/xsardata"
+    file_url = f"{base_url}/{fname}.zip"
+    if not os.path.exists(os.path.join(res_path, fname)):
+        warnings.warn(f"Downloading {file_url}")
+        local_file = url_get(file_url)
+        warnings.warn(f"Unzipping {os.path.join(res_path, fname)}")
+        with zipfile.ZipFile(local_file, "r") as zip_ref:
+            zip_ref.extractall(res_path)
+    return os.path.join(res_path, fname)


=====================================
src/xradarsat2/xradarsat2-config.yaml
=====================================
@@ -1 +1 @@
-folder_path: ./2021/137/RS2_OK129673_PK1136693_DK1093025_SCWA_20210517_010235_VV_VH_SGF
+folder_path: RS2_OK135107_PK1187782_DK1151894_SCWA_20220407_182127_VV_VH_SGF


=====================================
test/test_opening_datatree_radarsat2.py
=====================================
@@ -1,7 +1,8 @@
-from xradarsat2.utils import load_config
-import xradarsat2
-import time
 import logging
+import time
+
+import xradarsat2
+from xradarsat2.utils import get_test_file, load_config
 
 logging.basicConfig(level=logging.DEBUG)
 logging.debug("start opening RadarSAT-2 product")
@@ -11,13 +12,14 @@ logging.debug("start opening RadarSAT-2 product")
 t0 = time.time()
 conf = load_config()
 folder_path = conf["folder_path"]
-dt = xradarsat2.rs2_reader(folder_path)
+rs2_product_path = get_test_file(conf["folder_path"])
+dt = xradarsat2.rs2_reader(rs2_product_path)
 elapse_t = time.time() - t0
 
 print(type(dt), dt)
 print("out of the reader")
 print(dt)
-print("time to read the SAFE through nfs: %1.2f sec" % elapse_t)
+print(f"time to read the SAFE through nfs: {elapse_t:1.2f} sec")
 dt = xradarsat2.load_digital_number(
     dt, chunks={"pol": "VV", "line": 6000, "sample": 8000}
 )



View it on GitLab: https://salsa.debian.org/debian-gis-team/xradarsat2/-/commit/9c5bd2534b2f3480fcf9536606192fc55f582efe

-- 
View it on GitLab: https://salsa.debian.org/debian-gis-team/xradarsat2/-/commit/9c5bd2534b2f3480fcf9536606192fc55f582efe
You're receiving this email because of your account on salsa.debian.org.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-grass-devel/attachments/20251023/607c6d5e/attachment-0001.htm>


More information about the Pkg-grass-devel mailing list