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.
- buildingdata-0.2.0/LICENSE +21 -0
- buildingdata-0.2.0/PKG-INFO +110 -0
- buildingdata-0.2.0/README.md +77 -0
- buildingdata-0.2.0/buildingdata/__init__.py +26 -0
- buildingdata-0.2.0/buildingdata/_cli.py +29 -0
- buildingdata-0.2.0/buildingdata/cache.py +92 -0
- buildingdata-0.2.0/buildingdata/config.py +119 -0
- buildingdata-0.2.0/buildingdata/exceptions.py +17 -0
- buildingdata-0.2.0/buildingdata/gcs.py +82 -0
- buildingdata-0.2.0/buildingdata/reference/__init__.py +9 -0
- buildingdata-0.2.0/buildingdata/reference/census.py +48 -0
- buildingdata-0.2.0/buildingdata/reference/diagnosis.py +58 -0
- buildingdata-0.2.0/buildingdata/reference/districts.py +41 -0
- buildingdata-0.2.0/buildingdata/reference/elmas.py +69 -0
- buildingdata-0.2.0/buildingdata/reference/enedis.py +81 -0
- buildingdata-0.2.0/buildingdata/reference/gas_network.py +40 -0
- buildingdata-0.2.0/buildingdata/reference/occupant_diaries.py +48 -0
- buildingdata-0.2.0/buildingdata/reference/ore.py +51 -0
- buildingdata-0.2.0/buildingdata/simulation/__init__.py +3 -0
- buildingdata-0.2.0/buildingdata/simulation/bdtopo.py +87 -0
- buildingdata-0.2.0/buildingdata/simulation/era5.py +264 -0
- buildingdata-0.2.0/buildingdata/tests/__init__.py +1 -0
- buildingdata-0.2.0/buildingdata/tests/conftest.py +60 -0
- buildingdata-0.2.0/buildingdata/tests/test_cache.py +78 -0
- buildingdata-0.2.0/buildingdata/tests/test_config.py +80 -0
- buildingdata-0.2.0/buildingdata/tests/test_public_api.py +54 -0
- buildingdata-0.2.0/buildingdata/tests/test_reference.py +430 -0
- buildingdata-0.2.0/buildingdata/tests/test_simulation.py +184 -0
- buildingdata-0.2.0/buildingdata.egg-info/PKG-INFO +110 -0
- buildingdata-0.2.0/buildingdata.egg-info/SOURCES.txt +34 -0
- buildingdata-0.2.0/buildingdata.egg-info/dependency_links.txt +1 -0
- buildingdata-0.2.0/buildingdata.egg-info/entry_points.txt +2 -0
- buildingdata-0.2.0/buildingdata.egg-info/requires.txt +27 -0
- buildingdata-0.2.0/buildingdata.egg-info/top_level.txt +1 -0
- buildingdata-0.2.0/pyproject.toml +54 -0
- 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
|
+
[](https://pypi.org/project/buildingdata/)
|
|
37
|
+
[](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
|
+
[](https://pypi.org/project/buildingdata/)
|
|
4
|
+
[](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()
|