buildingdata 0.2.0__tar.gz → 0.2.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 (42) hide show
  1. {buildingdata-0.2.0 → buildingdata-0.2.2}/PKG-INFO +25 -6
  2. {buildingdata-0.2.0 → buildingdata-0.2.2}/README.md +25 -6
  3. {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata/cache.py +53 -2
  4. buildingdata-0.2.2/buildingdata/exceptions.py +29 -0
  5. buildingdata-0.2.2/buildingdata/gcs.py +132 -0
  6. buildingdata-0.2.2/buildingdata/reference/census.py +71 -0
  7. {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata/reference/diagnosis.py +27 -11
  8. {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata/reference/districts.py +5 -11
  9. {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata/reference/elmas.py +5 -11
  10. {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata/reference/enedis.py +6 -11
  11. {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata/reference/gas_network.py +5 -11
  12. {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata/reference/occupant_diaries.py +5 -12
  13. {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata/reference/ore.py +5 -12
  14. buildingdata-0.2.2/buildingdata/simulation/bdtopo.py +216 -0
  15. {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata/simulation/era5.py +148 -48
  16. {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata/tests/test_cache.py +25 -0
  17. {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata/tests/test_reference.py +31 -1
  18. buildingdata-0.2.2/buildingdata/tests/test_simulation.py +369 -0
  19. buildingdata-0.2.2/buildingdata/validation.py +77 -0
  20. {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata.egg-info/PKG-INFO +25 -6
  21. {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata.egg-info/SOURCES.txt +1 -0
  22. {buildingdata-0.2.0 → buildingdata-0.2.2}/pyproject.toml +5 -1
  23. buildingdata-0.2.0/buildingdata/exceptions.py +0 -17
  24. buildingdata-0.2.0/buildingdata/gcs.py +0 -82
  25. buildingdata-0.2.0/buildingdata/reference/census.py +0 -48
  26. buildingdata-0.2.0/buildingdata/simulation/bdtopo.py +0 -87
  27. buildingdata-0.2.0/buildingdata/tests/test_simulation.py +0 -184
  28. {buildingdata-0.2.0 → buildingdata-0.2.2}/LICENSE +0 -0
  29. {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata/__init__.py +0 -0
  30. {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata/_cli.py +0 -0
  31. {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata/config.py +0 -0
  32. {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata/reference/__init__.py +0 -0
  33. {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata/simulation/__init__.py +0 -0
  34. {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata/tests/__init__.py +0 -0
  35. {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata/tests/conftest.py +0 -0
  36. {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata/tests/test_config.py +0 -0
  37. {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata/tests/test_public_api.py +0 -0
  38. {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata.egg-info/dependency_links.txt +0 -0
  39. {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata.egg-info/entry_points.txt +0 -0
  40. {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata.egg-info/requires.txt +0 -0
  41. {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata.egg-info/top_level.txt +0 -0
  42. {buildingdata-0.2.0 → buildingdata-0.2.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: buildingdata
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Data management layer for buildingmodel — reference data download, BDTOPO retrieval, ERA5 weather
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.10
@@ -88,13 +88,32 @@ uses CRS **EPSG:2154 (Lambert-93)**.
88
88
 
89
89
  ## Configuration
90
90
 
91
- Settings are resolved from (in order) explicit arguments, environment variables,
92
- and `~/.config/buildingdata/config.ini`:
91
+ Settings are resolved in the following precedence order:
92
+ 1. Explicit function arguments / CLI options
93
+ 2. Environment variables (`BUILDINGDATA_BUCKET`, `BUILDINGDATA_CACHE_DIR`, `GOOGLE_APPLICATION_CREDENTIALS`)
94
+ 3. Global configuration file (`~/.config/buildingdata/config.ini`)
95
+ 4. Dynamic defaults
93
96
 
94
- - **bucket** — GCS bucket holding the reference datasets
97
+ Configurable settings:
98
+ - **bucket** — GCS bucket holding reference datasets (default: `building-inference-data`)
95
99
  - **cache directory** — where downloaded data is stored locally
96
- - **credentials** — path to a GCS service-account JSON (omit for anonymous access
97
- to the public bucket)
100
+ - **credentials** — path to a GCS service-account JSON (omit for anonymous access to the public bucket)
101
+
102
+ ### Cache Behavior & Multi-Project Sharing
103
+
104
+ - **Default (Unconfigured)**: The cache directory is namespaced per installation (`~/.local/share/buildingdata/cache/<install-id>` on Linux/macOS). Each virtualenv or package installation receives its own unique cache subfolder to prevent collisions between environments.
105
+ - **Sharing Across Projects**: To share a single cache directory across multiple repositories, virtualenvs, or Snakemake pipelines, set the `BUILDINGDATA_CACHE_DIR` environment variable or write a global configuration file:
106
+
107
+ ```bash
108
+ # Via CLI (writes ~/.config/buildingdata/config.ini):
109
+ buildingdata configure --cache-dir /path/to/shared/cache
110
+
111
+ # Or via environment variable:
112
+ export BUILDINGDATA_CACHE_DIR="/path/to/shared/cache"
113
+ ```
114
+
115
+ Because `~/.config/buildingdata/config.ini` is stored in your home directory, configuring it once applies globally to all projects and virtual environments for your user account.
116
+
98
117
 
99
118
  ## Installation extras
100
119
 
@@ -55,13 +55,32 @@ uses CRS **EPSG:2154 (Lambert-93)**.
55
55
 
56
56
  ## Configuration
57
57
 
58
- Settings are resolved from (in order) explicit arguments, environment variables,
59
- and `~/.config/buildingdata/config.ini`:
60
-
61
- - **bucket** GCS bucket holding the reference datasets
58
+ Settings are resolved in the following precedence order:
59
+ 1. Explicit function arguments / CLI options
60
+ 2. Environment variables (`BUILDINGDATA_BUCKET`, `BUILDINGDATA_CACHE_DIR`, `GOOGLE_APPLICATION_CREDENTIALS`)
61
+ 3. Global configuration file (`~/.config/buildingdata/config.ini`)
62
+ 4. Dynamic defaults
63
+
64
+ Configurable settings:
65
+ - **bucket** — GCS bucket holding reference datasets (default: `building-inference-data`)
62
66
  - **cache directory** — where downloaded data is stored locally
63
- - **credentials** — path to a GCS service-account JSON (omit for anonymous access
64
- to the public bucket)
67
+ - **credentials** — path to a GCS service-account JSON (omit for anonymous access to the public bucket)
68
+
69
+ ### Cache Behavior & Multi-Project Sharing
70
+
71
+ - **Default (Unconfigured)**: The cache directory is namespaced per installation (`~/.local/share/buildingdata/cache/<install-id>` on Linux/macOS). Each virtualenv or package installation receives its own unique cache subfolder to prevent collisions between environments.
72
+ - **Sharing Across Projects**: To share a single cache directory across multiple repositories, virtualenvs, or Snakemake pipelines, set the `BUILDINGDATA_CACHE_DIR` environment variable or write a global configuration file:
73
+
74
+ ```bash
75
+ # Via CLI (writes ~/.config/buildingdata/config.ini):
76
+ buildingdata configure --cache-dir /path/to/shared/cache
77
+
78
+ # Or via environment variable:
79
+ export BUILDINGDATA_CACHE_DIR="/path/to/shared/cache"
80
+ ```
81
+
82
+ Because `~/.config/buildingdata/config.ini` is stored in your home directory, configuring it once applies globally to all projects and virtual environments for your user account.
83
+
65
84
 
66
85
  ## Installation extras
67
86
 
@@ -1,5 +1,10 @@
1
1
  # -*- coding: utf-8 -*-
2
+ import fcntl
2
3
  import json
4
+ import os
5
+ import time
6
+ import uuid
7
+ from contextlib import contextmanager
3
8
  from datetime import datetime, timezone
4
9
  from pathlib import Path
5
10
 
@@ -7,6 +12,46 @@ from .config import get_cache_dir
7
12
  from .exceptions import CacheError
8
13
 
9
14
 
15
+ @contextmanager
16
+ def file_lock(lock_path, timeout=600):
17
+ """Context manager for acquiring an inter-process file lock.
18
+
19
+ Args:
20
+ lock_path (pathlib.Path or str): Path to the lock file.
21
+ timeout (int or float): Maximum seconds to wait for lock acquisition.
22
+ Defaults to 600 seconds.
23
+
24
+ Raises:
25
+ CacheError: If lock acquisition times out.
26
+ """
27
+ lock_path = Path(lock_path)
28
+ lock_path.parent.mkdir(parents=True, exist_ok=True)
29
+ fd = os.open(str(lock_path), os.O_CREAT | os.O_RDWR, 0o666)
30
+ start_time = time.monotonic()
31
+ acquired = False
32
+ try:
33
+ while True:
34
+ try:
35
+ fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
36
+ acquired = True
37
+ break
38
+ except (BlockingIOError, OSError):
39
+ if time.monotonic() - start_time >= timeout:
40
+ raise CacheError(f"Timed out waiting for lock on {lock_path}") from None
41
+ time.sleep(0.1)
42
+ yield
43
+ finally:
44
+ if acquired:
45
+ try:
46
+ fcntl.flock(fd, fcntl.LOCK_UN)
47
+ except OSError:
48
+ pass
49
+ try:
50
+ os.close(fd)
51
+ except OSError:
52
+ pass
53
+
54
+
10
55
  def cache_path(name):
11
56
  """Return the local Path where a named artifact should be cached.
12
57
 
@@ -59,7 +104,7 @@ def needs_refresh(name, blob):
59
104
 
60
105
 
61
106
  def write_sidecar(name, blob):
62
- """Write a .meta.json sidecar after a successful download.
107
+ """Write a .meta.json sidecar after a successful download atomically.
63
108
 
64
109
  Args:
65
110
  name (str): logical artifact name.
@@ -71,10 +116,15 @@ def write_sidecar(name, blob):
71
116
  "generation": blob.generation,
72
117
  "downloaded_at": datetime.now(timezone.utc).isoformat(),
73
118
  }
119
+ pid_suffix = f".tmp.{os.getpid()}_{uuid.uuid4().hex[:6]}"
120
+ tmp_sidecar = sidecar.with_name(sidecar.name + pid_suffix)
74
121
  try:
75
- with open(sidecar, "w") as f:
122
+ with open(tmp_sidecar, "w") as f:
76
123
  json.dump(meta, f, indent=2)
124
+ tmp_sidecar.replace(sidecar)
77
125
  except OSError as e:
126
+ if tmp_sidecar.exists():
127
+ tmp_sidecar.unlink(missing_ok=True)
78
128
  raise CacheError(f"Failed to write cache sidecar {sidecar}: {e}") from e
79
129
 
80
130
 
@@ -90,3 +140,4 @@ def ensure_subdir(subdir):
90
140
  path = get_cache_dir() / subdir
91
141
  path.mkdir(parents=True, exist_ok=True)
92
142
  return path
143
+
@@ -0,0 +1,29 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+
4
+ class BuildingDataError(Exception):
5
+ """Base exception for buildingdata errors."""
6
+
7
+
8
+ class CacheError(BuildingDataError):
9
+ """Raised when a cache read or write operation fails."""
10
+
11
+
12
+ class RemoteNotAvailableError(BuildingDataError):
13
+ """Raised when a remote endpoint returns a non-success status."""
14
+
15
+
16
+ class ConfigurationError(BuildingDataError):
17
+ """Raised when required configuration (credentials, API key) is missing."""
18
+
19
+
20
+ class SchemaValidationError(BuildingDataError):
21
+ """Raised when a fetched dataset violates its expected schema/unit contract.
22
+
23
+ Signals that the upstream file's columns, dtypes or value ranges no longer
24
+ match what this package documents and what its consumers rely on -- e.g. a
25
+ portal revised its schema, a join-key format changed, or a column switched
26
+ units (kWh vs MWh, fraction vs percent). Failing fast here is deliberate:
27
+ a clear error on a revised upstream file beats a silent NaN or a
28
+ wrong-scale value propagating into a simulation.
29
+ """
@@ -0,0 +1,132 @@
1
+ # -*- coding: utf-8 -*-
2
+ from pathlib import Path
3
+
4
+ from tqdm import tqdm
5
+
6
+ from .config import get_bucket, get_credentials_file
7
+
8
+
9
+ def get_client():
10
+ """Return a google.cloud.storage.Client.
11
+
12
+ Uses the credentials file from config if one is set. Otherwise returns
13
+ an anonymous client, which can read public buckets without any
14
+ authentication or Google Cloud project.
15
+
16
+ Returns:
17
+ google.cloud.storage.Client: storage client.
18
+ """
19
+ from google.cloud import storage
20
+
21
+ creds_file = get_credentials_file()
22
+ if creds_file is not None:
23
+ return storage.Client.from_service_account_json(str(creds_file))
24
+ return storage.Client.create_anonymous_client()
25
+
26
+
27
+ def get_blob(name):
28
+ """Return the Blob object for a given name, or None if not found.
29
+
30
+ Args:
31
+ name (str): blob name inside the configured bucket.
32
+
33
+ Returns:
34
+ google.cloud.storage.Blob or None.
35
+ """
36
+ client = get_client()
37
+ bucket = client.bucket(get_bucket())
38
+ blob = bucket.blob(name)
39
+ blob.reload()
40
+ return blob if blob.exists() else None
41
+
42
+
43
+ def download_blob(name, dest_path, show_progress=True):
44
+ """Download a blob from the configured bucket to dest_path.
45
+
46
+ Args:
47
+ name (str): blob name inside the bucket, e.g. "census_latest.parquet".
48
+ dest_path (pathlib.Path): local destination file path.
49
+ show_progress (bool): display a tqdm progress bar. Defaults to True.
50
+ """
51
+ import os
52
+ import uuid
53
+
54
+ from google.cloud import storage
55
+
56
+ dest_path = Path(dest_path)
57
+ dest_path.parent.mkdir(parents=True, exist_ok=True)
58
+
59
+ client = get_client()
60
+ bucket = client.bucket(get_bucket())
61
+ blob = bucket.blob(name)
62
+ blob.reload()
63
+
64
+ total = blob.size or 0
65
+ pid_suffix = f".tmp.{os.getpid()}_{uuid.uuid4().hex[:6]}"
66
+ tmp_path = dest_path.with_name(dest_path.name + pid_suffix)
67
+
68
+ try:
69
+ with open(tmp_path, "wb") as f:
70
+ if show_progress:
71
+ with tqdm(
72
+ total=total,
73
+ unit="B",
74
+ unit_scale=True,
75
+ desc=name,
76
+ leave=True,
77
+ ) as bar:
78
+ def _callback(chunk):
79
+ bar.update(len(chunk))
80
+
81
+ blob.download_to_file(f, checksum="md5", raw_download=True)
82
+ bar.update(total - bar.n)
83
+ else:
84
+ blob.download_to_file(f)
85
+
86
+ tmp_path.replace(dest_path)
87
+ finally:
88
+ if tmp_path.exists():
89
+ tmp_path.unlink(missing_ok=True)
90
+
91
+ return blob
92
+
93
+
94
+ def ensure_blob_cached(blob_name, refresh=False, get_blob_fn=None, download_blob_fn=None):
95
+ """Ensure a named GCS blob is downloaded and up-to-date locally, synchronized across processes.
96
+
97
+ Args:
98
+ blob_name (str): name of the blob in GCS, e.g. "census_latest.parquet".
99
+ refresh (bool): force re-download even if cached locally. Defaults to False.
100
+ get_blob_fn (callable, optional): function to retrieve Blob. Defaults to get_blob.
101
+ download_blob_fn (callable, optional): function to download Blob. Defaults to download_blob.
102
+
103
+ Returns:
104
+ pathlib.Path: absolute path to the cached file.
105
+
106
+ Raises:
107
+ RemoteNotAvailableError: if the blob does not exist in GCS.
108
+ """
109
+ from .cache import cache_path, file_lock, is_cached, needs_refresh, write_sidecar
110
+ from .exceptions import RemoteNotAvailableError
111
+
112
+ if get_blob_fn is None:
113
+ get_blob_fn = get_blob
114
+ if download_blob_fn is None:
115
+ download_blob_fn = download_blob
116
+
117
+ dest = cache_path(blob_name)
118
+ lock_file = dest.with_name(dest.name + ".lock")
119
+
120
+ with file_lock(lock_file):
121
+ if not is_cached(blob_name) or refresh:
122
+ blob = get_blob_fn(blob_name)
123
+ if blob is None:
124
+ raise RemoteNotAvailableError(f"Blob {blob_name!r} not found in GCS bucket.")
125
+ if not is_cached(blob_name) or needs_refresh(blob_name, blob):
126
+ download_blob_fn(blob_name, dest)
127
+ write_sidecar(blob_name, blob)
128
+
129
+ return dest
130
+
131
+
132
+
@@ -0,0 +1,71 @@
1
+ # -*- coding: utf-8 -*-
2
+ import polars as pl
3
+
4
+ from ..exceptions import RemoteNotAvailableError
5
+ from ..gcs import download_blob, ensure_blob_cached, get_blob
6
+ from ..validation import require_columns
7
+
8
+
9
+
10
+ _BLOB_NAME = "census_latest.parquet"
11
+
12
+ # Columns the processed INSEE census must expose. ``district``/``city_group``
13
+ # are the join keys ``building_eload`` uses to attach census records to IRIS
14
+ # districts; the dwelling-attribute columns feed ``buildingmodel``'s inference
15
+ # (occupancy/heating/cooking/construction) and the ``ipondl`` weight is the
16
+ # census sampling weight every aggregate is summed over. Missing any of these
17
+ # means the wrong file or a revised INSEE schema.
18
+ _REQUIRED_COLUMNS = (
19
+ "district",
20
+ "city_group",
21
+ "ipondl",
22
+ "occupancy_type",
23
+ "occupant_status",
24
+ "occupant_count",
25
+ "living_area_class",
26
+ "heating_system",
27
+ "residential_type",
28
+ "construction_year_class",
29
+ "main_cooking_energy",
30
+ "secondary_cooking_energy",
31
+ )
32
+
33
+
34
+ def get_census(city_group_list=None, refresh=False):
35
+ """Return the processed INSEE census DataFrame, optionally pre-filtered.
36
+
37
+ Downloads census_latest.parquet from GCS to the local cache on first call
38
+ (or when refresh=True). Filtering by city_group_list uses Polars lazy scan
39
+ so only the needed rows are loaded into memory.
40
+
41
+ Args:
42
+ city_group_list (list of str, optional): EPCI codes to filter on. If
43
+ None, the full national census is returned. Defaults to None.
44
+ refresh (bool): force re-download even if the cache is warm.
45
+ Defaults to False.
46
+
47
+ Returns:
48
+ polars.DataFrame: census records with columns occupancy_type,
49
+ occupant_status, living_area_class, occupant_count, heating_system,
50
+ residential_type, construction_year_class, district, city,
51
+ city_group, department, region, main_cooking_energy,
52
+ secondary_cooking_energy, ipondl. ``district``/``city_group`` are
53
+ string INSEE codes (IRIS / EPCI); ``ipondl`` is the INSEE sampling
54
+ weight every aggregate is summed over.
55
+
56
+ Raises:
57
+ RemoteNotAvailableError: if the blob is not found in the GCS bucket.
58
+ SchemaValidationError: if the fetched frame is missing a required
59
+ join-key or dwelling-attribute column (see ``_REQUIRED_COLUMNS``).
60
+ """
61
+ dest = ensure_blob_cached(
62
+ _BLOB_NAME, refresh=refresh, get_blob_fn=get_blob, download_blob_fn=download_blob
63
+ )
64
+
65
+
66
+ lf = pl.scan_parquet(dest)
67
+
68
+ require_columns(lf.collect_schema(), _REQUIRED_COLUMNS, "INSEE census")
69
+ if city_group_list is not None:
70
+ lf = lf.filter(pl.col("city_group").is_in(city_group_list))
71
+ return lf.collect()
@@ -1,15 +1,28 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  import polars as pl
3
3
 
4
- from ..cache import cache_path, is_cached, needs_refresh, write_sidecar
5
- from ..exceptions import RemoteNotAvailableError
6
- from ..gcs import download_blob, get_blob
4
+ from ..gcs import download_blob, ensure_blob_cached, get_blob
5
+ from ..validation import require_columns
6
+
7
7
 
8
8
  _BLOB_NAME = "energy_performance_diagnosis_latest.parquet"
9
9
 
10
+
10
11
  # Heating/DHW energies excluded from inference (no meaningful DPE data for coal)
11
12
  _EXCLUDED_ENERGIES = ["Charbon"]
12
13
 
14
+ # Columns this accessor operates on directly (coal filter + dtype casts). If any
15
+ # is absent the raw polars error is opaque; validating up front names the DPE
16
+ # schema drift explicitly. ``heating_system`` + ``region`` are the join keys
17
+ # ``buildingmodel``'s energy-system inference keys on; ``backup_heating_energy``
18
+ # / ``dhw_energy`` carry the fuel labels the coal filter reads.
19
+ _REQUIRED_COLUMNS = (
20
+ "heating_system",
21
+ "region",
22
+ "backup_heating_energy",
23
+ "dhw_energy",
24
+ )
25
+
13
26
 
14
27
  def get_diagnosis(refresh=False):
15
28
  """Return the cleaned DPE energy performance diagnosis DataFrame.
@@ -26,19 +39,22 @@ def get_diagnosis(refresh=False):
26
39
  polars.DataFrame: DPE records with columns heating_system (Categorical),
27
40
  region (Int64), backup_heating_energy, dhw_energy, and all U-value
28
41
  and efficiency columns used by inference/building_attributes.py.
42
+
43
+ Raises:
44
+ RemoteNotAvailableError: if the blob is not found in the GCS bucket.
45
+ SchemaValidationError: if the fetched frame is missing one of the
46
+ columns this accessor operates on (see ``_REQUIRED_COLUMNS``).
29
47
  """
30
- dest = cache_path(_BLOB_NAME)
48
+ dest = ensure_blob_cached(
49
+ _BLOB_NAME, refresh=refresh, get_blob_fn=get_blob, download_blob_fn=download_blob
50
+ )
31
51
 
32
- if not is_cached(_BLOB_NAME) or refresh:
33
- blob = get_blob(_BLOB_NAME)
34
- if blob is None:
35
- raise RemoteNotAvailableError(f"Blob {_BLOB_NAME!r} not found in GCS bucket.")
36
- if not is_cached(_BLOB_NAME) or needs_refresh(_BLOB_NAME, blob):
37
- download_blob(_BLOB_NAME, dest)
38
- write_sidecar(_BLOB_NAME, blob)
39
52
 
40
53
  df = pl.read_parquet(dest)
41
54
 
55
+
56
+ require_columns(df, _REQUIRED_COLUMNS, "DPE/EPC diagnosis")
57
+
42
58
  # Remove records with coal heating/DHW — no useful inference data
43
59
  df = df.filter(
44
60
  ~pl.col("backup_heating_energy").is_in(_EXCLUDED_ENERGIES)
@@ -1,9 +1,7 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  import geopandas as gpd
3
3
 
4
- from ..cache import cache_path, is_cached, needs_refresh, write_sidecar
5
- from ..exceptions import RemoteNotAvailableError
6
- from ..gcs import download_blob, get_blob
4
+ from ..gcs import download_blob, ensure_blob_cached, get_blob
7
5
 
8
6
  _BLOB_NAME = "districts_latest.parquet"
9
7
 
@@ -25,17 +23,13 @@ def get_districts(bbox=None, refresh=False):
25
23
  district, city, city_name, city_group, department, region,
26
24
  has_network_city_level, has_network_grdf_data, geometry.
27
25
  """
28
- dest = cache_path(_BLOB_NAME)
26
+ dest = ensure_blob_cached(
27
+ _BLOB_NAME, refresh=refresh, get_blob_fn=get_blob, download_blob_fn=download_blob
28
+ )
29
29
 
30
- if not is_cached(_BLOB_NAME) or refresh:
31
- blob = get_blob(_BLOB_NAME)
32
- if blob is None:
33
- raise RemoteNotAvailableError(f"Blob {_BLOB_NAME!r} not found in GCS bucket.")
34
- if not is_cached(_BLOB_NAME) or needs_refresh(_BLOB_NAME, blob):
35
- download_blob(_BLOB_NAME, dest)
36
- write_sidecar(_BLOB_NAME, blob)
37
30
 
38
31
  kwargs = {}
39
32
  if bbox is not None:
40
33
  kwargs["bbox"] = bbox
41
34
  return gpd.read_parquet(dest, **kwargs)
35
+
@@ -1,9 +1,7 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  import polars as pl
3
3
 
4
- from ..cache import cache_path, is_cached, needs_refresh, write_sidecar
5
- from ..exceptions import RemoteNotAvailableError
6
- from ..gcs import download_blob, get_blob
4
+ from ..gcs import download_blob, ensure_blob_cached, get_blob
7
5
 
8
6
  # The ELMAS non-residential dataset (Chiche/Duquesne et al., Sci. Data 2023,
9
7
  # 10.1038/s41597-023-02542-z) ships as several tables. Each is cached as its
@@ -56,14 +54,10 @@ def get_elmas(table="time_series", refresh=False):
56
54
  )
57
55
 
58
56
  blob_name = _BLOB_NAMES[table]
59
- dest = cache_path(blob_name)
57
+ dest = ensure_blob_cached(
58
+ blob_name, refresh=refresh, get_blob_fn=get_blob, download_blob_fn=download_blob
59
+ )
60
60
 
61
- if not is_cached(blob_name) or refresh:
62
- blob = get_blob(blob_name)
63
- if blob is None:
64
- raise RemoteNotAvailableError(f"Blob {blob_name!r} not found in GCS bucket.")
65
- if not is_cached(blob_name) or needs_refresh(blob_name, blob):
66
- download_blob(blob_name, dest)
67
- write_sidecar(blob_name, blob)
68
61
 
69
62
  return pl.read_parquet(dest)
63
+
@@ -1,9 +1,7 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  import polars as pl
3
3
 
4
- from ..cache import cache_path, is_cached, needs_refresh, write_sidecar
5
- from ..exceptions import RemoteNotAvailableError
6
- from ..gcs import download_blob, get_blob
4
+ from ..gcs import download_blob, ensure_blob_cached, get_blob
7
5
 
8
6
  # Enedis "conso-inf36" (<= 36 kVA) measured aggregated consumption / average
9
7
  # load curves, used by building_eload.core.validation as measured ground truth.
@@ -14,17 +12,14 @@ _REGIONAL_BLOB_NAME = "enedis_regional_conso_inf36_latest.parquet"
14
12
 
15
13
 
16
14
  def _fetch(blob_name, refresh):
17
- dest = cache_path(blob_name)
18
- if not is_cached(blob_name) or refresh:
19
- blob = get_blob(blob_name)
20
- if blob is None:
21
- raise RemoteNotAvailableError(f"Blob {blob_name!r} not found in GCS bucket.")
22
- if not is_cached(blob_name) or needs_refresh(blob_name, blob):
23
- download_blob(blob_name, dest)
24
- write_sidecar(blob_name, blob)
15
+ dest = ensure_blob_cached(
16
+ blob_name, refresh=refresh, get_blob_fn=get_blob, download_blob_fn=download_blob
17
+ )
25
18
  return pl.read_parquet(dest)
26
19
 
27
20
 
21
+
22
+
28
23
  def get_enedis_national(refresh=False):
29
24
  """Return Enedis national ``conso-inf36`` measured consumption (polars).
30
25
 
@@ -1,9 +1,7 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  import geopandas as gpd
3
3
 
4
- from ..cache import cache_path, is_cached, needs_refresh, write_sidecar
5
- from ..exceptions import RemoteNotAvailableError
6
- from ..gcs import download_blob, get_blob
4
+ from ..gcs import download_blob, ensure_blob_cached, get_blob
7
5
 
8
6
  _BLOB_NAME = "gas_network_route_latest.gpkg"
9
7
 
@@ -24,17 +22,13 @@ def get_gas_network(bbox=None, refresh=False):
24
22
  Returns:
25
23
  geopandas.GeoDataFrame: gas pipeline routes in EPSG:2154.
26
24
  """
27
- dest = cache_path(_BLOB_NAME)
25
+ dest = ensure_blob_cached(
26
+ _BLOB_NAME, refresh=refresh, get_blob_fn=get_blob, download_blob_fn=download_blob
27
+ )
28
28
 
29
- if not is_cached(_BLOB_NAME) or refresh:
30
- blob = get_blob(_BLOB_NAME)
31
- if blob is None:
32
- raise RemoteNotAvailableError(f"Blob {_BLOB_NAME!r} not found in GCS bucket.")
33
- if not is_cached(_BLOB_NAME) or needs_refresh(_BLOB_NAME, blob):
34
- download_blob(_BLOB_NAME, dest)
35
- write_sidecar(_BLOB_NAME, blob)
36
29
 
37
30
  kwargs = {}
38
31
  if bbox is not None:
39
32
  kwargs["bbox"] = bbox
40
33
  return gpd.read_file(dest, **kwargs)
34
+
@@ -1,9 +1,7 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  import polars as pl
3
3
 
4
- from ..cache import cache_path, is_cached, needs_refresh, write_sidecar
5
- from ..exceptions import RemoteNotAvailableError
6
- from ..gcs import download_blob, get_blob
4
+ from ..gcs import download_blob, ensure_blob_cached, get_blob
7
5
 
8
6
  _BLOB_NAME = "occupant_diaries_latest.parquet"
9
7
 
@@ -35,14 +33,9 @@ def get_occupant_diaries(refresh=False):
35
33
  Raises:
36
34
  RemoteNotAvailableError: if the blob is not found in the GCS bucket.
37
35
  """
38
- dest = cache_path(_BLOB_NAME)
36
+ dest = ensure_blob_cached(
37
+ _BLOB_NAME, refresh=refresh, get_blob_fn=get_blob, download_blob_fn=download_blob
38
+ )
39
+ return pl.read_parquet(dest)
39
40
 
40
- if not is_cached(_BLOB_NAME) or refresh:
41
- blob = get_blob(_BLOB_NAME)
42
- if blob is None:
43
- raise RemoteNotAvailableError(f"Blob {_BLOB_NAME!r} not found in GCS bucket.")
44
- if not is_cached(_BLOB_NAME) or needs_refresh(_BLOB_NAME, blob):
45
- download_blob(_BLOB_NAME, dest)
46
- write_sidecar(_BLOB_NAME, blob)
47
41
 
48
- return pl.read_parquet(dest)
@@ -1,9 +1,7 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  import polars as pl
3
3
 
4
- from ..cache import cache_path, is_cached, needs_refresh, write_sidecar
5
- from ..exceptions import RemoteNotAvailableError
6
- from ..gcs import download_blob, get_blob
4
+ from ..gcs import download_blob, ensure_blob_cached, get_blob
7
5
 
8
6
  _BLOB_NAME = "ore_annual_consumption_latest.parquet"
9
7
 
@@ -38,14 +36,9 @@ def get_ore(refresh=False):
38
36
  Raises:
39
37
  RemoteNotAvailableError: if the blob is not found in the GCS bucket.
40
38
  """
41
- dest = cache_path(_BLOB_NAME)
39
+ dest = ensure_blob_cached(
40
+ _BLOB_NAME, refresh=refresh, get_blob_fn=get_blob, download_blob_fn=download_blob
41
+ )
42
+ return pl.read_parquet(dest)
42
43
 
43
- if not is_cached(_BLOB_NAME) or refresh:
44
- blob = get_blob(_BLOB_NAME)
45
- if blob is None:
46
- raise RemoteNotAvailableError(f"Blob {_BLOB_NAME!r} not found in GCS bucket.")
47
- if not is_cached(_BLOB_NAME) or needs_refresh(_BLOB_NAME, blob):
48
- download_blob(_BLOB_NAME, dest)
49
- write_sidecar(_BLOB_NAME, blob)
50
44
 
51
- return pl.read_parquet(dest)