usdata 0.2.0__tar.gz → 0.3.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.
Files changed (31) hide show
  1. {usdata-0.2.0 → usdata-0.3.0}/PKG-INFO +32 -8
  2. {usdata-0.2.0 → usdata-0.3.0}/README.md +31 -7
  3. {usdata-0.2.0 → usdata-0.3.0}/pyproject.toml +14 -1
  4. {usdata-0.2.0 → usdata-0.3.0}/pyproject.toml.orig +12 -2
  5. {usdata-0.2.0 → usdata-0.3.0}/src/usdata/__init__.py +7 -2
  6. {usdata-0.2.0 → usdata-0.3.0}/src/usdata/cache.py +3 -0
  7. usdata-0.3.0/src/usdata/cli/__init__.py +5 -0
  8. {usdata-0.2.0 → usdata-0.3.0}/src/usdata/cli/app.py +59 -13
  9. usdata-0.3.0/src/usdata/data/registry.yaml +666 -0
  10. {usdata-0.2.0 → usdata-0.3.0}/src/usdata/fetch.py +4 -0
  11. {usdata-0.2.0 → usdata-0.3.0}/src/usdata/manifest.py +13 -0
  12. {usdata-0.2.0 → usdata-0.3.0}/src/usdata/models.py +72 -4
  13. {usdata-0.2.0 → usdata-0.3.0}/src/usdata/protocols/http.py +2 -0
  14. {usdata-0.2.0 → usdata-0.3.0}/src/usdata/protocols/s3.py +4 -0
  15. {usdata-0.2.0 → usdata-0.3.0}/src/usdata/provenance.py +3 -0
  16. {usdata-0.2.0 → usdata-0.3.0}/src/usdata/providers/base.py +7 -0
  17. {usdata-0.2.0 → usdata-0.3.0}/src/usdata/providers/noaa/coastwatch.py +4 -0
  18. {usdata-0.2.0 → usdata-0.3.0}/src/usdata/providers/noaa/ghcnd.py +4 -0
  19. {usdata-0.2.0 → usdata-0.3.0}/src/usdata/providers/noaa/nexrad.py +5 -0
  20. {usdata-0.2.0 → usdata-0.3.0}/src/usdata/providers/noaa/sites.py +7 -0
  21. usdata-0.3.0/src/usdata/pull.py +162 -0
  22. usdata-0.3.0/src/usdata/py.typed +0 -0
  23. {usdata-0.2.0 → usdata-0.3.0}/src/usdata/query.py +2 -0
  24. {usdata-0.2.0 → usdata-0.3.0}/src/usdata/registry.py +67 -5
  25. usdata-0.2.0/src/usdata/cli/__init__.py +0 -3
  26. usdata-0.2.0/src/usdata/data/registry.yaml +0 -50
  27. {usdata-0.2.0 → usdata-0.3.0}/src/usdata/data/nexrad_sites.csv +0 -0
  28. {usdata-0.2.0 → usdata-0.3.0}/src/usdata/data/places.yaml +0 -0
  29. {usdata-0.2.0 → usdata-0.3.0}/src/usdata/protocols/__init__.py +0 -0
  30. {usdata-0.2.0 → usdata-0.3.0}/src/usdata/providers/__init__.py +0 -0
  31. {usdata-0.2.0 → usdata-0.3.0}/src/usdata/providers/noaa/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: usdata
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Unified Python SDK and CLI for discovering, fetching, and tracking provenance of U.S. public scientific data
5
5
  Keywords: noaa,usgs,nasa,open-data,scientific-data,provenance
6
6
  Author: Jake Van Slyke
@@ -28,6 +28,22 @@ provenance of U.S. public scientific data (NOAA, USGS, NASA, and more).
28
28
  > data with provenance; other registry entries are stubs.
29
29
  > See [docs/roadmap.md](docs/roadmap.md).
30
30
 
