Repository: scottstanie/sentineleof

Issue:
crash on S1C filenames
if is not ready for S1C yet. If it encounters a S1C filename (in this case, an orbit filename), it produces this error:

```
File "/root/.local/lib/python3.12/site-packages/eof/download.py", line 138, in download_eofs
    urls = asf_client.get_download_urls(orbit_dts, missions, orbit_type=orbit_type)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/root/.local/lib/python3.12/site-packages/eof/asf_client.py", line 108, in get_download_urls
    eof_list = self.get_full_eof_list(orbit_type=orbit_type, max_dt=max(orbit_dts))
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/root/.local/lib/python3.12/site-packages/eof/asf_client.py", line 73, in get_full_eof_list
    eof_list = self._get_cached_filenames(orbit_type)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/root/.local/lib/python3.12/site-packages/eof/asf_client.py", line 153, in _get_cached_filenames
    return [SentinelOrbit(f) for f in f.read().splitlines()]
            ^^^^^^^^^^^^^^^^
  File "/root/.local/lib/python3.12/site-packages/eof/products.py", line 314, in __init__
    super(SentinelOrbit, self).__init__(filename, **kwargs)
  File "/root/.local/lib/python3.12/site-packages/eof/products.py", line 43, in __init__
    self.full_parse()  # Run a parse to check validity of filename
    ^^^^^^^^^^^^^^^^^
  File "/root/.local/lib/python3.12/site-packages/eof/products.py", line 69, in full_parse
    raise ValueError(
ValueError: Invalid SentinelOrbit filename: S1C_OPER_AUX_POEORB_OPOD_20250328T071010_V20250307T225942_20250309T005942.EOF 
```

I suppose at least the regexes need an additional "S1C" qualifier, but maybe more is necessary?

Hints from issue comments:
@scottstanie: Thanks for bringing this up, I thought I had a branch where I started this, but can't seem to find it.

