fimeval 0.1.57__py3-none-any.whl → 0.1.58__py3-none-any.whl

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.
@@ -2,4 +2,4 @@ from .access_benchfim import benchFIMquery
2
2
 
3
3
  __all__ = [
4
4
  "benchFIMquery",
5
- ]
5
+ ]
@@ -8,7 +8,7 @@ from __future__ import annotations
8
8
  from typing import Optional, Dict, Any, List, Tuple
9
9
  from pathlib import Path
10
10
  import os
11
- import json
11
+ import json
12
12
 
13
13
  import rasterio
14
14
  from rasterio.warp import transform_bounds
@@ -24,13 +24,15 @@ from .utilis import (
24
24
  _to_hour_or_none,
25
25
  _record_day,
26
26
  _record_hour_or_none,
27
- _pretty_date_for_print,
27
+ _pretty_date_for_print,
28
+ _ensure_local_gpkg,
28
29
  )
29
30
 
30
31
  # Preferred area CRSs for area calculations
31
- AREA_CRS_US = "EPSG:5070" # for CONUS
32
+ AREA_CRS_US = "EPSG:5070" # for CONUS
32
33
  AREA_CRS_GLOBAL = "EPSG:6933" # WGS 84 / NSIDC EASE-Grid 2.0 Global (equal-area) for rest of world: in future if the data is added into the catalog.
33
34
 
35
+
34
36
  # Helper: pretty-print container so that print(response) shows the structured text.
35
37
  # If "printable" is empty (e.g., download=True), nothing is printed.
36
38
  class PrettyDict(dict):
@@ -40,6 +42,7 @@ class PrettyDict(dict):
40
42
  return txt
41
43
  # Empty string when we do not want anything printed (e.g., download=True)
42
44
  return ""
45
+
43
46
  __repr__ = __str__
44
47
 
45
48
 
@@ -106,7 +109,8 @@ def _record_bbox_polygon(rec: Dict[str, Any]) -> Polygon:
106
109
  minx, miny, maxx, maxy = _get_record_bbox_xy(rec)
107
110
  return box(minx, miny, maxx, maxy)
108
111
 
109
- #Return AOI polygon in WGS84 from a raster file --> this will be useful when user have model predicted raster and looking for the benchmrk FIM into the database
112
+
113
+ # Return AOI polygon in WGS84 from a raster file --> this will be useful when user have model predicted raster and looking for the benchmrk FIM into the database
110
114
  def _raster_aoi_polygon_wgs84(path: str) -> Polygon:
111
115
  with rasterio.open(path) as ds:
112
116
  if ds.crs is None:
@@ -117,24 +121,29 @@ def _raster_aoi_polygon_wgs84(path: str) -> Polygon:
117
121
  )
118
122
  return box(minx, miny, maxx, maxy)
119
123
 
120
- #Return AOI polygon in WGS84 from a vector file --> this will be useful when user have model predicted vector and looking for the benchmrk FIM into the database
124
+
125
+ # Return AOI polygon in WGS84 from a vector file --> this will be useful when user have model predicted vector and looking for the benchmrk FIM into the database
121
126
  def _vector_aoi_polygon_wgs84(path: str) -> Polygon:
122
127
  gdf = gpd.read_file(path)
123
128
  if gdf.empty:
124
129
  raise ValueError(f"Vector file {path} contains no features.")
125
130
  if gdf.crs is None:
126
- raise ValueError(f"Vector file {path} has no CRS; cannot derive WGS84 geometry.")
131
+ raise ValueError(
132
+ f"Vector file {path} has no CRS; cannot derive WGS84 geometry."
133
+ )
127
134
  gdf = gdf.to_crs("EPSG:4326")
128
135
  geom = unary_union(gdf.geometry)
129
136
  if geom.is_empty:
130
137
  raise ValueError(f"Vector file {path} has empty geometry after union.")
131
138
  return geom
132
139
 
140
+
133
141
  def _ensure_dir(path: str | Path) -> Path:
134
142
  p = Path(path)
135
143
  p.mkdir(parents=True, exist_ok=True)
136
144
  return p
137
145
 
146
+
138
147
  # benchmark FIM filtering by date