31
+ ## Providers
32
+
33
+ <!-- registry:start -->
34
+ | Provider | Available | Stub | Planned | Next up (0.3) | Datasets |
35
+ |---|---:|---:|---:|---|---|
36
+ | [NOAA](docs/providers/noaa.md) | 2 | 1 | 26 | `coastwatch-sst` | `ghcn-daily`, `nexrad-level2`, _coastwatch-sst_, +26 planned |
37
+ | [Census Bureau](docs/providers/census.md) | 0 | 0 | 1 | — | +1 planned |
38
+ | [EPA](docs/providers/epa.md) | 0 | 0 | 1 | — | +1 planned |
39
+ | [FEMA](docs/providers/fema.md) | 0 | 0 | 1 | — | +1 planned |
40
+ | [NASA](docs/providers/nasa.md) | 0 | 0 | 1 | — | +1 planned |
41
+ | [USDA](docs/providers/usda.md) | 0 | 0 | 1 | — | +1 planned |
42
+ | [USGS](docs/providers/usgs.md) | 0 | 0 | 3 | — | +3 planned |
43
+
44
+ Available datasets are in `code`, stubs in _italics_; planned ones are counted. Each provider page has access notes and full dataset details; [docs/roadmap.md](docs/roadmap.md) lists datasets by target version.
45
+ <!-- registry:end -->
46
+
31
47
  ## Install
32
48
 
33
49
  ```sh
@@ -65,15 +81,20 @@ usdata fetch noaa:ghcn-daily -p stations=USW00013967 --start 2024-01-01 --end 20
65
81
  usdata fetch noaa:nexrad-level2 --lat 35.47 --lon -97.52 \
66
82
  --start 2024-05-06T20:00 --end 2024-05-06T23:00 # nearest radar (KTLX)
67
83
  usdata fetch noaa:nexrad-level2 -p site=KTLX --start 2024-05-06T20:00 --end 2024-05-06T20:30 --dry-run
68
- usdata pull dataset.yaml
84
+ usdata pull dataset.yaml # resolve, fetch, write dataset.lock.json
85
+ usdata verify dataset.yaml # exit 1 if any cached input drifted
69
86
  ```
70
87
 
71
88
  Fetched files land in `~/.cache/usdata/<provider>/<dataset>/` (override with
72
89
  `USDATA_CACHE_DIR` or `--cache-dir`), each with a `.provenance.json` sidecar
73
90
  recording source URL, retrieval time, checksum, size, and license.
74
91
 
75
- A manifest declares every input a project needs; `pull` fetches them and writes
76
- a lockfile with checksums and provenance so the inputs can be reproduced:
92
+ A manifest declares every input a project needs. `pull` resolves each source,
93
+ fetches it, and writes `dataset.lock.json` pinning every asset with its checksum
94
+ and provenance. A second `pull` restores exactly what the lockfile pins without
95
+ re-querying upstream, so the inputs stay reproducible even if the source
96
+ changes. `verify` re-hashes the cached files against the lockfile. Editing the
97
+ manifest after locking requires `pull --force` to re-resolve.
77
98
 
