diff --git a/PHX/PHPP/phpp_localization/EN_10_3.json b/PHX/PHPP/phpp_localization/EN_10_3.json index 320c8e2..0068505 100644 --- a/PHX/PHPP/phpp_localization/EN_10_3.json +++ b/PHX/PHPP/phpp_localization/EN_10_3.json @@ -245,6 +245,18 @@ "unit": "C" } } + }, + "named_ranges": { + "country": "Klima_Region", + "region": "Klima_Region2", + "data_set": "Klima_Standort" + }, + "defined_ranges": { + "climate_zone": "D13", + "weather_station_altitude": "D17", + "site_altitude": "D18", + "latitude": "F25", + "longitude": "H25" } }, "UVALUES": { diff --git a/PHX/PHPP/phpp_localization/EN_10_4A.json b/PHX/PHPP/phpp_localization/EN_10_4A.json index 8991a7b..a1aaa3e 100644 --- a/PHX/PHPP/phpp_localization/EN_10_4A.json +++ b/PHX/PHPP/phpp_localization/EN_10_4A.json @@ -245,6 +245,18 @@ "unit": "C" } } + }, + "named_ranges": { + "country": "Klima_Region", + "region": "Klima_Region2", + "data_set": "Klima_Standort" + }, + "defined_ranges": { + "climate_zone": "D13", + "weather_station_altitude": "D17", + "site_altitude": "D18", + "latitude": "F25", + "longitude": "H25" } }, "UVALUES": { diff --git a/PHX/PHPP/phpp_localization/EN_9_6A.json b/PHX/PHPP/phpp_localization/EN_9_6A.json index 0fc9a6e..eb9efb7 100644 --- a/PHX/PHPP/phpp_localization/EN_9_6A.json +++ b/PHX/PHPP/phpp_localization/EN_9_6A.json @@ -240,6 +240,18 @@ "unit": "C" } } + }, + "named_ranges": { + "country": "Klima_Region", + "region": "Klima_Region2", + "data_set": "Klima_Standort" + }, + "defined_ranges": { + "climate_zone": "D13", + "weather_station_altitude": "D17", + "site_altitude": "D18", + "latitude": "F23", + "longitude": "H23" } }, "UVALUES": { diff --git a/PHX/PHPP/phpp_localization/EN_9_7IP.json b/PHX/PHPP/phpp_localization/EN_9_7IP.json index ce670a9..3632a48 100644 --- a/PHX/PHPP/phpp_localization/EN_9_7IP.json +++ b/PHX/PHPP/phpp_localization/EN_9_7IP.json @@ -240,6 +240,18 @@ "unit": "F" } } + }, + "named_ranges": { + "country": "Klima_Region", + "region": "Klima_Region2", + "data_set": "Klima_Standort" + }, + "defined_ranges": { + "climate_zone": "D13", + "weather_station_altitude": "D17", + "site_altitude": "D18", + "latitude": "F23", + "longitude": "H23" } }, "UVALUES": { diff --git a/PHX/PHPP/phpp_localization/shape_model.py b/PHX/PHPP/phpp_localization/shape_model.py index 262f3e7..d820600 100644 --- a/PHX/PHPP/phpp_localization/shape_model.py +++ b/PHX/PHPP/phpp_localization/shape_model.py @@ -100,6 +100,20 @@ class Variants(BaseModel): # ----------------------------------------------------------------------------- +class ClimateNamedRanges(BaseModel): + country: str + region: str + data_set: str + + +class ClimateDefinedRanges(BaseModel): + climate_zone: str + weather_station_altitude: str + site_altitude: str + latitude: str + longitude: str + + class ClimateActiveDatasetCol(BaseModel): country: str region: str @@ -164,6 +178,8 @@ class Climate(BaseModel): name: str active_dataset: ClimateActiveDataset ud_block: ClimateUDBlock + named_ranges: Optional[ClimateNamedRanges] + defined_ranges: Optional[ClimateDefinedRanges] # ----------------------------------------------------------------------------- diff --git a/PHX/PHPP/sheet_io/io_climate.py b/PHX/PHPP/sheet_io/io_climate.py index fd91a14..57c8af8 100644 --- a/PHX/PHPP/sheet_io/io_climate.py +++ b/PHX/PHPP/sheet_io/io_climate.py @@ -40,3 +40,48 @@ def write_active_climate( start_row = 9 for item in _active_climate.create_xl_items(self.shape.name, start_row): self.xl.write_xl_item(item) + + def read_active_country(self) -> str: + return str( + self.xl.get_single_data_item(self.shape.name, self.shape.named_ranges.country) + ) + + def read_active_region(self) -> str: + return str( + self.xl.get_single_data_item(self.shape.name, self.shape.named_ranges.region) + ) + + def read_active_data_set(self) -> str: + return str( + self.xl.get_single_data_item( + self.shape.name, self.shape.named_ranges.data_set + ) + ) + + def read_station_elevation(self) -> str: + return str( + self.xl.get_single_data_item( + self.shape.name, self.shape.defined_ranges.weather_station_altitude + ) + ) + + def read_site_elevation(self) -> str: + return str( + self.xl.get_single_data_item( + self.shape.name, self.shape.defined_ranges.site_altitude + ) + ) + + def read_latitude(self) -> float: + return float( + self.xl.get_single_data_item( + self.shape.name, self.shape.defined_ranges.latitude + ) + ) + + def read_longitude(self) -> float: + return float( + self.xl.get_single_data_item( + self.shape.name, self.shape.defined_ranges.longitude + ) + ) diff --git a/PHX/PHPP/sheet_io/io_verification.py b/PHX/PHPP/sheet_io/io_verification.py index c2a2b0b..f531343 100644 --- a/PHX/PHPP/sheet_io/io_verification.py +++ b/PHX/PHPP/sheet_io/io_verification.py @@ -4,6 +4,8 @@ """Controller Class for the PHPP Climate worksheet.""" from __future__ import annotations +from dataclasses import dataclass +from typing import List from PHX.xl import xl_app from PHX.PHPP.phpp_localization import shape_model @@ -47,6 +49,44 @@ def find_input_row(self, _row_start: int = 1, _row_end: int = 200) -> int: ) +@dataclass +class TeamMemberData: + """A Dataclass to store team-member information when read in from the PHPP.""" + + name: str + street_name: str + post_code: str + city: str + state: str + country: str + + @classmethod + def from_raw_excel_data(cls, _xl_data) -> TeamMemberData: + """Create a new TeamMemberData object from raw excel data read in from PHPP + + Arguments: + ---------- + * _xl_data: List[List[str]]: A list of lists containing the data read + in from PHPP. + Returns: + -------- + * (TeamMemberData): A new TeamMemberData object with the values from the + input data set. + """ + try: + return cls( + name=_xl_data[0][0], + street_name=_xl_data[1][0], + post_code=_xl_data[2][0], + city=_xl_data[2][1], + state=_xl_data[3][0], + country=_xl_data[3][2], + ) + except: + msg = f"Error reading in Team Member Data from the PHPP? Got:\n{_xl_data}" + raise Exception(msg) + + class Verification: def __init__(self, _xl: xl_app.XLConnection, _shape: shape_model.Verification): self.xl = _xl @@ -73,3 +113,33 @@ def write_item(self, _phpp_model_obj: verification_data.VerificationInput) -> No input_row = input_object.find_input_row() xl_item = _phpp_model_obj.create_xl_item(self.shape.name, input_row) self.xl.write_xl_item(xl_item) + + def read_architect(self) -> TeamMemberData: + """Return a TeamMemberData object with the architect info from PHPP.""" + data = self.xl.get_data(self.shape.name, "F18:H21") + return TeamMemberData.from_raw_excel_data(data) + + def read_energy_consultant(self) -> TeamMemberData: + """Return a TeamMemberData object with the consultant info from PHPP.""" + data = self.xl.get_data(self.shape.name, "F23:H26") + return TeamMemberData.from_raw_excel_data(data) + + def read_building(self) -> TeamMemberData: + """Return a TeamMemberData object with the Building address from PHPP.""" + data = self.xl.get_data(self.shape.name, "K5:M8") + return TeamMemberData.from_raw_excel_data(data) + + def read_site_owner(self) -> TeamMemberData: + """Return a TeamMemberData object with the owner info from PHPP.""" + data = self.xl.get_data(self.shape.name, "K13:M16") + return TeamMemberData.from_raw_excel_data(data) + + def read_mech_engineer(self) -> TeamMemberData: + data = self.xl.get_data(self.shape.name, "K18:M21") + """Return a TeamMemberData object with the ME info from PHPP.""" + return TeamMemberData.from_raw_excel_data(data) + + def read_ph_certification(self) -> TeamMemberData: + """Return a TeamMemberData object with the Certifier info from PHPP.""" + data = self.xl.get_data(self.shape.name, "K23:M26") + return TeamMemberData.from_raw_excel_data(data) diff --git a/PHX/xl/xl_app.py b/PHX/xl/xl_app.py index ab64f31..1c0ce75 100644 --- a/PHX/xl/xl_app.py +++ b/PHX/xl/xl_app.py @@ -6,14 +6,17 @@ from typing import Optional, List, Set, Callable, Any, Union, Dict from contextlib import contextmanager import os +import pathlib from PHX.xl import xl_data from PHX.xl.xl_typing import ( xl_Framework_Protocol, xl_Book_Protocol, + xl_Books_Protocol, xl_Sheets_Protocol, xl_Sheet_Protocol, xl_Range_Protocol, + xl_apps_Protocol, ) # ----------------------------------------------------------------------------- @@ -67,6 +70,12 @@ def __init__(self, _range: str): super().__init__(self.msg) +class NoSuchFileError(Exception): + def __init__(self, _file: pathlib.Path): + self.msg = f"\n\tError: Cannot locate the file \n{_file}?" + super().__init__(self.msg) + + # ----------------------------------------------------------------------------- @@ -81,42 +90,95 @@ def silent_print(_input: Any) -> None: class XLConnection: """An Excel connection Facade / Interface.""" - def __init__(self, xl_framework, output: Optional[Callable] = silent_print): + def __init__( + self, + xl_framework, + output: Callable[[str], Any] = silent_print, + xl_file_path: Optional[pathlib.Path] = None, + ) -> None: """Facade class for Excel Interop Arguments: ---------- * xl (xl_Framework_Protocol): The Excel framework to use to interact with XL. - * _output (Callable[[Any], None]): The output function to use. - Default is silent (no output), or provide 'print' for standard-out, etc. + + * output (Callable[[str], Any]): The output function to use. Default is silent + (no output), or provide the standard python 'print' for standard-out, etc. + + * xl_file_path (Optional[pathlib.Path]): The full path to an XL file to use. If none, + will default to the 'active' xl workbook. """ # -- Note: can not type-hint xl_framework in the Class argument line # -- cus' Python-3.7 doesn't have Protocols yet. It does see to work # -- when type-hinting the actual attribute though. self.xl: xl_Framework_Protocol = xl_framework - self._output = output - self.output(f"> connected to excel doc: {self.wb}") + self._output: Callable[[str], Any] = output + self.xl_file_path: Optional[pathlib.Path] = xl_file_path + + self._wb: Optional[xl_Book_Protocol] = None + self.output(f"> connected to excel doc: '{self.wb.fullname}'") @property - def sheets(self) -> xl_Sheets_Protocol: - return self.wb.sheets + def excel_running(self) -> bool: + """Returns True if Excel is currently running, False if not""" + return self.apps.count > 0 + + def start_excel_app(self) -> None: + """Starts Excel Application if it is not currently running.""" + if not self.excel_running: + self.apps.add() + + @property + def apps(self) -> xl_apps_Protocol: + """Return the right 'apps' object (os dependant).""" + if self.os_is_windows: + return self.xl.Apps + else: + return self.xl.apps + + @property + def books(self) -> xl_Books_Protocol: + """Return the right 'apps' object (os dependant).""" + if self.os_is_windows: + return self.xl.Books + else: + return self.xl.books @property def os_is_windows(self) -> bool: """Return True if the current OS is Windows. False if it is Mac/Linux""" return os.name == "nt" + def get_workbook(self) -> xl_Book_Protocol: + """Return the right Workbook, depending on the App state.""" + if not self.excel_running: + self.start_excel_app() + + # -- If a specific file path is provided, open that one + if self.xl_file_path: + if not self.xl_file_path.exists(): + raise NoSuchFileError(self.xl_file_path) + return self.books.open(self.xl_file_path) + + # -- If no books are open yet, create and return a new one + if self.books.count == 0: + return self.books.add() + + # -- Otherwise, just return the Active workbook + return self.books.active + @property def wb(self) -> xl_Book_Protocol: - try: - return self.xl.books.active - except: - raise NoActiveExcelRunningError + """Returns the Workbook of the active Excel Instance.""" + if self._wb is not None: # -- Cache + return self._wb + else: + self._wb = self.get_workbook() + return self._wb def autofit_columns(self, _sheet_name: str) -> None: """Runs autofit on all the columns in a sheet.""" sht = self.get_sheet_by_name(_sheet_name) - # sht.activate() sht.autofit(axis="c") # by-columns def autofit_rows(self, _sheet_name: str) -> None: @@ -147,7 +209,9 @@ def clear_sheet_all(self, _sheet_name: str) -> None: """Clears the content and formatting of the whole sheet.""" self.get_sheet_by_name(_sheet_name).clear() - def create_new_worksheet(self, _sheet_name: str, before: Optional[str]=None, after: Optional[str]=None) -> None: + def create_new_worksheet( + self, _sheet_name: str, before: Optional[str] = None, after: Optional[str] = None + ) -> None: """Try and add a new Worksheet to the Workbook.""" try: self.wb.sheets.add(_sheet_name, before, after) @@ -224,7 +288,7 @@ def get_sheet_by_name(self, _sheet_name: Union[str, int]) -> xl_Sheet_Protocol: if str(_sheet_name).upper() not in self.get_worksheet_names(): msg = f"Error: Key '{_sheet_name}' was not found in the Workbook '{self.wb.name}' Sheets?" raise KeyError(msg) - + return self.wb.sheets[_sheet_name] def get_last_sheet(self) -> xl_Sheet_Protocol: @@ -494,7 +558,7 @@ def output(self, _input): * (None) """ try: - self._output(str(_input)) # type: ignore + self._output(str(_input)) except: # If _input=None, ignore... pass diff --git a/PHX/xl/xl_typing.py b/PHX/xl/xl_typing.py index f7a7dca..ba744d3 100644 --- a/PHX/xl/xl_typing.py +++ b/PHX/xl/xl_typing.py @@ -3,21 +3,12 @@ """XL-App Protocol Classes.""" -from typing import Optional, Dict, Tuple, Any +import pathlib +from typing import Optional, Dict, Tuple, Any, Callable from PHX.xl import xl_data -class xl_app_Protocol: - def __init__(self): - self.screen_updating: bool = True - self.display_alerts: bool = True - self.calculation: str = "automatic" - - def calculate(self) -> None: - return None - - class xl_Range_Font: def __init__(self): self.color: Optional[Tuple[int, ...]] @@ -106,6 +97,7 @@ def autofit(self, *args, **kwargs) -> None: def delete(self) -> None: ... + class xl_Sheets_Protocol: def __init__(self): self.storage: Dict[str, xl_Sheet_Protocol] = {} @@ -140,6 +132,7 @@ def __len__(self): class xl_Book_Protocol: def __init__(self): self.name: str = "" + self.fullname: str = "" self.app = xl_app_Protocol() self.sheets = xl_Sheets_Protocol() @@ -147,11 +140,39 @@ def __init__(self): class xl_Books_Protocol: def __init__(self): self.active = xl_Book_Protocol() + self.count: int = 1 + + def add(self) -> xl_Book_Protocol: + ... + + def open(self, _p: pathlib.Path) -> xl_Book_Protocol: + ... + + +class xl_apps_Protocol: + def __init__(self): + self.count: int = 1 + + def add(self) -> None: + ... + + +class xl_app_Protocol: + def __init__(self): + self.screen_updating: bool = True + self.display_alerts: bool = True + self.calculation: str = "automatic" + + def calculate(self) -> None: + return None class xl_Framework_Protocol: def __init__(self): - self.books = xl_Books_Protocol() + self.books: xl_Books_Protocol # Mac + self.Books: xl_Books_Protocol # PC + self.apps: xl_apps_Protocol # Mac + self.Apps: xl_apps_Protocol # PC def Range(self, *args, **kwargs) -> xl_Range_Protocol: return xl_Range_Protocol() diff --git a/_testing_to_PHPP.py b/_testing_to_PHPP.py index 747cb34..66e7feb 100644 --- a/_testing_to_PHPP.py +++ b/_testing_to_PHPP.py @@ -15,7 +15,7 @@ # --- Input file Path # ------------------------------------------------------------------------- SOURCE_FILE = pathlib.Path( - "/Users/em/Dropbox/bldgtyp-00/00_PH_Tools/PHX/sample/hbjson/HarmsDavis_2302328.hbjson" + "/Users/em/Dropbox/bldgtyp-00/00_PH_Tools/PHX/sample/hbjson/Pratt_Wellness_Ctr_230412.hbjson" ) if __name__ == "__main__": @@ -46,8 +46,8 @@ with phpp_conn.xl.in_silent_mode(): phpp_conn.xl.unprotect_all_sheets() - # phpp_conn.write_certification_config(phx_project) - # phpp_conn.write_climate_data(phx_project) + phpp_conn.write_certification_config(phx_project) + phpp_conn.write_climate_data(phx_project) phpp_conn.write_project_constructions(phx_project) phpp_conn.write_project_tfa(phx_project) phpp_conn.write_project_opaque_surfaces(phx_project) diff --git a/dev-requirements.txt b/dev-requirements.txt index 36dd109..a23567c 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,5 +1,5 @@ +black==23.1.0 coverage -black pytest pytest-cov rich diff --git a/tests/test_to_PHPP/test_xl_app.py b/tests/test_to_PHPP/test_xl_app.py index a1fa5bf..b8336bb 100644 --- a/tests/test_to_PHPP/test_xl_app.py +++ b/tests/test_to_PHPP/test_xl_app.py @@ -15,6 +15,7 @@ def __init__(self): "Sheet1": xl_typing.xl_Sheet_Protocol(), "Sheet2": xl_typing.xl_Sheet_Protocol(), } + self.apps = xl_typing.xl_apps_Protocol() # ----------------------------------------------------------------------------- @@ -39,9 +40,9 @@ def test_xl_app_get_WorkBook_success(): def test_xl_app_get_WorkBook_fail(): mock_xw = Mock_XL_Framework() - mock_xw.books = None # type: ignore #<--------- + mock_xw.books = None # type: ignore #<--------- This should cause the error - with pytest.raises(xl_app.NoActiveExcelRunningError): + with pytest.raises(Exception): app = xl_app.XLConnection(xl_framework=mock_xw)