earthscope-cli 1.1.1__tar.gz → 1.1.2__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.
Files changed (27) hide show
  1. {earthscope_cli-1.1.1/src/earthscope_cli.egg-info → earthscope_cli-1.1.2}/PKG-INFO +3 -3
  2. {earthscope_cli-1.1.1 → earthscope_cli-1.1.2}/pyproject.toml +4 -4
  3. earthscope_cli-1.1.2/src/earthscope_cli/__init__.py +1 -0
  4. {earthscope_cli-1.1.1 → earthscope_cli-1.1.2}/src/earthscope_cli/common.py +1 -4
  5. {earthscope_cli-1.1.1 → earthscope_cli-1.1.2}/src/earthscope_cli/debug.py +1 -5
  6. {earthscope_cli-1.1.1 → earthscope_cli-1.1.2}/src/earthscope_cli/dropoff/render.py +43 -1
  7. {earthscope_cli-1.1.1 → earthscope_cli-1.1.2}/src/earthscope_cli/dropoff/typer.py +52 -4
  8. {earthscope_cli-1.1.1 → earthscope_cli-1.1.2}/src/earthscope_cli/dropoff/util.py +49 -0
  9. {earthscope_cli-1.1.1 → earthscope_cli-1.1.2}/src/earthscope_cli/login.py +1 -4
  10. {earthscope_cli-1.1.1 → earthscope_cli-1.1.2}/src/earthscope_cli/main.py +1 -4
  11. {earthscope_cli-1.1.1 → earthscope_cli-1.1.2}/src/earthscope_cli/user.py +1 -5
  12. {earthscope_cli-1.1.1 → earthscope_cli-1.1.2/src/earthscope_cli.egg-info}/PKG-INFO +3 -3
  13. {earthscope_cli-1.1.1 → earthscope_cli-1.1.2}/src/earthscope_cli.egg-info/requires.txt +2 -2
  14. {earthscope_cli-1.1.1 → earthscope_cli-1.1.2}/tests/test_dropoff.py +167 -4
  15. earthscope_cli-1.1.1/src/earthscope_cli/__init__.py +0 -1
  16. {earthscope_cli-1.1.1 → earthscope_cli-1.1.2}/LICENSE +0 -0
  17. {earthscope_cli-1.1.1 → earthscope_cli-1.1.2}/README.md +0 -0
  18. {earthscope_cli-1.1.1 → earthscope_cli-1.1.2}/setup.cfg +0 -0
  19. {earthscope_cli-1.1.1 → earthscope_cli-1.1.2}/setup.py +0 -0
  20. {earthscope_cli-1.1.1 → earthscope_cli-1.1.2}/src/earthscope_cli/util.py +0 -0
  21. {earthscope_cli-1.1.1 → earthscope_cli-1.1.2}/src/earthscope_cli.egg-info/SOURCES.txt +0 -0
  22. {earthscope_cli-1.1.1 → earthscope_cli-1.1.2}/src/earthscope_cli.egg-info/dependency_links.txt +0 -0
  23. {earthscope_cli-1.1.1 → earthscope_cli-1.1.2}/src/earthscope_cli.egg-info/entry_points.txt +0 -0
  24. {earthscope_cli-1.1.1 → earthscope_cli-1.1.2}/src/earthscope_cli.egg-info/top_level.txt +0 -0
  25. {earthscope_cli-1.1.1 → earthscope_cli-1.1.2}/tests/test_cli.py +0 -0
  26. {earthscope_cli-1.1.1 → earthscope_cli-1.1.2}/tests/test_login.py +0 -0
  27. {earthscope_cli-1.1.1 → earthscope_cli-1.1.2}/tests/test_user.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: earthscope-cli
3
- Version: 1.1.1
3
+ Version: 1.1.2
4
4
  Summary: A CLI for the EarthScope API
5
5
  Author-email: EarthScope <data-help@earthscope.org>
6
6
  License-Expression: Apache-2.0
@@ -11,9 +11,9 @@ Classifier: Operating System :: OS Independent
11
11
  Requires-Python: >=3.9
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
- Requires-Dist: typer-slim<0.24.0,>=0.17.3
14
+ Requires-Dist: typer<0.24.0
15
15
  Requires-Dist: typer-di>=0.1.3
