Skip to content

Commit

Permalink
Merge pull request #3640 from architecture-building-systems/3568-fastapi
Browse files Browse the repository at this point in the history
Replace flask with fastapi
  • Loading branch information
ShiZhongming authored Sep 17, 2024
2 parents c449530 + 5eab559 commit 54f6082
Show file tree
Hide file tree
Showing 38 changed files with 4,322 additions and 2,804 deletions.
11 changes: 6 additions & 5 deletions .github/workflows/setup_build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
run: |
python -m pip install build
python -m build
mv dist/cityenergyanalyst-$CEA_VERSION.tar.gz setup/cityenergyanalyst.tar.gz
mv dist/cityenergyanalyst-*.tar.gz setup/cityenergyanalyst.tar.gz
- name: Cache CEA env
id: cache-env
Expand Down Expand Up @@ -70,18 +70,19 @@ jobs:

- uses: actions/setup-node@v4
with:
node-version: 20
node-version: 22

- name: Enable Corepack
shell: bash
run: corepack enable

- name: Package CEA GUI
shell: bash -el {0}
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GUI_GH_TOKEN }}
run: |
cd $GITHUB_WORKSPACE/gui
yarn
yarn install
yarn version $CEA_VERSION
yarn electron:release
mv "out/CityEnergyAnalyst-GUI Setup ${CEA_VERSION}.exe" $GITHUB_WORKSPACE/setup/gui_setup.exe
Expand Down Expand Up @@ -147,7 +148,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 20
node-version: 22

- name: Enable Corepack
run: corepack enable
Expand Down
10 changes: 6 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,12 @@ ARG MAMBA_DOCKERFILE_ACTIVATE=1
# install cea after dependencies to avoid running conda too many times when rebuilding
COPY --chown=$MAMBA_USER:$MAMBA_USER . /tmp/cea
RUN pip install /tmp/cea
RUN cea-config write --general:project /project
RUN cea-config write --radiation:daysim-bin-directory /Daysim
# required for flask to receive reqests from the docker host
RUN cea-config write --server:host 0.0.0.0

# write config files
RUN cea-config write --general:project /project/reference-case-open \
&& cea-config write --general:scenario-name baseline \
&& cea-config write --radiation:daysim-bin-directory /Daysim \
&& cea-config write --server:host 0.0.0.0 # required for flask to receive requests from the docker host

