usdata 0.29.0__tar.gz → 0.31.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 (85) hide show
  1. {usdata-0.29.0 → usdata-0.31.0}/PKG-INFO +2 -2
  2. {usdata-0.29.0 → usdata-0.31.0}/README.md +1 -1
  3. {usdata-0.29.0 → usdata-0.31.0}/pyproject.toml +1 -1
  4. {usdata-0.29.0 → usdata-0.31.0}/pyproject.toml.orig +1 -1
  5. usdata-0.31.0/src/usdata/_census.py +51 -0
  6. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/_fetch.py +14 -1
  7. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/_grib.py +12 -7
  8. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/cache.py +14 -5
  9. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/data/places.csv +3300 -3292
  10. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/data/places.sources.json +17 -1
  11. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/data/registry.yaml +43 -8
  12. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/manifest.py +79 -8
  13. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/models.py +20 -4
  14. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/provenance.py +1 -3
  15. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/providers/base.py +22 -0
  16. usdata-0.31.0/src/usdata/providers/census/__init__.py +1 -0
  17. usdata-0.31.0/src/usdata/providers/census/acs.py +243 -0
  18. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/providers/epa/aqs.py +4 -1
  19. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/providers/fema/declarations.py +6 -0
  20. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/providers/noaa/coastwatch.py +5 -5
  21. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/providers/noaa/nws_vtec.py +4 -1
  22. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/pull.py +40 -11
  23. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/query.py +54 -5
  24. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/readers.py +21 -10
  25. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/testing.py +2 -1
  26. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/__init__.py +0 -0
  27. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/__main__.py +0 -0
  28. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/_aqs.py +0 -0
  29. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/_files.py +0 -0
  30. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/_hurdat2.py +0 -0
  31. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/_netcdf.py +0 -0
  32. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/_progress.py +0 -0
  33. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/_radar.py +0 -0
  34. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/cache_ops.py +0 -0
  35. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/cite.py +0 -0
  36. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/cli/__init__.py +0 -0
  37. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/cli/app.py +0 -0
  38. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/cli/cache.py +0 -0
  39. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/cli/cite.py +0 -0
  40. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/cli/doctor.py +0 -0
  41. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/cli/inspect.py +0 -0
  42. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/cli/progress.py +0 -0
  43. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/data/nexrad_sites.csv +0 -0
  44. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/doctor.py +0 -0
  45. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/inspect.py +0 -0
  46. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/mirror.py +0 -0
  47. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/protocols/__init__.py +0 -0
  48. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/protocols/erddap.py +0 -0
  49. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/protocols/http.py +0 -0
  50. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/protocols/listing.py +0 -0
  51. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/protocols/s3.py +0 -0
  52. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/providers/__init__.py +0 -0
  53. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/providers/credentials.py +0 -0
  54. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/providers/epa/__init__.py +0 -0
  55. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/providers/fema/__init__.py +0 -0
  56. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/providers/http.py +0 -0
  57. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/providers/noaa/__init__.py +0 -0
  58. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/providers/noaa/coops.py +0 -0
  59. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/providers/noaa/gfs.py +0 -0
  60. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/providers/noaa/ghcnd.py +0 -0
  61. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/providers/noaa/glm.py +0 -0
  62. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/providers/noaa/goes.py +0 -0
  63. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/providers/noaa/grib_index.py +0 -0
  64. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/providers/noaa/gsom.py +0 -0
  65. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/providers/noaa/gsoy.py +0 -0
  66. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/providers/noaa/hrrr.py +0 -0
  67. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/providers/noaa/hurdat2.py +0 -0
  68. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/providers/noaa/ibtracs.py +0 -0
  69. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/providers/noaa/lcd.py +0 -0
  70. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/providers/noaa/mrms.py +0 -0
  71. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/providers/noaa/nbm.py +0 -0
  72. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/providers/noaa/nexrad.py +0 -0
  73. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/providers/noaa/nexrad_level3.py +0 -0
  74. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/providers/noaa/normals.py +0 -0
  75. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/providers/noaa/rap.py +0 -0
  76. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/providers/noaa/sites.py +0 -0
  77. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/providers/noaa/spc.py +0 -0
  78. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/providers/noaa/storm_events.py +0 -0
  79. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/providers/params.py +0 -0
  80. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/providers/usgs/__init__.py +0 -0
  81. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/providers/usgs/daily.py +0 -0
  82. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/providers/usgs/earthquakes.py +0 -0
  83. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/py.typed +0 -0
  84. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/registry.py +0 -0
  85. {usdata-0.29.0 → usdata-0.31.0}/src/usdata/selection.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: usdata
3
- Version: 0.29.0
3
+ Version: 0.31.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,open-data,scientific-data,provenance,reproducible-research,weather,climate,meteorology
6
6
  Author: Jake Van Slyke