78
99
  ```yaml
79
100
  name: tornado-environment
@@ -102,11 +123,14 @@ just run search radar
102
123
 
103
124
  Integration tests that hit live services run with `just test-integration`.
104
125
 
105
- Releases are published to PyPI by tagging: create a GitHub release and the
106
- `publish.yml` workflow uploads via trusted publishing.
126
+ Releases: `just release minor` opens a version-bump PR; merging it publishes
127
+ to PyPI and creates the tag and GitHub release. See
128
+ [docs/versioning.md](docs/versioning.md).
107
129
 
108
- See [docs/architecture.md](docs/architecture.md) for how the pieces fit and
109
- [docs/adr/](docs/adr/) for why.
130
+ See [docs/providers/](docs/providers/) for per-provider access notes,
131
+ [docs/architecture.md](docs/architecture.md) for how the pieces fit,
132
+ [docs/adr/](docs/adr/) for why, and [CONTRIBUTING.md](CONTRIBUTING.md) to add
133
+ a dataset.
110
134
 
111
135
  ## License
112
136
 
@@ -7,6 +7,22 @@ provenance of U.S. public scientific data (NOAA, USGS, NASA, and more).
7
7
  > data with provenance; other registry entries are stubs.
8
8
  > See [docs/roadmap.md](docs/roadmap.md).
9
9
 
10
+ ## Providers
11
+
12
+ <!-- registry:start -->
13
+ | Provider | Available | Stub | Planned | Next up (0.3) | Datasets |
14
+ |---|---:|---:|---:|---|---|
15
+ | [NOAA](docs/providers/noaa.md) | 2 | 1 | 26 | `coastwatch-sst` | `ghcn-daily`, `nexrad-level2`, _coastwatch-sst_, +26 planned |
16
+ | [Census Bureau](docs/providers/census.md) | 0 | 0 | 1 | — | +1 planned |
17
+ | [EPA](docs/providers/epa.md) | 0 | 0 | 1 | — | +1 planned |
18
+ | [FEMA](docs/providers/fema.md) | 0 | 0 | 1 | — | +1 planned |
19
+ | [NASA](docs/providers/nasa.md) | 0 | 0 | 1 | — | +1 planned |
20
+ | [USDA](docs/providers/usda.md) | 0 | 0 | 1 | — | +1 planned |
21
+ | [USGS](docs/providers/usgs.md) | 0 | 0 | 3 | — | +3 planned |
22
+
23
+ Available datasets are in `code`, stubs in _italics_; planned ones are counted. Each provider page has access notes and full dataset details; [docs/roadmap.md](docs/roadmap.md) lists datasets by target version.
24
+ <!-- registry:end -->
25
+
10
26
  ## Install
11
27
 
12
28
  ```sh
@@ -44,15 +60,20 @@ usdata fetch noaa:ghcn-daily -p stations=USW00013967 --start 2024-01-01 --end 20
44
60
  usdata fetch noaa:nexrad-level2 --lat 35.47 --lon -97.52 \
45
61
  --start 2024-05-06T20:00 --end 2024-05-06T23:00 # nearest radar (KTLX)
46
62
  usdata fetch noaa:nexrad-level2 -p site=KTLX --start 2024-05-06T20:00 --end 2024-05-06T20:30 --dry-run
47
- usdata pull dataset.yaml
63
+ usdata pull dataset.yaml # resolve, fetch, write dataset.lock.json
64
+ usdata verify dataset.yaml # exit 1 if any cached input drifted
48
65
  ```
49
66
 
50
67
  Fetched files land in `~/.cache/usdata/<provider>/<dataset>/` (override with
51
68
  `USDATA_CACHE_DIR` or `--cache-dir`), each with a `.provenance.json` sidecar
52
69
  recording source URL, retrieval time, checksum, size, and license.
53
70
 
54
- A manifest declares every input a project needs; `pull` fetches them and writes
55
- a lockfile with checksums and provenance so the inputs can be reproduced:
71
+ A manifest declares every input a project needs. `pull` resolves each source,
72
+ fetches it, and writes `dataset.lock.json` pinning every asset with its checksum
73
+ and provenance. A second `pull` restores exactly what the lockfile pins without
74
+ re-querying upstream, so the inputs stay reproducible even if the source
75
+ changes. `verify` re-hashes the cached files against the lockfile. Editing the
76
+ manifest after locking requires `pull --force` to re-resolve.
56
77
 
57
78
  ```yaml
58
79
  name: tornado-environment
@@ -81,11 +102,14 @@ just run search radar
81
102
 
82
103
  Integration tests that hit live services run with `just test-integration`.
83
104
 
84
- Releases are published to PyPI by tagging: create a GitHub release and the
85
- `publish.yml` workflow uploads via trusted publishing.
105
+ Releases: `just release minor` opens a version-bump PR; merging it publishes
106
+ to PyPI and creates the tag and GitHub release. See
107
+ [docs/versioning.md](docs/versioning.md).
86
108
 