139
148
  def _filter_by_date_exact(
140
149
  records: List[Dict[str, Any]],
@@ -167,7 +176,8 @@ def _filter_by_date_exact(
167
176
  out.append(r)
168
177
  return out
169
178
 
170
- #Filter available benchmark FIMs by date range
179
+
180
+ # Filter available benchmark FIMs by date range
171
181
  def _filter_by_date_range(
172
182
  records: List[Dict[str, Any]],
173
183
  start_date: Optional[str],
@@ -191,6 +201,7 @@ def _filter_by_date_range(
191
201
  out.append(r)
192
202
  return out
193
203
 
204
+
194
205
  # Dynamic area CRS selection and overlap stats
195
206
  def _pick_area_crs_for_bounds(bounds: Tuple[float, float, float, float]) -> str:
196
207
  """
@@ -219,7 +230,8 @@ def _pick_area_crs_for_bounds(bounds: Tuple[float, float, float, float]) -> str:
219
230
  return AREA_CRS_US
220
231
  return AREA_CRS_GLOBAL
221
232
 
222
- #Compute the area overlap statistics between user passed AOI/raster and benchmark AOI
233
+
234
+ # Compute the area overlap statistics between user passed AOI/raster and benchmark AOI
223
235
  def _compute_area_overlap_stats(
224
236
  aoi_geom: Polygon,
225
237
  benchmark_geom: Polygon,
@@ -280,6 +292,7 @@ def _aoi_context_str(
280
292
  return ", ".join(parts)
281
293
  # fall back to the non-AOI context from utils
282
294
  from .utilis import _context_str as _ctx
295
+
283
296
  return _ctx(
284
297
  huc8=huc8,
285
298
  date_input=date_input,
@@ -288,15 +301,28 @@ def _aoi_context_str(
288
301
  end_date=end_date,
289
302
  )
290
303
 
304
+ def _display_raster_name(rec: Dict[str, Any]) -> str:
305
+ tif_url = rec.get("tif_url")
306
+ if isinstance(tif_url, str) and tif_url.strip():
307
+ # drop querystring if any
308
+ tif_url = tif_url.split("?", 1)[0]
309
+ return os.path.basename(tif_url)
310
+
311
+ # fallback: last path token of id
312
+ rid = rec.get("id")
313
+ if isinstance(rid, str) and rid.strip():
314
+ return rid.strip().split("/")[-1] + ".tif"
315
+
316
+ return "NA"
291
317
 
292
318
  # Build a single printable block, optionally with overlap appended
293
- def _format_block_with_overlap(rec: Dict[str, Any],
294
- pct: Optional[float],
295
- km2: Optional[float]) -> str:
296
- tier = rec.get("tier") or rec.get("quality") or "Unknown"
319
+ def _format_block_with_overlap(
320
+ rec: Dict[str, Any], pct: Optional[float], km2: Optional[float]
321
+ ) -> str:
322
+ tier = rec.get("tier") or rec.get("HWM") or rec.get("quality") or "Unknown"
297
323
  res = rec.get("resolution_m")
298
324
  res_txt = f"{res}m" if res is not None else "NA"
299
- fname = rec.get("file_name") or "NA"
325
+ fname = _display_raster_name(rec)
300
326
 
301
327
  lines = [f"Data Tier: {tier}"]
302
328
 
@@ -307,22 +333,27 @@ def _format_block_with_overlap(rec: Dict[str, Any],
307
333
  date_str = _pretty_date_for_print(rec)
308
334
  lines.append(f"Benchmark FIM date: {date_str}")
309
335
 
310
- lines.extend([
311
- f"Spatial Resolution: {res_txt}",
312
- f"Raster Filename in DB: {fname}",
313
- ])
336
+ lines.extend(
337
+ [
338
+ f"Spatial Resolution: {res_txt}",
339
+ f"Raster Filename in DB: {fname}",
340
+ ]
341
+ )
314
342
 
315
343
  if pct is not None and km2 is not None:
316
- lines.append(f"Overlap with respect to benchmark FIM: {pct:.1f}% / {km2:.2f} km²")
344
+ lines.append(
345
+ f"Overlap with respect to benchmark FIM: {pct:.1f}% / {km2:.2f} km²"
346
+ )
317
347
 
318
348
  return "\n".join(lines)
319
349
 
320
- #For Tier-4- adding synthetic event year while reflecting the outcomes
350
+
351
+ # For Tier-4- adding synthetic event year while reflecting the outcomes
321
352
  def _is_synthetic_tier(rec: Dict[str, Any]) -> bool:
322
353
  """Return True when the record is a synthetic (Tier_4) event."""
323
354
  tier = str(rec.get("tier") or rec.get("quality") or "").lower()
324
355
  return "tier_4" in tier or tier.strip() == "4"
325
-
356
+
326
357
 
327
358
  def _return_period_text(rec: Dict[str, Any]) -> str:
328
359
  """
@@ -344,13 +375,19 @@ def _return_period_text(rec: Dict[str, Any]) -> str:
344
375
  except Exception:
345
376
  return f"{rp} synthetic flow"
346
377
 
347
- #helpers to read AOI GPKG geometry directly
378
+
379
+ # helpers to read AOI GPKG geometry directly
348
380
  def _storage_options_for_uri(uri: str) -> Optional[Dict[str, Any]]:
349
381
  if isinstance(uri, str) and uri.startswith("s3://"):
350
- anon = str(os.environ.get("AWS_NO_SIGN_REQUEST", "")).upper() in {"YES", "TRUE", "1"}
382
+ anon = str(os.environ.get("AWS_NO_SIGN_REQUEST", "")).upper() in {
383
+ "YES",
384
+ "TRUE",
385
+ "1",
386
+ }
351
387
  return {"anon": anon}
352
388
  return None
353
389
 
390
+
354
391
  def _gpkg_urls_from_record(rec: Dict[str, Any]) -> List[str]:
355
392
  urls: List[str] = []
356
393
  for key in ("aoi_gpkg", "aoi_gpkg_url", "gpkg_url"):
@@ -382,23 +419,36 @@ def _gpkg_urls_from_record(rec: Dict[str, Any]) -> List[str]:
382
419
  return out
383
420
 
384
421
  def _read_benchmark_aoi_union_geom(rec: Dict[str, Any]) -> Optional[Polygon]:
385
- # Read and union AOI geometries referenced by the record (kept permissive)
422
+ # Read and union AOI geometries referenced by the record
386
423
  urls = _gpkg_urls_from_record(rec)
387
424
  if not urls:
388
425
  return None
426
+
389
427
  geoms: List[Polygon] = []
390
428
  for uri in urls:
391
429
  try:
392
430
  storage_opts = _storage_options_for_uri(uri)
393
- gdf = gpd.read_file(uri, storage_options=storage_opts) if storage_opts else gpd.read_file(uri)
431
+
432
+ uri_to_read = _ensure_local_gpkg(uri)
433
+
434
+ gdf = (
435
+ gpd.read_file(uri_to_read, storage_options=storage_opts)
436
+ if storage_opts
437
+ else gpd.read_file(uri_to_read)
438
+ )
394
439
  if gdf.empty:
395
440
  continue
396
- gdf = gdf.to_crs("EPSG:4326") if gdf.crs else gdf.set_crs("EPSG:4326", allow_override=True)
441
+ gdf = (
442
+ gdf.to_crs("EPSG:4326")
443
+ if gdf.crs
444
+ else gdf.set_crs("EPSG:4326", allow_override=True)
445
+ )
397
446
  u = unary_union(gdf.geometry)
398
447
  if not u.is_empty:
399
448
  geoms.append(u)
400
- except Exception:
449
+ except Exception as e:
401
450
  continue
451
+
402
452
  if not geoms:
403
453
  return None
404
454
  uall = unary_union(geoms)
@@ -419,6 +469,7 @@ class benchFIMquery:
419
469
  into a local directory
420
470
  - or, as a special-case, fetch a specific benchmark by its filename.
421
471
  """
472
+
422
473
  def __init__(self, catalog: Optional[Dict[str, Any]] = None) -> None:
423
474
  """
424
475
  Create a new service.
@@ -510,12 +561,14 @@ class benchFIMquery:
510
561
  )
511
562
 
512
563
  if download and not out_dir:
513
- return PrettyDict({
514
- "status": "error",
515
- "message": "When download=True, you must provide out_dir.",
516
- "matches": [],
517
- "printable": "",
518
- })
564
+ return PrettyDict(
565
+ {
566
+ "status": "error",
567
+ "message": "When download=True, you must provide out_dir.",
568
+ "matches": [],
569
+ "printable": "",
570
+ }
571
+ )
519
572
 
520
573
  # Direct filename-only workflow (no AOI, no dates)
521
574
  if (
@@ -535,9 +588,7 @@ class benchFIMquery:
535
588
  ]
536
589
  if not candidates:
537
590
  candidates = [
538
- r
539
- for r in recs
540
- if str(r.get("file_name", "")).strip() == fname
591
+ r for r in recs if str(r.get("file_name", "")).strip() == fname
541
592
  ]
542
593
  else:
543
594
  candidates = [
@@ -545,39 +596,41 @@ class benchFIMquery:
545
596
  ]
546
597
 
547
598
  if not candidates:
548
- return PrettyDict({
549
- "status": "not_found",
550
- "message": f"File name {fname!r} not found in catalog.",
551
- "matches": [],
552
- "printable": "",
553
- })
599
+ return PrettyDict(
600
+ {
601
+ "status": "not_found",
602
+ "message": f"File name {fname!r} not found in catalog.",
603
+ "matches": [],
604
+ "printable": "",
605
+ }
606
+ )
554
607
 
555
608
  target = candidates[0]
556
609
  out_dir_path = _ensure_dir(out_dir)
557
610
  dl = download_fim_assets(target, str(out_dir_path))
558
611
 
559
- return PrettyDict({
560
- "status": "ok",
561
- "message": f"Downloaded benchmark FIM '{fname}' to '{out_dir_path}'.",
562
- "matches": [
563
- {
564
- "record": target,
565
- "bbox_intersects": False,
566
- "intersection_area_pct": None,
567
- "intersection_area_km2": None,
568
- "downloads": dl,
569
- }
570
- ],
571
- "printable": "",
572
- })
612
+ return PrettyDict(
613
+ {
614
+ "status": "ok",
615
+ "message": f"Downloaded benchmark FIM '{fname}' to '{out_dir_path}'.",
616
+ "matches": [
617
+ {
618
+ "record": target,
619
+ "bbox_intersects": False,
620
+ "intersection_area_pct": None,
621
+ "intersection_area_km2": None,
622
+ "downloads": dl,
623
+ }
624
+ ],
625
+ "printable": "",
626
+ }
627
+ )
573
628
 
574
629
  # AOI-based workflows
575
630
  records = self.records
576
631
  if huc8:
577
632
  huc8_str = str(huc8).strip()
578
- records = [
579
- r for r in records if str(r.get("huc8", "")).strip() == huc8_str
580
- ]
633
+ records = [r for r in records if str(r.get("huc8", "")).strip() == huc8_str]
581
634
 
582
635
  # Date filters
583
636
  if event_date:
@@ -586,12 +639,14 @@ class benchFIMquery:
586
639
  records = _filter_by_date_range(records, start_date, end_date)
587
640
 
588
641
  if not records:
589
- return PrettyDict({
590
- "status": "not_found",
591
- "message": "No catalog records match the provided filters.",
592
- "matches": [],
593
- "printable": "",
594
- })
642
+ return PrettyDict(
643
+ {
644
+ "status": "not_found",
645
+ "message": "No catalog records match the provided filters.",
646
+ "matches": [],
647
+ "printable": "",
648
+ }
649
+ )
595
650
 
596
651
  # If no AOI is provided at all
597
652
  if aoi_geom is None:
@@ -615,14 +670,14 @@ class benchFIMquery:
615
670
  end_date=end_date,
616
671
  file_name=file_name,
617
672
  )
618
- printable = format_records_for_print([m["record"] for m in matches], context=ctx)
673
+ printable = format_records_for_print(
674
+ [m["record"] for m in matches], context=ctx
675
+ )
619
676
 
620
677
  if download:
621
678
  out_dir_path = _ensure_dir(out_dir)
622
679
  for m in matches:
623
- m["downloads"] = download_fim_assets(
624
- m["record"], str(out_dir_path)
625
- )
680
+ m["downloads"] = download_fim_assets(m["record"], str(out_dir_path))
626
681
  msg = (
627
682
  f"Downloaded {len(matches)} benchmark record(s) "
628
683
  f"to '{out_dir_path}'."
@@ -633,12 +688,14 @@ class benchFIMquery:
633
688
  f"for the provided filters."
634
689
  )
635
690
 
636
- return PrettyDict({
637
- "status": "ok",
638
- "message": msg,
639
- "matches": matches,
640
- "printable": "" if download else printable,
641
- })
691
+ return PrettyDict(
692
+ {
693
+ "status": "ok",
694
+ "message": msg,
695
+ "matches": matches,
696
+ "printable": "" if download else printable,
697
+ }
698
+ )
642
699
 
643
700
  # AOI is present: intersect with bbox
644
701
  intersecting: List[Dict[str, Any]] = []
@@ -652,12 +709,14 @@ class benchFIMquery:
652
709
  intersecting.append(r)
653
710
 
654
711
  if not intersecting:
655
- return PrettyDict({
656
- "status": "not_found",
657
- "message": "No benchmark FIM bbox intersects the provided AOI.",
658
- "matches": [],
659
- "printable": "",
660
- })
712
+ return PrettyDict(
713
+ {
714
+ "status": "not_found",
715
+ "message": "No benchmark FIM bbox intersects the provided AOI.",
716
+ "matches": [],
717
+ "printable": "",
718
+ }
719
+ )
661
720
 
662
721
  out_matches: List[Dict[str, Any]] = []
663
722
  out_dir_path = _ensure_dir(out_dir) if (download and out_dir) else None
@@ -725,12 +784,14 @@ class benchFIMquery:
725
784
  blocks.append(_format_block_with_overlap(rec, None, None))
726
785
  printable = header + "\n\n".join(blocks)
727
786
 
728
- return PrettyDict({
729
- "status": "ok",
730
- "message": msg,
731
- "matches": out_matches,
732
- "printable": printable,
733
- })
787
+ return PrettyDict(
788
+ {
789
+ "status": "ok",
790
+ "message": msg,
791
+ "matches": out_matches,
792
+ "printable": printable,
793
+ }
794
+ )
734
795
 
735
796
  def __call__(
736
797
  self,
@@ -758,4 +819,6 @@ class benchFIMquery:
758
819
  download=download,
759
820
  out_dir=out_dir,
760
821
  )
761
- benchFIMquery = benchFIMquery()
822
+
823
+
824
+ benchFIMquery = benchFIMquery()
@@ -38,12 +38,72 @@ _YMDH_RE = re.compile(r"^\d{4}-\d{2}-\d{2}[ T]\d{2}$")
38
38
  _YMDHMS_RE = re.compile(r"^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}(:\d{2})?$")
39
39
 
40
40
 
41
+ #Support functions to ensute the geopackage file is local
42
+ def _s3_bucket_key_from_http_url(url: str) -> Optional[tuple[str, str]]:
43
+ try:
44
+ u = urllib.parse.urlparse(url)
45
+ host = (u.netloc or "").lower()
46
+ path = (u.path or "").lstrip("/")
47
+ if not host or not path:
48
+ return None
49
+
50
+ if ".s3." in host or host.endswith(".s3.amazonaws.com"):
51
+ bucket = host.split(".s3", 1)[0]
52
+ key = path
53
+ return bucket, key
54
+
55
+ return None
56
+ except Exception:
57
+ return None
58
+
59
+
60
+ def _ensure_local_gpkg(uri: str) -> str:
61
+ """
62
+ Ensure we can read a GPKG even when uri is an https S3 URL by caching locally.
63
+ Returns a path usable by geopandas.read_file().
64
+ """
65
+ if not isinstance(uri, str) or not uri.strip():
66
+ return uri
67
+
68
+ u = uri.strip()
69
+ if os.path.exists(u):
70
+ return u
71
+
72
+ # Cache https S3 gpkg locally
73
+ if u.lower().startswith("http") and ".amazonaws.com/" in u.lower() and u.lower().endswith(".gpkg"):
74
+ parsed = _s3_bucket_key_from_http_url(u.split("?", 1)[0])
75
+ if not parsed:
76
+ return u
77
+
78
+ bucket, key = parsed
79
+ cache_dir = os.path.join(os.path.expanduser("~"), ".fimeval_cache", "aoi_gpkg")
80
+ os.makedirs(cache_dir, exist_ok=True)
81
+
82
+ local = os.path.join(cache_dir, os.path.basename(key))
83
+ if not os.path.exists(local):
84
+ _download(bucket, key, local)
85
+ return local
86
+ return u
87
+
41
88
  def _normalize_user_dt(s: str) -> str:
42
89
  s = s.strip()
43
90
  s = s.replace("/", "-")
44
91
  s = re.sub(r"\s+", " ", s)
45
92
  return s
46
93
 
94
+ def _display_raster_name(rec: Dict[str, Any]) -> str:
95
+ tif_url = rec.get("tif_url")
96
+ if isinstance(tif_url, str) and tif_url.strip():
97
+ # drop querystring if any
98
+ tif_url = tif_url.split("?", 1)[0]
99
+ return os.path.basename(tif_url)
100
+
101
+ # fallback: last path token of id
102
+ rid = rec.get("id")
103
+ if isinstance(rid, str) and rid.strip():
104
+ return rid.strip().split("/")[-1] + ".tif"
105
+
106
+ return "NA"
47
107
 
48
108
  def _to_date(s: str) -> dt.date:
49
109
  s = _normalize_user_dt(s)
@@ -186,7 +246,7 @@ def format_records_for_print(
186
246
  tier = r.get("tier") or r.get("quality") or "Unknown"
187
247
  res = r.get("resolution_m")
188
248
  res_txt = f"{res}m" if res is not None else "NA"
189
- fname = r.get("file_name") or "NA"
249
+ fname = _display_raster_name(r)
190
250
 
191
251
  # Build lines with Tier-aware event text
192
252
  lines = [f"Data Tier: {tier}"]
@@ -196,14 +256,17 @@ def format_records_for_print(
196
256
  date_str = _pretty_date_for_print(r)
197
257
  lines.append(f"Benchmark FIM date: {date_str}")
198
258
 
199
- lines.extend([
200
- f"Spatial Resolution: {res_txt}",
201
- f"Benchmark FIM raster name in DB: {fname}",
202
- ])
259
+ lines.extend(
260
+ [
261
+ f"Spatial Resolution: {res_txt}",
262
+ f"Benchmark FIM raster name in DB: {fname}",
263
+ ]
264
+ )
203
265
  blocks.append("\n".join(lines))
204
266
 
205
267
  return (header + "\n\n".join(blocks)).strip()
206
268
 
269
+
207
270
  # S3 and json catalog
208
271
  def load_catalog_core() -> Dict[str, Any]:
209
272
  obj = _S3.get_object(Bucket=BUCKET, Key=CATALOG_KEY)
@@ -224,6 +287,7 @@ def _download(bucket: str, key: str, dest_path: str) -> str:
224
287
  _S3.download_file(bucket, key, dest_path)
225
288
  return dest_path
226
289
 
290
+
227
291
  # Get the files from s3 bucket
228
292
  def _folder_from_record(rec: Dict[str, Any]) -> str:
229
293
  s3_key = rec.get("s3_key")
@@ -241,7 +305,8 @@ def _tif_key_from_record(rec: Dict[str, Any]) -> Optional[str]:
241
305
  return None
242
306
  return _folder_from_record(rec) + fname
243
307
 
244
- #Download that tif and the boundary file --> need to add building footprint automation as well.
308
+
309
+ # Download that tif and the boundary file --> need to add building footprint automation as well.
245
310
  def download_fim_assets(record: Dict[str, Any], dest_dir: str) -> Dict[str, Any]:
246
311
  """
247
312
  Download the .tif (if present) and any .gpkg from the record's folder to dest_dir.
@@ -266,4 +331,4 @@ def download_fim_assets(record: Dict[str, Any], dest_dir: str) -> Dict[str, Any]
266
331
  _download(BUCKET, key, local)
267
332
  out["gpkg_files"].append(local)
268
333
 
269
- return out
334
+ return out
@@ -1,3 +1,4 @@
1
1
  from .evaluationwithBF import EvaluationWithBuildingFootprint
2
+ from .arcgis_API import getBuildingFootprint
2
3
 
3
- __all__ = ["EvaluationWithBuildingFootprint"]
4
+ __all__ = ["EvaluationWithBuildingFootprint", "getBuildingFootprint"]