@@ -113,7 +113,7 @@ breaking changes.
113
113
  | [docs.usdata.dev](https://docs.usdata.dev/) | How to use it: [install](https://docs.usdata.dev/install/), [getting started](https://docs.usdata.dev/getting-started/), guides, dataset notes, and reference |
114
114
  | [Severe-weather case study](https://usdata.dev/studies/severe-weather-case-study/) | One tornado, six sources, one manifest and lockfile, ending in a citation |
115
115
 
116
- Twenty-seven datasets are available today and nineteen more are planned, grouped
116
+ Twenty-eight datasets are available today and eighteen more are planned, grouped
117
117
  by agency and product family in the [catalog](docs/providers/README.md).
118
118
 
119
119
  ## How this compares
@@ -68,7 +68,7 @@ breaking changes.
68
68
  | [docs.usdata.dev](https://docs.usdata.dev/) | How to use it: [install](https://docs.usdata.dev/install/), [getting started](https://docs.usdata.dev/getting-started/), guides, dataset notes, and reference |
69
69
  | [Severe-weather case study](https://usdata.dev/studies/severe-weather-case-study/) | One tornado, six sources, one manifest and lockfile, ending in a citation |
70
70
 
71
- Twenty-seven datasets are available today and nineteen more are planned, grouped
71
+ Twenty-eight datasets are available today and eighteen more are planned, grouped
72
72
  by agency and product family in the [catalog](docs/providers/README.md).
73
73
 
74
74
  ## How this compares
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "usdata"
3
- version = "0.29.0"
3
+ version = "0.31.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"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "usdata"
3
- version = "0.29.0"
3
+ version = "0.31.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"
@@ -0,0 +1,51 @@
1
+ """Local reading of Census Data API tables into a pandas DataFrame, behind pandas.
2
+
3
+ A fetched ``census:acs-5year`` file is the service's response as served: a JSON
4
+ array whose first row names the columns and whose other rows are one geography
5
+ each, every value a string. The table is those rows under those names, with the
6
+ estimate and margin-of-error columns as numbers and everything else, geography
7
+ codes and their leading zeros included, as text.
8
+
9
+ Negative annotation codes such as ``-555555555`` stay the numbers they are.
10
+ Each means something different (too few sample cases, not applicable, a
11
+ controlled estimate), so they are not turned into missing values (ADR 0045).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import re
18
+ from importlib import import_module
19
+ from typing import TYPE_CHECKING, Any
20
+
21
+ from usdata.readers import MissingReaderDependency, source_attrs
22
+
23
+ if TYPE_CHECKING:
24
+ from usdata._fetch import FetchedAsset
25
+
26
+ NUMERIC = re.compile(r"[A-Z]\d{5}[A-Z0-9]*_\d{3}[EM]")
27
+ """An estimate (``...E``) or margin of error (``...M``) variable, such as ``B01003_001E``.
28
+
29
+ Annotation columns (``...EA``, ``...MA``) and ``NAME`` do not match and stay text.
30
+ """
31
+
32
+
33
+ def open_acs(fetched: FetchedAsset) -> Any:
34
+ """Parse a fetched ACS table into a pandas DataFrame, one row per geography."""
35
+ try:
36
+ pandas = import_module("pandas")
37
+ except ModuleNotFoundError as error:
38
+ if error.name != "pandas":
39
+ raise
40
+ raise MissingReaderDependency(
41
+ 'ACS reading requires pandas; install it with: pip install "usdata[pandas]" '
42
+ '(or uv add "usdata[pandas]")'
43
+ ) from error
44
+ # Reading a fetched asset is strictly local; the cached file is never rewritten.
45
+ header, *rows = json.loads(fetched.path.read_text(encoding="utf-8"))
46
+ frame = pandas.DataFrame(rows, columns=header, dtype="string")
47
+ for column in header:
48
+ if NUMERIC.fullmatch(column):
49
+ frame[column] = pandas.to_numeric(frame[column])
50
+ frame.attrs["usdata"] = source_attrs(fetched)
51
+ return frame
@@ -39,7 +39,7 @@ class FetchedAsset(BaseModel):
39
39
  def open(self) -> Any:
40
40
  """Open local data with the reader its format implies, with that reader's defaults.
41
41
 
42
- CSV and ERDDAP CSV, HURDAT2 and AQS daily JSON return a pandas DataFrame;
42
+ CSV and ERDDAP CSV, HURDAT2, AQS daily JSON, and ACS tables return a pandas DataFrame;
43
43
  NetCDF4 and GRIB2 a loaded xarray Dataset; NEXRAD Level II an xarray
44
44
  DataTree. Provenance is kept in the result's ``attrs["usdata"]``. A file
45
45
  that needs options, or whose metadata leaves its format ambiguous, is
@@ -234,6 +234,19 @@ def _fetch_with(
234
234
  ``staging`` is passed to each fetch; see ``_fetch_asset``.
235
235
  """
236
236
  assets = ordered(adapter.list_assets(query))
237
+ return _fetch_listed(adapter, dataset, assets, root=root, force=force, staging=staging)
238
+
239
+
240
+ def _fetch_listed(
241
+ adapter: Provider,
242
+ dataset: Dataset,
243
+ assets: list[Asset],
244
+ *,
245
+ root: Path | None = None,
246
+ force: bool = False,
247
+ staging: Path | None = None,
248
+ ) -> list[FetchedAsset]:
249
+ """Fetch assets already listed and put in order, as the loop does once it has listed them."""
237
250
  _progress.batch([asset.size for asset in assets])
238
251
  return [
239
252
  _fetch_asset(dataset, a, adapter, root=root, force=force, staging=staging) for a in assets
@@ -304,15 +304,19 @@ def base_name(short_name: str | None, parameter: Sequence[Any], source: str) ->
304
304
  return _product_name(source) or f"parameter_{discipline}_{category}_{number}"
305
305
 
306
306
 
307
- def _time(date: Any, time: Any) -> str | None:
307
+ def _time(date: Any, time: Any, second: Any = None) -> str | None:
308
+ """An ecCodes date and ``HHMM`` time as ISO 8601 UTC, with the message's seconds.
309
+
310
+ ecCodes spells times to the minute and keeps the reference time's seconds
311
+ in a key of their own, which MRMS sets: a file stamped ``20:00:41`` would
312
+ otherwise read as ``20:00:00``. A validity time is the reference time plus
313
+ a whole-minute step, so it carries the same seconds.
314
+ """
308
315
  if date is None or time is None:
309
316
  return None
310
317
  try:
311
- return (
312
- datetime.strptime(f"{int(date):08d}{int(time):04d}", "%Y%m%d%H%M")
313
- .replace(tzinfo=UTC)
314
- .isoformat()
315
- )
318
+ stamp = datetime.strptime(f"{int(date):08d}{int(time):04d}", "%Y%m%d%H%M")
319
+ return stamp.replace(second=int(second or 0), tzinfo=UTC).isoformat()
316
320
  except ValueError:
317
321
  return None
318
322
 
@@ -494,11 +498,12 @@ def open_grib2(
494
498
  # The file's own spelling stays beside the rewritten one, as cfgrib keeps it.
495
499
  attrs["GRIB_units"] = units
496
500
  attrs["units"] = plain
501
+ second = _get(eccodes, h, "second", int)
497
502
  for label, date_key, time_key in (
498
503
  ("reference_time", "dataDate", "dataTime"),
499
504
  ("valid_time", "validityDate", "validityTime"),
500
505
  ):
501
- stamp = _time(attrs.get(date_key), attrs.get(time_key))
506
+ stamp = _time(attrs.get(date_key), attrs.get(time_key), second)
502
507
  if stamp:
503
508
  attrs[label] = stamp
504
509
  message: dict[str, Any] = {
@@ -10,6 +10,8 @@ from pathlib import Path
10
10
  from usdata.models import Asset
11
11
 
12
12
  ENV_VAR = "USDATA_CACHE_DIR"
13
+ SIDECAR_SUFFIX = ".provenance.json"
14
+ """What a provenance sidecar's name adds to its data file's; ``provenance`` names it from here."""
13
15
 
14
16
 
15
17
  def cache_dir() -> Path:
@@ -38,7 +40,9 @@ def cached_path(dataset_id: str, asset_id: str, root: Path | None = None) -> Pat
38
40
  The path the cache uses for that asset, whether or not it exists.
39
41
 
40
42
  Raises:
41
- ValueError: Either id is unsafe, or the path would escape the cache root.
43
+ ValueError: Either id is unsafe, names a sidecar or temporary file, or the
44
+ path would escape the cache root. Symlinks inside the root are
45
+ followed like any directory, so part of the cache can live elsewhere.
42
46
  """
43
47
  provider, sep, name = dataset_id.partition(":")
44
48
  if not sep or any(
@@ -48,11 +52,16 @@ def cached_path(dataset_id: str, asset_id: str, root: Path | None = None) -> Pat
48
52
  safe_id = asset_id.replace("/", "_")
49
53
  if not safe_id or safe_id in {".", ".."} or "\\" in safe_id or "\x00" in safe_id:
50
54
  raise ValueError(f"unsafe asset id: {asset_id!r}")
55
+ # A file of this name would be taken for another asset's sidecar, or for a temporary file.
56
+ if safe_id.endswith(SIDECAR_SUFFIX) or (safe_id.startswith(".") and safe_id.endswith(".part")):
57
+ raise ValueError(f"asset id {asset_id!r} names a file the cache keeps for itself")
51
58
  base = (root or cache_dir()).expanduser().resolve()
52
- path = base / provider / name / safe_id
53
- for candidate in (path, path.with_name(path.name + ".provenance.json")):
54
- if not candidate.resolve().is_relative_to(base):
55
- raise ValueError(f"cache path escapes root: {candidate}")
59
+ home = base / provider / name
60
+ path = home / safe_id
61
+ # Checked on the path as written, so a directory the user symlinked elsewhere is still
62
+ # inside the cache; only an id that re-anchors the path, such as C:x on Windows, is not.
63
+ if path.parent != home:
64
+ raise ValueError(f"cache path escapes root: {path}")
56
65
  return path
57
66
 
58
67