16
- Requires-Dist: earthscope-sdk>=1.3.0
16
+ Requires-Dist: earthscope-sdk>=1.3.1
17
17
  Requires-Dist: rich>=13.8.0
18
18
  Provides-Extra: dev
19
19
  Requires-Dist: black; extra == "dev"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "earthscope-cli"
7
- version = "1.1.1"
7
+ version = "1.1.2"
8
8
  description = "A CLI for the EarthScope API"
9
9
  readme = "README.md"
10
10
  authors = [{ name = "EarthScope", email = "data-help@earthscope.org" }]
@@ -18,9 +18,9 @@ classifiers = [
18
18
 
19
19
  #Suggestion not to pin dependencies since the package should work in many different python environments
20
20
  dependencies = [
21
- "typer-slim>=0.17.3,<0.24.0",
21
+ "typer<0.24.0",
22
22
  "typer-di>=0.1.3",
23
- "earthscope-sdk>=1.3.0",
23
+ "earthscope-sdk>=1.3.1",
24
24
  "rich>=13.8.0",
25
25
  ]
26
26
  requires-python = ">=3.9"
@@ -43,7 +43,7 @@ Homepage = "https://gitlab.com/earthscope/public/earthscope-cli"
43
43
  es = "earthscope_cli.main:app"
44
44
 
45
45
  [tool.bumpver]
46
- current_version = "1.1.1"
46
+ current_version = "1.1.2"
47
47
  version_pattern = "MAJOR.MINOR.PATCH[PYTAGNUM]"
48
48
  commit_message = "chore: bump version {old_version} -> {new_version}"
49
49
  commit = true
@@ -0,0 +1 @@
1
+ __version__ = "1.1.2"
@@ -1,12 +1,9 @@
1
- from contextlib import suppress
2
1
  from typing import Annotated, Optional
3
2
 
4
3
  import typer
4
+ from rich import print
5
5
  from typer_di import Depends
6
6
 
7
- with suppress(ModuleNotFoundError):
8
- from rich import print
9
-
10
7
  GLOBAL_HELP_PANEL = "Global Options"
11
8
 
12
9
 
@@ -1,13 +1,9 @@
1
- from contextlib import suppress
2
-
3
1
  import typer
2
+ from rich import print
4
3
  from typer_di import Depends, TyperDI
5
4
 
6
5
  from earthscope_cli.common import get_sdk
7
6
 
8
- with suppress(ModuleNotFoundError):
9
- from rich import print
10
-
11
7
  app = TyperDI()
12
8
 
13
9
 
@@ -2,7 +2,7 @@ import threading
2
2
  from typing import Dict, List, Optional
3
3
 
4
4
  from earthscope_sdk.client.dropoff._multipart_uploader import UploadStatus
5
- from earthscope_sdk.client.dropoff.models import DropoffObject
5
+ from earthscope_sdk.client.dropoff.models import DropoffObject, DropoffSummary
6
6
  from rich import print
7
7
  from rich.console import Group
8
8
  from rich.live import Live
@@ -84,6 +84,31 @@ def print_dropoff_objects_table(
84
84
  print()
85
85
 
86
86
 
87
+ def print_dropoff_summary(summary: DropoffSummary, *, prefix: Optional[str] = None):
88
+ if not summary.status_counts:
89
+ print(f"[yellow]No objects found in prefix '{prefix}'")
90
+ return
91
+
92
+ title = "Dropoff Summary"
93
+ if prefix:
94
+ title += f" for {prefix}"
95
+
96
+ table = Table(
97
+ title=title,
98
+ show_header=True,
99
+ title_style="bold magenta",
100
+ header_style="bold magenta",
101
+ )
102
+ table.add_column("Status")
103
+ table.add_column("Count")
104
+ for status in summary.status_counts:
105
+ table.add_row(status.status, str(status.count))
106
+
107
+ print()
108
+ print(table)
109
+ print()
110
+
111
+
87
112
  class AggregateBytesColumn(ProgressColumn):
88
113
  """
89
114
  Custom column that shows aggregate bytes across all uploads.
@@ -181,6 +206,23 @@ class CompositeProgressBarColumn(ProgressColumn):
181
206
  return result
182
207
 
183
208
 
209
+ class UploadSummary:
210
+ def __init__(self, total: int, succeeded: int):
211
+ self.total = total
212
+ self.succeeded = succeeded
213
+ self.failed = total - succeeded
214
+
215
+ def print(self):
216
+ color = (
217
+ "green"
218
+ if self.failed == 0
219
+ else ("red" if self.succeeded == 0 else "yellow")
220
+ )
221
+ print(
222
+ f"[{color}]Summary: {self.succeeded}/{self.total} files succeeded, {self.failed} failed"
223
+ )
224
+
225
+
184
226
  class ConcurrentUploadProgressDisplay:
185
227
  """
186
228
  Rich progress display for concurrent file uploads.
@@ -12,13 +12,17 @@ from typer_di import Depends, TyperDI
12
12
  from earthscope_cli.common import get_sdk_refreshed
13
13
  from earthscope_cli.dropoff.render import (
14
14
  ConcurrentUploadProgressDisplay,
15
+ UploadSummary,
15
16
  print_dropoff_objects_table,
17
+ print_dropoff_summary,
16
18
  )
17
19
  from earthscope_cli.dropoff.util import (
18
20
  DropoffOptions,
21
+ FilterOptions,
19
22
  collect_files,
20
23
  get_category,
21
24
  get_dropoff_options,
25
+ get_filter_options,
22
26
  )
23
27
 
24
28
  app = TyperDI()
@@ -67,10 +71,38 @@ def get_object_history(
67
71
  print(f"[yellow]No history found for category '{cat_str}' with key '{key}'")
68
72
 
69
73
 
74
+ @app.command(name="get-summary")
75
+ def get_summary(
76
+ sdk: Annotated[EarthScopeClient, Depends(get_sdk_refreshed)],
77
+ options: Annotated[DropoffOptions, Depends(get_dropoff_options)],
78
+ filters: Annotated[FilterOptions, Depends(get_filter_options)],
79
+ prefix: Annotated[
80
+ str,
81
+ typer.Option(help="The prefix of the objects to get the summary for"),
82
+ ],
83
+ ):
84
+ """
85
+ Get a summary of a dropoff prefix.
86
+
87
+ es dropoff get-summary -c <type> --prefix <prefix>
88
+ es dropoff get-summary -c miniseed --prefix demo/
89
+ es dropoff get-summary -c miniseed --prefix demo/ --submitted-after 2025-01-01T00:00:00Z
90
+ es dropoff get-summary -c miniseed --prefix demo/ --submitted-before 2025-12-31T23:59:59Z
91
+ """
92
+ result = sdk.dropoff.get_summary(
93
+ category=options.category.value,
94
+ prefix=prefix,
95
+ submitted_after=filters.submitted_after,
96
+ submitted_before=filters.submitted_before,
97
+ )
98
+ print_dropoff_summary(result, prefix=prefix)
99
+
100
+
70
101
  @app.command(name="list-objects")
71
102
  def list_objects(
72
103
  sdk: Annotated[EarthScopeClient, Depends(get_sdk_refreshed)],
73
104
  options: Annotated[DropoffOptions, Depends(get_dropoff_options)],
105
+ filters: Annotated[FilterOptions, Depends(get_filter_options)],
74
106
  prefix: Annotated[
75
107
  str,
76
108
  typer.Option(help="Filter by object prefix"),
@@ -83,12 +115,18 @@ def list_objects(
83
115
  es dropoff list-objects -c miniseed
84
116
  es dropoff list-objects -c miniseed --prefix demo/
85
117
  es dropoff list-objects -c miniseed --offset 100 --limit 100
118
+ es dropoff list-objects -c miniseed --status FAILED
119
+ es dropoff list-objects -c miniseed --submitted-after 2025-01-01T00:00:00Z
120
+ es dropoff list-objects -c miniseed --submitted-before 2025-12-31T23:59:59Z
86
121
  """
87
122
  result = sdk.dropoff.list_objects(
88
123
  category=options.category.value,
89
124
  prefix=prefix,
90
125
  offset=options.offset,
91
126
  limit=options.limit,
127
+ submitted_after=filters.submitted_after,
128
+ submitted_before=filters.submitted_before,
129
+ status=filters.status,
92
130
  )
93
131
 
94
132
  suffix = ""
@@ -104,7 +142,9 @@ def list_objects(
104
142
  next_offset=result.offset + result.limit,
105
143
  )
106
144
 
107
- print(f"[yellow]No objects found for category '{cat_str}'{suffix}")
145
+ print(
146
+ f"[yellow]No objects matched specified criteria: category='{cat_str}', prefix='{prefix}', offset={options.offset}, limit={options.limit}', submitted_after={filters.submitted_after}, submitted_before={filters.submitted_before}, status={filters.status}'"
147
+ )
108
148
 
109
149
 
110
150
  @app.command(name="upload")
@@ -176,22 +216,27 @@ def upload(
176
216
  don't overwrite each other and maintains meaningful organization in S3.
177
217
  """
178
218
  # Parse the input path to get a list of files
219
+ total_collected = 0
220
+ succeeded = 0
179
221
  files: list[tuple[Path, str]] = []
180
222
  for p, k in collect_files(paths):
223
+ total_collected += 1
181
224
  try:
182
225
  Validator(p, category=category).validate_all()
183
226
  except Exception as e:
184
227
  print(f"[red]{e}")
185
- raise typer.Exit(code=1)
186
228
  else:
187
229
  files.append((p, k))
188
230
 
189
231
  if not files:
190
- print(f"[red]No files found in paths: {paths}")
232
+ paths_str = ", ".join(str(p) for p in paths)
233
+ print(f"[red]No valid {category.value} files found in paths: {paths_str}")
234
+ UploadSummary(total=total_collected, succeeded=succeeded).print()
191
235
  raise typer.Exit(code=1)
192
236
 
237
+ display = ConcurrentUploadProgressDisplay(len(files))
193
238
  try:
194
- with ConcurrentUploadProgressDisplay(len(files)) as display:
239
+ with display:
195
240
  sdk.dropoff.put_objects(
196
241
  objects=files,
197
242
  category=category,
@@ -203,3 +248,6 @@ def upload(
203
248
  except KeyboardInterrupt:
204
249
  print()
205
250
  print("[yellow]Upload interrupted. Use the same command to resume the upload.")
251
+ finally:
252
+ succeeded = len(display.completed_files)
253
+ UploadSummary(total=total_collected, succeeded=succeeded).print()
@@ -18,6 +18,13 @@ class PaginationOptions:
18
18
  limit: int
19
19
 
20
20
 
21
+ @dataclass
22
+ class FilterOptions:
23
+ submitted_before: Optional[datetime] = None
24
+ submitted_after: Optional[datetime] = None
25
+ status: Optional[str] = None
26
+
27
+
21
28
  @dataclass
22
29
  class DropoffOptions(PaginationOptions):
23
30
  category: DropoffCategory
@@ -86,6 +93,48 @@ def get_category(
86
93
  return DropoffCategory(cat_str)
87
94
 
88
95
 
96
+ def get_filter_options(
97
+ submitted_after: Annotated[
98
+ Optional[datetime],
99
+ typer.Option(
100
+ "--submitted-after",
101
+ help="Filter results submitted after this datetime (ISO 8601)",
102
+ formats=[
103
+ "%Y-%m-%dT%H:%M:%S%z",
104
+ "%Y-%m-%dT%H:%M:%SZ",
105
+ "%Y-%m-%d%z",
106
+ "%Y-%m-%dZ",
107
+ ],
108
+ ),
109
+ ] = None,
110
+ submitted_before: Annotated[
111
+ Optional[datetime],
112
+ typer.Option(
113
+ "--submitted-before",
114
+ help="Filter results submitted before this datetime (ISO 8601)",
115
+ formats=[
116
+ "%Y-%m-%dT%H:%M:%S%z",
117
+ "%Y-%m-%dT%H:%M:%SZ",
118
+ "%Y-%m-%d%z",
119
+ "%Y-%m-%dZ",
120
+ ],
121
+ ),
122
+ ] = None,
123
+ status: Annotated[
124
+ Optional[str],
125
+ typer.Option(
126
+ "--status",
127
+ help="Filter results by status",
128
+ ),
129
+ ] = None,
130
+ ) -> FilterOptions:
131
+ return FilterOptions(
132
+ submitted_before=submitted_before,
133
+ submitted_after=submitted_after,
134
+ status=status,
135
+ )
136
+
137
+
89
138
  def get_dropoff_options(
90
139
  category: Annotated[DropoffCategory, Depends(get_category)],
91
140
  offset: Annotated[
@@ -1,15 +1,12 @@
1
- from contextlib import suppress
2
1
  from textwrap import dedent
3
2
  from typing import TYPE_CHECKING
4
3
 
5
4
  import typer
5
+ from rich import print
6
6
  from typer_di import Depends, TyperDI
7
7
 
8
8
  from earthscope_cli.common import get_sdk
9
9
 
10
- with suppress(ModuleNotFoundError):
11
- from rich import print
12
-
13
10
  if TYPE_CHECKING:
14
11
  from earthscope_sdk import EarthScopeClient
15
12
 
@@ -1,4 +1,3 @@
1
- from contextlib import suppress
2
1
  from typing import Annotated
3
2
 
4
3
  import typer
@@ -10,15 +9,13 @@ from earthscope_sdk.auth.error import (
10
9
  NoTokensError,
11
10
  UnauthorizedError,
12
11
  )
12
+ from rich import print
13
13
 
14
14
  from earthscope_cli import __version__ as cli_version
15
15
  from earthscope_cli import debug, login, user
16
16
  from earthscope_cli.dropoff.typer import app as dropoff_app
17
17
  from earthscope_cli.util import ErrorHandlingTyper
18
18
 
19
- with suppress(ModuleNotFoundError):
20
- from rich import print
21
-
22
19
  app = ErrorHandlingTyper(
23
20
  pretty_exceptions_enable=False,
24
21
  )
@@ -1,14 +1,10 @@
1
- from contextlib import suppress
2
-
3
1
  import typer
2
+ from rich import print
4
3
  from typer_di import Depends, TyperDI
5
4
 
6
5
  from earthscope_cli import login
7
6
  from earthscope_cli.common import get_sdk, get_sdk_refreshed
8
7
 
9
- with suppress(ModuleNotFoundError):
10
- from rich import print
11
-
12
8
  app = TyperDI()
13
9
 
14
10
  # Register aliases to login/logout
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: earthscope-cli
3
- Version: 1.1.1
3
+ Version: 1.1.2
4
4
  Summary: A CLI for the EarthScope API
5
5
  Author-email: EarthScope <data-help@earthscope.org>
6
6
  License-Expression: Apache-2.0
@@ -11,9 +11,9 @@ Classifier: Operating System :: OS Independent
11
11
  Requires-Python: >=3.9
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
- Requires-Dist: typer-slim<0.24.0,>=0.17.3
14
+ Requires-Dist: typer<0.24.0
15
15
  Requires-Dist: typer-di>=0.1.3
16
- Requires-Dist: earthscope-sdk>=1.3.0
16
+ Requires-Dist: earthscope-sdk>=1.3.1
17
17
  Requires-Dist: rich>=13.8.0
18
18
  Provides-Extra: dev
19
19
  Requires-Dist: black; extra == "dev"
@@ -1,6 +1,6 @@
1
- typer-slim<0.24.0,>=0.17.3
1
+ typer<0.24.0
2
2
  typer-di>=0.1.3
3
- earthscope-sdk>=1.3.0
3
+ earthscope-sdk>=1.3.1
4
4
  rich>=13.8.0
5
5
 
6
6
  [dev]
@@ -17,7 +17,10 @@ from types import SimpleNamespace
17
17
  from unittest.mock import MagicMock, Mock
18
18
 
19
19
  import pytest
20
- from earthscope_sdk.client.dropoff.models import DropoffCategory, DropoffObject
20
+ from earthscope_sdk.client.dropoff.models import (
21
+ DropoffCategory,
22
+ DropoffObject,
23
+ )
21
24
 
22
25
  from earthscope_cli.dropoff.typer import app
23
26
  from earthscope_cli.dropoff.util import collect_files, key_from_path, safe_str
@@ -288,14 +291,15 @@ class TestUploadCommand:
288
291
  mock_sdk = mock_sdk_context_manager(monkeypatch)
289
292
 
290
293
  # Run command
294
+ cat = "miniseed"
291
295
  result = runner.invoke(
292
296
  app,
293
- ["upload", "-c", "miniseed", str(empty_dir)],
297
+ ["upload", "-c", cat, str(empty_dir)],
294
298
  )
295
299
 
296
300
  # Verify exit code indicates error
297
301
  assert result.exit_code == 1
298
- assert "No files found" in result.stdout
302
+ assert f"No valid {cat} files found" in result.stdout
299
303
 
300
304
  # Verify SDK upload was not called
301
305
  mock_sdk.dropoff.put_objects.assert_not_called()
@@ -338,6 +342,9 @@ class TestListObjectsCommand:
338
342
  prefix="",
339
343
  offset=0,
340
344
  limit=100,
345
+ submitted_after=None,
346
+ submitted_before=None,
347
+ status=None,
341
348
  )
342
349
 
343
350
  # Verify output contains file name
@@ -368,6 +375,9 @@ class TestListObjectsCommand:
368
375
  prefix="demo/",
369
376
  offset=0,
370
377
  limit=100,
378
+ submitted_after=None,
379
+ submitted_before=None,
380
+ status=None,
371
381
  )
372
382
 
373
383
  def test_list_objects_with_pagination(self, monkeypatch):
@@ -395,6 +405,159 @@ class TestListObjectsCommand:
395
405
  prefix="",
396
406
  offset=50,
397
407
  limit=25,
408
+ submitted_after=None,
409
+ submitted_before=None,
410
+ status=None,
411
+ )
412
+
413
+ @pytest.mark.parametrize(
414
+ "flag,value,expected_dt",
415
+ [
416
+ # --submitted-after formats
417
+ (
418
+ "--submitted-after",
419
+ "2026-01-15T12:30:45Z",
420
+ datetime(2026, 1, 15, 12, 30, 45, tzinfo=timezone.utc),
421
+ ),
422
+ (
423
+ "--submitted-after",
424
+ "2026-01-15T12:30:45+00:00",
425
+ datetime(2026, 1, 15, 12, 30, 45, tzinfo=timezone.utc),
426
+ ),
427
+ (
428
+ "--submitted-after",
429
+ "2026-02-21Z",
430
+ datetime(2026, 2, 21, 0, 0, 0, tzinfo=timezone.utc),
431
+ ),
432
+ # --submitted-before formats
433
+ (
434
+ "--submitted-before",
435
+ "2026-03-01T06:00:00Z",
436
+ datetime(2026, 3, 1, 6, 0, 0, tzinfo=timezone.utc),
437
+ ),
438
+ (
439
+ "--submitted-before",
440
+ "2026-03-01T06:00:00+00:00",
441
+ datetime(2026, 3, 1, 6, 0, 0, tzinfo=timezone.utc),
442
+ ),
443
+ (
444
+ "--submitted-before",
445
+ "2026-03-01Z",
446
+ datetime(2026, 3, 1, 0, 0, 0, tzinfo=timezone.utc),
447
+ ),
448
+ ],
449
+ )
450
+ def test_list_objects_timestamp_parsing(
451
+ self, monkeypatch, flag, value, expected_dt
452
+ ):
453
+ """Test that timestamp strings are correctly parsed and passed to the SDK."""
454
+ mock_sdk = mock_sdk_context_manager(monkeypatch)
455
+ mock_sdk.dropoff.list_objects = Mock(
456
+ return_value=SimpleNamespace(items=[], has_next=False, offset=0, limit=100)
457
+ )
458
+
459
+ result = runner.invoke(
460
+ app,
461
+ ["list-objects", "-c", "miniseed", flag, value],
462
+ )
463
+
464
+ assert result.exit_code == 0, (
465
+ f"Command failed: {result.stdout}\n{result.exception}"
466
+ )
467
+
468
+ _, kwargs = mock_sdk.dropoff.list_objects.call_args
469
+ if flag == "--submitted-after":
470
+ assert kwargs["submitted_after"] == expected_dt
471
+ assert kwargs["submitted_before"] is None
472
+ else:
473
+ assert kwargs["submitted_before"] == expected_dt
474
+ assert kwargs["submitted_after"] is None
475
+
476
+ def test_list_objects_with_both_timestamps(self, monkeypatch):
477
+ """Test that both --submitted-after and --submitted-before are forwarded together."""
478
+ mock_sdk = mock_sdk_context_manager(monkeypatch)
479
+ mock_sdk.dropoff.list_objects = Mock(
480
+ return_value=SimpleNamespace(items=[], has_next=False, offset=0, limit=100)
481
+ )
482
+
483
+ result = runner.invoke(
484
+ app,
485
+ [
486
+ "list-objects",
487
+ "-c",
488
+ "miniseed",
489
+ "--submitted-after",
490
+ "2026-01-01T00:00:00Z",
491
+ "--submitted-before",
492
+ "2026-02-01T00:00:00Z",
493
+ ],
494
+ )
495
+
496
+ assert result.exit_code == 0, (
497
+ f"Command failed: {result.stdout}\n{result.exception}"
498
+ )
499
+
500
+ mock_sdk.dropoff.list_objects.assert_called_once_with(
501
+ category="miniseed",
502
+ prefix="",
503
+ offset=0,
504
+ limit=100,
505
+ submitted_after=datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
506
+ submitted_before=datetime(2026, 2, 1, 0, 0, 0, tzinfo=timezone.utc),
507
+ status=None,
508
+ )
509
+
510
+ def test_list_objects_invalid_timestamp(self, monkeypatch):
511
+ """Test that an invalid timestamp string causes a non-zero exit."""
512
+ mock_sdk = mock_sdk_context_manager(monkeypatch)
513
+ mock_sdk.dropoff.list_objects = Mock(
514
+ return_value=SimpleNamespace(items=[], has_next=False, offset=0, limit=100)
515
+ )
516
+
517
+ result = runner.invoke(
518
+ app,
519
+ ["list-objects", "-c", "miniseed", "--submitted-after", "not-a-date"],
520
+ )
521
+
522
+ assert result.exit_code != 0
523
+ mock_sdk.dropoff.list_objects.assert_not_called()
524
+
525
+ def test_list_objects_all_filters_combined(self, monkeypatch):
526
+ """Test that all three new filters can be used together."""
527
+ mock_sdk = mock_sdk_context_manager(monkeypatch)
528
+ mock_sdk.dropoff.list_objects = Mock(
529
+ return_value=SimpleNamespace(items=[], has_next=False, offset=0, limit=100)
530
+ )
531
+
532
+ result = runner.invoke(
533
+ app,
534
+ [
535
+ "list-objects",
536
+ "-c",
537
+ "miniseed",
538
+ "--prefix",
539
+ "demo/",
540
+ "--submitted-after",
541
+ "2026-01-01T00:00:00Z",
542
+ "--submitted-before",
543
+ "2026-02-01Z",
544
+ "--status",
545
+ "ACCEPTED",
546
+ ],
547
+ )
548
+
549
+ assert result.exit_code == 0, (
550
+ f"Command failed: {result.stdout}\n{result.exception}"
551
+ )
552
+
553
+ mock_sdk.dropoff.list_objects.assert_called_once_with(
554
+ category="miniseed",
555
+ prefix="demo/",
556
+ offset=0,
557
+ limit=100,
558
+ submitted_after=datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
559
+ submitted_before=datetime(2026, 2, 1, 0, 0, 0, tzinfo=timezone.utc),
560
+ status="ACCEPTED",
398
561
  )
399
562
 
400
563
  def test_list_objects_empty_result(self, monkeypatch):
@@ -420,7 +583,7 @@ class TestListObjectsCommand:
420
583
  assert result.exit_code == 0
421
584
 
422
585
  # Verify message about no objects
423
- assert "No objects found" in result.stdout
586
+ assert "No objects matched" in result.stdout
424
587
 
425
588
  def test_list_objects_has_next_indicator(self, monkeypatch):
426
589
  """Test that pagination indicators are shown when has_next is True."""
@@ -1 +0,0 @@
1
- __version__ = "1.1.1"
File without changes
File without changes
File without changes
File without changes