Would you mind posting the input file you used that needed `S1C_OPER_AUX_POEORB_OPOD_20250328T071010_V20250307T225942_20250309T005942`?@anibalrivero: Hi! 
I am experiencing the same error using the `--search-path` parameter. The strangest thing is that we do not have any S1C files in our archive!! @vincentschut: sorry, we don't use the CLI (so no input file to share), we use it as a library. For the time being, I worked around this by monkeypatching the FILE_REGEX in products.SentinelOrbit (add the "S1C" part) before our code calls `download_eofs`.@scottstanie: Yes that's what the fix should be here. I realized since the `.relative_orbit` is just informational here, we don't need to wait for confirmation. Fix should be merged on the main branch, and I can make a release soon.
@scottstanie: (I see github doesn't like `addresses`)

Let me know if this still fails for you after the fix

Solution patch:
diff --git a/README.md b/README.md
index ad3c03f..a27034c 100644
--- a/README.md
+++ b/README.md
@@ -95,9 +95,9 @@ Options:
                                   Sentinel-1 file/folder
   -d, --date TEXT                 Alternative to specifying Sentinel products:
                                   choose date to download for.
-  -m, --mission [S1A|S1B]         If using `--date`, optionally specify
+  -m, --mission [S1A|S1B|S1C]     If using `--date`, optionally specify
                                   Sentinel satellite to download (default:
-                                  gets both S1A and S1B)
+                                  gets S1A, S1B, and S1C)
   --orbit-type [precise|restituted]
                                   Optionally specify the type of orbit file to
                                   get (default: precise (POEORB), but fallback
@@ -135,5 +135,5 @@ To use the function from python, you can pass a list of dates:
 from eof.download import download_eofs
 
 download_eofs([datetime.datetime(2018, 5, 3, 0, 0, 0)])
-download_eofs(['20180503', '20180507'], ['S1A', 'S1B'])
+download_eofs(['20180503', '20180507'], ['S1A', 'S1B', 'S1C'])
 ```
diff --git a/eof/asf_client.py b/eof/asf_client.py
index 6eaff8e..a46ab57 100644
--- a/eof/asf_client.py
+++ b/eof/asf_client.py
@@ -110,6 +110,7 @@ class ASFClient:
         mission_to_eof_list = {
             "S1A": [eof for eof in eof_list if eof.mission == "S1A"],
             "S1B": [eof for eof in eof_list if eof.mission == "S1B"],
+            "S1C": [eof for eof in eof_list if eof.mission == "S1C"],
         }
         # For precise orbits, we can have a larger front margin to ensure we
         # cover the ascending node crossing
diff --git a/eof/cli.py b/eof/cli.py
index c807e55..6de6c16 100644
--- a/eof/cli.py
+++ b/eof/cli.py
@@ -44,10 +44,10 @@ from eof._auth import NASA_HOST, DATASPACE_HOST, setup_netrc
 @click.option(
     "--mission",
     "-m",
-    type=click.Choice(["S1A", "S1B"]),
+    type=click.Choice(["S1A", "S1B", "S1C"]),
     help=(
         "If using `--date`, optionally specify Sentinel satellite to download"
-        " (default: gets both S1A and S1B)"
+        " (default: gets S1A, S1B, and S1C)"
     ),
 )
 @click.option(
diff --git a/eof/dataspace_client.py b/eof/dataspace_client.py
index 21e27d9..ce8b494 100644
--- a/eof/dataspace_client.py
+++ b/eof/dataspace_client.py
@@ -75,7 +75,7 @@ class DataspaceClient:
         satellite_id: str,
         product_type: str = "AUX_POEORB",
     ) -> list[dict]:
-        assert satellite_id in {"S1A", "S1B"}
+        assert satellite_id in {"S1A", "S1B", "S1C"}
         assert product_type in {"AUX_POEORB", "AUX_RESORB"}
         # return run_query(t0, t1, satellite_id, product_type)
         # Construct the query based on the time range parsed from the input file
@@ -119,7 +119,7 @@ class DataspaceClient:
         ----------
         orbit_dts : list[datetime.datetime]
             List of datetimes to query for
-        missions : list[str], choices = {"S1A", "S1B"}
+        missions : list[str], choices = {"S1A", "S1B", "S1C"}
             List of missions to query for. Must be same length as orbit_dts
         orbit_type : str, choices = {"precise", "restituted"}
             String identifying the type of orbit file to query for.
@@ -210,7 +210,7 @@ def _construct_orbit_file_query(
     ----------
     mission_id : str
         The mission ID parsed from the SAFE file name, should always be one
-        of S1A or S1B.
+        of S1A, S1B or S1C.
     orbit_type : str
         String identifying the type of orbit file to query for. Should be either
         POEORB for Precise Orbit files, or RESORB for Restituted.
diff --git a/eof/download.py b/eof/download.py
index fbf35fa..bd0f4fe 100644
--- a/eof/download.py
+++ b/eof/download.py
@@ -63,7 +63,7 @@ def download_eofs(
 
     Args:
         orbit_dts (list[str] or list[datetime.datetime]): datetime for orbit coverage
-        missions (list[str]): optional, to specify S1A or S1B
+        missions (list[str]): optional, to specify S1A, S1B or S1C
             No input downloads both, must be same len as orbit_dts
         sentinel_file (str): path to Sentinel-1 filename to download one .EOF for
         save_dir (str): directory to save the EOF files into
@@ -73,12 +73,12 @@ def download_eofs(
         list[str]: all filenames of saved orbit files
 
     Raises:
-        ValueError - for missions argument not being one of 'S1A', 'S1B',
+        ValueError - for missions argument not being one of 'S1A', 'S1B', 'S1C',
             having different lengths, or `sentinel_file` being invalid
     """
     # TODO: condense list of same dates, different hours?
-    if missions and all(m not in ("S1A", "S1B") for m in missions):
-        raise ValueError('missions argument must be "S1A" or "S1B"')
+    if missions and all(m not in ("S1A", "S1B", "S1C") for m in missions):
+        raise ValueError('missions argument must be "S1A", "S1B" or "S1C"')
     if sentinel_file:
         sent = Sentinel(sentinel_file)
         orbit_dts, missions = [sent.start_time], [sent.mission]
@@ -240,7 +240,7 @@ def main(
         # Handle parsing in download_eof
         orbit_dts, missions = None, None
     elif date:
-        missions = [mission] if mission else ["S1A", "S1B"]
+        missions = [mission] if mission else ["S1A", "S1B", "S1C"]
         orbit_dts = [parse(date)] * len(missions)
         # Check they didn't pass a whole datetime
         if all((dt.hour == 0 and dt.minute == 0) for dt in orbit_dts):
diff --git a/eof/parsing.py b/eof/parsing.py
index c699814..ee5f238 100644
--- a/eof/parsing.py
+++ b/eof/parsing.py
@@ -1,4 +1,5 @@
 """Module for parsing the orbit state vectors (OSVs) from the .EOF file"""
+
 from __future__ import annotations
 
 from datetime import datetime, timezone
@@ -13,12 +14,11 @@ class EOFLinkFinder(HTMLParser):
 
     Example page to search:
     http://step.esa.int/auxdata/orbits/Sentinel-1/POEORB/S1B/2020/10/
+    S1C follows the same pattern:
 
     Usage:
     >>> import requests
     >>> resp = requests.get("http://step.esa.int/auxdata/orbits/Sentinel-1/POEORB/S1B/2020/10/")
-    >>> parser = EOFLinkFinder()
-    >>> parser.feed(resp.text)
     """
 
     def __init__(self):
diff --git a/eof/products.py b/eof/products.py
index 4d2c190..6f047f5 100644
--- a/eof/products.py
+++ b/eof/products.py
@@ -99,7 +99,7 @@ class Sentinel(Base):
     File name format:
         MMM_BB_TTTR_LFPP_YYYYMMDDTHHMMSS_YYYYMMDDTHHMMSS_OOOOOO_DDDDDD_CCCC.EEEE
 
-    MMM: mission/satellite S1A or S1B
+    MMM: mission/satellite S1A, S1B or S1C
     BB: Mode/beam identifier. The S1-S6 beams apply to SM products IW,
       EW and WV identifiers appply to products from the respective modes.
     TTT: Product Type: RAW, SLC, GRD, OCN
@@ -122,7 +122,7 @@ class Sentinel(Base):
     """
 
     FILE_REGEX = re.compile(
-        r"(?P<mission>S1A|S1B)_"
+        r"(?P<mission>S1A|S1B|S1C)_"
         r"(?P<beam>[\w\d]{2})_"
         r"(?P<product_type>[\w_]{3})"
         r"(?P<resolution_class>[FHM_])_"
@@ -209,7 +209,7 @@ class Sentinel(Base):
 
     @property
     def mission(self):
-        """Returns satellite/mission of product (S1A/S1B)
+        """Returns satellite/mission of product (S1A/S1B/S1C)
 
         Example:
             >>> s = Sentinel('S1A_IW_SLC__1SDV_20180408T043025_20180408T043053_021371_024C9B_1B70')
@@ -248,6 +248,8 @@ class Sentinel(Base):
             return ((self.absolute_orbit - 73) % 175) + 1
         elif self.mission == "S1B":
             return ((self.absolute_orbit - 27) % 175) + 1
+        elif self.mission == "S1C":
+            return ((self.absolute_orbit - 172) % 175) + 1
 
     @property
     def path(self):
@@ -280,7 +282,7 @@ class SentinelOrbit(Base):
     The filename must comply with the following pattern:
         MMM_CCCC_TTTTTTTTTT_<instance_id>.EOF
 
-    MMM = mission, S1A or S1B
+    MMM = mission, S1A, S1B or S1C
     CCCC =  File Class, we only want OPER = routine operational
     TTTTTTTTTT = File type
      = FFFF DDDDDD
@@ -303,7 +305,7 @@ class SentinelOrbit(Base):
 
     TIME_FMT = "%Y%m%dT%H%M%S"
     FILE_REGEX = (
-        r"(?P<mission>S1A|S1B)_OPER_AUX_"
+        r"(?P<mission>S1A|S1B|S1C)_OPER_AUX_"
         r"(?P<orbit_type>[\w_]{6})_OPOD_"
         r"(?P<created_datetime>[T\d]{15})_"
         r"V(?P<start_datetime>[T\d]{15})_"
@@ -340,7 +342,7 @@ class SentinelOrbit(Base):
 
     @property
     def mission(self):
-        """Returns satellite/mission of product (S1A/S1B)
+        """Returns satellite/mission of product (S1A/S1B/S1C)
 
         Example:
             >>> s = SentinelOrbit('S1A_OPER_AUX_POEORB_OPOD_20200121T120654_V20191231T225942_20200102T005942.EOF')


Test patch:
diff --git a/eof/tests/test_products.py b/eof/tests/test_products.py
index 96b6444..b9f338f 100644
--- a/eof/tests/test_products.py
+++ b/eof/tests/test_products.py
@@ -50,3 +50,15 @@ def test_pathlib():
         Path("S1A_IW_SLC__1SDV_20230823T154908_20230823T154935_050004_060418_521B.zip")
     )
     assert p1 == p2
+
+
+def test_s1c():
+    fname = "S1C_IW_SLC__1SDV_20250331T060116_20250331T060143_001681_002CD0_8D44"
+    p = Sentinel(fname)
+
+    assert p.filename == fname
+    assert p.start_time == datetime(2025, 3, 31, 6, 1, 16)
+    assert p.stop_time == datetime(2025, 3, 31, 6, 1, 43)
+    assert p.relative_orbit == 110
+    assert p.polarization == "DV"
+    assert p.mission == "S1C"