ENTRYPOINT ["/usr/local/bin/_entrypoint.sh"]
CMD cea dashboard
4 changes: 3 additions & 1 deletion cea/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -1132,9 +1132,11 @@ def is_valid_scenario(folder_name):
"""
folder_path = os.path.join(project_path, folder_name)

# TODO: Use .gitignore to ignore scenarios
return all([os.path.isdir(folder_path),
not folder_name.startswith('.'),
folder_name != "__pycache__"])
folder_name != "__pycache__",
folder_name != "__MACOSX"])

return [folder_name for folder_name in os.listdir(project_path) if is_valid_scenario(folder_name)]

Expand Down
12 changes: 8 additions & 4 deletions cea/databases/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@



import os
from collections import OrderedDict

Expand All @@ -10,7 +7,14 @@

def get_regions():
return [folder for folder in os.listdir(databases_folder_path) if folder != "weather"
and os.path.isdir(os.path.join(databases_folder_path, folder))]
and os.path.isdir(os.path.join(databases_folder_path, folder))
and not folder.startswith('.')
and not folder.startswith('__')]


def get_weather_files():
weather_folder_path = os.path.join(databases_folder_path, 'weather')
return [os.path.splitext(f)[0] for f in os.listdir(weather_folder_path) if f.endswith('.epw')]


def get_categories(db_path):
Expand Down
58 changes: 32 additions & 26 deletions cea/datamanagement/create_new_scenario.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
"""
A tool to create a new project / scenario with the CEA.
"""





import os
from shutil import copyfile

Expand All @@ -29,6 +24,35 @@
__status__ = "Production"


def copy_terrain(terrain_path, locator, lat, lon):
terrain = raster_to_WSG_and_UTM(terrain_path, lat, lon)
driver = gdal.GetDriverByName('GTiff')
verify_input_terrain(terrain)
driver.CreateCopy(locator.get_terrain(), terrain)


def copy_typology(typology_path, locator):
# import file
occupancy_file = dbf_to_dataframe(typology_path)
occupancy_file_test = occupancy_file[COLUMNS_ZONE_TYPOLOGY]
# verify if input file is correct for CEA, if not an exception will be released
verify_input_typology(occupancy_file_test)
# create new file
copyfile(typology_path, locator.get_building_typology())

def generate_default_typology(zone_geometry: Gdf, locator: cea.inputlocator.InputLocator):
zone = zone_geometry.drop('geometry', axis=1)
zone['STANDARD'] = 'STANDARD1'
zone['YEAR'] = 2020
zone['1ST_USE'] = 'MULTI_RES'
zone['1ST_USE_R'] = 1.0
zone['2ND_USE'] = "NONE"
zone['2ND_USE_R'] = 0.0
zone['3RD_USE'] = "NONE"
zone['3RD_USE_R'] = 0.0
dataframe_to_dbf(zone[COLUMNS_ZONE_TYPOLOGY], locator.get_building_typology())


def create_new_scenario(locator, config):
# Local variables
zone_geometry_path = config.create_new_scenario.zone
Expand All @@ -53,10 +77,7 @@ def create_new_scenario(locator, config):
if terrain_path == '':
print("there is no terrain file, run pour datamanagement tools later on for this please")
else:
terrain = raster_to_WSG_and_UTM(terrain_path, lat, lon)
driver = gdal.GetDriverByName('GTiff')
verify_input_terrain(terrain)
driver.CreateCopy(locator.get_terrain(), terrain)
copy_terrain(terrain_path, locator, lat, lon)

# now create the surroundings file if it does not exist
if surroundings_geometry_path == '':
Expand All @@ -79,24 +100,9 @@ def create_new_scenario(locator, config):
## create occupancy file and year file
if typology_path == '':
print("there is no typology file, we proceed to create it based on the geometry of your zone")
zone = Gdf.from_file(zone_geometry_path).drop('geometry', axis=1)
zone['STANDARD'] = 'STANDARD1'
zone['YEAR'] = 2020
zone['1ST_USE'] = 'MULTI_RES'
zone['1ST_USE_R'] = 1.0
zone['2ND_USE'] = "NONE"
zone['2ND_USE_R'] = 0.0
zone['3RD_USE'] = "NONE"
zone['3RD_USE_R'] = 0.0
dataframe_to_dbf(zone[COLUMNS_ZONE_TYPOLOGY], locator.get_building_typology())
generate_default_typology(zone, locator)
else:
# import file
occupancy_file = dbf_to_dataframe(typology_path)
occupancy_file_test = occupancy_file[COLUMNS_ZONE_TYPOLOGY]
# verify if input file is correct for CEA, if not an exception will be released
verify_input_typology(occupancy_file_test)
# create new file
copyfile(typology_path, locator.get_building_typology())
copy_typology(typology_path, locator)

# add other folders by calling the locator
locator.get_input_network_folder("DH", "")
Expand Down
5 changes: 4 additions & 1 deletion cea/datamanagement/surroundings_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
__email__ = "[email protected]"
__status__ = "Production"

def generate_empty_surroundings(crs) -> gdf:
return gdf(columns=["Name", "height_ag", "floors_ag"], geometry=[], crs=crs)


def calc_surrounding_area(zone_gdf, buffer_m):
"""
Expand Down Expand Up @@ -189,7 +192,7 @@ def geometry_extractor_osm(locator, config):
if not surroundings.shape[0] > 0:
print('No buildings were found within range based on buffer parameter.')
# Create an empty surroundings file
result = gdf(columns=["Name", "height_ag", "floors_ag"], geometry=[], crs=surroundings.crs)
result = generate_empty_surroundings(surroundings.crs)
else:
# clean attributes of height, name and number of floors
result = clean_attributes(surroundings, buildings_height, buildings_floors, key="CEA")
Expand Down
2 changes: 1 addition & 1 deletion cea/default.config
Original file line number Diff line number Diff line change
Expand Up @@ -1455,7 +1455,7 @@ filename.type = StringParameter
filename.help = Name to use for polygon shapefile outputs

[server]
project-root = {general:project}/..
project-root =
project-root.type = PathParameter
project-root.help = Path to the root of all projects, assuming they are all stored in one location

Expand Down
46 changes: 20 additions & 26 deletions cea/interfaces/dashboard/api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,23 @@
from flask import Blueprint
from flask_restx import Api
from .tools import api as tools
from .project import api as project
from .inputs import api as inputs
from .dashboard import api as dashboard
from .glossary import api as glossary
from .databases import api as databases
from .contents import api as contents
from fastapi import APIRouter

