Skip to content

Commit 6c4a30d

Browse files
authored
Use same margin0 = timedelta(seconds=T_ORBIT + 60) for ASF as CDSE (#71)
* Fix bad typing and docstring on `download_eofs` * Use same `margin0 = timedelta(seconds=T_ORBIT + 60)` for ASF as CDSE Fixes #68 * Add tests verifying #68 fix * Add cassettes
1 parent 4895a47 commit 6c4a30d

File tree

8 files changed

+305
-35
lines changed

8 files changed

+305
-35
lines changed

eof/_select_orbit.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Module for filtering/selecting from orbit query"""
2+
23
from __future__ import annotations
34

45
import operator
@@ -24,7 +25,7 @@ def last_valid_orbit(
2425
t1: datetime,
2526
data: Sequence[SentinelOrbit],
2627
margin0=timedelta(seconds=T_ORBIT + 60),
27-
margin1=timedelta(minutes=5),
28+
margin1=timedelta(seconds=60),
2829
) -> str:
2930
# Using a start margin of > 1 orbit so that the start of the orbit file will
3031
# cover the ascending node crossing of the acquisition

eof/asf_client.py

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66
from __future__ import annotations
77

88
import os
9-
from datetime import datetime, timedelta
9+
from datetime import datetime
1010
from pathlib import Path
1111
from typing import Literal
1212

1313
import requests
1414

1515
from ._asf_s3 import get_orbit_files, ASF_BUCKET_NAME
16-
from ._select_orbit import T_ORBIT, ValidityError, last_valid_orbit
16+
from ._select_orbit import ValidityError, last_valid_orbit
1717
from ._types import Filename
1818
from .log import logger
1919
from .products import SentinelOrbit
@@ -102,19 +102,12 @@ def get_download_urls(
102102
"S1B": [eof for eof in eof_list if eof.mission == "S1B"],
103103
"S1C": [eof for eof in eof_list if eof.mission == "S1C"],
104104
}
105-
# For precise orbits, use a larger front margin to ensure coverage
106-
if orbit_type == "precise":
107-
margin0 = timedelta(seconds=T_ORBIT + 60)
108-
else:
109-
margin0 = timedelta(seconds=60)
110105

111106
remaining_orbits = []
112107
urls = []
113108
for dt, mission in zip(orbit_dts, missions):
114109
try:
115-
filename = last_valid_orbit(
116-
dt, dt, mission_to_eof_list[mission], margin0=margin0
117-
)
110+
filename = last_valid_orbit(dt, dt, mission_to_eof_list[mission])
118111
# Construct the full download URL using the bucket name from _asf_s3
119112
url = f"https://{ASF_BUCKET_NAME}.s3.amazonaws.com/{filename}"
120113
urls.append(url)

eof/dataspace_client.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Client to get orbit files from dataspace.copernicus.eu ."""
2+
23
from __future__ import annotations
34

45
from concurrent.futures import ThreadPoolExecutor
@@ -91,7 +92,7 @@ def query_orbit(
9192

9293
@staticmethod
9394
def query_orbit_for_product(
94-
product,
95+
product: str | S1Product,
9596
orbit_type: str = "precise",
9697
t0_margin: timedelta = T0,
9798
t1_margin: timedelta = T1,

eof/download.py

Lines changed: 61 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,11 @@
2727
import glob
2828
import itertools
2929
import os
30+
from collections.abc import Sequence
31+
from datetime import datetime
3032
from multiprocessing.pool import ThreadPool
3133
from pathlib import Path
32-
from typing import Optional
34+
from typing import Literal, Optional
3335

3436
from dateutil.parser import parse
3537
from requests.exceptions import HTTPError
@@ -44,11 +46,11 @@
4446

4547

4648
def download_eofs(
47-
orbit_dts=None,
48-
missions=None,
49-
sentinel_file=None,
50-
save_dir=".",
51-
orbit_type="precise",
49+
orbit_dts: Sequence[datetime | str] | None = None,
50+
missions: Sequence[str] | None = None,
51+
sentinel_file: str | None = None,
52+
save_dir: str = ".",
53+
orbit_type: Literal["precise", "restituted"] = "precise",
5254
force_asf: bool = False,
5355
asf_user: str = "",
5456
asf_password: str = "",
@@ -59,29 +61,65 @@ def download_eofs(
5961
netrc_file: Optional[Filename] = None,
6062
max_workers: int = MAX_WORKERS,
6163
) -> list[Path]:
62-
"""Downloads and saves EOF files for specific dates
63-
64-
Args:
65-
orbit_dts (list[str] or list[datetime.datetime]): datetime for orbit coverage
66-
missions (list[str]): optional, to specify S1A, S1B or S1C
67-
No input downloads both, must be same len as orbit_dts
68-
sentinel_file (str): path to Sentinel-1 filename to download one .EOF for
69-
save_dir (str): directory to save the EOF files into
70-
orbit_type (str): precise or restituted
71-
72-
Returns:
73-
list[str]: all filenames of saved orbit files
74-
75-
Raises:
76-
ValueError - for missions argument not being one of 'S1A', 'S1B', 'S1C',
77-
having different lengths, or `sentinel_file` being invalid
64+
"""Downloads and saves Sentinel precise or restituted orbit files (EOF).
65+
66+
By default, it tries to download from Copernicus Data Space Ecosystem first,
67+
then falls back to ASF if the first source fails (unless `force_asf` is True).
68+
69+
Parameters
70+
----------
71+
orbit_dts : list[datetime] or list[str] or None
72+
List of datetimes for orbit coverage.
73+
If strings are provided, they will be parsed into datetime objects.
74+
missions : list[str] or None
75+
List of mission identifiers ('S1A', 'S1B', or 'S1C'). If None,
76+
orbits for all missions will be attempted. Must be same length as orbit_dts.
77+
sentinel_file : str or None
78+
Path to a Sentinel-1 file to download a matching EOF for.
79+
If provided, orbit_dts and missions are ignored.
80+
save_dir : str, default="."
81+
Directory to save the downloaded EOF files into.
82+
orbit_type : {'precise', 'restituted'}, default='precise'
83+
Type of orbit file to download (precise=POEORB or restituted=RESORB).
84+
force_asf : bool, default=False
85+
If True, skip trying Copernicus Data Space and download directly from ASF.
86+
asf_user : str, default=""
87+
ASF username (deprecated, ASF orbits are now publicly available).
88+
asf_password : str, default=""
89+
ASF password (deprecated, ASF orbits are now publicly available).
90+
cdse_access_token : str or None, default=None
91+
Copernicus Data Space Ecosystem access token.
92+
cdse_user : str, default=""
93+
Copernicus Data Space Ecosystem username.
94+
cdse_password : str, default=""
95+
Copernicus Data Space Ecosystem password.
96+
cdse_2fa_token : str, default=""
97+
Copernicus Data Space Ecosystem two-factor authentication token.
98+
netrc_file : str or None, default=None
99+
Path to .netrc file for authentication credentials.
100+
max_workers : int, default=MAX_WORKERS
101+
Number of parallel downloads to run.
102+
103+
Returns
104+
-------
105+
list[Path]
106+
Paths to all successfully downloaded orbit files.
107+
108+
Raises
109+
------
110+
ValueError
111+
If missions argument contains values other than 'S1A', 'S1B', 'S1C',
112+
if missions and orbit_dts have different lengths, or if sentinel_file is invalid.
113+
HTTPError
114+
If there's an HTTP error during download that's not a rate limit error.
78115
"""
79116
# TODO: condense list of same dates, different hours?
80117
if missions and all(m not in ("S1A", "S1B", "S1C") for m in missions):
81118
raise ValueError('missions argument must be "S1A", "S1B" or "S1C"')
82119
if sentinel_file:
83120
sent = Sentinel(sentinel_file)
84121
orbit_dts, missions = [sent.start_time], [sent.mission]
122+
assert orbit_dts is not None and missions is not None
85123
if missions and len(missions) != len(orbit_dts):
86124
raise ValueError("missions arg must be same length as orbit_dts")
87125
if not missions:
@@ -114,7 +152,7 @@ def download_eofs(
114152
)
115153

116154
if query:
117-
logger.info("Attempting download from SciHub")
155+
logger.info("Attempting download from Copernicus Data Space Ecosystem")
118156
try:
119157
results = client.download_all(
120158
query, output_directory=save_dir, max_workers=max_workers

eof/tests/cassettes/test_asf_client/test_query_resorb_s1_reader_issue68.yaml

Lines changed: 110 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
interactions:
2+
- request:
3+
body: client_id=cdse-public&username=scott.stanie%40gmail.com&password=1Z8Xmfl2KbSx%21&grant_type=password
4+
headers:
5+
Accept:
6+
- '*/*'
7+
Accept-Encoding:
8+
- gzip, deflate, br, zstd
9+
Connection:
10+
- keep-alive
11+
Content-Length:
12+
- '100'
13+
Content-Type:
14+
- application/x-www-form-urlencoded
15+
User-Agent:
16+
- python-requests/2.32.3
17+
method: POST
18+
uri: https://identity.dataspace.copernicus.eu/auth/realms/CDSE/protocol/openid-connect/token
19+
response:
20+
body:
21+
string: '{"access_token":"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJYVUh3VWZKaHVDVWo0X3k4ZF8xM0hxWXBYMFdwdDd2anhob2FPLUxzREZFIn0.eyJleHAiOjE3NDQwNTA3MTMsImlhdCI6MTc0NDA1MDExMywianRpIjoiNGVhZWI1MzAtYjIxNS00MWFkLTkzMWUtYTk4MjRhMTQ0Njg1IiwiaXNzIjoiaHR0cHM6Ly9pZGVudGl0eS5kYXRhc3BhY2UuY29wZXJuaWN1cy5ldS9hdXRoL3JlYWxtcy9DRFNFIiwiYXVkIjpbIkNMT1VERkVSUk9fUFVCTElDIiwiYWNjb3VudCJdLCJzdWIiOiI2YjUyOTQyZS0yMzEwLTRkZGItYjhkZS0wNjU1MjEyOTQwNjEiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJjZHNlLXB1YmxpYyIsInNlc3Npb25fc3RhdGUiOiI2NmI1YTc2ZS02MzVhLTQ2ZDItYjczOC0zNzVmYmU1NDRkOGMiLCJhbGxvd2VkLW9yaWdpbnMiOlsiaHR0cHM6Ly9sb2NhbGhvc3Q6NDIwMCIsIioiLCJodHRwczovL3dvcmtzcGFjZS5zdGFnaW5nLWNkc2UtZGF0YS1leHBsb3Jlci5hcHBzLnN0YWdpbmcuaW50cmEuY2xvdWRmZXJyby5jb20iXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iLCJkZWZhdWx0LXJvbGVzLWNkYXMiLCJjb3Blcm5pY3VzLWdlbmVyYWwiXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6IkFVRElFTkNFX1BVQkxJQyBvcGVuaWQgZW1haWwgcHJvZmlsZSBvbmRlbWFuZF9wcm9jZXNzaW5nIHVzZXItY29udGV4dCIsInNpZCI6IjY2YjVhNzZlLTYzNWEtNDZkMi1iNzM4LTM3NWZiZTU0NGQ4YyIsImdyb3VwX21lbWJlcnNoaXAiOlsiL2FjY2Vzc19ncm91cHMvdXNlcl90eXBvbG9neS9jb3Blcm5pY3VzX2dlbmVyYWwiLCIvb3JnYW5pemF0aW9ucy9kZWZhdWx0LTZiNTI5NDJlLTIzMTAtNGRkYi1iOGRlLTA2NTUyMTI5NDA2MS9yZWd1bGFyX3VzZXIiXSwiZW1haWxfdmVyaWZpZWQiOnRydWUsIm5hbWUiOiJTY290dCBTdGFuaWV3aWN6Iiwib3JnYW5pemF0aW9ucyI6WyJkZWZhdWx0LTZiNTI5NDJlLTIzMTAtNGRkYi1iOGRlLTA2NTUyMTI5NDA2MSJdLCJ1c2VyX2NvbnRleHRfaWQiOiJjN2JlMjE3Yi04Mzc4LTRkOTQtYmFmNi0yN2RmNWYxMGY3NmMiLCJjb250ZXh0X3JvbGVzIjp7fSwiY29udGV4dF9ncm91cHMiOlsiL2FjY2Vzc19ncm91cHMvdXNlcl90eXBvbG9neS9jb3Blcm5pY3VzX2dlbmVyYWwvIiwiL29yZ2FuaXphdGlvbnMvZGVmYXVsdC02YjUyOTQyZS0yMzEwLTRkZGItYjhkZS0wNjU1MjEyOTQwNjEvcmVndWxhcl91c2VyLyJdLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJzY290dC5zdGFuaWVAZ21haWwuY29tIiwiZ2l2ZW5fbmFtZSI6IlNjb3R0IiwiZmFtaWx5X25hbWUiOiJTdGFuaWV3aWN6IiwidXNlcl9jb250ZXh0IjoiZGVmYXVsdC02YjUyOTQyZS0yMzEwLTRkZGItYjhkZS0wNjU1MjEyOTQwNjEiLCJlbWFpbCI6InNjb3R0LnN0YW5pZUBnbWFpbC5jb20ifQ.MX_4QR1SLWjqRY_wib_01mMnmlpHVbUVagadHj9iQ1f8h6TuXavmiCGD0coI4i66nxLVTC5yIMrdq6w92MUFvNpIbxwSZPfCXgATsWKTfBB8oeMpYecrDOWpyzyChfnUwPwmMNDjibUqS4KONjY9QDhJGmtyFkjcqwY4h4ssKX_0A1dBG_DuwNbsvRDPB2yga3D24PsrwYUFF-DKbGZACGmE35plekLkq4WDPIQLxeOSGOx04SXZzv4MYlaQzO1QkJkKxML2zm3vj2LyYWoVVa5Ik-SnDdpiYWjtbian7lelzTQ6ahRKlNO7qtHKiSHgoPKOzqOpU__Q6t5veDKjfg","expires_in":600,"refresh_expires_in":3600,"refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJhZmFlZTU2Zi1iNWZiLTRiMzMtODRlYS0zMWY2NzMyMzNhNzgifQ.eyJleHAiOjE3NDQwNTM3MTMsImlhdCI6MTc0NDA1MDExMywianRpIjoiODQ2NTE0ZDctZGM0Mi00M2JkLTgwM2ItNjRlMjhjYTQ4MWJiIiwiaXNzIjoiaHR0cHM6Ly9pZGVudGl0eS5kYXRhc3BhY2UuY29wZXJuaWN1cy5ldS9hdXRoL3JlYWxtcy9DRFNFIiwiYXVkIjoiaHR0cHM6Ly9pZGVudGl0eS5kYXRhc3BhY2UuY29wZXJuaWN1cy5ldS9hdXRoL3JlYWxtcy9DRFNFIiwic3ViIjoiNmI1Mjk0MmUtMjMxMC00ZGRiLWI4ZGUtMDY1NTIxMjk0MDYxIiwidHlwIjoiUmVmcmVzaCIsImF6cCI6ImNkc2UtcHVibGljIiwic2Vzc2lvbl9zdGF0ZSI6IjY2YjVhNzZlLTYzNWEtNDZkMi1iNzM4LTM3NWZiZTU0NGQ4YyIsInNjb3BlIjoiQVVESUVOQ0VfUFVCTElDIG9wZW5pZCBlbWFpbCBwcm9maWxlIG9uZGVtYW5kX3Byb2Nlc3NpbmcgdXNlci1jb250ZXh0Iiwic2lkIjoiNjZiNWE3NmUtNjM1YS00NmQyLWI3MzgtMzc1ZmJlNTQ0ZDhjIn0.gTNW81DWt5Nje-dW4F9mfi-KoH4PgHXD0qmR9y0tgzo","token_type":"Bearer","not-before-policy":0,"session_state":"66b5a76e-635a-46d2-b738-375fbe544d8c","scope":"AUDIENCE_PUBLIC
22+
openid email profile ondemand_processing user-context"}'
23+
headers:
24+
cache-control:
25+
- no-store
26+
content-length:
27+
- '3423'
28+
content-type:
29+
- application/json
30+
pragma:
31+
- no-cache
32+
referrer-policy:
33+
- no-referrer
34+
set-cookie:
35+
- KEYCLOAK_LOCALE=; Version=1; Comment=Expiring cookie; Expires=Thu, 01-Jan-1970
36+
00:00:10 GMT; Max-Age=0; Path=/auth/realms/CDSE/; Secure; HttpOnly
37+
- KC_RESTART=; Version=1; Expires=Thu, 01-Jan-1970 00:00:10 GMT; Max-Age=0;
38+
Path=/auth/realms/CDSE/; Secure; HttpOnly
39+
- KC_AUTH_STATE=; Version=1; Expires=Thu, 01-Jan-1970 00:00:10 GMT; Max-Age=0;
40+
Path=/auth/realms/CDSE/; Secure
41+
- SERVERID=keycloak001.waw2; path=/
42+
strict-transport-security:
43+
- max-age=31536000; includeSubDomains
44+
x-content-type-options:
45+
- nosniff
46+
x-frame-options:
47+
- ALLOW-FROM https://www.google.com
48+
x-xss-protection:
49+
- 1; mode=block
50+
status:
51+
code: 200
52+
message: OK
53+
- request:
54+
body: null
55+
headers:
56+
Accept:
57+
- '*/*'
58+
Accept-Encoding:
59+
- gzip, deflate, br, zstd
60+
Connection:
61+
- keep-alive
62+
User-Agent:
63+
- python-requests/2.32.3
64+
method: GET
65+
uri: https://catalogue.dataspace.copernicus.eu/odata/v1/Products?%24filter=startswith%28Name%2C%27S1A%27%29+and+contains%28Name%2C%27AUX_RESORB%27%29+and+ContentDate%2FStart+lt+%272025-03-10T19%3A02%3A43.428571Z%27+and+ContentDate%2FEnd+gt+%272025-03-10T20%3A43%3A28.000000Z%27&%24orderby=ContentDate%2FStart+asc&%24top=1
66+
response:
67+
body:
68+
string: '{"@odata.context":"$metadata#Products","value":[{"@odata.mediaContentType":"application/octet-stream","Id":"6f5734c8-fdfc-11ef-a200-0242ac120002","Name":"S1A_OPER_AUX_RESORB_OPOD_20250310T220905_V20250310T180852_20250310T212622.EOF","ContentType":"application/octet-stream","ContentLength":590749,"OriginDate":"2025-03-10T22:10:11.570000Z","PublicationDate":"2025-03-10T22:10:47.026472Z","ModificationDate":"2025-03-10T22:10:47.026472Z","Online":true,"EvictionDate":"9999-12-31T23:59:59.999999Z","S3Path":"/eodata/Sentinel-1/AUX/AUX_RESORB/2025/03/10/S1A_OPER_AUX_RESORB_OPOD_20250310T220905_V20250310T180852_20250310T212622.EOF","Checksum":[{"Value":"ead3d2760014f518fe0521c6930d5fc5","Algorithm":"MD5","ChecksumDate":"2025-03-10T22:10:45.527687Z"},{"Value":"485c5afb96399347b4e75dcdb0532d6b438a9cd2bb0ae0d9c0fbb29382777ad3","Algorithm":"BLAKE3","ChecksumDate":"2025-03-10T22:10:45.546864Z"}],"ContentDate":{"Start":"2025-03-10T18:08:52.000000Z","End":"2025-03-10T21:26:22.000000Z"},"Footprint":null,"GeoFootprint":null}],"@odata.nextLink":"https://catalogue.dataspace.copernicus.eu/odata/v1/Products?%24filter=startswith%28Name%2C%27S1A%27%29+and+contains%28Name%2C%27AUX_RESORB%27%29+and+ContentDate%2FStart+lt+%272025-03-10T19%3A02%3A43.428571Z%27+and+ContentDate%2FEnd+gt+%272025-03-10T20%3A43%3A28.000000Z%27&%24orderby=ContentDate%2FStart+asc&%24top=1&%24skip=1"}'
69+
headers:
70+
Access-Control-Allow-Credentials:
71+
- 'true'
72+
Access-Control-Allow-Headers:
73+
- DNT,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization
74+
Access-Control-Allow-Methods:
75+
- GET, PUT, POST, DELETE, PATCH, OPTIONS
76+
Access-Control-Allow-Origin:
77+
- '*'
78+
Access-Control-Max-Age:
79+
- '1728000'
80+
Connection:
81+
- keep-alive
82+
Content-Length:
83+
- '1370'
84+
Content-Type:
85+
- application/json
86+
Date:
87+
- Mon, 07 Apr 2025 18:21:54 GMT
88+
Strict-Transport-Security:
89+
- max-age=15724800; includeSubDomains
90+
request-id:
91+
- 44b4898c-19e8-44c4-89af-2db17409d0f9
92+
x-envoy-upstream-service-time:
93+
- '394'
94+
status:
95+
code: 200
96+
message: OK
97+
version: 1

eof/tests/test_asf_client.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import pytest
44

55
from eof.asf_client import ASFClient
6+
from eof.products import Sentinel
67
from eof._asf_s3 import ASF_BUCKET_NAME, list_public_bucket
78

89

@@ -52,3 +53,19 @@ def test_list_public_bucket_poeorb():
5253
precise[0]
5354
== "AUX_POEORB/S1A_OPER_AUX_POEORB_OPOD_20210203T122423_V20210113T225942_20210115T005942.EOF"
5455
)
56+
57+
58+
@pytest.mark.vcr
59+
def test_query_resorb_s1_reader_issue68():
60+
f = "S1A_IW_SLC__1SDV_20250310T204228_20250310T204253_058247_0732D8_1AA3"
61+
sent = Sentinel(f)
62+
orbit_dts, missions = [sent.start_time], [sent.mission]
63+
64+
client = ASFClient()
65+
66+
urls = client.get_download_urls(orbit_dts, missions, orbit_type="restituted")
67+
assert len(urls) == 1
68+
expected = (
69+
"S1A_OPER_AUX_RESORB_OPOD_20250310T220905_V20250310T180852_20250310T212622.EOF"
70+
)
71+
assert urls[0].split("/")[-1] == expected

eof/tests/test_dataspace_client.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,16 @@ def test_query_resorb_edge_case():
3939
r["title"]
4040
== "S1A_OPER_AUX_RESORB_OPOD_20230823T174849_V20230823T141024_20230823T172754"
4141
)
42+
43+
44+
@pytest.mark.vcr
45+
def test_query_resorb_s1_reader_issue68():
46+
f = "S1A_IW_SLC__1SDV_20250310T204228_20250310T204253_058247_0732D8_1AA3"
47+
48+
client = DataspaceClient()
49+
query = client.query_orbit_for_product(f, orbit_type="restituted")
50+
assert len(query) == 1
51+
expected = (
52+
"S1A_OPER_AUX_RESORB_OPOD_20250310T220905_V20250310T180852_20250310T212622.EOF"
53+
)
54+
assert query[0]["Name"] == expected

0 commit comments

Comments
 (0)