Skip to content

Commit 328fadd

Browse files
committed
2 parents a9d6b27 + df52a35 commit 328fadd

File tree

5 files changed

+83
-35
lines changed

5 files changed

+83
-35
lines changed

cea/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1279,7 +1279,7 @@ def _choices(self):
12791279
return list(codes)
12801280
except FileNotFoundError as e:
12811281
# FIXME: This might cause default config to fail since the file does not exist, maybe should be a warning?
1282-
raise FileNotFoundError(f'Could not find source file at {location}') from e
1282+
raise FileNotFoundError(f'Could not find source file at {location} to generate choices for {self.name}') from e
12831283
except Exception as e:
12841284
raise ValueError(f'There was an error generating choices for {self.name} from {location}') from e
12851285

cea/default.config

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -750,7 +750,7 @@ results-summary-and-analytics.help = True if generating a summary and advanced a
750750
folder-name-to-save-exported-results =
751751
folder-name-to-save-exported-results.type = StringParameter
752752
folder-name-to-save-exported-results.nullable = true
753-
folder-name-to-save-exported-results.help = Name the folder to store the exported .csv files, which to be found under Path - current_Scenario/export/results/summary_name_done_time. When left blank, the default path is to set as - current_Scenario/export/results/hours_period_range_done_time.
753+
folder-name-to-save-exported-results.help = Name the folder to store the exported .csv files, which to be found under Path - current_Scenario/export/results/summary_name_current_time
754754

755755
buildings =
756756
buildings.type = BuildingsParameter

cea/import_export/result_summary.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import numpy as np
99
import cea.config
1010
import time
11-
from datetime import datetime
11+
from datetime import datetime, UTC
1212
import cea.inputlocator
1313
import geopandas as gpd
1414

