From ea49300d77153d502f44f943cb411578d9a8feb8 Mon Sep 17 00:00:00 2001 From: Alex Chapin Date: Fri, 11 Aug 2023 13:41:18 -0400 Subject: [PATCH 01/15] Added ESPM functions to py-seed client --- pyseed/seed_client.py | 112 +++++++++++++++++++++++++++++++++++++ pyseed/seed_client_base.py | 2 + 2 files changed, 114 insertions(+) diff --git a/pyseed/seed_client.py b/pyseed/seed_client.py index 0f770fd..dc874b6 100644 --- a/pyseed/seed_client.py +++ b/pyseed/seed_client.py @@ -10,6 +10,8 @@ # Imports from Third Party Modules import json import logging +import openpyxl +import os import time from collections import Counter from datetime import date @@ -1148,6 +1150,116 @@ def check_meters_tab_exist(self, import_file_id: int) -> bool: ) # if the data is set to True, then return such return response + + def get_pm_report_template_names(self, pm_username: str, pm_password: str) -> dict: + """Download the PM report templates. + + Args: + pm_username (str): username for Energystar Portfolio Manager + pm_password (str): password for Energystar Portfolio Manager + + Sample return shown below. + Returns: dict: { + "status": "success", + "templates": [ + { + 'id': 4438244, + 'name': '179D Test', + 'date': '7/03/2023 1:09 PM', + 'timestamp': 1688404158086, + 'hasData': 1, + 'newReport': 0, + 'pending': 0, + 'errored': 0, + 'type': 0, + 'subtype': 4, + 'hasSiteEUIOrWaterUseNAMessages': False, + 'children': [], + 'hasChildrenRows': False, + 'countOfChildrenRows': 0, + 'z_seed_child_row': False, + 'display_name': '179D Test' + } + ], + } + """ + response = self.client.post( + endpoint="portfolio_manager_report_templates", + json={"username": pm_username, "password": pm_password}, + ) + # Return the report templates + return response + + def download_pm_report(self, pm_username: str, pm_password: str, pm_template: dict) -> dict: + """Download a PM report. + + Args: + pm_username (str): username for Energystar Portfolio Manager + pm_password (str): password for Energystar Portfolio Manager + pm_template (dict): the full template object dict returned from get_pm_report_template_names + + Sample return shown below. + Returns: + dict: { + "status": "success", + "properties": [{ + "properties_information_1": string, + "properties_information_2": integer, + [other keys....]: string + }] + "message": string # this includes error message if any + } + """ + response = self.client.post( + endpoint="portfolio_manager_report", + json={"username": pm_username, + "password": pm_password, + "template": pm_template}, + ) + + # Parse the dict response. + data = response + + # Get the "properties" key from the dictionary. + properties = data["properties"] + + # Create an XLSX workbook object. + workbook = openpyxl.Workbook() + + # Create a sheet object in the workbook. + sheet = workbook.active + + # Get the header row from the API response. + header_row = [] + for property in properties: + for key in property: + if key not in header_row: + header_row.append(key) + + # Write the header row to the sheet object. + sheet.append(header_row) + + # Loop over the list of dictionaries and write the data to the sheet object. + for property in properties: + row = [] + for key in header_row: + row.append(property[key]) + sheet.append(row) + + # Save the workbook object. + workbook.save("properties.xlsx") + + # Current directory + curdir = os.getcwd() + + # Filename + file_name = "properties.xlsx" + + # Define the datafile path + datafile_path = os.path.join(curdir, file_name) + + # Return the report templates + return datafile_path def import_files_reuse_inventory_file_for_meters(self, import_file_id: int) -> dict: """Reuse an import file to create all the meter entries. This method is used diff --git a/pyseed/seed_client_base.py b/pyseed/seed_client_base.py index 93b3b3b..e28d3d2 100644 --- a/pyseed/seed_client_base.py +++ b/pyseed/seed_client_base.py @@ -64,6 +64,8 @@ 'import_files_start_matching_pk': '/api/v3/import_files/PK/start_system_matching_and_geocoding/', 'import_files_check_meters_tab_exists_pk': '/api/v3/import_files/PK/check_meters_tab_exists/', 'org_column_mapping_import_file': 'api/v3/organizations/ORG_ID/column_mappings/', + 'portfolio_manager_report_templates': '/api/v3/portfolio_manager/template_list/', + 'portfolio_manager_report': '/api/v3/portfolio_manager/report/', # GETs with replaceable keys 'import_files_matching_results': '/api/v3/import_files/PK/matching_and_geocoding_results/', 'progress': '/api/v3/progress/PROGRESS_KEY/', From 06c137156693a70915c39d70f8aaf3fce2416642 Mon Sep 17 00:00:00 2001 From: Alex Chapin Date: Fri, 11 Aug 2023 15:12:04 -0400 Subject: [PATCH 02/15] Cleaned up the code and put the downloads into its own folder called "reports" --- pyseed/seed_client.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/pyseed/seed_client.py b/pyseed/seed_client.py index dc874b6..141ea21 100644 --- a/pyseed/seed_client.py +++ b/pyseed/seed_client.py @@ -1217,11 +1217,8 @@ def download_pm_report(self, pm_username: str, pm_password: str, pm_template: di "template": pm_template}, ) - # Parse the dict response. - data = response - # Get the "properties" key from the dictionary. - properties = data["properties"] + properties = response["properties"] # Create an XLSX workbook object. workbook = openpyxl.Workbook() @@ -1244,19 +1241,31 @@ def download_pm_report(self, pm_username: str, pm_password: str, pm_template: di row = [] for key in header_row: row.append(property[key]) - sheet.append(row) + sheet.append(row) + + # Report Template name + report_template_name = pm_template['name'] + + # Filename + file_name = f"{pm_username}_{report_template_name}.xlsx" + + # Folder name + folder_name = "reports" + + if not os.path.exists(folder_name): + os.mkdir(folder_name) + + # Set the file path. + file_path = os.path.join(folder_name, file_name) # Save the workbook object. - workbook.save("properties.xlsx") + workbook.save(file_path) # Current directory curdir = os.getcwd() - # Filename - file_name = "properties.xlsx" - # Define the datafile path - datafile_path = os.path.join(curdir, file_name) + datafile_path = os.path.join(curdir, file_path) # Return the report templates return datafile_path From dc0924fde2f32414dc19063df8e8877c4ff08e98 Mon Sep 17 00:00:00 2001 From: Alex Chapin Date: Fri, 11 Aug 2023 15:25:25 -0400 Subject: [PATCH 03/15] Added openpyxl to requirements --- requirements-test.txt | 1 + requirements.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/requirements-test.txt b/requirements-test.txt index 15cb708..d0c5bff 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -2,6 +2,7 @@ flake8==4.0.1 mock==4.0.3 mypy==0.910 +openpyxl==3.1.2 pre-commit==2.19.0 pytest==7.1.2 pytest-cov==3.0.0 diff --git a/requirements.txt b/requirements.txt index 5b3eaca..c276d53 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ requests>=2.28.0 typing==3.6.1 +openpyxl==3.1.2 \ No newline at end of file From 4ec0119de3795481644927f501162946512faed0 Mon Sep 17 00:00:00 2001 From: Alex Chapin Date: Fri, 11 Aug 2023 17:03:17 -0400 Subject: [PATCH 04/15] Fixing tests --- pyseed/seed_client.py | 4 ++-- requirements.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyseed/seed_client.py b/pyseed/seed_client.py index 141ea21..a88ae18 100644 --- a/pyseed/seed_client.py +++ b/pyseed/seed_client.py @@ -1150,7 +1150,7 @@ def check_meters_tab_exist(self, import_file_id: int) -> bool: ) # if the data is set to True, then return such return response - + def get_pm_report_template_names(self, pm_username: str, pm_password: str) -> dict: """Download the PM report templates. @@ -1190,7 +1190,7 @@ def get_pm_report_template_names(self, pm_username: str, pm_password: str) -> di # Return the report templates return response - def download_pm_report(self, pm_username: str, pm_password: str, pm_template: dict) -> dict: + def download_pm_report(self, pm_username: str, pm_password: str, pm_template: dict) -> str: """Download a PM report. Args: diff --git a/requirements.txt b/requirements.txt index c276d53..c50220a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ +openpyxl==3.1.2 requests>=2.28.0 typing==3.6.1 -openpyxl==3.1.2 \ No newline at end of file From ce34f13cc8c8e4160809825a7166df5921c6533e Mon Sep 17 00:00:00 2001 From: anchapin Date: Fri, 10 May 2024 16:18:06 -0400 Subject: [PATCH 05/15] First pass at adding test --- tests/test_seed_client.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/tests/test_seed_client.py b/tests/test_seed_client.py index 1bf2766..480cf41 100644 --- a/tests/test_seed_client.py +++ b/tests/test_seed_client.py @@ -9,6 +9,7 @@ import unittest from datetime import date from pathlib import Path +from unittest.mock import patch # Local Imports from pyseed.seed_client import SeedClient @@ -46,10 +47,17 @@ def setup_class(cls): "tests/data/test-seed-data-mappings.csv", ) + # Mock the API endpoint for get_pm_report_template_names + cls.mock_report_templates = patch( + "pyseed.seed_client.SeedClient.get_pm_report_template_names", + return_value=["Template 1", "Template 2"], + ) + cls.mock_report_templates.start() + @classmethod def teardown_class(cls): - # remove all of the test buildings? - pass + # Stop the mock + cls.mock_report_templates.stop() def test_seed_orgs(self): orgs = self.seed_client.get_organizations() @@ -66,6 +74,13 @@ def test_seed_buildings(self): # ESPM test creates a building now too, assert building count is 10 or 11? assert len(buildings) == 10 + def test_get_pm_report_template_names(self): + template_names = self.seed_client.get_pm_report_template_names() + assert isinstance(template_names, list) + assert len(template_names) >= 2 + assert "Template 1" in template_names + assert "Template 2" in template_names + def test_search_buildings(self): # set cycle self.seed_client.get_cycle_by_name('pyseed-api-test', set_cycle_id=True) From 2c5ebd238dce6962b7dc803a2386f38d215dba0d Mon Sep 17 00:00:00 2001 From: anchapin Date: Tue, 28 May 2024 13:31:12 -0400 Subject: [PATCH 06/15] First draft of finishing the test --- tests/test_seed_client.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/tests/test_seed_client.py b/tests/test_seed_client.py index 480cf41..355292b 100644 --- a/tests/test_seed_client.py +++ b/tests/test_seed_client.py @@ -5,17 +5,17 @@ # Imports from Third Party Modules import os -import pytest +# import pytest import unittest from datetime import date from pathlib import Path from unittest.mock import patch +from unittest import skipIf # Local Imports from pyseed.seed_client import SeedClient - -@pytest.mark.integration +# @pytest.mark.integration class SeedClientTest(unittest.TestCase): @classmethod def setup_class(cls): @@ -75,11 +75,17 @@ def test_seed_buildings(self): assert len(buildings) == 10 def test_get_pm_report_template_names(self): - template_names = self.seed_client.get_pm_report_template_names() + pm_un = os.environ.get('SEED_PM_UN', False) + pm_pw = os.environ.get('SEED_PM_PW', False) + if not pm_un or not pm_pw: + self.fail(f"Somehow PM test was initiated without {pm_un} or {pm_pw} in the environment") + template_names = self.seed_client.get_pm_report_template_names(pm_un, pm_pw) assert isinstance(template_names, list) - assert len(template_names) >= 2 - assert "Template 1" in template_names - assert "Template 2" in template_names + assert len(template_names) >= 17 + assert "BPS Workflow 2021" in template_names + assert "AT Properties" in template_names + # check that the status is success + assert template_names["status"] == "success" def test_search_buildings(self): # set cycle From 5855ccec659752cb560d02fc774cc3137bc72f9a Mon Sep 17 00:00:00 2001 From: anchapin Date: Tue, 28 May 2024 15:58:04 -0400 Subject: [PATCH 07/15] Finished get report template names test --- tests/test_seed_client.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/tests/test_seed_client.py b/tests/test_seed_client.py index 355292b..f734ca2 100644 --- a/tests/test_seed_client.py +++ b/tests/test_seed_client.py @@ -47,17 +47,10 @@ def setup_class(cls): "tests/data/test-seed-data-mappings.csv", ) - # Mock the API endpoint for get_pm_report_template_names - cls.mock_report_templates = patch( - "pyseed.seed_client.SeedClient.get_pm_report_template_names", - return_value=["Template 1", "Template 2"], - ) - cls.mock_report_templates.start() - @classmethod def teardown_class(cls): - # Stop the mock - cls.mock_report_templates.stop() + # remove all of the test buildings? + pass def test_seed_orgs(self): orgs = self.seed_client.get_organizations() @@ -79,13 +72,18 @@ def test_get_pm_report_template_names(self): pm_pw = os.environ.get('SEED_PM_PW', False) if not pm_un or not pm_pw: self.fail(f"Somehow PM test was initiated without {pm_un} or {pm_pw} in the environment") - template_names = self.seed_client.get_pm_report_template_names(pm_un, pm_pw) + response = self.seed_client.get_pm_report_template_names(pm_un, pm_pw) + templates = response["templates"] + # loop through the array templates and make a list of all the name keys + template_names = [] + for template in templates: + template_names.append(template["name"]) assert isinstance(template_names, list) assert len(template_names) >= 17 assert "BPS Workflow 2021" in template_names assert "AT Properties" in template_names # check that the status is success - assert template_names["status"] == "success" + assert response["status"] == "success" def test_search_buildings(self): # set cycle From ee7b60ce8fb2a04d254366d4698b7ed84b3bdbcb Mon Sep 17 00:00:00 2001 From: anchapin Date: Tue, 28 May 2024 17:15:42 -0400 Subject: [PATCH 08/15] Started troubleshooting mypy errors --- pyseed/seed_client.py | 279 ++++++++++++++++++++---------------------- requirements-test.txt | 1 + 2 files changed, 136 insertions(+), 144 deletions(-) diff --git a/pyseed/seed_client.py b/pyseed/seed_client.py index 53d5e85..58f0843 100644 --- a/pyseed/seed_client.py +++ b/pyseed/seed_client.py @@ -32,14 +32,14 @@ class SeedClientWrapper(object): def __init__( self, organization_id: int, - connection_params: Optional[dict] = None, + connection_params: Optional[Dict] = None, connection_config_filepath: Optional[Path] = None, ) -> None: """wrapper around SEEDReadWriteClient. Args: organization_id (int): _description_ - connection_params (dict, optional): parameters to connect to SEED. Defaults to None. If using, then must contain the following: + connection_params (Dict, Optional): parameters to connect to SEED. Defaults to None. If using, then must contain the following: { "name": "not used - can be any string", "base_url": "http://127.0.0.1", @@ -48,7 +48,7 @@ def __init__( "port": 8000, "use_ssl": false } - connection_config_filepath (Path, optional): path to the parameters (JSON file). Defaults to None. + connection_config_filepath (Path, Optional): path to the parameters (JSON file). Defaults to None. Raises: Exception: SeedClientWrapper @@ -72,7 +72,7 @@ def __init__( self.client = SEEDReadWriteClient(organization_id, **self.payload) @classmethod - def read_connection_config_file(cls, filepath: Path) -> dict: + def read_connection_config_file(cls, filepath: Path) -> Dict: """Read in the connection config file and return the connection params. This file can be mostly created by calling the following from the SEED root directory: @@ -106,7 +106,7 @@ class SeedClient(SeedClientWrapper): def __init__( self, organization_id: int, - connection_params: dict = None, + connection_params: Dict = None, connection_config_filepath: Path = None, ) -> None: super().__init__(organization_id, connection_params, connection_config_filepath) @@ -119,7 +119,7 @@ def get_org_id(self) -> int: """Return the org ID that is set""" return self.client.org_id - def get_org_by_name(self, org_name: str, set_org_id: bool = False) -> dict: + def get_org_by_name(self, org_name: str, set_org_id: bool = False) -> Dict: """Set the current organization by name. Args: @@ -127,7 +127,7 @@ def get_org_by_name(self, org_name: str, set_org_id: bool = False) -> dict: set_org_id (bool): set the org_id on the object for later use. Defaults to None. Returns: - dict: { + Dict: { org data } """ @@ -140,11 +140,11 @@ def get_org_by_name(self, org_name: str, set_org_id: bool = False) -> dict: raise ValueError(f"Organization '{org_name}' not found") - def instance_information(self) -> dict: + def instance_information(self) -> Dict: """Return the instance information. Returns: - dict: instance information + Dict: instance information """ # http://localhost:8000/api/version/ # add in URL to the SEED instance @@ -155,10 +155,10 @@ def instance_information(self) -> dict: return info def get_organizations(self, brief: bool = True) -> Dict: - """Get a list organizations (that one is allowed to view) + """Get a List organizations (that one is allowed to view) Args: - brief (bool, optional): if True, then only return the organization id with some other basic info. Defaults to True. + brief (bool, Optional): if True, then only return the organization id with some other basic info. Defaults to True. Returns: Dict: [ { @@ -180,11 +180,11 @@ def get_organizations(self, brief: bool = True) -> Dict: ) return orgs - def get_buildings(self) -> List[dict]: + def get_buildings(self) -> List[Dict]: total_qry = self.client.list(endpoint="properties", data_name="pagination", per_page=100) # step through each page of the results - buildings: List[dict] = [] + buildings: List[Dict] = [] for i in range(1, total_qry['num_pages'] + 1): buildings = buildings + self.client.list( endpoint="properties", @@ -197,7 +197,7 @@ def get_buildings(self) -> List[dict]: return buildings - def get_property_view(self, property_view_id: int) -> dict: + def get_property_view(self, property_view_id: int) -> Dict: """Return a single property (view and state) by the property view id. It is recommended to use the more verbose version of `get_property` below. @@ -205,7 +205,7 @@ def get_property_view(self, property_view_id: int) -> dict: property_view_id (int): ID of the property to return. This is the ID that is in the URL http://SEED_URL/app/#/properties/{property_view_id} and resolves to {host}/api/v3/property_views/{property_view_id} Returns: - dict: { + Dict: { 'id': property_view_id, 'state': { 'extra_data': {}, @@ -218,14 +218,14 @@ def get_property_view(self, property_view_id: int) -> dict: property_view_id, endpoint="property_views", data_name="property_views" ) - def get_property(self, property_view_id: int) -> dict: + def get_property(self, property_view_id: int) -> Dict: """Return a single property by the property view id. Args: property__id (int): ID of the property to return. This is the ID that is in the URL http://SEED_URL/app/#/properties/{property_view_id} Returns: - dict: { + Dict: { 'state': { 'extra_data': {}, }, @@ -236,14 +236,14 @@ def get_property(self, property_view_id: int) -> dict: ... } """ - # NOTE: this seems to be the call that OEP uses (returns property and labels dictionaries) + # NOTE: this seems to be the call that OEP uses (returns property and labels Dictionaries) return self.client.get( property_view_id, endpoint="properties", data_name="properties" ) def search_buildings( self, identifier_filter: str = None, identifier_exact: str = None, cycle_id: int = None - ) -> dict: + ) -> Dict: if not cycle_id: cycle_id = self.cycle_id payload: Dict[str, Any] = { @@ -260,14 +260,14 @@ def search_buildings( ) return properties - def get_labels(self, filter_by_name: list = None) -> list: - """Get a list of all the labels in the organization. Filter by name if desired. + def get_labels(self, filter_by_name: List = None) -> List: + """Get a List of all the labels in the organization. Filter by name if desired. Args: - filter_by_name (list, optional): List of subset of labels to return. Defaults to None. + filter_by_name (List, Optional): List of subset of labels to return. Defaults to None. Returns: - list: [ + List: [ { 'id': 8, 'name': 'Call', @@ -290,16 +290,16 @@ def get_labels(self, filter_by_name: list = None) -> list: def get_or_create_label( self, label_name: str, color: str = "blue", show_in_list: bool = False - ) -> dict: + ) -> Dict: """_summary_ Args: label_name (str): Name of label. SEED enforces uniqueness of label names within an organization. - color (str, optional): Default color of the label. Must be from red, blue, light blue, green, white, orange, gray. 'blue' is the default. - show_in_list (bool, optional): If true, then the label is shown in the inventory list page as part of the column. Defaults to False. + color (str, Optional): Default color of the label. Must be from red, blue, light blue, green, white, orange, gray. 'blue' is the default. + show_in_list (bool, Optional): If true, then the label is shown in the inventory List page as part of the column. Defaults to False. Returns: - dict: { + Dict: { 'id': 87, 'name': 'label name', 'color': 'green', @@ -321,20 +321,20 @@ def update_label( new_label_name: str = None, new_color: str = None, new_show_in_list: bool = None, - ) -> dict: + ) -> Dict: """Update an existing label with the new_* fields. If the new_* fields are not provided, then the existing values are used. Args: label_name (str): Name of existing label. This is required and must match an existing label name for the organization - new_label_name (str, optional): New name of the label. Defaults to None. - new_color (str, optional): New color of the label. Must be from red, blue, light blue, green, white, orange, gray. Defaults to None - new_show_in_list (bool, optional): New boolean on whether to show the label in the inventory list page. Defaults to None. + new_label_name (str, Optional): New name of the label. Defaults to None. + new_color (str, Optional): New color of the label. Must be from red, blue, light blue, green, white, orange, gray. Defaults to None + new_show_in_list (bool, Optional): New boolean on whether to show the label in the inventory List page. Defaults to None. Raises: Exception: If the label does not exist, then throw an error. Returns: - dict: { + Dict: { 'id': 87, 'name': 'label name', 'color': 'green', @@ -342,7 +342,7 @@ def update_label( 'show_in_list': true } """ - # color (str, optional): Default color of the label. Must be from red, blue, light blue, green, white, orange, gray. 'blue' is the default. + # color (str, Optional): Default color of the label. Must be from red, blue, light blue, green, white, orange, gray. 'blue' is the default. # get the existing label label = self.get_labels(filter_by_name=[label_name]) if len(label) != 1: @@ -365,14 +365,14 @@ def update_label( current_label["id"], endpoint="labels", json=current_label ) - def delete_label(self, label_name: str) -> dict: + def delete_label(self, label_name: str) -> Dict: """Deletes an existing label. This method will look up the ID of the label to delete. Args: label_name (str): Name of the label to delete. Returns: - dict: _description_ + Dict: _description_ """ label = self.get_labels(filter_by_name=[label_name]) if len(label) != 1: @@ -381,21 +381,21 @@ def delete_label(self, label_name: str) -> dict: return self.client.delete(id, endpoint="labels") - def get_view_ids_with_label(self, label_names: Union[str, list] = []) -> list: + def get_view_ids_with_label(self, label_names: Union[str, List] = []) -> List: """Get the view IDs of the properties with a given label name(s). Can be a single - label or a list of labels. + label or a List of labels. Note that with labels, the data.selected field is for property view ids! SEED was updated in June 2022 to add in the label_names to filter on. Args: - label_names (str, list, optional): list of the labels to filter on. Defaults to []. + label_names (str, List, Optional): List of the labels to filter on. Defaults to []. Returns: - list: list of labels and the views they are associated with + List: List of labels and the views they are associated with """ - # if the label_names is not a list, then make it one - if not isinstance(label_names, list): + # if the label_names is not a List, then make it one + if not isinstance(label_names, List): label_names = [label_names] properties = self.client.post( @@ -407,24 +407,24 @@ def get_view_ids_with_label(self, label_names: Union[str, list] = []) -> list: def update_labels_of_buildings( self, - add_label_names: list, - remove_label_names: list, - building_ids: list, + add_label_names: List, + remove_label_names: List, + building_ids: List, inventory_type: str = "property", - ) -> dict: + ) -> Dict: """Add label names to the passed building ids. Args: - add_label_names (list): list of label names to add, will be converted to IDs - remove_label_names (list): list of label names to remove, will be converted to IDs - building_ids (list): list of building IDs (property_view_id) to add/remove labels - inventory_type (str, optional): taxlot or property inventory. Defaults to 'property'. + add_label_names (List): List of label names to add, will be converted to IDs + remove_label_names (List): List of label names to remove, will be converted to IDs + building_ids (List): List of building IDs (property_view_id) to add/remove labels + inventory_type (str, Optional): taxlot or property inventory. Defaults to 'property'. Raises: ValueError: if you don't pass the inventory type correction it will error out Returns: - dict: { + Dict: { 'status': 'success', 'num_updated': 3, 'labels': [ @@ -470,10 +470,10 @@ def update_labels_of_buildings( ) return result - def create_building(self, params: dict) -> list: + def create_building(self, params: Dict) -> List: """ Creates a building with unique ID (either pm_property_id or custom_id_1 for now) - Expects params to contain a state dictionary and a cycle id + Expects params to contain a state Dictionary and a cycle id Optionally pass in a cycle ID Returns the created property_view id @@ -502,10 +502,10 @@ def create_building(self, params: dict) -> list: results = self.client.post(endpoint="properties", json=params) return results - def update_building(self, id, params: dict) -> list: + def update_building(self, id, params: Dict) -> List: """ Updates a building's property_view - Expects id and params to contain a state dictionary + Expects id and params to contain a state Dictionary """ results = self.client.put(id, endpoint="properties", json=params) return results @@ -514,7 +514,7 @@ def get_cycles(self) -> list: """Return a list of all the cycles for the organization. Returns: - list: [ + List: [ { 'name': '2021 Calendar Year', 'start': '2020-12-31T23:53:00-08:00', @@ -538,7 +538,7 @@ def get_cycles(self) -> list: cycles = self.client.list(endpoint="cycles") return cycles["cycles"] - def create_cycle(self, cycle_name: str, start_date: date, end_date: date) -> dict: + def create_cycle(self, cycle_name: str, start_date: date, end_date: date) -> Dict: """Name of the cycle to create. If the cycle already exists, then it will create a new one. This is the default behavior of SEED. @@ -548,7 +548,7 @@ def create_cycle(self, cycle_name: str, start_date: date, end_date: date) -> dic end_date (date): MM/DD/YYYY of end data for cycle Returns: - dict: { + Dict: { 'name': 'new cycle 351cd7e1', 'start': '2021-01-01T00:00:00-08:00', 'end': '2022-01-01T00:00:00-08:00', @@ -581,7 +581,7 @@ def get_or_create_cycle( start_date: date, end_date: date, set_cycle_id: bool = False, - ) -> dict: + ) -> Dict: """Get or create a new cycle. If the cycle_name already exists, then it simply returns the existing cycle. However, if the cycle_name does not exist, then it will create a new cycle. Args: @@ -591,7 +591,7 @@ def get_or_create_cycle( set_cycle_id (str): Set the object's cycle_id to the resulting cycle that is returned (either existing or newly created) Returns: - dict: { + Dict: { 'name': 'Calendar Year 2022', 'start': '2021-01-01T00:00:00-08:00', 'end': '2022-01-01T00:00:00-08:00', @@ -633,7 +633,7 @@ def get_or_create_cycle( # to keep the response consistent add back in the status return selected - def get_cycle_by_name(self, cycle_name: str, set_cycle_id: bool = None) -> dict: + def get_cycle_by_name(self, cycle_name: str, set_cycle_id: bool = None) -> Dict: """Set the current cycle by name. Args: @@ -641,7 +641,7 @@ def get_cycle_by_name(self, cycle_name: str, set_cycle_id: bool = None) -> dict: set_cycle_id (bool): set the cycle_id on the object for later use. Defaults to None. Returns: - dict: { + Dict: { 'name': 'Calendar Year 2022', 'start': '2021-01-01T00:00:00-08:00', 'end': '2022-01-01T00:00:00-08:00', @@ -659,14 +659,14 @@ def get_cycle_by_name(self, cycle_name: str, set_cycle_id: bool = None) -> dict: raise ValueError(f"cycle '{cycle_name}' not found") - def delete_cycle(self, cycle_id: str) -> dict: + def delete_cycle(self, cycle_id: str) -> Dict: """Delete the cycle. This will only work if there are no properties or tax lots in the cycle Args: cycle_id (str): ID of the cycle to delete Returns: - dict: + Dict: """ result = self.client.delete(cycle_id, endpoint="cycles") progress_key = result.get("progress_key", None) @@ -676,7 +676,7 @@ def delete_cycle(self, cycle_id: str) -> dict: return result - def get_or_create_dataset(self, dataset_name: str) -> dict: + def get_or_create_dataset(self, dataset_name: str) -> Dict: """Get or create a SEED dataset which is used to hold data files that are uploaded to SEED. @@ -684,7 +684,7 @@ def get_or_create_dataset(self, dataset_name: str) -> dict: dataset_name (str): dataset name to get or create. Names can be duplicated! Returns: - dict: resulting dataset record + Dict: resulting dataset record """ post_data = {"name": dataset_name} @@ -694,7 +694,7 @@ def get_or_create_dataset(self, dataset_name: str) -> dict: logger.info(f"Dataset already created, returning {dataset['name']}") return dataset - # create a new dataset - this doesn't return the entire dict back + # create a new dataset - this doesn't return the entire Dict back # so after creating go and get the individual dataset dataset = self.client.post(endpoint="datasets", json=post_data) selected = {} @@ -706,7 +706,7 @@ def get_or_create_dataset(self, dataset_name: str) -> dict: def upload_datafile( self, dataset_id: int, data_file: str, upload_datatype: str - ) -> dict: + ) -> Dict: """Upload a datafile file Args: @@ -715,7 +715,7 @@ def upload_datafile( upload_datatype (str): Type of data in file ('Assessed Raw', 'Portfolio Raw') Returns: - dict: uploaded file record + Dict: uploaded file record { "import_file_id": 54, "success": true, @@ -737,14 +737,14 @@ def upload_datafile( files=files_params, ) - def track_progress_result(self, progress_key) -> dict: + def track_progress_result(self, progress_key) -> Dict: """Delays the sequence until progress is at 100 percent Args: progress_key (str): the key to track Returns: - dict: progress_result + Dict: progress_result { 'status': 'success', # 'not_started', 'in_progress', 'parsing', 'success', 'error' 'status_message': '', @@ -781,15 +781,15 @@ def track_progress_result(self, progress_key) -> dict: return progress_result - def get_column_mapping_profiles(self, profile_type: str = "All") -> dict: + def get_column_mapping_profiles(self, profile_type: str = "All") -> Dict: """get the list of column mapping profiles. If profile_type is provided then return the list of profiles of that type. Args: - profile_type (str, optional): Type of column mappings to return, can be 'Normal', 'BuildingSync Default'. Defaults to 'All', which includes both Normal and BuildingSync. + profile_type (str, Optional): Type of column mappings to return, can be 'Normal', 'BuildingSync Default'. Defaults to 'All', which includes both Normal and BuildingSync. Returns: - dict: column mapping profiles + Dict: column mapping profiles """ result = self.client.post(endpoint="column_mapping_profiles_filter") indices_to_remove = [] @@ -811,7 +811,7 @@ def get_column_mapping_profiles(self, profile_type: str = "All") -> dict: def get_column_mapping_profile( self, column_mapping_profile_name: str - ) -> Optional[dict]: + ) -> Optional[Dict]: """get a specific column mapping profile. Currently, filter does not take an argument by name, so return them all and find the one that matches the column_mapping_profile_name. @@ -820,7 +820,7 @@ def get_column_mapping_profile( column_mapping_profile_name (str): Name of column_mapping_profile to return Returns: - dict: single column mapping profile + Dict: single column mapping profile """ results = self.client.post(endpoint="column_mapping_profiles_filter") for item in results: @@ -831,8 +831,8 @@ def get_column_mapping_profile( return None def create_or_update_column_mapping_profile( - self, mapping_profile_name: str, mappings: list - ) -> dict: + self, mapping_profile_name: str, mappings: List + ) -> Dict: """Create or update an existing mapping profile from a list of mappings This only works for 'Normal' column mapping profiles, that is, it does not work for @@ -841,7 +841,7 @@ def create_or_update_column_mapping_profile( Args: mapping_profile_name (str): profile name - mappings (list): list of mappings in the form of + mappings (List): list of mappings in the form of [ { "from_field": "Address 1", @@ -859,7 +859,7 @@ def create_or_update_column_mapping_profile( ] Returns: - dict: { + Dict: { 'id': 1 'profile_type': 'Normal', 'name': 'Profile Name', @@ -893,7 +893,7 @@ def create_or_update_column_mapping_profile( def create_or_update_column_mapping_profile_from_file( self, mapping_profile_name: str, mapping_file: str - ) -> dict: + ) -> Dict: """creates or updates a mapping profile. The format of the mapping file is a CSV with the following format: Raw Columns, units, SEED Table, SEED Columns\n @@ -910,7 +910,7 @@ def create_or_update_column_mapping_profile_from_file( mapping_file (str): _description_ Returns: - dict: { + Dict: { 'id': 1 'profile_type': 'Normal', 'name': 'Profile Name', @@ -928,16 +928,16 @@ def create_or_update_column_mapping_profile_from_file( ) def set_import_file_column_mappings( - self, import_file_id: int, mappings: list - ) -> dict: + self, import_file_id: int, mappings: List + ) -> Dict: """Sets the column mappings onto the import file record. Args: import_file_id (int): ID of the import file of interest - mappings (list): list of column mappings in the form of the results of column mapping profiles + mappings (List): list of column mappings in the form of the results of column mapping profiles Returns: - dict: dict of status + Dict: Dict of status """ return self.client.post( "org_column_mapping_import_file", @@ -946,11 +946,11 @@ def set_import_file_column_mappings( json={"mappings": mappings}, ) - def get_columns(self) -> dict: + def get_columns(self) -> Dict: """get the list of columns. Returns: - dict: { + Dict: { "status": "success", "columns: [{...}] } @@ -958,7 +958,7 @@ def get_columns(self) -> dict: result = self.client.list(endpoint="columns") return result - def create_extra_data_column(self, column_name: str, display_name: str, inventory_type: str, column_description: str, data_type: str) -> dict: + def create_extra_data_column(self, column_name: str, display_name: str, inventory_type: str, column_description: str, data_type: str) -> Dict: """ create an extra data column. If column exists, skip Args: 'column_name': 'project_type', @@ -968,7 +968,7 @@ def create_extra_data_column(self, column_name: str, display_name: str, inventor 'data_type': 'string', Returns: - dict:{ + Dict:{ "status": "success", "column": { "id": 151, @@ -984,7 +984,7 @@ def create_extra_data_column(self, column_name: str, display_name: str, inventor extra_data_cols = [item for item in columns if item['is_extra_data']] # see if extra data column already exists (for now don't update it, just skip it) - res = list(filter(lambda extra_data_cols: extra_data_cols['column_name'] == column_name, extra_data_cols)) + res = List(filter(lambda extra_data_cols: extra_data_cols['column_name'] == column_name, extra_data_cols)) if res: # column already exists result = {"status": "noop", "message": "column already exists"} @@ -1002,7 +1002,7 @@ def create_extra_data_column(self, column_name: str, display_name: str, inventor return result - def create_extra_data_columns_from_file(self, columns_csv_filepath: str) -> list: + def create_extra_data_columns_from_file(self, columns_csv_filepath: str) -> List: """ create extra data columns from a csv file. if column exist, skip. Args: 'columns_csv_filepath': 'path/to/file' @@ -1012,7 +1012,7 @@ def create_extra_data_columns_from_file(self, columns_csv_filepath: str) -> list See example file at tests/data/test-seed-create-columns.csv Returns: - list:[{ + List:[{ "status": "success", "column": { "id": 151, @@ -1023,8 +1023,8 @@ def create_extra_data_columns_from_file(self, columns_csv_filepath: str) -> list """ # open file in read mode with open(columns_csv_filepath, 'r') as f: - dict_reader = DictReader(f) - columns = list(dict_reader) + Dict_reader = DictReader(f) + columns = List(Dict_reader) results = [] for col in columns: @@ -1033,7 +1033,7 @@ def create_extra_data_columns_from_file(self, columns_csv_filepath: str) -> list return results - def get_meters(self, property_id: int) -> list: + def get_meters(self, property_id: int) -> List: """Return the list of meters assigned to a property (the property view id). Note that meters are attached to the property (not the state nor the property view). @@ -1041,7 +1041,7 @@ def get_meters(self, property_id: int) -> list: property_id (int): property id to get the meters Returns: - dict: [ + Dict: [ { 'id': 584, 'type': 'Cost', @@ -1057,7 +1057,7 @@ def get_meters(self, property_id: int) -> list: url_args={"PK": property_id}) return meters - def get_meter(self, property_view_id: int, meter_type: str, source: str, source_id: str) -> Union[dict, None]: + def get_meter(self, property_view_id: int, meter_type: str, source: str, source_id: str) -> Union[Dict, None]: """get a meter for a property view. Args: @@ -1067,7 +1067,7 @@ def get_meter(self, property_view_id: int, meter_type: str, source: str, source_ source_id (str): Identifier, if GreenButton, then format is xpath like Returns: - dict: meter object + Dict: meter object """ # return all the meters for the property and see if the meter exists, if so, return it meters = self.get_meters(property_view_id) @@ -1087,7 +1087,7 @@ def get_or_create_meter(self, property_view_id: int, meter_type: str, source: st source_id (str): Identifier, if GreenButton, then format is xpath like Returns: - dict: meter object + Dict: meter object """ # return all the meters for the property and see if the meter exists, if so, return it meter = self.get_meter(property_view_id, meter_type, source, source_id) @@ -1107,7 +1107,7 @@ def get_or_create_meter(self, property_view_id: int, meter_type: str, source: st return meter - def delete_meter(self, property_view_id: int, meter_id: int) -> dict: + def delete_meter(self, property_view_id: int, meter_id: int) -> Dict: """Delete a meter from the property. Args: @@ -1115,22 +1115,22 @@ def delete_meter(self, property_view_id: int, meter_id: int) -> dict: meter_id (int): meter id Returns: - dict: status of the deletion + Dict: status of the deletion """ return self.client.delete( meter_id, endpoint='properties_meters', url_args={"PK": property_view_id} ) - def upsert_meter_readings_bulk(self, property_view_id: int, meter_id: int, data: list) -> dict: + def upsert_meter_readings_bulk(self, property_view_id: int, meter_id: int, data: List) -> Dict: """Upsert meter readings for a property's meter with the bulk method. Args: property_view_id (int): property view id meter_id (int): meter id - data (list): list of dictionaries of meter readings + data (List): list of dictionaries of meter readings Returns: - dict: list of all meter reading objects + Dict: list of all meter reading objects """ # get the meter data for the property readings = self.client.post( @@ -1138,13 +1138,13 @@ def upsert_meter_readings_bulk(self, property_view_id: int, meter_id: int, data: ) return readings - def get_meter_data(self, property_id, interval: str = 'Exact', excluded_meter_ids: list = []): + def get_meter_data(self, property_id, interval: str = 'Exact', excluded_meter_ids: List = []): """Return the meter data from the property. Args: property_id (_type_): property view id - interval (str, optional): How to aggregate the data, can be 'Exact', 'Month', or 'Year'. Defaults to 'Exact'. - excluded_meter_ids (list, optional): IDs to exclude. Defaults to []]. + interval (str, Optional): How to aggregate the data, can be 'Exact', 'Month', or 'Year'. Defaults to 'Exact'. + excluded_meter_ids (List, Optional): IDs to exclude. Defaults to []]. """ payload = { "interval": interval, @@ -1153,10 +1153,10 @@ def get_meter_data(self, property_id, interval: str = 'Exact', excluded_meter_id meter_data = self.client.post(endpoint='properties_meter_usage', url_args={"PK": property_id}, json=payload) return meter_data - def save_meter_data(self, property_id: int, meter_id: int, meter_data) -> dict: + def save_meter_data(self, property_id: int, meter_id: int, meter_data) -> Dict: pass - def start_save_data(self, import_file_id: int, multiple_cycle_upload: bool = False) -> dict: + def start_save_data(self, import_file_id: int, multiple_cycle_upload: bool = False) -> Dict: """start the background process to save the data file to the database. This is the state before the mapping. @@ -1165,7 +1165,7 @@ def start_save_data(self, import_file_id: int, multiple_cycle_upload: bool = Fal multiple_cycle_upload (bool): whether to use multiple cycle upload Returns: - dict: progress key + Dict: progress key { "status": "success", "progress_key": ":1:SEED:start_save_data:PROG:90", @@ -1179,7 +1179,7 @@ def start_save_data(self, import_file_id: int, multiple_cycle_upload: bool = Fal "multiple_cycle_upload": multiple_cycle_upload}, ) - def start_map_data(self, import_file_id: int) -> dict: + def start_map_data(self, import_file_id: int) -> Dict: """start the background process to save the data file to the database. This is the state before the mapping. @@ -1187,7 +1187,7 @@ def start_map_data(self, import_file_id: int) -> dict: import_file_id (int): id of the import file to save Returns: - dict: progress key + Dict: progress key { "status": "success", "progress_key": ":1:SEED:map_data:PROG:90", @@ -1200,7 +1200,7 @@ def start_map_data(self, import_file_id: int) -> dict: json={"remap": True}, ) - def start_system_matching_and_geocoding(self, import_file_id: int) -> dict: + def start_system_matching_and_geocoding(self, import_file_id: int) -> Dict: """start the background process save mappings and start system matching/geocoding. This is the state after the mapping. @@ -1208,7 +1208,7 @@ def start_system_matching_and_geocoding(self, import_file_id: int) -> dict: import_file_id (int): id of the import file to save Returns: - dict: progress key + Dict: progress key { "progress_data": { "status": "success", @@ -1240,14 +1240,14 @@ def start_system_matching_and_geocoding(self, import_file_id: int) -> dict: "import_files_start_matching_pk", url_args={"PK": import_file_id} ) - def get_matching_results(self, import_file_id: int) -> dict: + def get_matching_results(self, import_file_id: int) -> Dict: """matching results summary Args: import_file_id (int): ID of the import file Returns: - dict: { + Dict: { 'initial_incoming': 0, 'duplicates_against_existing': 0, 'duplicates_within_file': 0, @@ -1286,7 +1286,7 @@ def check_meters_tab_exist(self, import_file_id: int) -> bool: # if the data is set to True, then return such return response - def get_pm_report_template_names(self, pm_username: str, pm_password: str) -> dict: + def get_pm_report_template_names(self, pm_username: str, pm_password: str) -> Dict: """Download the PM report templates. Args: @@ -1294,7 +1294,7 @@ def get_pm_report_template_names(self, pm_username: str, pm_password: str) -> di pm_password (str): password for Energystar Portfolio Manager Sample return shown below. - Returns: dict: { + Returns: Dict: { "status": "success", "templates": [ { @@ -1325,25 +1325,16 @@ def get_pm_report_template_names(self, pm_username: str, pm_password: str) -> di # Return the report templates return response - def download_pm_report(self, pm_username: str, pm_password: str, pm_template: dict) -> str: + def download_pm_report(self, pm_username: str, pm_password: str, pm_template: Dict) -> str: """Download a PM report. Args: pm_username (str): username for Energystar Portfolio Manager pm_password (str): password for Energystar Portfolio Manager - pm_template (dict): the full template object dict returned from get_pm_report_template_names + pm_template (Dict): the full template object dict returned from get_pm_report_template_names Sample return shown below. - Returns: - dict: { - "status": "success", - "properties": [{ - "properties_information_1": string, - "properties_information_2": integer, - [other keys....]: string - }] - "message": string # this includes error message if any - } + Returns the path to the report template workbook file """ response = self.client.post( endpoint="portfolio_manager_report", @@ -1405,7 +1396,7 @@ def download_pm_report(self, pm_username: str, pm_password: str, pm_template: di # Return the report templates return datafile_path - def import_files_reuse_inventory_file_for_meters(self, import_file_id: int) -> dict: + def import_files_reuse_inventory_file_for_meters(self, import_file_id: int) -> Dict: """Reuse an import file to create all the meter entries. This method is used for ESPM related data files. The result will be another import_file ID for the meters that will then need to be "re-saved". Note that the returning import_file_id @@ -1415,7 +1406,7 @@ def import_files_reuse_inventory_file_for_meters(self, import_file_id: int) -> d import_file_id (int): ID of the import file to reuse. Returns: - dict: { + Dict: { "status": "success", "import_file_id": 16 } @@ -1434,7 +1425,7 @@ def upload_and_match_datafile( column_mappings_file: str, import_meters_if_exist: bool = False, **kwargs, - ) -> dict: + ) -> Dict: """Upload a file to the cycle_id that is defined in the constructor. This carries the upload of the file through the whole ingestion process (map, merge, pair, geocode). @@ -1447,7 +1438,7 @@ def upload_and_match_datafile( multiple_cycle_upload (bool): Whether to use multiple cycle upload. Defaults to False. Returns: - dict: { + Dict: { matching summary } """ @@ -1508,7 +1499,7 @@ def upload_and_match_datafile( return matching_results - def retrieve_at_building_and_update(self, audit_template_building_id: int, cycle_id: int, seed_id: int) -> dict: + def retrieve_at_building_and_update(self, audit_template_building_id: int, cycle_id: int, seed_id: int) -> Dict: """Connect to audit template and retrieve audit XML by building ID Args: @@ -1517,7 +1508,7 @@ def retrieve_at_building_and_update(self, audit_template_building_id: int, cycle seed_id (int): PropertyView ID in SEED Returns: - dict: Response from the SEED API + Dict: Response from the SEED API """ # api/v3/audit_template/pk/get_building_xml @@ -1548,7 +1539,7 @@ def retrieve_at_building_and_update(self, audit_template_building_id: int, cycle return response - def retrieve_at_submission_and_update(self, audit_template_submission_id: int, cycle_id: int, seed_id: int, report_format: str = 'pdf', filename: str = None) -> dict: + def retrieve_at_submission_and_update(self, audit_template_submission_id: int, cycle_id: int, seed_id: int, report_format: str = 'pdf', filename: str = None) -> Dict: """Connect to audit template and retrieve audit report by submission ID Args: @@ -1559,7 +1550,7 @@ def retrieve_at_submission_and_update(self, audit_template_submission_id: int, c filename (str): filename to use to upload to SEED Returns: - dict: Response from the SEED API + Dict: Response from the SEED API including the PDF file (if that format was requested) """ @@ -1613,7 +1604,7 @@ def retrieve_at_submission_and_update(self, audit_template_submission_id: int, c return response2 - def retrieve_portfolio_manager_property(self, username: str, password: str, pm_property_id: int, save_file_name: Path) -> dict: + def retrieve_portfolio_manager_property(self, username: str, password: str, pm_property_id: int, save_file_name: Path) -> Dict: """Connect to portfolio manager and download an individual properties data in Excel format Args: @@ -1642,7 +1633,7 @@ def retrieve_portfolio_manager_property(self, username: str, password: str, pm_p result['status'] = 'success' return result - def import_portfolio_manager_property(self, seed_id: int, cycle_id: int, mapping_profile_id: int, file_path: str) -> dict: + def import_portfolio_manager_property(self, seed_id: int, cycle_id: int, mapping_profile_id: int, file_path: str) -> Dict: """Import the downloaded xlsx file into SEED on a specific propertyID Args: seed_id (int): Property view ID to update with the ESPM file @@ -1668,7 +1659,7 @@ def import_portfolio_manager_property(self, seed_id: int, cycle_id: int, mapping return response - def retrieve_analyses_for_property(self, property_id: int) -> dict: + def retrieve_analyses_for_property(self, property_id: int) -> Dict: """Retrieve a list of all the analyses for a single property id. Since this is a property ID, then it is all the analyses for the all cycles. Note that this endpoint requires the passing of the organization id as a query parameter, otherwise it fails. @@ -1677,7 +1668,7 @@ def retrieve_analyses_for_property(self, property_id: int) -> dict: property_id (int): Property view id to return the list of analyses Returns: - dict: list of all the analyses that have run (or failed) for the property view + Dict: list of all the analyses that have run (or failed) for the property view """ return self.client.get( None, @@ -1687,7 +1678,7 @@ def retrieve_analyses_for_property(self, property_id: int) -> dict: include_org_id_query_param=True, ) - def retrieve_analysis_result(self, analysis_id: int, analysis_view_id: int) -> dict: + def retrieve_analysis_result(self, analysis_id: int, analysis_view_id: int) -> Dict: """Return the detailed JSON of a single analysis view. The endpoint in SEED is typically: https://dev1.seed-platform.org/app/#/analyses/274/runs/14693. @@ -1696,7 +1687,7 @@ def retrieve_analysis_result(self, analysis_id: int, analysis_view_id: int) -> d analysis_view_id (int): ID of the analysis view Returns: - dict: Return the detailed results of a single analysis view + Dict: Return the detailed results of a single analysis view """ return self.client.get( None, diff --git a/requirements-test.txt b/requirements-test.txt index d0c5bff..8d50798 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -3,6 +3,7 @@ flake8==4.0.1 mock==4.0.3 mypy==0.910 openpyxl==3.1.2 +openpyxl-stubs>=0.1.24 pre-commit==2.19.0 pytest==7.1.2 pytest-cov==3.0.0 From c2b81409c0105a261c733b1688db51a30c6c0bc2 Mon Sep 17 00:00:00 2001 From: anchapin Date: Wed, 29 May 2024 09:26:38 -0400 Subject: [PATCH 09/15] Fixed remaining conflict --- pyseed/seed_client.py | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/pyseed/seed_client.py b/pyseed/seed_client.py index 951be2f..5c9a12f 100644 --- a/pyseed/seed_client.py +++ b/pyseed/seed_client.py @@ -1539,32 +1539,7 @@ def retrieve_at_building_and_update(self, audit_template_building_id: int, cycle return response -<<<<<<< HEAD def retrieve_at_submission_and_update(self, audit_template_submission_id: int, cycle_id: int, seed_id: int, report_format: str = 'pdf', filename: str = None) -> Dict: -======= - def retrieve_at_submission_metadata(self, audit_template_submission_id: int) -> dict: - """Connect to audit template and retrieve audit report json (metadata only) by submission ID - - Args: - audit_template_submission_id (int): ID of the AT submission report (different than building ID) - - Returns: - dict: Response from the SEED API - """ - - # api/v3/audit_template/pk/get_submission - response = self.client.get( - None, - required_pk=False, - endpoint="audit_template_submission", - url_args={"PK": audit_template_submission_id}, - report_format='json' - ) - - return response - - def retrieve_at_submission_and_update(self, audit_template_submission_id: int, cycle_id: int, seed_id: int, report_format: str = 'pdf', filename: str = None) -> dict: ->>>>>>> develop """Connect to audit template and retrieve audit report by submission ID Args: From 706e18353174d4176c27de6138084e72b5ae4d9f Mon Sep 17 00:00:00 2001 From: anchapin Date: Wed, 29 May 2024 09:37:03 -0400 Subject: [PATCH 10/15] Fixed integration test errors --- pyseed/seed_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyseed/seed_client.py b/pyseed/seed_client.py index 5c9a12f..bf92b0c 100644 --- a/pyseed/seed_client.py +++ b/pyseed/seed_client.py @@ -984,7 +984,7 @@ def create_extra_data_column(self, column_name: str, display_name: str, inventor extra_data_cols = [item for item in columns if item['is_extra_data']] # see if extra data column already exists (for now don't update it, just skip it) - res = List(filter(lambda extra_data_cols: extra_data_cols['column_name'] == column_name, extra_data_cols)) + res = list(filter(lambda extra_data_cols: extra_data_cols['column_name'] == column_name, extra_data_cols)) if res: # column already exists result = {"status": "noop", "message": "column already exists"} @@ -1024,7 +1024,7 @@ def create_extra_data_columns_from_file(self, columns_csv_filepath: str) -> List # open file in read mode with open(columns_csv_filepath, 'r') as f: Dict_reader = DictReader(f) - columns = List(Dict_reader) + columns = list(Dict_reader) results = [] for col in columns: From 030f238078e2cd4fa52f9fc990266eb2d21f4cea Mon Sep 17 00:00:00 2001 From: Alex Swindler Date: Wed, 29 May 2024 09:29:52 -0600 Subject: [PATCH 11/15] Fix mypy errors --- mypy.ini | 2 ++ pyseed/seed_client.py | 17 ++++++++--------- requirements-test.txt | 4 +--- 3 files changed, 11 insertions(+), 12 deletions(-) create mode 100644 mypy.ini diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..976ba02 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,2 @@ +[mypy] +ignore_missing_imports = True diff --git a/pyseed/seed_client.py b/pyseed/seed_client.py index bf92b0c..5f29800 100644 --- a/pyseed/seed_client.py +++ b/pyseed/seed_client.py @@ -3,20 +3,17 @@ See also https://github.com/seed-platform/py-seed/main/LICENSE """ -# Imports from Standard Library -from typing import Any, Dict, List, Optional, Set, Tuple, Union - -# Imports from Third Party Modules import json import logging -import openpyxl import os import time from collections import Counter from csv import DictReader from datetime import date from pathlib import Path -from urllib.parse import _NetlocResultMixinStr +from typing import Any, Dict, List, Optional, Union + +from openpyxl import Workbook # Local Imports from pyseed.seed_client_base import SEEDReadWriteClient @@ -1347,7 +1344,7 @@ def download_pm_report(self, pm_username: str, pm_password: str, pm_template: Di properties = response["properties"] # Create an XLSX workbook object. - workbook = openpyxl.Workbook() + workbook = Workbook() # Create a sheet object in the workbook. sheet = workbook.active @@ -1360,14 +1357,16 @@ def download_pm_report(self, pm_username: str, pm_password: str, pm_template: Di header_row.append(key) # Write the header row to the sheet object. - sheet.append(header_row) + if sheet: + sheet.append(header_row) # Loop over the list of dictionaries and write the data to the sheet object. for property in properties: row = [] for key in header_row: row.append(property[key]) - sheet.append(row) + if sheet: + sheet.append(row) # Report Template name report_template_name = pm_template['name'] diff --git a/requirements-test.txt b/requirements-test.txt index 8d50798..39ec4a5 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,9 +1,7 @@ -r requirements.txt flake8==4.0.1 mock==4.0.3 -mypy==0.910 -openpyxl==3.1.2 -openpyxl-stubs>=0.1.24 +mypy==1.10.0 pre-commit==2.19.0 pytest==7.1.2 pytest-cov==3.0.0 From ea8cb90a51eb92e37491ac760f0771da7d2a33c4 Mon Sep 17 00:00:00 2001 From: Alex Swindler Date: Wed, 29 May 2024 11:09:21 -0600 Subject: [PATCH 12/15] Fixed mypy for Python 3.10 --- .github/workflows/ci.yml | 10 +++++----- cspell.json | 6 ++++++ pyseed/seed_client.py | 33 ++++++++++++++++++++------------- 3 files changed, 31 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8cd4af5..6b1d46c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,9 +17,9 @@ jobs: test_env: [python, precommit, mypy] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Display system info @@ -36,9 +36,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.9" - name: Install dependencies @@ -66,7 +66,7 @@ jobs: - name: Wait for web server uses: nev7n/wait_for_response@v1 with: - # Increase the timeout significanlty, the EEEJ census tract + # Increase the timeout significantly, the EEEJ census tract # migration take ~6 minutes to run along. url: "http://localhost:8000/" responseCode: 200 diff --git a/cspell.json b/cspell.json index 29e58b8..98107d9 100644 --- a/cspell.json +++ b/cspell.json @@ -11,14 +11,20 @@ "dname", "durl", "ECAM", + "EEEJ", "ESPM", "excpt", "geocoded", "greenbuildingregistry", + "jakejarvis", "JSONAPI", + "jwalton", "Munday", + "ndeloof", "officedocument", "openxmlformats", + "precommit", + "printenv", "pyseed", "pytest", "sdist", diff --git a/pyseed/seed_client.py b/pyseed/seed_client.py index 5f29800..93d9269 100644 --- a/pyseed/seed_client.py +++ b/pyseed/seed_client.py @@ -103,8 +103,8 @@ class SeedClient(SeedClientWrapper): def __init__( self, organization_id: int, - connection_params: Dict = None, - connection_config_filepath: Path = None, + connection_params: Optional[Dict] = None, + connection_config_filepath: Optional[Path] = None, ) -> None: super().__init__(organization_id, connection_params, connection_config_filepath) @@ -239,7 +239,10 @@ def get_property(self, property_view_id: int) -> Dict: ) def search_buildings( - self, identifier_filter: str = None, identifier_exact: str = None, cycle_id: int = None + self, + identifier_filter: Optional[str] = None, + identifier_exact: Optional[str] = None, + cycle_id: Optional[int] = None, ) -> Dict: if not cycle_id: cycle_id = self.cycle_id @@ -257,7 +260,7 @@ def search_buildings( ) return properties - def get_labels(self, filter_by_name: List = None) -> List: + def get_labels(self, filter_by_name: Optional[List] = None) -> List: """Get a List of all the labels in the organization. Filter by name if desired. Args: @@ -315,9 +318,9 @@ def get_or_create_label( def update_label( self, label_name: str, - new_label_name: str = None, - new_color: str = None, - new_show_in_list: bool = None, + new_label_name: Optional[str] = None, + new_color: Optional[str] = None, + new_show_in_list: Optional[bool] = None, ) -> Dict: """Update an existing label with the new_* fields. If the new_* fields are not provided, then the existing values are used. @@ -630,7 +633,7 @@ def get_or_create_cycle( # to keep the response consistent add back in the status return selected - def get_cycle_by_name(self, cycle_name: str, set_cycle_id: bool = None) -> Dict: + def get_cycle_by_name(self, cycle_name: str, set_cycle_id: Optional[bool] = None) -> Dict: """Set the current cycle by name. Args: @@ -1150,9 +1153,6 @@ def get_meter_data(self, property_id, interval: str = 'Exact', excluded_meter_id meter_data = self.client.post(endpoint='properties_meter_usage', url_args={"PK": property_id}, json=payload) return meter_data - def save_meter_data(self, property_id: int, meter_id: int, meter_data) -> Dict: - pass - def start_save_data(self, import_file_id: int, multiple_cycle_upload: bool = False) -> Dict: """start the background process to save the data file to the database. This is the state before the mapping. @@ -1538,14 +1538,21 @@ def retrieve_at_building_and_update(self, audit_template_building_id: int, cycle return response - def retrieve_at_submission_and_update(self, audit_template_submission_id: int, cycle_id: int, seed_id: int, report_format: str = 'pdf', filename: str = None) -> Dict: + def retrieve_at_submission_and_update( + self, + audit_template_submission_id: int, + cycle_id: int, + seed_id: int, + report_format: str = 'pdf', + filename: Optional[str] = None, + ) -> Dict: """Connect to audit template and retrieve audit report by submission ID Args: audit_template_submission_id (int): ID of the AT submission report (different than building ID) cycle_id (int): Cycle ID in SEED (needed for XML but not actually for PDF) seed_id (int): PropertyView ID in SEED - file_format (str): pdf or xml report, defaults to pdf + report_format (str): pdf or xml report, defaults to pdf filename (str): filename to use to upload to SEED Returns: From a60efbd1fb1ad21bcd90bc374a46cd19d8484e55 Mon Sep 17 00:00:00 2001 From: Alex Swindler Date: Wed, 29 May 2024 11:17:34 -0600 Subject: [PATCH 13/15] Precommit --- pyseed/seed_client.py | 20 +++++++++++--------- tests/test_seed_client.py | 4 +++- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/pyseed/seed_client.py b/pyseed/seed_client.py index 93d9269..965dd02 100644 --- a/pyseed/seed_client.py +++ b/pyseed/seed_client.py @@ -3,6 +3,10 @@ See also https://github.com/seed-platform/py-seed/main/LICENSE """ +# Imports from Standard Library +from typing import Any, Dict, List, Optional, Union + +# Imports from Third Party Modules import json import logging import os @@ -10,10 +14,8 @@ from collections import Counter from csv import DictReader from datetime import date -from pathlib import Path -from typing import Any, Dict, List, Optional, Union - from openpyxl import Workbook +from pathlib import Path # Local Imports from pyseed.seed_client_base import SEEDReadWriteClient @@ -1539,12 +1541,12 @@ def retrieve_at_building_and_update(self, audit_template_building_id: int, cycle return response def retrieve_at_submission_and_update( - self, - audit_template_submission_id: int, - cycle_id: int, - seed_id: int, - report_format: str = 'pdf', - filename: Optional[str] = None, + self, + audit_template_submission_id: int, + cycle_id: int, + seed_id: int, + report_format: str = 'pdf', + filename: Optional[str] = None, ) -> Dict: """Connect to audit template and retrieve audit report by submission ID diff --git a/tests/test_seed_client.py b/tests/test_seed_client.py index f734ca2..6a883e0 100644 --- a/tests/test_seed_client.py +++ b/tests/test_seed_client.py @@ -9,13 +9,15 @@ import unittest from datetime import date from pathlib import Path -from unittest.mock import patch from unittest import skipIf +from unittest.mock import patch # Local Imports from pyseed.seed_client import SeedClient # @pytest.mark.integration + + class SeedClientTest(unittest.TestCase): @classmethod def setup_class(cls): From 3234595a28261bf42c7e1cf389972d09c8fbb614 Mon Sep 17 00:00:00 2001 From: Alex Swindler Date: Wed, 29 May 2024 12:38:32 -0600 Subject: [PATCH 14/15] Fix unnecessary typing --- pyseed/seed_client.py | 283 ++++++++++++++++++------------------- pyseed/seed_client_base.py | 26 ++-- 2 files changed, 154 insertions(+), 155 deletions(-) diff --git a/pyseed/seed_client.py b/pyseed/seed_client.py index 965dd02..a26bfe5 100644 --- a/pyseed/seed_client.py +++ b/pyseed/seed_client.py @@ -4,7 +4,7 @@ """ # Imports from Standard Library -from typing import Any, Dict, List, Optional, Union +from typing import Any, Optional, Union # Imports from Third Party Modules import json @@ -31,14 +31,14 @@ class SeedClientWrapper(object): def __init__( self, organization_id: int, - connection_params: Optional[Dict] = None, + connection_params: Optional[dict] = None, connection_config_filepath: Optional[Path] = None, ) -> None: """wrapper around SEEDReadWriteClient. Args: organization_id (int): _description_ - connection_params (Dict, Optional): parameters to connect to SEED. Defaults to None. If using, then must contain the following: + connection_params (dict, optional): parameters to connect to SEED. Defaults to None. If using, then must contain the following: { "name": "not used - can be any string", "base_url": "http://127.0.0.1", @@ -47,7 +47,7 @@ def __init__( "port": 8000, "use_ssl": false } - connection_config_filepath (Path, Optional): path to the parameters (JSON file). Defaults to None. + connection_config_filepath (Path, optional): path to the parameters (JSON file). Defaults to None. Raises: Exception: SeedClientWrapper @@ -71,7 +71,7 @@ def __init__( self.client = SEEDReadWriteClient(organization_id, **self.payload) @classmethod - def read_connection_config_file(cls, filepath: Path) -> Dict: + def read_connection_config_file(cls, filepath: Path) -> dict: """Read in the connection config file and return the connection params. This file can be mostly created by calling the following from the SEED root directory: @@ -105,7 +105,7 @@ class SeedClient(SeedClientWrapper): def __init__( self, organization_id: int, - connection_params: Optional[Dict] = None, + connection_params: Optional[dict] = None, connection_config_filepath: Optional[Path] = None, ) -> None: super().__init__(organization_id, connection_params, connection_config_filepath) @@ -118,7 +118,7 @@ def get_org_id(self) -> int: """Return the org ID that is set""" return self.client.org_id - def get_org_by_name(self, org_name: str, set_org_id: bool = False) -> Dict: + def get_org_by_name(self, org_name: str, set_org_id: bool = False) -> dict: """Set the current organization by name. Args: @@ -126,7 +126,7 @@ def get_org_by_name(self, org_name: str, set_org_id: bool = False) -> Dict: set_org_id (bool): set the org_id on the object for later use. Defaults to None. Returns: - Dict: { + dict: { org data } """ @@ -139,11 +139,11 @@ def get_org_by_name(self, org_name: str, set_org_id: bool = False) -> Dict: raise ValueError(f"Organization '{org_name}' not found") - def instance_information(self) -> Dict: + def instance_information(self) -> dict: """Return the instance information. Returns: - Dict: instance information + dict: instance information """ # http://localhost:8000/api/version/ # add in URL to the SEED instance @@ -153,13 +153,13 @@ def instance_information(self) -> Dict: info["username"] = self.client.username return info - def get_organizations(self, brief: bool = True) -> Dict: - """Get a List organizations (that one is allowed to view) + def get_organizations(self, brief: bool = True) -> dict: + """Get a list organizations (that one is allowed to view) Args: - brief (bool, Optional): if True, then only return the organization id with some other basic info. Defaults to True. + brief (bool, optional): if True, then only return the organization id with some other basic info. Defaults to True. Returns: - Dict: [ + dict: [ { "name": "test-org", "org_id": 1, @@ -179,11 +179,11 @@ def get_organizations(self, brief: bool = True) -> Dict: ) return orgs - def get_buildings(self) -> List[Dict]: + def get_buildings(self) -> list[dict]: total_qry = self.client.list(endpoint="properties", data_name="pagination", per_page=100) # step through each page of the results - buildings: List[Dict] = [] + buildings: list[dict] = [] for i in range(1, total_qry['num_pages'] + 1): buildings = buildings + self.client.list( endpoint="properties", @@ -196,7 +196,7 @@ def get_buildings(self) -> List[Dict]: return buildings - def get_property_view(self, property_view_id: int) -> Dict: + def get_property_view(self, property_view_id: int) -> dict: """Return a single property (view and state) by the property view id. It is recommended to use the more verbose version of `get_property` below. @@ -204,7 +204,7 @@ def get_property_view(self, property_view_id: int) -> Dict: property_view_id (int): ID of the property to return. This is the ID that is in the URL http://SEED_URL/app/#/properties/{property_view_id} and resolves to {host}/api/v3/property_views/{property_view_id} Returns: - Dict: { + dict: { 'id': property_view_id, 'state': { 'extra_data': {}, @@ -217,14 +217,14 @@ def get_property_view(self, property_view_id: int) -> Dict: property_view_id, endpoint="property_views", data_name="property_views" ) - def get_property(self, property_view_id: int) -> Dict: + def get_property(self, property_view_id: int) -> dict: """Return a single property by the property view id. Args: - property__id (int): ID of the property to return. This is the ID that is in the URL http://SEED_URL/app/#/properties/{property_view_id} + property_view_id (int): ID of the property view with a property to return. This is the ID that is in the URL http://SEED_URL/app/#/properties/{property_view_id} Returns: - Dict: { + dict: { 'state': { 'extra_data': {}, }, @@ -235,7 +235,7 @@ def get_property(self, property_view_id: int) -> Dict: ... } """ - # NOTE: this seems to be the call that OEP uses (returns property and labels Dictionaries) + # NOTE: this seems to be the call that OEP uses (returns property and labels dictionaries) return self.client.get( property_view_id, endpoint="properties", data_name="properties" ) @@ -245,10 +245,10 @@ def search_buildings( identifier_filter: Optional[str] = None, identifier_exact: Optional[str] = None, cycle_id: Optional[int] = None, - ) -> Dict: + ) -> dict: if not cycle_id: cycle_id = self.cycle_id - payload: Dict[str, Any] = { + payload: dict[str, Any] = { "cycle": cycle_id, } if identifier_filter is not None: @@ -262,14 +262,14 @@ def search_buildings( ) return properties - def get_labels(self, filter_by_name: Optional[List] = None) -> List: - """Get a List of all the labels in the organization. Filter by name if desired. + def get_labels(self, filter_by_name: Optional[list] = None) -> list: + """Get a list of all the labels in the organization. Filter by name if desired. Args: - filter_by_name (List, Optional): List of subset of labels to return. Defaults to None. + filter_by_name (list, optional): list of subset of labels to return. Defaults to None. Returns: - List: [ + list: [ { 'id': 8, 'name': 'Call', @@ -292,16 +292,16 @@ def get_labels(self, filter_by_name: Optional[List] = None) -> List: def get_or_create_label( self, label_name: str, color: str = "blue", show_in_list: bool = False - ) -> Dict: + ) -> dict: """_summary_ Args: label_name (str): Name of label. SEED enforces uniqueness of label names within an organization. - color (str, Optional): Default color of the label. Must be from red, blue, light blue, green, white, orange, gray. 'blue' is the default. - show_in_list (bool, Optional): If true, then the label is shown in the inventory List page as part of the column. Defaults to False. + color (str, optional): Default color of the label. Must be from red, blue, light blue, green, white, orange, gray. 'blue' is the default. + show_in_list (bool, optional): If true, then the label is shown in the inventory list page as part of the column. Defaults to False. Returns: - Dict: { + dict: { 'id': 87, 'name': 'label name', 'color': 'green', @@ -323,20 +323,20 @@ def update_label( new_label_name: Optional[str] = None, new_color: Optional[str] = None, new_show_in_list: Optional[bool] = None, - ) -> Dict: + ) -> dict: """Update an existing label with the new_* fields. If the new_* fields are not provided, then the existing values are used. Args: label_name (str): Name of existing label. This is required and must match an existing label name for the organization - new_label_name (str, Optional): New name of the label. Defaults to None. - new_color (str, Optional): New color of the label. Must be from red, blue, light blue, green, white, orange, gray. Defaults to None - new_show_in_list (bool, Optional): New boolean on whether to show the label in the inventory List page. Defaults to None. + new_label_name (str, optional): New name of the label. Defaults to None. + new_color (str, optional): New color of the label. Must be from red, blue, light blue, green, white, orange, gray. Defaults to None + new_show_in_list (bool, optional): New boolean on whether to show the label in the inventory list page. Defaults to None. Raises: Exception: If the label does not exist, then throw an error. Returns: - Dict: { + dict: { 'id': 87, 'name': 'label name', 'color': 'green', @@ -344,7 +344,7 @@ def update_label( 'show_in_list': true } """ - # color (str, Optional): Default color of the label. Must be from red, blue, light blue, green, white, orange, gray. 'blue' is the default. + # color (str, optional): Default color of the label. Must be from red, blue, light blue, green, white, orange, gray. 'blue' is the default. # get the existing label label = self.get_labels(filter_by_name=[label_name]) if len(label) != 1: @@ -367,14 +367,14 @@ def update_label( current_label["id"], endpoint="labels", json=current_label ) - def delete_label(self, label_name: str) -> Dict: + def delete_label(self, label_name: str) -> dict: """Deletes an existing label. This method will look up the ID of the label to delete. Args: label_name (str): Name of the label to delete. Returns: - Dict: _description_ + dict: _description_ """ label = self.get_labels(filter_by_name=[label_name]) if len(label) != 1: @@ -383,21 +383,21 @@ def delete_label(self, label_name: str) -> Dict: return self.client.delete(id, endpoint="labels") - def get_view_ids_with_label(self, label_names: Union[str, List] = []) -> List: + def get_view_ids_with_label(self, label_names: Union[str, list] = []) -> list: """Get the view IDs of the properties with a given label name(s). Can be a single - label or a List of labels. + label or a list of labels. Note that with labels, the data.selected field is for property view ids! SEED was updated in June 2022 to add in the label_names to filter on. Args: - label_names (str, List, Optional): List of the labels to filter on. Defaults to []. + label_names (str, list, optional): list of the labels to filter on. Defaults to []. Returns: - List: List of labels and the views they are associated with + list: list of labels and the views they are associated with """ - # if the label_names is not a List, then make it one - if not isinstance(label_names, List): + # if the label_names is not a list, then make it one + if not isinstance(label_names, list): label_names = [label_names] properties = self.client.post( @@ -409,24 +409,24 @@ def get_view_ids_with_label(self, label_names: Union[str, List] = []) -> List: def update_labels_of_buildings( self, - add_label_names: List, - remove_label_names: List, - building_ids: List, + add_label_names: list, + remove_label_names: list, + building_ids: list, inventory_type: str = "property", - ) -> Dict: + ) -> dict: """Add label names to the passed building ids. Args: - add_label_names (List): List of label names to add, will be converted to IDs - remove_label_names (List): List of label names to remove, will be converted to IDs - building_ids (List): List of building IDs (property_view_id) to add/remove labels - inventory_type (str, Optional): taxlot or property inventory. Defaults to 'property'. + add_label_names (list): list of label names to add, will be converted to IDs + remove_label_names (list): list of label names to remove, will be converted to IDs + building_ids (list): list of building IDs (property_view_id) to add/remove labels + inventory_type (str, optional): taxlot or property inventory. Defaults to 'property'. Raises: ValueError: if you don't pass the inventory type correction it will error out Returns: - Dict: { + dict: { 'status': 'success', 'num_updated': 3, 'labels': [ @@ -472,10 +472,10 @@ def update_labels_of_buildings( ) return result - def create_building(self, params: Dict) -> List: + def create_building(self, params: dict) -> list: """ Creates a building with unique ID (either pm_property_id or custom_id_1 for now) - Expects params to contain a state Dictionary and a cycle id + Expects params to contain a state dictionary and a cycle id Optionally pass in a cycle ID Returns the created property_view id @@ -504,10 +504,10 @@ def create_building(self, params: Dict) -> List: results = self.client.post(endpoint="properties", json=params) return results - def update_building(self, id, params: Dict) -> List: + def update_building(self, id, params: dict) -> list: """ Updates a building's property_view - Expects id and params to contain a state Dictionary + Expects id and params to contain a state dictionary """ results = self.client.put(id, endpoint="properties", json=params) return results @@ -516,7 +516,7 @@ def get_cycles(self) -> list: """Return a list of all the cycles for the organization. Returns: - List: [ + list: [ { 'name': '2021 Calendar Year', 'start': '2020-12-31T23:53:00-08:00', @@ -540,7 +540,7 @@ def get_cycles(self) -> list: cycles = self.client.list(endpoint="cycles") return cycles["cycles"] - def create_cycle(self, cycle_name: str, start_date: date, end_date: date) -> Dict: + def create_cycle(self, cycle_name: str, start_date: date, end_date: date) -> dict: """Name of the cycle to create. If the cycle already exists, then it will create a new one. This is the default behavior of SEED. @@ -550,7 +550,7 @@ def create_cycle(self, cycle_name: str, start_date: date, end_date: date) -> Dic end_date (date): MM/DD/YYYY of end data for cycle Returns: - Dict: { + dict: { 'name': 'new cycle 351cd7e1', 'start': '2021-01-01T00:00:00-08:00', 'end': '2022-01-01T00:00:00-08:00', @@ -583,7 +583,7 @@ def get_or_create_cycle( start_date: date, end_date: date, set_cycle_id: bool = False, - ) -> Dict: + ) -> dict: """Get or create a new cycle. If the cycle_name already exists, then it simply returns the existing cycle. However, if the cycle_name does not exist, then it will create a new cycle. Args: @@ -593,7 +593,7 @@ def get_or_create_cycle( set_cycle_id (str): Set the object's cycle_id to the resulting cycle that is returned (either existing or newly created) Returns: - Dict: { + dict: { 'name': 'Calendar Year 2022', 'start': '2021-01-01T00:00:00-08:00', 'end': '2022-01-01T00:00:00-08:00', @@ -635,15 +635,15 @@ def get_or_create_cycle( # to keep the response consistent add back in the status return selected - def get_cycle_by_name(self, cycle_name: str, set_cycle_id: Optional[bool] = None) -> Dict: + def get_cycle_by_name(self, cycle_name: str, set_cycle_id: bool = False) -> dict: """Set the current cycle by name. Args: cycle_name (str): name of the cycle to set - set_cycle_id (bool): set the cycle_id on the object for later use. Defaults to None. + set_cycle_id (bool): set the cycle_id on the object for later use. Defaults to False. Returns: - Dict: { + dict: { 'name': 'Calendar Year 2022', 'start': '2021-01-01T00:00:00-08:00', 'end': '2022-01-01T00:00:00-08:00', @@ -661,14 +661,14 @@ def get_cycle_by_name(self, cycle_name: str, set_cycle_id: Optional[bool] = None raise ValueError(f"cycle '{cycle_name}' not found") - def delete_cycle(self, cycle_id: str) -> Dict: + def delete_cycle(self, cycle_id: str) -> dict: """Delete the cycle. This will only work if there are no properties or tax lots in the cycle Args: cycle_id (str): ID of the cycle to delete Returns: - Dict: + dict: """ result = self.client.delete(cycle_id, endpoint="cycles") progress_key = result.get("progress_key", None) @@ -678,7 +678,7 @@ def delete_cycle(self, cycle_id: str) -> Dict: return result - def get_or_create_dataset(self, dataset_name: str) -> Dict: + def get_or_create_dataset(self, dataset_name: str) -> dict: """Get or create a SEED dataset which is used to hold data files that are uploaded to SEED. @@ -686,7 +686,7 @@ def get_or_create_dataset(self, dataset_name: str) -> Dict: dataset_name (str): dataset name to get or create. Names can be duplicated! Returns: - Dict: resulting dataset record + dict: resulting dataset record """ post_data = {"name": dataset_name} @@ -696,7 +696,7 @@ def get_or_create_dataset(self, dataset_name: str) -> Dict: logger.info(f"Dataset already created, returning {dataset['name']}") return dataset - # create a new dataset - this doesn't return the entire Dict back + # create a new dataset - this doesn't return the entire dict back # so after creating go and get the individual dataset dataset = self.client.post(endpoint="datasets", json=post_data) selected = {} @@ -708,7 +708,7 @@ def get_or_create_dataset(self, dataset_name: str) -> Dict: def upload_datafile( self, dataset_id: int, data_file: str, upload_datatype: str - ) -> Dict: + ) -> dict: """Upload a datafile file Args: @@ -717,7 +717,7 @@ def upload_datafile( upload_datatype (str): Type of data in file ('Assessed Raw', 'Portfolio Raw') Returns: - Dict: uploaded file record + dict: uploaded file record { "import_file_id": 54, "success": true, @@ -739,14 +739,14 @@ def upload_datafile( files=files_params, ) - def track_progress_result(self, progress_key) -> Dict: + def track_progress_result(self, progress_key) -> dict: """Delays the sequence until progress is at 100 percent Args: progress_key (str): the key to track Returns: - Dict: progress_result + dict: progress_result { 'status': 'success', # 'not_started', 'in_progress', 'parsing', 'success', 'error' 'status_message': '', @@ -783,15 +783,15 @@ def track_progress_result(self, progress_key) -> Dict: return progress_result - def get_column_mapping_profiles(self, profile_type: str = "All") -> Dict: + def get_column_mapping_profiles(self, profile_type: str = "All") -> dict: """get the list of column mapping profiles. If profile_type is provided then return the list of profiles of that type. Args: - profile_type (str, Optional): Type of column mappings to return, can be 'Normal', 'BuildingSync Default'. Defaults to 'All', which includes both Normal and BuildingSync. + profile_type (str, optional): Type of column mappings to return, can be 'Normal', 'BuildingSync Default'. Defaults to 'All', which includes both Normal and BuildingSync. Returns: - Dict: column mapping profiles + dict: column mapping profiles """ result = self.client.post(endpoint="column_mapping_profiles_filter") indices_to_remove = [] @@ -813,7 +813,7 @@ def get_column_mapping_profiles(self, profile_type: str = "All") -> Dict: def get_column_mapping_profile( self, column_mapping_profile_name: str - ) -> Optional[Dict]: + ) -> Optional[dict]: """get a specific column mapping profile. Currently, filter does not take an argument by name, so return them all and find the one that matches the column_mapping_profile_name. @@ -822,7 +822,7 @@ def get_column_mapping_profile( column_mapping_profile_name (str): Name of column_mapping_profile to return Returns: - Dict: single column mapping profile + dict: single column mapping profile """ results = self.client.post(endpoint="column_mapping_profiles_filter") for item in results: @@ -833,8 +833,8 @@ def get_column_mapping_profile( return None def create_or_update_column_mapping_profile( - self, mapping_profile_name: str, mappings: List - ) -> Dict: + self, mapping_profile_name: str, mappings: list + ) -> dict: """Create or update an existing mapping profile from a list of mappings This only works for 'Normal' column mapping profiles, that is, it does not work for @@ -843,7 +843,7 @@ def create_or_update_column_mapping_profile( Args: mapping_profile_name (str): profile name - mappings (List): list of mappings in the form of + mappings (list): list of mappings in the form of [ { "from_field": "Address 1", @@ -861,7 +861,7 @@ def create_or_update_column_mapping_profile( ] Returns: - Dict: { + dict: { 'id': 1 'profile_type': 'Normal', 'name': 'Profile Name', @@ -895,7 +895,7 @@ def create_or_update_column_mapping_profile( def create_or_update_column_mapping_profile_from_file( self, mapping_profile_name: str, mapping_file: str - ) -> Dict: + ) -> dict: """creates or updates a mapping profile. The format of the mapping file is a CSV with the following format: Raw Columns, units, SEED Table, SEED Columns\n @@ -912,7 +912,7 @@ def create_or_update_column_mapping_profile_from_file( mapping_file (str): _description_ Returns: - Dict: { + dict: { 'id': 1 'profile_type': 'Normal', 'name': 'Profile Name', @@ -930,16 +930,16 @@ def create_or_update_column_mapping_profile_from_file( ) def set_import_file_column_mappings( - self, import_file_id: int, mappings: List - ) -> Dict: + self, import_file_id: int, mappings: list + ) -> dict: """Sets the column mappings onto the import file record. Args: import_file_id (int): ID of the import file of interest - mappings (List): list of column mappings in the form of the results of column mapping profiles + mappings (list): list of column mappings in the form of the results of column mapping profiles Returns: - Dict: Dict of status + dict: dict of status """ return self.client.post( "org_column_mapping_import_file", @@ -948,11 +948,11 @@ def set_import_file_column_mappings( json={"mappings": mappings}, ) - def get_columns(self) -> Dict: + def get_columns(self) -> dict: """get the list of columns. Returns: - Dict: { + dict: { "status": "success", "columns: [{...}] } @@ -960,7 +960,7 @@ def get_columns(self) -> Dict: result = self.client.list(endpoint="columns") return result - def create_extra_data_column(self, column_name: str, display_name: str, inventory_type: str, column_description: str, data_type: str) -> Dict: + def create_extra_data_column(self, column_name: str, display_name: str, inventory_type: str, column_description: str, data_type: str) -> dict: """ create an extra data column. If column exists, skip Args: 'column_name': 'project_type', @@ -970,7 +970,7 @@ def create_extra_data_column(self, column_name: str, display_name: str, inventor 'data_type': 'string', Returns: - Dict:{ + dict:{ "status": "success", "column": { "id": 151, @@ -1004,17 +1004,16 @@ def create_extra_data_column(self, column_name: str, display_name: str, inventor return result - def create_extra_data_columns_from_file(self, columns_csv_filepath: str) -> List: + def create_extra_data_columns_from_file(self, columns_csv_filepath: str) -> list: """ create extra data columns from a csv file. if column exist, skip. Args: 'columns_csv_filepath': 'path/to/file' file is expected to have headers: column_name, display_name, column_description, - inventory_type (Property or Taxlot), and data_type (SEED column data_types) See example file at tests/data/test-seed-create-columns.csv Returns: - List:[{ + list:[{ "status": "success", "column": { "id": 151, @@ -1025,8 +1024,8 @@ def create_extra_data_columns_from_file(self, columns_csv_filepath: str) -> List """ # open file in read mode with open(columns_csv_filepath, 'r') as f: - Dict_reader = DictReader(f) - columns = list(Dict_reader) + dict_reader = DictReader(f) + columns = list(dict_reader) results = [] for col in columns: @@ -1035,7 +1034,7 @@ def create_extra_data_columns_from_file(self, columns_csv_filepath: str) -> List return results - def get_meters(self, property_id: int) -> List: + def get_meters(self, property_id: int) -> list: """Return the list of meters assigned to a property (the property view id). Note that meters are attached to the property (not the state nor the property view). @@ -1043,7 +1042,7 @@ def get_meters(self, property_id: int) -> List: property_id (int): property id to get the meters Returns: - Dict: [ + dict: [ { 'id': 584, 'type': 'Cost', @@ -1059,7 +1058,7 @@ def get_meters(self, property_id: int) -> List: url_args={"PK": property_id}) return meters - def get_meter(self, property_view_id: int, meter_type: str, source: str, source_id: str) -> Union[Dict, None]: + def get_meter(self, property_view_id: int, meter_type: str, source: str, source_id: str) -> Union[dict, None]: """get a meter for a property view. Args: @@ -1069,7 +1068,7 @@ def get_meter(self, property_view_id: int, meter_type: str, source: str, source_ source_id (str): Identifier, if GreenButton, then format is xpath like Returns: - Dict: meter object + dict: meter object """ # return all the meters for the property and see if the meter exists, if so, return it meters = self.get_meters(property_view_id) @@ -1079,7 +1078,7 @@ def get_meter(self, property_view_id: int, meter_type: str, source: str, source_ else: return None - def get_or_create_meter(self, property_view_id: int, meter_type: str, source: str, source_id: str) -> Optional[Dict[Any, Any]]: + def get_or_create_meter(self, property_view_id: int, meter_type: str, source: str, source_id: str) -> Optional[dict[Any, Any]]: """get or create a meter for a property view. Args: @@ -1089,7 +1088,7 @@ def get_or_create_meter(self, property_view_id: int, meter_type: str, source: st source_id (str): Identifier, if GreenButton, then format is xpath like Returns: - Dict: meter object + dict: meter object """ # return all the meters for the property and see if the meter exists, if so, return it meter = self.get_meter(property_view_id, meter_type, source, source_id) @@ -1109,7 +1108,7 @@ def get_or_create_meter(self, property_view_id: int, meter_type: str, source: st return meter - def delete_meter(self, property_view_id: int, meter_id: int) -> Dict: + def delete_meter(self, property_view_id: int, meter_id: int) -> dict: """Delete a meter from the property. Args: @@ -1117,22 +1116,22 @@ def delete_meter(self, property_view_id: int, meter_id: int) -> Dict: meter_id (int): meter id Returns: - Dict: status of the deletion + dict: status of the deletion """ return self.client.delete( meter_id, endpoint='properties_meters', url_args={"PK": property_view_id} ) - def upsert_meter_readings_bulk(self, property_view_id: int, meter_id: int, data: List) -> Dict: + def upsert_meter_readings_bulk(self, property_view_id: int, meter_id: int, data: list) -> dict: """Upsert meter readings for a property's meter with the bulk method. Args: property_view_id (int): property view id meter_id (int): meter id - data (List): list of dictionaries of meter readings + data (list): list of dictionaries of meter readings Returns: - Dict: list of all meter reading objects + dict: list of all meter reading objects """ # get the meter data for the property readings = self.client.post( @@ -1140,13 +1139,13 @@ def upsert_meter_readings_bulk(self, property_view_id: int, meter_id: int, data: ) return readings - def get_meter_data(self, property_id, interval: str = 'Exact', excluded_meter_ids: List = []): + def get_meter_data(self, property_id, interval: str = 'Exact', excluded_meter_ids: list = []): """Return the meter data from the property. Args: property_id (_type_): property view id - interval (str, Optional): How to aggregate the data, can be 'Exact', 'Month', or 'Year'. Defaults to 'Exact'. - excluded_meter_ids (List, Optional): IDs to exclude. Defaults to []]. + interval (str, optional): How to aggregate the data, can be 'Exact', 'Month', or 'Year'. Defaults to 'Exact'. + excluded_meter_ids (list, optional): IDs to exclude. Defaults to []]. """ payload = { "interval": interval, @@ -1155,7 +1154,7 @@ def get_meter_data(self, property_id, interval: str = 'Exact', excluded_meter_id meter_data = self.client.post(endpoint='properties_meter_usage', url_args={"PK": property_id}, json=payload) return meter_data - def start_save_data(self, import_file_id: int, multiple_cycle_upload: bool = False) -> Dict: + def start_save_data(self, import_file_id: int, multiple_cycle_upload: bool = False) -> dict: """start the background process to save the data file to the database. This is the state before the mapping. @@ -1164,7 +1163,7 @@ def start_save_data(self, import_file_id: int, multiple_cycle_upload: bool = Fal multiple_cycle_upload (bool): whether to use multiple cycle upload Returns: - Dict: progress key + dict: progress key { "status": "success", "progress_key": ":1:SEED:start_save_data:PROG:90", @@ -1178,7 +1177,7 @@ def start_save_data(self, import_file_id: int, multiple_cycle_upload: bool = Fal "multiple_cycle_upload": multiple_cycle_upload}, ) - def start_map_data(self, import_file_id: int) -> Dict: + def start_map_data(self, import_file_id: int) -> dict: """start the background process to save the data file to the database. This is the state before the mapping. @@ -1186,7 +1185,7 @@ def start_map_data(self, import_file_id: int) -> Dict: import_file_id (int): id of the import file to save Returns: - Dict: progress key + dict: progress key { "status": "success", "progress_key": ":1:SEED:map_data:PROG:90", @@ -1199,7 +1198,7 @@ def start_map_data(self, import_file_id: int) -> Dict: json={"remap": True}, ) - def start_system_matching_and_geocoding(self, import_file_id: int) -> Dict: + def start_system_matching_and_geocoding(self, import_file_id: int) -> dict: """start the background process save mappings and start system matching/geocoding. This is the state after the mapping. @@ -1207,7 +1206,7 @@ def start_system_matching_and_geocoding(self, import_file_id: int) -> Dict: import_file_id (int): id of the import file to save Returns: - Dict: progress key + dict: progress key { "progress_data": { "status": "success", @@ -1239,14 +1238,14 @@ def start_system_matching_and_geocoding(self, import_file_id: int) -> Dict: "import_files_start_matching_pk", url_args={"PK": import_file_id} ) - def get_matching_results(self, import_file_id: int) -> Dict: + def get_matching_results(self, import_file_id: int) -> dict: """matching results summary Args: import_file_id (int): ID of the import file Returns: - Dict: { + dict: { 'initial_incoming': 0, 'duplicates_against_existing': 0, 'duplicates_within_file': 0, @@ -1285,7 +1284,7 @@ def check_meters_tab_exist(self, import_file_id: int) -> bool: # if the data is set to True, then return such return response - def get_pm_report_template_names(self, pm_username: str, pm_password: str) -> Dict: + def get_pm_report_template_names(self, pm_username: str, pm_password: str) -> dict: """Download the PM report templates. Args: @@ -1293,7 +1292,7 @@ def get_pm_report_template_names(self, pm_username: str, pm_password: str) -> Di pm_password (str): password for Energystar Portfolio Manager Sample return shown below. - Returns: Dict: { + Returns: dict: { "status": "success", "templates": [ { @@ -1324,13 +1323,13 @@ def get_pm_report_template_names(self, pm_username: str, pm_password: str) -> Di # Return the report templates return response - def download_pm_report(self, pm_username: str, pm_password: str, pm_template: Dict) -> str: + def download_pm_report(self, pm_username: str, pm_password: str, pm_template: dict) -> str: """Download a PM report. Args: pm_username (str): username for Energystar Portfolio Manager pm_password (str): password for Energystar Portfolio Manager - pm_template (Dict): the full template object dict returned from get_pm_report_template_names + pm_template (dict): the full template object dict returned from get_pm_report_template_names Sample return shown below. Returns the path to the report template workbook file @@ -1397,7 +1396,7 @@ def download_pm_report(self, pm_username: str, pm_password: str, pm_template: Di # Return the report templates return datafile_path - def import_files_reuse_inventory_file_for_meters(self, import_file_id: int) -> Dict: + def import_files_reuse_inventory_file_for_meters(self, import_file_id: int) -> dict: """Reuse an import file to create all the meter entries. This method is used for ESPM related data files. The result will be another import_file ID for the meters that will then need to be "re-saved". Note that the returning import_file_id @@ -1407,7 +1406,7 @@ def import_files_reuse_inventory_file_for_meters(self, import_file_id: int) -> D import_file_id (int): ID of the import file to reuse. Returns: - Dict: { + dict: { "status": "success", "import_file_id": 16 } @@ -1426,7 +1425,7 @@ def upload_and_match_datafile( column_mappings_file: str, import_meters_if_exist: bool = False, **kwargs, - ) -> Dict: + ) -> dict: """Upload a file to the cycle_id that is defined in the constructor. This carries the upload of the file through the whole ingestion process (map, merge, pair, geocode). @@ -1439,7 +1438,7 @@ def upload_and_match_datafile( multiple_cycle_upload (bool): Whether to use multiple cycle upload. Defaults to False. Returns: - Dict: { + dict: { matching summary } """ @@ -1500,7 +1499,7 @@ def upload_and_match_datafile( return matching_results - def retrieve_at_building_and_update(self, audit_template_building_id: int, cycle_id: int, seed_id: int) -> Dict: + def retrieve_at_building_and_update(self, audit_template_building_id: int, cycle_id: int, seed_id: int) -> dict: """Connect to audit template and retrieve audit XML by building ID Args: @@ -1509,7 +1508,7 @@ def retrieve_at_building_and_update(self, audit_template_building_id: int, cycle seed_id (int): PropertyView ID in SEED Returns: - Dict: Response from the SEED API + dict: Response from the SEED API """ # api/v3/audit_template/pk/get_building_xml @@ -1547,7 +1546,7 @@ def retrieve_at_submission_and_update( seed_id: int, report_format: str = 'pdf', filename: Optional[str] = None, - ) -> Dict: + ) -> dict: """Connect to audit template and retrieve audit report by submission ID Args: @@ -1558,7 +1557,7 @@ def retrieve_at_submission_and_update( filename (str): filename to use to upload to SEED Returns: - Dict: Response from the SEED API + dict: Response from the SEED API including the PDF file (if that format was requested) """ @@ -1616,7 +1615,7 @@ def retrieve_at_submission_and_update( return response2 - def retrieve_portfolio_manager_property(self, username: str, password: str, pm_property_id: int, save_file_name: Path) -> Dict: + def retrieve_portfolio_manager_property(self, username: str, password: str, pm_property_id: int, save_file_name: Path) -> dict: """Connect to portfolio manager and download an individual properties data in Excel format Args: @@ -1626,7 +1625,7 @@ def retrieve_portfolio_manager_property(self, username: str, password: str, pm_p save_file_name (Path): Location to save the file, preferably an absolute path Returns: - bool: Did the file download? + dict: Did the file download? """ if save_file_name.exists(): raise Exception(f"Save filename already exists, save to a new file name: {save_file_name}") @@ -1645,13 +1644,13 @@ def retrieve_portfolio_manager_property(self, username: str, password: str, pm_p result['status'] = 'success' return result - def import_portfolio_manager_property(self, seed_id: int, cycle_id: int, mapping_profile_id: int, file_path: str) -> Dict: + def import_portfolio_manager_property(self, seed_id: int, cycle_id: int, mapping_profile_id: int, file_path: str) -> dict: """Import the downloaded xlsx file into SEED on a specific propertyID Args: seed_id (int): Property view ID to update with the ESPM file cycle_id (int): Cycle ID mapping_profile_id (int): Column Mapping Profile ID - file: path to file downloaded from the retrieve_portfolio_manager_report method above + file_path: path to file downloaded from the retrieve_portfolio_manager_report method above ESPM file will have meter data that we want to handle (electricity and natural gas) in the 'Meter Entries' tab""" @@ -1671,7 +1670,7 @@ def import_portfolio_manager_property(self, seed_id: int, cycle_id: int, mapping return response - def retrieve_analyses_for_property(self, property_id: int) -> Dict: + def retrieve_analyses_for_property(self, property_id: int) -> dict: """Retrieve a list of all the analyses for a single property id. Since this is a property ID, then it is all the analyses for the all cycles. Note that this endpoint requires the passing of the organization id as a query parameter, otherwise it fails. @@ -1680,7 +1679,7 @@ def retrieve_analyses_for_property(self, property_id: int) -> Dict: property_id (int): Property view id to return the list of analyses Returns: - Dict: list of all the analyses that have run (or failed) for the property view + dict: list of all the analyses that have run (or failed) for the property view """ return self.client.get( None, @@ -1690,7 +1689,7 @@ def retrieve_analyses_for_property(self, property_id: int) -> Dict: include_org_id_query_param=True, ) - def retrieve_analysis_result(self, analysis_id: int, analysis_view_id: int) -> Dict: + def retrieve_analysis_result(self, analysis_id: int, analysis_view_id: int) -> dict: """Return the detailed JSON of a single analysis view. The endpoint in SEED is typically: https://dev1.seed-platform.org/app/#/analyses/274/runs/14693. @@ -1699,7 +1698,7 @@ def retrieve_analysis_result(self, analysis_id: int, analysis_view_id: int) -> D analysis_view_id (int): ID of the analysis view Returns: - Dict: Return the detailed results of a single analysis view + dict: Return the detailed results of a single analysis view """ return self.client.get( None, diff --git a/pyseed/seed_client_base.py b/pyseed/seed_client_base.py index a661e9f..22a009c 100644 --- a/pyseed/seed_client_base.py +++ b/pyseed/seed_client_base.py @@ -33,21 +33,23 @@ # Constants (Should end with a slash) URLS = { 'v3': { - 'columns': '/api/v3/columns/', 'column_mapping_profiles': '/api/v3/column_mapping_profiles/', 'column_mapping_profiles_filter': '/api/v3/column_mapping_profiles/filter/', + 'columns': '/api/v3/columns/', 'cycles': '/api/v3/cycles/', 'datasets': '/api/v3/datasets/', 'gbr_properties': '/api/v3/gbr_properties/', 'green_assessment': '/api/v3/green_assessments/', 'green_assessment_property': '/api/v3/green_assessment_properties/', 'green_assessment_url': '/api/v3/green_assessment_urls/', + 'import_files': '/api/v3/import_files/', + 'import_files_reuse_inventory_file_for_meters': '/api/v3/import_files/reuse_inventory_file_for_meters/', 'labels': '/api/v3/labels/', 'labels_property': '/api/v3/labels_property/', 'labels_taxlot': '/api/v3/labels_taxlot/', - 'import_files': '/api/v3/import_files/', - 'import_files_reuse_inventory_file_for_meters': '/api/v3/import_files/reuse_inventory_file_for_meters/', 'organizations': '/api/v3/organizations/', + 'portfolio_manager_report': '/api/v3/portfolio_manager/report/', + 'portfolio_manager_report_templates': '/api/v3/portfolio_manager/template_list/', 'properties': '/api/v3/properties/', 'properties_labels': '/api/v3/properties/labels/', 'properties_search': '/api/v3/properties/search/', @@ -58,27 +60,25 @@ # No versioning endpoints 'version': '/api/version/', # POSTs with replaceable keys - 'import_files_start_save_data_pk': '/api/v3/import_files/PK/start_save_data/', + 'import_files_check_meters_tab_exists_pk': '/api/v3/import_files/PK/check_meters_tab_exists/', 'import_files_start_map_data_pk': '/api/v3/import_files/PK/map/', 'import_files_start_matching_pk': '/api/v3/import_files/PK/start_system_matching_and_geocoding/', - 'import_files_check_meters_tab_exists_pk': '/api/v3/import_files/PK/check_meters_tab_exists/', + 'import_files_start_save_data_pk': '/api/v3/import_files/PK/start_save_data/', 'org_column_mapping_import_file': 'api/v3/organizations/ORG_ID/column_mappings/', - 'portfolio_manager_report_templates': '/api/v3/portfolio_manager/template_list/', - 'portfolio_manager_report': '/api/v3/portfolio_manager/report/', - # PUTs with replaceable keys: 'portfolio_manager_property_download': '/api/v3/portfolio_manager/PK/download/', + # PUTs with replaceable keys: 'properties_update_with_buildingsync': 'api/v3/properties/PK/update_with_building_sync/', - 'property_update_with_espm': 'api/v3/properties/PK/update_with_espm/', 'properties_upload_inventory_document': 'api/v3/properties/PK/upload_inventory_document', + 'property_update_with_espm': 'api/v3/properties/PK/update_with_espm/', # GETs with replaceable keys 'analyses_views': '/api/v3/analyses/PK/views/ANALYSIS_VIEW_PK/', + 'audit_template_building_xml': '/api/v3/audit_template/PK/get_building_xml', + 'audit_template_submission': '/api/v3/audit_template/PK/get_submission', 'import_files_matching_results': '/api/v3/import_files/PK/matching_and_geocoding_results/', 'progress': '/api/v3/progress/PROGRESS_KEY/', - 'properties_meters': '/api/v3/properties/PK/meters/', - 'properties_meter_usage': '/api/v3/properties/PK/meter_usage/', 'properties_analyses': '/api/v3/properties/PK/analyses/', - 'audit_template_building_xml': '/api/v3/audit_template/PK/get_building_xml', - 'audit_template_submission': '/api/v3/audit_template/PK/get_submission', + 'properties_meter_usage': '/api/v3/properties/PK/meter_usage/', + 'properties_meters': '/api/v3/properties/PK/meters/', # GET & POST with replaceable keys 'properties_meters_reading': '/api/v3/properties/PK/meters/METER_PK/readings/', } From 5a48e599cd215a5daf7054e50002e0b75e3a85d6 Mon Sep 17 00:00:00 2001 From: Alex Swindler Date: Wed, 29 May 2024 12:53:57 -0600 Subject: [PATCH 15/15] Fixed integration tests --- .github/workflows/ci.yml | 2 +- tests/test_seed_client.py | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6b1d46c..7e2ee11 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,7 +86,7 @@ jobs: SEED_PM_UN: ${{ secrets.SEED_PM_UN }} SEED_PM_PW: ${{ secrets.SEED_PM_PW }} run: | - pytest -m "integration" -s + pytest -m integration -s - name: Dump docker logs on failure if: failure() uses: jwalton/gh-docker-logs@v2 diff --git a/tests/test_seed_client.py b/tests/test_seed_client.py index 6a883e0..baeb588 100644 --- a/tests/test_seed_client.py +++ b/tests/test_seed_client.py @@ -5,19 +5,16 @@ # Imports from Third Party Modules import os -# import pytest +import pytest import unittest from datetime import date from pathlib import Path -from unittest import skipIf -from unittest.mock import patch # Local Imports from pyseed.seed_client import SeedClient -# @pytest.mark.integration - +@pytest.mark.integration class SeedClientTest(unittest.TestCase): @classmethod def setup_class(cls):