87
- See [docs/architecture.md](docs/architecture.md) for how the pieces fit and
88
- [docs/adr/](docs/adr/) for why.
109
+ See [docs/providers/](docs/providers/) for per-provider access notes,
110
+ [docs/architecture.md](docs/architecture.md) for how the pieces fit,
111
+ [docs/adr/](docs/adr/) for why, and [CONTRIBUTING.md](CONTRIBUTING.md) to add
112
+ a dataset.
89
113
 
90
114
  ## License
91
115
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "usdata"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  description = "Unified Python SDK and CLI for discovering, fetching, and tracking provenance of U.S. public scientific data"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -41,6 +41,7 @@ usdata = "usdata.cli:app"
41
41
  dev = [
42
42
  "pyright>=1.1.380",
43
43
  "pytest>=8.0",
44
+ "pytest-cov>=7.1.0",
44
45
  "respx>=0.23.1",
45
46
  "ruff>=0.6",
46
47
  ]
@@ -66,7 +67,19 @@ select = [
66
67
  "B",
67
68
  "SIM",
68
69
  "RUF",
70
+ "D",
69
71
  ]
72
+ ignore = [
73
+ "D105",
74
+ "D107",
75
+ ]
76
+
77
+ [tool.ruff.lint.pydocstyle]
78
+ convention = "google"
79
+
80
+ [tool.ruff.lint.per-file-ignores]
81
+ "tests/**" = ["D"]
82
+ "scripts/**" = ["D"]
70
83
 
71
84
  [tool.pyright]
72
85
  include = [
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "usdata"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  description = "Unified Python SDK and CLI for discovering, fetching, and tracking provenance of U.S. public scientific data"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -33,6 +33,7 @@ usdata = "usdata.cli:app"
33
33
  dev = [
34
34
  "pyright>=1.1.380",
35
35
  "pytest>=8.0",
36
+ "pytest-cov>=7.1.0",
36
37
  "respx>=0.23.1",
37
38
  "ruff>=0.6",
38
39
  ]
@@ -47,7 +48,16 @@ target-version = "py311"
47
48
  src = ["src", "tests"]
48
49
 
49
50
  [tool.ruff.lint]
50
- select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
51
+ select = ["E", "F", "I", "UP", "B", "SIM", "RUF", "D"]
52
+ # Docstrings are required on the public API only; magic methods and __init__ are exempt.
53
+ ignore = ["D105", "D107"]
54
+
55
+ [tool.ruff.lint.pydocstyle]
56
+ convention = "google"
57
+
58
+ [tool.ruff.lint.per-file-ignores]
59
+ "tests/**" = ["D"]
60
+ "scripts/**" = ["D"]
51
61
 
52
62
  [tool.pyright]
53
63
  include = ["src", "tests"]
@@ -11,6 +11,7 @@ except PackageNotFoundError: # running from a source tree without an install
11
11
  __version__ = "0.0.0"
12
12
 
13
13
  from usdata.models import Asset, BBox, Dataset, Provenance, Query, TimeRange
14
+ from usdata.pull import pull, verify
14
15
  from usdata.query import build_query
15
16
  from usdata.registry import DatasetNotFound, Registry, SearchResult, default_registry
16
17
 
@@ -28,13 +29,17 @@ __all__ = [
28
29
  "build_query",
29
30
  "default_registry",
30
31
  "get",
32
+ "pull",
31
33
  "search",
34
+ "verify",
32
35
  ]
33
36
 
34
37
 
35
- def search(text: str | None = None, **kwargs: Any) -> list[SearchResult]:
38
+ def search(
39
+ text: str | None = None, *, include_planned: bool = False, **kwargs: Any
40
+ ) -> list[SearchResult]:
36
41
  """Search the curated registry. Keyword arguments match ``build_query``."""
37
- return default_registry().search(build_query(text, **kwargs))
42
+ return default_registry().search(build_query(text, **kwargs), include_planned=include_planned)
38
43
 
39
44
 
40
45
  def get(dataset_id: str) -> Dataset:
@@ -12,6 +12,7 @@ ENV_VAR = "USDATA_CACHE_DIR"
12
12
 
13
13
 
14
14
  def cache_dir() -> Path:
15
+ """Cache root: $USDATA_CACHE_DIR, else $XDG_CACHE_HOME/usdata, else ~/.cache/usdata."""
15
16
  if override := os.environ.get(ENV_VAR):
16
17
  return Path(override).expanduser()
17
18
  xdg = os.environ.get("XDG_CACHE_HOME")
@@ -20,12 +21,14 @@ def cache_dir() -> Path:
20
21
 
21
22
 
22
23
  def asset_path(asset: Asset, root: Path | None = None) -> Path:
24
+ """Where an asset lives in the cache: <root>/<provider>/<dataset>/<asset id>."""
23
25
  provider, name = asset.dataset_id.split(":", 1)
24
26
  safe_id = asset.id.replace("/", "_")
25
27
  return (root or cache_dir()) / provider / name / safe_id
26
28
 
27
29
 
28
30
  def sha256_file(path: Path, chunk_size: int = 1 << 20) -> str:
31
+ """Hex sha256 of a file, prefixed 'sha256:' to match Asset.checksum."""
29
32
  h = hashlib.sha256()
30
33
  with path.open("rb") as f:
31
34
  while chunk := f.read(chunk_size):
@@ -0,0 +1,5 @@
1
+ """Command-line interface. The Typer app lives in ``usdata.cli.app``."""
2
+
3
+ from usdata.cli.app import app
4
+
5
+ __all__ = ["app"]
@@ -1,3 +1,5 @@
1
+ """Typer application: argument parsing and exit codes only; logic lives in the library."""
2
+
1
3
  from __future__ import annotations
2
4
 
3
5
  from pathlib import Path
@@ -7,10 +9,13 @@ import httpx
7
9
  import typer
8
10
 
9
11
  from usdata import __version__, build_query, default_registry
10
- from usdata.fetch import fetch_asset
11
- from usdata.manifest import Manifest
12
+ from usdata.fetch import ChecksumMismatch, fetch_asset
13
+ from usdata.manifest import lockfile_path
12
14
  from usdata.providers import load_adapter
13
15
  from usdata.providers.base import NotImplementedProvider
16
+ from usdata.pull import ManifestChanged, UnknownDatasets
17
+ from usdata.pull import pull as pull_manifest
18
+ from usdata.pull import verify as verify_manifest
14
19
  from usdata.query import UnknownPlace
15
20
  from usdata.registry import DatasetNotFound
16
21
 
@@ -34,6 +39,7 @@ def main(
34
39
  typer.Option("--version", callback=_version_callback, is_eager=True, help="Show version."),
35
40
  ] = None,
36
41
  ) -> None:
42
+ """usdata: discover, fetch, and track provenance of U.S. public scientific data."""
37
43
  pass
38
44
 
39
45
 
@@ -46,6 +52,9 @@ def search(
46
52
  state: Annotated[str | None, typer.Option(help="State name or postal code.")] = None,
47
53
  start: Annotated[str | None, typer.Option(help="ISO date or datetime.")] = None,
48
54
  end: Annotated[str | None, typer.Option(help="ISO date or datetime.")] = None,
55
+ planned: Annotated[
56
+ bool, typer.Option("--planned", help="Include planned datasets that have no adapter yet.")
57
+ ] = False,
49
58
  ) -> None:
50
59
  """Search the curated dataset registry."""
51
60
  try:
@@ -53,13 +62,14 @@ def search(
53
62
  except UnknownPlace as e:
54
63
  typer.secho(f"Unknown place: {e}", err=True, fg="red")
55
64
  raise typer.Exit(code=2) from None
56
- results = default_registry().search(query)
65
+ results = default_registry().search(query, include_planned=planned)
57
66
  if not results:
58
67
  typer.echo("No datasets matched.")
59
68
  raise typer.Exit(code=1)
60
69
  width = max(len(r.dataset.id) for r in results)
61
70
  for r in results:
62
- typer.echo(f"{r.dataset.id:<{width}} {r.dataset.title}")
71
+ ds = r.dataset
72
+ typer.echo(f"{ds.id:<{width}} {ds.status.value:<9} {ds.version_label:<12} {ds.title}")
63
73
 
64
74
 
65
75
  @app.command()
@@ -74,6 +84,8 @@ def info(
74
84
  raise typer.Exit(code=2) from None
75
85
  typer.echo(f"{ds.id}\n {ds.title}\n")
76
86
  typer.echo(f" {ds.description.strip()}\n")
87
+ typer.echo(f" status: {ds.status.value} ({ds.version_label})")
88
+ typer.echo(f" domain: {ds.domain}")
77
89
  typer.echo(f" provider: {ds.provider}")
78
90
  typer.echo(f" protocol: {ds.protocol.value}")
79
91
  typer.echo(f" license: {ds.license or 'unknown'}")
@@ -166,13 +178,47 @@ def fetch(
166
178
 
167
179
 
168
180
  @app.command()
169
- def pull(manifest: Annotated[Path, typer.Argument(exists=True, dir_okay=False)]) -> None:
170
- """Fetch every source in a manifest and write a lockfile."""
171
- m = Manifest.load(manifest)
172
- missing = m.validate_against()
173
- if missing:
174
- typer.secho(f"Unknown datasets in manifest: {', '.join(missing)}", err=True, fg="red")
181
+ def pull(
182
+ manifest: Annotated[Path, typer.Argument(exists=True, dir_okay=False)],
183
+ cache_dir: Annotated[Path | None, typer.Option(help="Override the cache directory.")] = None,
184
+ force: Annotated[
185
+ bool,
186
+ typer.Option(help="Ignore an existing lockfile: re-resolve every source and rewrite it."),
187
+ ] = False,
188
+ ) -> None:
189
+ """Fetch every source in a manifest and write (or restore from) its lockfile."""
190
+ try:
191
+ result = pull_manifest(manifest, root=cache_dir, force=force)
192
+ except (UnknownDatasets, ManifestChanged, UnknownPlace, ValueError) as e:
193
+ typer.secho(str(e), err=True, fg="red")
194
+ raise typer.Exit(code=2) from None
195
+ except NotImplementedProvider as e:
196
+ typer.secho(str(e), err=True, fg="yellow")
197
+ raise typer.Exit(code=3) from None
198
+ except (httpx.HTTPError, ChecksumMismatch) as e:
199
+ typer.secho(f"fetch failed: {e}", err=True, fg="red")
200
+ raise typer.Exit(code=4) from None
201
+ for f in result.fetched:
202
+ tag = "cached" if f.from_cache else "fetched"
203
+ typer.echo(f"{f.path}\t{tag}\t{f.provenance.size} bytes")
204
+ mode = "restored from" if result.from_lockfile else "wrote"
205
+ typer.echo(f"{len(result.fetched)} asset(s); {mode} {result.lockfile_path}", err=True)
206
+
207
+
208
+ @app.command()
209
+ def verify(
210
+ manifest: Annotated[Path, typer.Argument(exists=True, dir_okay=False)],
211
+ cache_dir: Annotated[Path | None, typer.Option(help="Override the cache directory.")] = None,
212
+ ) -> None:
213
+ """Check cached files against a manifest's lockfile. Exit 1 on any drift."""
214
+ lock = lockfile_path(manifest)
215
+ if not lock.exists():
216
+ typer.secho(f"no lockfile at {lock}; run pull first", err=True, fg="red")
175
217
  raise typer.Exit(code=2)
176
- typer.echo(f"{m.name} v{m.version}: {len(m.sources)} source(s) validated")
177
- typer.secho("pull is not implemented yet; no data was fetched.", err=True, fg="yellow")
178
- raise typer.Exit(code=3)
218
+ drift = verify_manifest(manifest, root=cache_dir)
219
+ for d in drift:
220
+ typer.echo(f"{d.asset_id}\t{d.problem}\t{d.path}")
221
+ if drift:
222
+ typer.secho(f"{len(drift)} asset(s) drifted from {lock.name}", err=True, fg="red")
223
+ raise typer.Exit(code=1)
224
+ typer.echo(f"all assets match {lock.name}", err=True)