@@ -2406,8 +2406,9 @@ def process_building_summary(config, locator,
24062406

24072407
# Step 2: Get User-Defined Folder Name & Create Folder if it Doesn't Exist
24082408
if not plot:
2409-
folder_name = config.result_summary.folder_name_to_save_exported_results
2410-
summary_folder = locator.get_export_results_summary_folder(hour_start, hour_end, folder_name)
2409+
folder_name = config.result_summary.folder_name_to_save_exported_results or "summary"
2410+
summary_folder = locator.get_export_results_summary_folder(f"{folder_name}-{datetime.now(UTC).strftime('%Y%m%d-%H%M%S')}")
2411+
print(f"Results will be saved to: {summary_folder}")
24112412
else:
24122413
summary_folder = locator.get_export_plots_folder()
24132414
os.makedirs(summary_folder, exist_ok=True)

cea/inputlocator.py

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -152,23 +152,9 @@ def get_export_plots_selected_building_file(self):
152152
"""scenario/export/plots/{plot_cea_feature}/selected_buildings.csv"""
153153
return os.path.join(self.get_export_plots_folder(), 'selected_buildings.csv')
154154

155-
def get_export_results_summary_folder(self, hour_start, hour_end, folder_name):
156-
if folder_name is None or folder_name.strip() == "":
157-
"""scenario/export/results/hours_{hour_start}_{hour_end}_done_{current_time}"""
158-
path = os.path.join(self.get_export_results_folder(), f'unnamed_hours_{hour_start}_{hour_end}')
159-
else:
160-
"""scenario/export/results/{folder_name}_done_{current_time}"""
161-
path = os.path.join(self.get_export_results_folder(), f'{folder_name}_hours_{hour_start}_{hour_end}')
162-
163-
# new path ending with _1, _2, _3 if the user-defined path exists
164-
base_path = path
165-
counter = 1
166-
167-
while os.path.exists(path):
168-
path = f"{base_path}_{counter}"
169-
counter += 1
170-
171-
return path
155+
def get_export_results_summary_folder(self, folder_name):
156+
"""scenario/export/results/{folder_name}"""
157+
return os.path.join(self.get_export_results_folder(), folder_name)
172158

173159
def get_export_results_summary_selected_building_file(self, summary_folder):
174160
"""scenario/export/results/{folder_name}/selected_buildings.csv"""

cea/interfaces/dashboard/api/contents.py

Lines changed: 74 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@
99
from typing import Optional, List
1010

1111
from fastapi import APIRouter, HTTPException, status, Form, UploadFile
12+
from fastapi.concurrency import run_in_threadpool
1213
from pydantic import BaseModel
1314
from starlette.responses import StreamingResponse
1415
from typing_extensions import Annotated, Literal
1516

1617
import cea.config
18+
import cea.inputlocator
1719
from cea.datamanagement.format_helper.cea4_migrate import migrate_cea3_to_cea4
1820
from cea.datamanagement.format_helper.cea4_migrate_db import migrate_cea3_to_cea4_db
1921
from cea.datamanagement.format_helper.cea4_verify import cea4_verify
@@ -397,6 +399,23 @@ class DownloadScenario(BaseModel):
397399
project: str
398400
scenarios: List[str]
399401
input_files: bool
402+
output_files: Literal["summary", "detailed"]
403+
404+
405+
def run_summary(project: str, scenario_name: str):
406+
"""Run the summary function to ensure all summary files are generated"""
407+
config = cea.config.Configuration(cea.config.DEFAULT_CONFIG)
408+
config.project = project
409+
config.scenario_name = scenario_name
410+
411+
config.result_summary.aggregate_by_building = True
412+
413+
try:
414+
from cea.import_export.result_summary import main as result_summary_main
415+
result_summary_main(config)
416+
except Exception as e:
417+
logger.error(f"Error generating summary for {scenario_name}: {str(e)}")
418+
raise e
400419

401420

402421
@router.post("/scenario/download")
@@ -414,33 +433,75 @@ async def download_scenario(form: DownloadScenario, project_root: CEAProjectRoot
414433

415434
project = form.project.strip()
416435
scenarios = form.scenarios
417-
input_files_only = form.input_files
436+
input_files = form.input_files
437+
output_files_level = form.output_files
418438

419439
filename = f"{project}_scenarios.zip" if len(scenarios) > 1 else f"{project}_{scenarios[0]}.zip"
420440

421441
temp_file_path = None
422442
try:
423443
with tempfile.NamedTemporaryFile(delete=False) as temp_file:
424444
temp_file_path = temp_file.name
425-
with zipfile.ZipFile(temp_file, 'w', zipfile.ZIP_DEFLATED) as zip_file:
426-
base_path = Path(project_root) / project
445+
446+
# Use compresslevel=1 for faster zipping, at the cost of compression ratio
447+
with zipfile.ZipFile(temp_file, 'w', zipfile.ZIP_DEFLATED, compresslevel=1) as zip_file:
448+
base_path = Path(secure_path(Path(project_root, project).resolve()))
427449

450+
# Collect all files first for batch processing
451+
files_to_zip = []
428452
for scenario in scenarios:
429-
scenario_path = base_path / scenario
453+
# sanitize scenario for fs ops and zip arcnames
454+
scenario_name = Path(scenario).name
455+
scenario_path = Path(secure_path((base_path / scenario_name).resolve()))
456+
430457
if not scenario_path.exists():
431458
continue
432-
433-
target_path = (scenario_path / "inputs") if input_files_only else scenario_path
434-
prefix = f"{scenario}/inputs" if input_files_only else scenario
435-
436-
for item_path in target_path.rglob('*'):
437-
if item_path.is_file() and item_path.suffix in VALID_EXTENSIONS:
438-
relative_path = str(Path(prefix) / item_path.relative_to(target_path))
439-
zip_file.write(item_path, arcname=relative_path)
459+
460+
input_paths = (scenario_path / "inputs")
461+
if input_files and input_paths.exists():
462+
for root, dirs, files in os.walk(input_paths):
463+
root_path = Path(root)
464+
for file in files:
465+
if Path(file).suffix in VALID_EXTENSIONS:
466+
item_path = root_path / file
467+
relative_path = str(Path(scenario_name) / "inputs" / item_path.relative_to(input_paths))
468+
files_to_zip.append((item_path, relative_path))
469+
470+
output_paths = (scenario_path / "outputs")
471+
if output_files_level == "detailed" and output_paths.exists():
472+
for root, dirs, files in os.walk(output_paths):
473+
root_path = Path(root)
474+
for file in files:
475+
if Path(file).suffix in VALID_EXTENSIONS:
476+
item_path = root_path / file
477+
relative_path = str(Path(scenario_name) / "outputs" / item_path.relative_to(output_paths))
478+
files_to_zip.append((item_path, relative_path))
479+
480+
elif output_files_level == "summary":
481+
# create summary files first
482+
await run_in_threadpool(run_summary, str(base_path), scenario_name)
483+
484+
export_paths = (scenario_path / "export" / "results")
485+
if not export_paths.exists():
486+
raise ValueError(f"Export results path does not exist for scenario {scenario_name}")
487+
488+
for root, dirs, files in os.walk(export_paths):
489+
root_path = Path(root)
490+
for file in files:
491+
if Path(file).suffix in VALID_EXTENSIONS:
492+
item_path = root_path / file
493+
relative_path = str(
494+
Path(scenario_name) / "export" / "results" / item_path.relative_to(export_paths))
495+
files_to_zip.append((item_path, relative_path))
496+
497+
# Batch write all files to zip
498+
logger.info(f"Writing {len(files_to_zip)} files to zip...")
499+
for item_path, archive_name in files_to_zip:
500+
zip_file.write(item_path, arcname=archive_name)
440501

441502
# Get the file size for Content-Length header
442503
file_size = os.path.getsize(temp_file_path)
443-
504+
444505
# Define the streaming function
445506
async def file_streamer():
446507
try:

0 commit comments

Comments
 (0)