buildingdata 0.2.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 (36) hide show
  1. buildingdata-0.2.0/LICENSE +21 -0
  2. buildingdata-0.2.0/PKG-INFO +110 -0
  3. buildingdata-0.2.0/README.md +77 -0
  4. buildingdata-0.2.0/buildingdata/__init__.py +26 -0
  5. buildingdata-0.2.0/buildingdata/_cli.py +29 -0
  6. buildingdata-0.2.0/buildingdata/cache.py +92 -0
  7. buildingdata-0.2.0/buildingdata/config.py +119 -0
  8. buildingdata-0.2.0/buildingdata/exceptions.py +17 -0
  9. buildingdata-0.2.0/buildingdata/gcs.py +82 -0
  10. buildingdata-0.2.0/buildingdata/reference/__init__.py +9 -0
  11. buildingdata-0.2.0/buildingdata/reference/census.py +48 -0
  12. buildingdata-0.2.0/buildingdata/reference/diagnosis.py +58 -0
  13. buildingdata-0.2.0/buildingdata/reference/districts.py +41 -0
  14. buildingdata-0.2.0/buildingdata/reference/elmas.py +69 -0
  15. buildingdata-0.2.0/buildingdata/reference/enedis.py +81 -0
  16. buildingdata-0.2.0/buildingdata/reference/gas_network.py +40 -0
  17. buildingdata-0.2.0/buildingdata/reference/occupant_diaries.py +48 -0
  18. buildingdata-0.2.0/buildingdata/reference/ore.py +51 -0
  19. buildingdata-0.2.0/buildingdata/simulation/__init__.py +3 -0
  20. buildingdata-0.2.0/buildingdata/simulation/bdtopo.py +87 -0
  21. buildingdata-0.2.0/buildingdata/simulation/era5.py +264 -0
  22. buildingdata-0.2.0/buildingdata/tests/__init__.py +1 -0
  23. buildingdata-0.2.0/buildingdata/tests/conftest.py +60 -0
  24. buildingdata-0.2.0/buildingdata/tests/test_cache.py +78 -0
  25. buildingdata-0.2.0/buildingdata/tests/test_config.py +80 -0
  26. buildingdata-0.2.0/buildingdata/tests/test_public_api.py +54 -0
  27. buildingdata-0.2.0/buildingdata/tests/test_reference.py +430 -0
  28. buildingdata-0.2.0/buildingdata/tests/test_simulation.py +184 -0
  29. buildingdata-0.2.0/buildingdata.egg-info/PKG-INFO +110 -0
  30. buildingdata-0.2.0/buildingdata.egg-info/SOURCES.txt +34 -0
  31. buildingdata-0.2.0/buildingdata.egg-info/dependency_links.txt +1 -0
  32. buildingdata-0.2.0/buildingdata.egg-info/entry_points.txt +2 -0
  33. buildingdata-0.2.0/buildingdata.egg-info/requires.txt +27 -0
  34. buildingdata-0.2.0/buildingdata.egg-info/top_level.txt +1 -0
  35. buildingdata-0.2.0/pyproject.toml +54 -0
  36. buildingdata-0.2.0/setup.cfg +4 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yassine Abdelouadoud
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,110 @@
1
+ Metadata-Version: 2.4
2
+ Name: buildingdata
3
+ Version: 0.2.0
4
+ Summary: Data management layer for buildingmodel — reference data download, BDTOPO retrieval, ERA5 weather
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: google-cloud-storage>=2.0
10
+ Requires-Dist: polars>=0.20
11
+ Requires-Dist: geopandas>=0.14
12
+ Requires-Dist: pyarrow>=14.0
13
+ Requires-Dist: tqdm>=4.0
14
+ Requires-Dist: requests>=2.28
15
+ Requires-Dist: platformdirs>=3.0
16
+ Provides-Extra: era5
17
+ Requires-Dist: cdsapi>=0.6; extra == "era5"
18
+ Requires-Dist: xarray>=2023.0; extra == "era5"
19
+ Requires-Dist: pvlib>=0.10; extra == "era5"
20
+ Requires-Dist: netcdf4>=1.6; extra == "era5"
21
+ Requires-Dist: zarr>=2.18; extra == "era5"
22
+ Provides-Extra: pipeline
23
+ Requires-Dist: snakemake>=8.0; extra == "pipeline"
24
+ Requires-Dist: openpyxl>=3.0; extra == "pipeline"
25
+ Requires-Dist: py7zr>=0.20; extra == "pipeline"
26
+ Provides-Extra: docs
27
+ Requires-Dist: sphinx>=7.0; extra == "docs"
28
+ Requires-Dist: sphinx-autoapi>=3.0; extra == "docs"
29
+ Requires-Dist: pydata-sphinx-theme>=0.15; extra == "docs"
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest>=8.0; extra == "dev"
32
+ Dynamic: license-file
33
+
34
+ # buildingdata
35
+
36
+ [![PyPI](https://img.shields.io/pypi/v/buildingdata.svg)](https://pypi.org/project/buildingdata/)
37
+ [![Python](https://img.shields.io/pypi/pyversions/buildingdata.svg)](https://pypi.org/project/buildingdata/)
38
+
39
+ **Data-management layer for [`buildingmodel`](https://gitlab.com/energytransition).**
40
+ `buildingdata` delivers clean, ready-to-use building, demographic and weather
41
+ datasets for French building energy inference, and caches everything locally so
42
+ repeated calls don't re-download.
43
+
44
+ ```bash
45
+ pip install buildingdata
46
+ ```
47
+
48
+ ## Quick start
49
+
50
+ ```python
51
+ import buildingdata as bd
52
+
53
+ # One-time configuration (bucket, cache dir, credentials).
54
+ # The public bucket works anonymously, so this is optional.
55
+ bd.configure()
56
+
57
+ # Reference datasets (downloaded once from Google Cloud Storage, then cached)
58
+ census = bd.get_census() # INSEE census -> polars DataFrame
59
+ districts = bd.get_districts() # IRIS district geometry -> geopandas GeoDataFrame
60
+ diagnosis = bd.get_diagnosis() # ADEME energy diagnoses -> polars DataFrame
61
+ gas = bd.get_gas_network() # GRDF gas network routes -> geopandas GeoDataFrame
62
+
63
+ # On-demand datasets (fetched live from public APIs)
64
+ buildings = bd.get_bdtopo("751010101") # per-IRIS building geometry (IGN WFS)
65
+ epw = bd.get_era5_climate(48.85, 2.35, 2020) # ERA5 weather -> synthetic EPW
66
+ ```
67
+
68
+ You can also configure from the command line:
69
+
70
+ ```bash
71
+ buildingdata configure --bucket my-bucket --cache-dir ~/.cache/buildingdata
72
+ ```
73
+
74
+ ## What it provides
75
+
76
+ | Function | Source | Returns |
77
+ | --- | --- | --- |
78
+ | `get_census()` | INSEE census (GCS) | polars `DataFrame` |
79
+ | `get_districts()` | IRIS geometries (GCS) | geopandas `GeoDataFrame` |
80
+ | `get_diagnosis()` | ADEME energy performance diagnoses (GCS) | polars `DataFrame` |
81
+ | `get_gas_network()` | GRDF gas network routes (GCS) | geopandas `GeoDataFrame` |
82
+ | `get_bdtopo(iris_code)` | IGN Géoplateforme WFS (live) | geopandas `GeoDataFrame` |
83
+ | `get_era5_climate(lat, lon, year)` | Copernicus CDS (live) | path to EPW file |
84
+
85
+ Reference datasets are pulled from a public Google Cloud Storage bucket and
86
+ cached locally with generation-based freshness checks. French geospatial data
87
+ uses CRS **EPSG:2154 (Lambert-93)**.
88
+
89
+ ## Configuration
90
+
91
+ Settings are resolved from (in order) explicit arguments, environment variables,
92
+ and `~/.config/buildingdata/config.ini`:
93
+
94
+ - **bucket** — GCS bucket holding the reference datasets
95
+ - **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)
98
+
99
+ ## Installation extras
100
+
101
+ ```bash
102
+ pip install "buildingdata[era5]" # ERA5 weather (cdsapi, xarray, pvlib, ...)
103
+ pip install "buildingdata[docs]" # build the Sphinx documentation
104
+ ```
105
+
106
+ Requires **Python ≥ 3.10**.
107
+
108
+ ## License
109
+
110
+ Released under the [MIT License](LICENSE).
@@ -0,0 +1,77 @@
1
+ # buildingdata
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/buildingdata.svg)](https://pypi.org/project/buildingdata/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/buildingdata.svg)](https://pypi.org/project/buildingdata/)
5
+
6
+ **Data-management layer for [`buildingmodel`](https://gitlab.com/energytransition).**
7
+ `buildingdata` delivers clean, ready-to-use building, demographic and weather
8
+ datasets for French building energy inference, and caches everything locally so
9
+ repeated calls don't re-download.
10
+
11
+ ```bash
12
+ pip install buildingdata
13
+ ```
14
+
15
+ ## Quick start
16
+
17
+ ```python
18
+ import buildingdata as bd
19
+
20
+ # One-time configuration (bucket, cache dir, credentials).
21
+ # The public bucket works anonymously, so this is optional.
22
+ bd.configure()
23
+
24
+ # Reference datasets (downloaded once from Google Cloud Storage, then cached)
25
+ census = bd.get_census() # INSEE census -> polars DataFrame
26
+ districts = bd.get_districts() # IRIS district geometry -> geopandas GeoDataFrame
27
+ diagnosis = bd.get_diagnosis() # ADEME energy diagnoses -> polars DataFrame
28
+ gas = bd.get_gas_network() # GRDF gas network routes -> geopandas GeoDataFrame
29
+
30
+ # On-demand datasets (fetched live from public APIs)
31
+ buildings = bd.get_bdtopo("751010101") # per-IRIS building geometry (IGN WFS)
32
+ epw = bd.get_era5_climate(48.85, 2.35, 2020) # ERA5 weather -> synthetic EPW
33
+ ```
34
+
35
+ You can also configure from the command line:
36
+
37
+ ```bash
38
+ buildingdata configure --bucket my-bucket --cache-dir ~/.cache/buildingdata
39
+ ```
40
+
41
+ ## What it provides
42
+
43
+ | Function | Source | Returns |
44
+ | --- | --- | --- |
45
+ | `get_census()` | INSEE census (GCS) | polars `DataFrame` |
46
+ | `get_districts()` | IRIS geometries (GCS) | geopandas `GeoDataFrame` |
47
+ | `get_diagnosis()` | ADEME energy performance diagnoses (GCS) | polars `DataFrame` |
48
+ | `get_gas_network()` | GRDF gas network routes (GCS) | geopandas `GeoDataFrame` |
49
+ | `get_bdtopo(iris_code)` | IGN Géoplateforme WFS (live) | geopandas `GeoDataFrame` |
50
+ | `get_era5_climate(lat, lon, year)` | Copernicus CDS (live) | path to EPW file |
51
+
52
+ Reference datasets are pulled from a public Google Cloud Storage bucket and
53
+ cached locally with generation-based freshness checks. French geospatial data
54
+ uses CRS **EPSG:2154 (Lambert-93)**.
55
+
56
+ ## Configuration
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
62
+ - **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)
65
+
66
+ ## Installation extras
67
+
68
+ ```bash
69
+ pip install "buildingdata[era5]" # ERA5 weather (cdsapi, xarray, pvlib, ...)
70
+ pip install "buildingdata[docs]" # build the Sphinx documentation
71
+ ```
72
+
73
+ Requires **Python ≥ 3.10**.
74
+
75
+ ## License
76
+
77
+ Released under the [MIT License](LICENSE).
@@ -0,0 +1,26 @@
1
+ # -*- coding: utf-8 -*-
2
+ from .config import write_config
3
+ from .reference import (
4
+ get_census,
5
+ get_diagnosis,
6
+ get_districts,
7
+ get_elmas,
8
+ get_enedis_national,
9
+ get_enedis_regional,
10
+ get_gas_network,
11
+ get_occupant_diaries,
12
+ get_ore,
13
+ )
14
+ from .simulation import get_bdtopo, get_era5_climate
15
+
16
+
17
+ def configure(bucket=None, cache_dir=None, credentials=None):
18
+ """Write or update ~/.config/buildingdata/config.ini.
19
+
20
+ Args:
21
+ bucket (str, optional): GCS bucket name.
22
+ cache_dir (str or pathlib.Path, optional): local cache directory.
23
+ credentials (str or pathlib.Path, optional): path to GCS service
24
+ account JSON. Pass None to use Application Default Credentials.
25
+ """
26
+ write_config(bucket=bucket, cache_dir=cache_dir, credentials=credentials)
@@ -0,0 +1,29 @@
1
+ # -*- coding: utf-8 -*-
2
+ import argparse
3
+
4
+ from .config import write_config
5
+
6
+
7
+ def main():
8
+ parser = argparse.ArgumentParser(
9
+ prog="buildingdata",
10
+ description="Configure the buildingdata package.",
11
+ )
12
+ sub = parser.add_subparsers(dest="command")
13
+
14
+ cfg_parser = sub.add_parser("configure", help="Write ~/.config/buildingdata/config.ini")
15
+ cfg_parser.add_argument("--bucket", default=None, help="GCS bucket name")
16
+ cfg_parser.add_argument("--cache-dir", default=None, dest="cache_dir", help="Local cache directory")
17
+ cfg_parser.add_argument("--credentials", default=None, help="Path to GCS service account JSON")
18
+
19
+ args = parser.parse_args()
20
+
21
+ if args.command == "configure":
22
+ write_config(
23
+ bucket=args.bucket,
24
+ cache_dir=args.cache_dir,
25
+ credentials=args.credentials,
26
+ )
27
+ print("Configuration saved.")
28
+ else:
29
+ parser.print_help()
@@ -0,0 +1,92 @@
1
+ # -*- coding: utf-8 -*-
2
+ import json
3
+ from datetime import datetime, timezone
4
+ from pathlib import Path
5
+
6
+ from .config import get_cache_dir
7
+ from .exceptions import CacheError
8
+
9
+
10
+ def cache_path(name):
11
+ """Return the local Path where a named artifact should be cached.
12
+
13
+ Args:
14
+ name (str): logical artifact name, e.g. "census_latest.parquet".
15
+
16
+ Returns:
17
+ pathlib.Path: absolute path under the configured cache dir.
18
+ """
19
+ return get_cache_dir() / name
20
+
21
+
22
+ def _sidecar_path(name):
23
+ return get_cache_dir() / (name + ".meta.json")
24
+
25
+
26
+ def is_cached(name):
27
+ """Return True if the artifact file and a valid sidecar both exist.
28
+
29
+ Does not contact GCS. Use needs_refresh() for a freshness check.
30
+
31
+ Args:
32
+ name (str): logical artifact name.
33
+
34
+ Returns:
35
+ bool: True if the local copy can be used without downloading.
36
+ """
37
+ return cache_path(name).exists() and _sidecar_path(name).exists()
38
+
39
+
40
+ def needs_refresh(name, blob):
41
+ """Return True if the local copy is older than the GCS blob.
42
+
43
+ Args:
44
+ name (str): logical artifact name.
45
+ blob (google.cloud.storage.Blob): live blob object from GCS.
46
+
47
+ Returns:
48
+ bool: True if the blob generation has advanced past the cached one.
49
+ """
50
+ sidecar = _sidecar_path(name)
51
+ if not sidecar.exists():
52
+ return True
53
+ try:
54
+ with open(sidecar) as f:
55
+ meta = json.load(f)
56
+ return int(meta.get("generation", -1)) < blob.generation
57
+ except Exception:
58
+ return True
59
+
60
+
61
+ def write_sidecar(name, blob):
62
+ """Write a .meta.json sidecar after a successful download.
63
+
64
+ Args:
65
+ name (str): logical artifact name.
66
+ blob (google.cloud.storage.Blob): the blob that was downloaded.
67
+ """
68
+ sidecar = _sidecar_path(name)
69
+ meta = {
70
+ "blob_name": blob.name,
71
+ "generation": blob.generation,
72
+ "downloaded_at": datetime.now(timezone.utc).isoformat(),
73
+ }
74
+ try:
75
+ with open(sidecar, "w") as f:
76
+ json.dump(meta, f, indent=2)
77
+ except OSError as e:
78
+ raise CacheError(f"Failed to write cache sidecar {sidecar}: {e}") from e
79
+
80
+
81
+ def ensure_subdir(subdir):
82
+ """Return a subdirectory of the cache dir, creating it if absent.
83
+
84
+ Args:
85
+ subdir (str): subdirectory name, e.g. "bdtopo" or "era5".
86
+
87
+ Returns:
88
+ pathlib.Path: absolute path to the subdirectory.
89
+ """
90
+ path = get_cache_dir() / subdir
91
+ path.mkdir(parents=True, exist_ok=True)
92
+ return path
@@ -0,0 +1,119 @@
1
+ # -*- coding: utf-8 -*-
2
+ import configparser
3
+ import hashlib
4
+ import os
5
+ from pathlib import Path
6
+
7
+ from platformdirs import user_data_dir
8
+
9
+ _DEFAULT_BUCKET = "building-inference-data"
10
+ _CONFIG_FILE = Path.home() / ".config" / "buildingdata" / "config.ini"
11
+
12
+
13
+ def _install_id():
14
+ """Return a short, stable id for this package installation.
15
+
16
+ Derived from the on-disk location of the installed package, so that
17
+ each install (e.g. a separate virtualenv) maps to a distinct id while
18
+ reinstalling in place keeps the same one.
19
+
20
+ Returns:
21
+ str: 8-char hex digest identifying this installation.
22
+ """
23
+ install_root = str(Path(__file__).resolve().parent)
24
+ return hashlib.sha256(install_root.encode("utf-8")).hexdigest()[:8]
25
+
26
+
27
+ def _default_cache_dir():
28
+ """Return the per-installation default cache directory.
29
+
30
+ Uses the platform user-data dir as the base and namespaces it by
31
+ installation id so multiple installs do not overwrite each other's
32
+ cache.
33
+
34
+ Returns:
35
+ pathlib.Path: absolute path to the default cache directory.
36
+ """
37
+ return Path(user_data_dir("buildingdata")) / "cache" / _install_id()
38
+
39
+
40
+ def _read_config():
41
+ cfg = configparser.ConfigParser()
42
+ if _CONFIG_FILE.exists():
43
+ cfg.read(_CONFIG_FILE)
44
+ return cfg
45
+
46
+
47
+ def get_bucket():
48
+ """Return the GCS bucket name.
49
+
50
+ Precedence: BUILDINGDATA_BUCKET env var → config file → default.
51
+
52
+ Returns:
53
+ str: GCS bucket name without gs:// prefix.
54
+ """
55
+ if "BUILDINGDATA_BUCKET" in os.environ:
56
+ return os.environ["BUILDINGDATA_BUCKET"]
57
+ cfg = _read_config()
58
+ return cfg.get("gcs", "bucket", fallback=_DEFAULT_BUCKET)
59
+
60
+
61
+ def get_cache_dir():
62
+ """Return the local cache directory, creating it if absent.
63
+
64
+ Precedence: BUILDINGDATA_CACHE_DIR env var → config file → default.
65
+
66
+ Returns:
67
+ pathlib.Path: absolute path to the cache directory.
68
+ """
69
+ if "BUILDINGDATA_CACHE_DIR" in os.environ:
70
+ path = Path(os.environ["BUILDINGDATA_CACHE_DIR"])
71
+ else:
72
+ cfg = _read_config()
73
+ raw = cfg.get("cache", "dir", fallback=str(_default_cache_dir()))
74
+ path = Path(raw)
75
+ path.mkdir(parents=True, exist_ok=True)
76
+ return path
77
+
78
+
79
+ def get_credentials_file():
80
+ """Return path to a GCS service account JSON, or None for anonymous access.
81
+
82
+ When None, the storage client reads public buckets anonymously, with no
83
+ authentication required.
84
+
85
+ Precedence: GOOGLE_APPLICATION_CREDENTIALS env var → config file → None.
86
+
87
+ Returns:
88
+ pathlib.Path or None: path to credentials JSON, or None.
89
+ """
90
+ if "GOOGLE_APPLICATION_CREDENTIALS" in os.environ:
91
+ return Path(os.environ["GOOGLE_APPLICATION_CREDENTIALS"])
92
+ cfg = _read_config()
93
+ raw = cfg.get("gcs", "credentials", fallback=None)
94
+ return Path(raw) if raw else None
95
+
96
+
97
+ def write_config(bucket=None, cache_dir=None, credentials=None):
98
+ """Write or update ~/.config/buildingdata/config.ini.
99
+
100
+ Args:
101
+ bucket (str, optional): GCS bucket name.
102
+ cache_dir (str or pathlib.Path, optional): local cache directory.
103
+ credentials (str or pathlib.Path, optional): path to GCS service
104
+ account JSON. Pass None to leave unchanged.
105
+ """
106
+ _CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
107
+ cfg = _read_config()
108
+ if not cfg.has_section("gcs"):
109
+ cfg.add_section("gcs")
110
+ if not cfg.has_section("cache"):
111
+ cfg.add_section("cache")
112
+ if bucket is not None:
113
+ cfg.set("gcs", "bucket", bucket)
114
+ if credentials is not None:
115
+ cfg.set("gcs", "credentials", str(credentials))
116
+ if cache_dir is not None:
117
+ cfg.set("cache", "dir", str(cache_dir))
118
+ with open(_CONFIG_FILE, "w") as f:
119
+ cfg.write(f)
@@ -0,0 +1,17 @@
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."""
@@ -0,0 +1,82 @@
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
+ from google.cloud import storage
52
+
53
+ dest_path = Path(dest_path)
54
+ dest_path.parent.mkdir(parents=True, exist_ok=True)
55
+
56
+ client = get_client()
57
+ bucket = client.bucket(get_bucket())
58
+ blob = bucket.blob(name)
59
+ blob.reload()
60
+
61
+ total = blob.size or 0
62
+ tmp_path = dest_path.with_suffix(dest_path.suffix + ".tmp")
63
+
64
+ with open(tmp_path, "wb") as f:
65
+ if show_progress:
66
+ with tqdm(
67
+ total=total,
68
+ unit="B",
69
+ unit_scale=True,
70
+ desc=name,
71
+ leave=True,
72
+ ) as bar:
73
+ def _callback(chunk):
74
+ bar.update(len(chunk))
75
+
76
+ blob.download_to_file(f, checksum="md5", raw_download=True)
77
+ bar.update(total - bar.n)
78
+ else:
79
+ blob.download_to_file(f)
80
+
81
+ tmp_path.replace(dest_path)
82
+ return blob
@@ -0,0 +1,9 @@
1
+ # -*- coding: utf-8 -*-
2
+ from .census import get_census
3
+ from .diagnosis import get_diagnosis
4
+ from .districts import get_districts
5
+ from .elmas import get_elmas
6
+ from .enedis import get_enedis_national, get_enedis_regional
7
+ from .gas_network import get_gas_network
8
+ from .occupant_diaries import get_occupant_diaries
9
+ from .ore import get_ore
@@ -0,0 +1,48 @@
1
+ # -*- coding: utf-8 -*-
2
+ import polars as pl
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
7
+
8
+ _BLOB_NAME = "census_latest.parquet"
9
+
10
+
11
+ def get_census(city_group_list=None, refresh=False):
12
+ """Return the processed INSEE census DataFrame, optionally pre-filtered.
13
+
14
+ Downloads census_latest.parquet from GCS to the local cache on first call
15
+ (or when refresh=True). Filtering by city_group_list uses Polars lazy scan
16
+ so only the needed rows are loaded into memory.
17
+
18
+ Args:
19
+ city_group_list (list of str, optional): EPCI codes to filter on. If
20
+ None, the full national census is returned. Defaults to None.
21
+ refresh (bool): force re-download even if the cache is warm.
22
+ Defaults to False.
23
+
24
+ Returns:
25
+ polars.DataFrame: census records with columns occupancy_type,
26
+ occupant_status, living_area_class, occupant_count, heating_system,
27
+ residential_type, construction_year_class, district, city,
28
+ city_group, department, region, main_cooking_energy,
29
+ secondary_cooking_energy, ipondl.
30
+ """
31
+ dest = cache_path(_BLOB_NAME)
32
+
33
+ if refresh or not is_cached(_BLOB_NAME):
34
+ blob = get_blob(_BLOB_NAME)
35
+ if blob is None:
36
+ raise RemoteNotAvailableError(f"Blob {_BLOB_NAME!r} not found in GCS bucket.")
37
+ download_blob(_BLOB_NAME, dest)
38
+ write_sidecar(_BLOB_NAME, blob)
39
+ elif refresh:
40
+ blob = get_blob(_BLOB_NAME)
41
+ if blob is not None and needs_refresh(_BLOB_NAME, blob):
42
+ download_blob(_BLOB_NAME, dest)
43
+ write_sidecar(_BLOB_NAME, blob)
44
+
45
+ lf = pl.scan_parquet(dest)
46
+ if city_group_list is not None:
47
+ lf = lf.filter(pl.col("city_group").is_in(city_group_list))
48
+ return lf.collect()