blueprint = Blueprint('api', __name__, url_prefix='/api')
api = Api(blueprint)
import cea.interfaces.dashboard.api.inputs as inputs
import cea.interfaces.dashboard.api.contents as contents
import cea.interfaces.dashboard.api.dashboards as dashboards
import cea.interfaces.dashboard.api.databases as databases
import cea.interfaces.dashboard.api.glossary as glossary
import cea.interfaces.dashboard.api.project as project
import cea.interfaces.dashboard.api.tools as tools
import cea.interfaces.dashboard.api.weather as weather
import cea.interfaces.dashboard.api.geometry as geometry

api.add_namespace(tools, path='/tools')
api.add_namespace(project, path='/project')
api.add_namespace(inputs, path='/inputs')
api.add_namespace(inputs, path='/inputs')
api.add_namespace(dashboard, path='/dashboards')
api.add_namespace(glossary, path='/glossary')
api.add_namespace(databases, path='/databases')
api.add_namespace(contents, path='/contents')
router = APIRouter()


@api.errorhandler
def default_error_handler(error):
"""Default error handler"""
import traceback
trace = traceback.format_exc()
return {'message': str(error), 'trace': trace}, 500
router.include_router(inputs.router, prefix="/inputs")
router.include_router(contents.router, prefix="/contents")
router.include_router(dashboards.router, prefix="/dashboards")
router.include_router(databases.router, prefix="/databases")
router.include_router(glossary.router, prefix="/glossary")
router.include_router(project.router, prefix="/project")
router.include_router(tools.router, prefix="/tools")
router.include_router(weather.router, prefix="/weather")
router.include_router(geometry.router, prefix="/geometry")
72 changes: 35 additions & 37 deletions cea/interfaces/dashboard/api/contents.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,21 @@
import os.path
from dataclasses import dataclass, asdict
from enum import Enum
from pathlib import Path
from typing import Optional, List

from flask import current_app
from flask_restx import Namespace, Resource
from fastapi import APIRouter, HTTPException, status

api = Namespace('Contents', description='Local path file contents')
from cea.interfaces.dashboard.dependencies import CEAConfig
from cea.interfaces.dashboard.utils import secure_path, InvalidPathError

router = APIRouter()


class ContentType(Enum):
directory = 'directory'
file = 'file'


contents_parser = api.parser()
contents_parser.add_argument('type', type=ContentType, required=True, location='args')
contents_parser.add_argument('show_hidden', type=bool, default=False, location='args')
contents_parser.add_argument('root', type=str, default=None, location='args')


class ContentPathNotFound(Exception):
pass

Expand Down Expand Up @@ -68,7 +63,7 @@ def get_content_info(root_path: str, content_path: str, content_type: ContentTyp
for item in os.listdir(full_path) if not item.startswith(".") or show_hidden
]
contents = [get_content_info(root_path, os.path.join(content_path, _path).replace("\\", "/"), _type,
depth - 1, show_hidden)
depth - 1, show_hidden)
for _path, _type in _contents]

size = None
Expand All @@ -85,29 +80,32 @@ def get_content_info(root_path: str, content_path: str, content_type: ContentTyp
)


@api.route('/', defaults={'content_path': ''})
@api.route('/<path:content_path>')
@api.expect(contents_parser)
class Contents(Resource):
def get(self, content_path: str):
"""
Get information of the content path provided
"""
args = contents_parser.parse_args()
content_type: ContentType = args["type"]
show_hidden: bool = args["show_hidden"]
root: Path = args["root"]

if root is None:
config = current_app.cea_config
root_path = config.server.project_root
else:
root_path = root

try:
content_info = get_content_info(root_path, content_path, content_type, show_hidden=show_hidden)
return content_info.as_dict()
except ContentPathNotFound:
return {"message": f"Path `{content_path}` does not exist"}, 404
except ContentTypeInvalid:
return {"message": f"Path `{content_path}` is not of type `{content_type.value}`"}, 400
@router.get('/')
@router.get('/{content_path}')
async def get_contents(config: CEAConfig, type: ContentType, root: str,
content_path: str = "", show_hidden: bool = False):
"""
Get information of the content path provided
"""
content_type = type

if root is None:
root_path = config.server.project_root
else:
root_path = root

try:
# Check path first
secure_path(os.path.join(root_path, content_path))
content_info = get_content_info(root_path, content_path, content_type, show_hidden=show_hidden)
return content_info.as_dict()
except (ContentPathNotFound, InvalidPathError):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Path `{content_path}` does not exist",
)
except ContentTypeInvalid:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Path `{content_path}` is not of type `{content_type.value}`",
)
Loading

0 comments on commit 54f6082

Please sign in to comment.