diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 116a486..bbd7acc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ exclude: | repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: trailing-whitespace - id: check-added-large-files @@ -40,7 +40,7 @@ repos: - id: flake8 args: ["--ignore=E501,E402,W503,W504,E731,F401"] - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.0.3 + rev: v3.1.0 hooks: - id: prettier types_or: [css, yaml, markdown, html, scss, javascript] diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6471ed2..601c904 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,19 @@ Changelog ========= +0.4.1 +----- + +What's Changed +************** + +* Configure seed to load small EEEJ dataset for integration test by @kflemin in https://github.com/SEED-platform/py-seed/pull/33 +* Add analysis retrieve methods by @nllong in https://github.com/SEED-platform/py-seed/pull/34 +* Added multiple cycle upload argument by @anchapin in https://github.com/SEED-platform/py-seed/pull/27 +* Endpoint to download an Audit Template Report Submission and store in SEED by @kflemin in https://github.com/SEED-platform/py-seed/pull/36 + +**Full Changelog**: https://github.com/SEED-platform/py-seed/commits/v0.4.1 + 0.4.0 ----- @@ -15,7 +28,9 @@ What's Changed * Add new endpoints to support 179D by @kflemin in https://github.com/SEED-platform/py-seed/pull/22 * Increase timeout for server start by @nllong in https://github.com/SEED-platform/py-seed/pull/31 -## New Contributors +New Contributors +**************** + * @kflemin made their first contribution in https://github.com/SEED-platform/py-seed/pull/22 **Full Changelog**: https://github.com/SEED-platform/py-seed/commits/v0.4.0 diff --git a/README.rst b/README.rst index 38ea3bd..e30b241 100644 --- a/README.rst +++ b/README.rst @@ -1,4 +1,4 @@ -Py-SEED +py-SEED ======= .. image:: https://github.com/seed-platform/py-seed/actions/workflows/ci.yml/badge.svg?branch=develop @@ -7,17 +7,28 @@ Py-SEED .. image:: https://badge.fury.io/py/py-seed.svg :target: https://pypi.python.org/pypi/py-seed/ -A python API client for the SEED Platform. This is an updated version of the Client. It is compatible with the latest version of the SEED Platform (>2.17.4). This client still has access to the previous format of generating a lower level API client by accessing `seed_client_base.SEEDOAuthReadOnlyClient`, `seed_client_base.SEEDOAuthReadWriteClient`, `seed_client_base.SEEDReadOnlyClient`, and `seed_client_base.SEEDReadWriteClient`. This lower level API is documented below under the `Low-Level Documentation` +py-SEED serves as a Python client for the SEED Platform API. This library is purpose-built for Python applications, enabling interaction with the SEED Platform API to access property lists, create properties, establish connections, and retrieve data from ENERGY STAR(R) Portfolio Manager, BETTER, and other sources. The SEED Platform has a robust API, granting users access to every front-end feature seamlessly via the API. Currently, this library exposes the most commonly used SEED API endpoints and will undergo continuous updates tailored to the community's evolving needs. py-SEED offers two interaction levels: a high-level API providing familiar endpoints for easy connectivity to SEED's API, and a low-level API that allows read-write access to any SEED API, demanding a deeper understanding of the SEED API architecture + +py-SEED is compatible with the latest version of the SEED Platform (>2.17.4) and only support SEED API Version 3. + +More information can be found here: + +* https://seed-platform.org +* https://energy.gov/eere/buildings/standard-energy-efficiency-data-platform +* https://github.com/SEED-platform +* https://buildingdata.energy.gov/#/seed +* https://github.com/SEED-platform/pyseed-examples + Stakeholders ------------- The following list of stakeholders should be considered when making changes to this module -- 179D Tax Deduction Web Application -- Earth Advantage Green Building Registry -- User scripts for managing building data -- ECAM +* 179D Tax Deduction Web Application +* Earth Advantage Green Building Registry +* User scripts for managing building data +* ECAM Documentation ------------- @@ -55,11 +66,12 @@ Within Python you can use the client like this: 'tests/data/test-seed-data-mappings.csv' ) - # See the projects unit tests for more examples. + # See the projects unit tests for more examples. https://github.com/SEED-platform/py-seed/blob/develop/tests/test_seed_client.py + # Or look at the py-SEED examples repository: https://github.com/SEED-platform/pyseed-examples Low-Level Documentation ----------------------- -This provides two user authentication based Python clients and two OAuth2 authentication based Python clients for interacting with the SEED Platform Api:: +This client has access to the lower level API client by accessing `seed_client_base.SEEDOAuthReadOnlyClient`, `seed_client_base.SEEDOAuthReadWriteClient`, `seed_client_base.SEEDReadOnlyClient`, and `seed_client_base.SEEDReadWriteClient`. This provides two user authentication based Python clients and two authentication methods, basic and `OAuth2 `_. More information on authentication can be seen in the following py-SEED classes: SEEDOAuthReadOnlyClient @@ -67,27 +79,7 @@ This provides two user authentication based Python clients and two OAuth2 authen SEEDReadOnlyClient SEEDReadWriteClient - - -(The OAuthMixin is constructed around the the JWTGrantClient found in jwt-oauth2lib. see https://github.com/GreenBuildingRegistry/jwt_oauth2) - -SEED (Standard Energy Efficiency Data Platform™) is an open source "web-based application that helps organizations easily manage data on the energy performance of large groups of buildings" funded by the United States Department of Energy. - -More information can be found here: - -* https://energy.gov/eere/buildings/standard-energy-efficiency-data-platform -* https://seed-platform.org -* https://github.com/SEED-platform -* https://buildingdata.energy.gov/#/seed - - -Note the clients do not provide per api-call methods, but does provide the standard CRUD methods: get, list, put, post, patch, delete - -The intended use of these clients is to be further subclassed or wrapped in functions to provide the desired functionality. The CRUD methods are provided via mixins so its possible to create a client for example without the ability to delete by subclassing SEEDUserAuthBaseClient, or SEEDOAuthBaseClient, and adding only the mixins that provided the Create, Read and Update capabilities. - -Basic usage for the provided clients is below. - -Usage: +Note the clients do not provide per api-call methods, but does provide the standard CRUD methods: get, list, put, post, patch, delete. The intended use of these clients is to be further subclassed or wrapped in functions to provide the desired functionality. The CRUD methods are provided via mixins so its possible to create a client for example without the ability to delete by subclassing SEEDUserAuthBaseClient, or SEEDOAuthBaseClient, and adding only the mixins that provided the Create, Read and Update capabilities. Basic usage for the provided low-level clients is as follows: .. code-block:: python @@ -107,8 +99,8 @@ Usage: # get a single property seed_client.get(property_pk, endpoint='properties') -Local Testing -------------- +Testing +------- Tests can be run via the `pytest` command. @@ -120,15 +112,15 @@ You will need to export environment variables for a test portfolio manager accou SEED_PM_PW +SEED Platform +------------- +SEED (Standard Energy Efficiency Data Platform™) is an open source "web-based application that helps organizations easily manage data on the energy performance of large groups of buildings" funded by the United States Department of Energy. + + License ------- Full details in LICENSE file. -Changelog ---------- -py-SEED was developed for use in the greenbuildingregistry project but has been extended for various uses, including Salesforce data transfer and SEED data analysis. - -For a full changelog see `CHANGELOG.rst `_. Releasing --------- @@ -142,4 +134,6 @@ Releasing rm -rf dist python setup.py sdist pip install twine + # make sure to check the dist package for errors in the RST files + twine check dist/* twine upload dist/* diff --git a/cspell.json b/cspell.json index 6be73e6..29e58b8 100644 --- a/cspell.json +++ b/cspell.json @@ -7,6 +7,7 @@ "apibase", "buildingsync", "codeauthor", + "datafile", "dname", "durl", "ECAM", diff --git a/pyseed/seed_client.py b/pyseed/seed_client.py index e16e8e8..15f3e53 100644 --- a/pyseed/seed_client.py +++ b/pyseed/seed_client.py @@ -216,8 +216,8 @@ 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_id: int) -> dict: - """Return a single property by the property id. + 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} @@ -236,7 +236,7 @@ def get_property(self, property_id: int) -> dict: """ # NOTE: this seems to be the call that OEP uses (returns property and labels dictionaries) return self.client.get( - property_id, endpoint="properties", data_name="properties" + property_view_id, endpoint="properties", data_name="properties" ) def search_buildings( @@ -379,18 +379,23 @@ def delete_label(self, label_name: str) -> dict: return self.client.delete(id, endpoint="labels") - def get_view_ids_with_label(self, label_names: list = []) -> list: - """Get the view IDs of the properties with a given label name. + 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. 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 (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 """ + # 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( endpoint="properties_labels", cycle=self.cycle_id, @@ -1149,12 +1154,13 @@ def get_meter_data(self, property_id, interval: str = 'Exact', excluded_meter_id def save_meter_data(self, property_id: int, meter_id: int, meter_data) -> dict: pass - def start_save_data(self, import_file_id: int) -> 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. Args: import_file_id (int): id of the import file to save + multiple_cycle_upload (bool): whether to use multiple cycle upload Returns: dict: progress key @@ -1167,7 +1173,8 @@ def start_save_data(self, import_file_id: int) -> dict: return self.client.post( "import_files_start_save_data_pk", url_args={"PK": import_file_id}, - json={"cycle_id": self.cycle_id}, + json={"cycle_id": self.cycle_id, + "multiple_cycle_upload": multiple_cycle_upload}, ) def start_map_data(self, import_file_id: int) -> dict: @@ -1316,6 +1323,7 @@ def upload_and_match_datafile( column_mapping_profile_name (str): Name of the column mapping profile to use column_mappings_file (str): Mapping that will be uploaded to the column_mapping_profile_name import_meters_if_exist (bool): If true, will import meters from the meter tab if they exist in the datafile. Defaults to False. + multiple_cycle_upload (bool): Whether to use multiple cycle upload. Defaults to False. Returns: dict: { @@ -1326,9 +1334,10 @@ def upload_and_match_datafile( dataset = self.get_or_create_dataset(dataset_name) result = self.upload_datafile(dataset["id"], datafile, datafile_type) import_file_id = result["import_file_id"] + multiple_cycle_upload = kwargs.pop("multiple_cycle_upload", False) # start processing - result = self.start_save_data(import_file_id) + result = self.start_save_data(import_file_id, multiple_cycle_upload) progress_key = result.get("progress_key", None) # wait until upload is complete @@ -1418,6 +1427,71 @@ 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: + """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 + filename (str): filename to use to upload to SEED + + Returns: + dict: Response from the SEED API + including the PDF file (if that format was requested) + """ + + # api/v3/audit_template/pk/get_submission + # accepts pdf or xml + response = self.client.get( + None, + required_pk=False, + endpoint="audit_template_submission", + url_args={"PK": audit_template_submission_id}, + report_format=report_format + ) + + if response['status'] == 'success': + if report_format.lower() == 'pdf': + pdf_file = response['content'] + if not filename: + filename = 'at_submission_report_' + str(audit_template_submission_id) + '.pdf' + files = [ + ('file', (filename, pdf_file)), + ('file_type', (None, 1)) + ] + response2 = self.client.put( + None, + required_pk=False, + endpoint="properties_upload_inventory_document", + url_args={"PK": seed_id}, + files=files + ) + response2['pdf_report'] = pdf_file + else: + # assume XML + # now post to api/v3/properties/PK/update_with_buildingsync + xml_file = response['content'] + if not filename: + filename = 'at_' + str(int(time.time() * 1000)) + '.xml' + + files = [ + ('file', (filename, xml_file)), + ('file_type', (None, 1)) + ] + + response2 = self.client.put( + None, + required_pk=False, + endpoint="properties_update_with_buildingsync", + url_args={"PK": seed_id}, + files=files, + cycle_id=cycle_id + ) + + return response2 + 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 @@ -1472,3 +1546,41 @@ 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: + """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. + + Args: + 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 + """ + return self.client.get( + None, + required_pk=False, + endpoint="properties_analyses", + url_args={"PK": property_id}, + include_org_id_query_param=True, + ) + + 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. + + Args: + analysis_id (int): ID of the analysis + analysis_view_id (int): ID of the analysis view + + Returns: + dict: Return the detailed results of a single analysis view + """ + return self.client.get( + None, + required_pk=False, + endpoint="analyses_views", + url_args={"PK": analysis_id, "ANALYSIS_VIEW_PK": analysis_view_id}, + include_org_id_query_param=True, + ) diff --git a/pyseed/seed_client_base.py b/pyseed/seed_client_base.py index bffbbb0..9a10599 100644 --- a/pyseed/seed_client_base.py +++ b/pyseed/seed_client_base.py @@ -51,7 +51,6 @@ 'properties': '/api/v3/properties/', 'properties_labels': '/api/v3/properties/labels/', 'properties_search': '/api/v3/properties/search/', - 'property_states': '/api/v3/property_states/', 'property_views': '/api/v3/property_views/', 'taxlots': '/api/v3/taxlots/', 'upload': '/api/v3/upload/', @@ -68,12 +67,16 @@ # 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', # GETs with replaceable keys + 'analyses_views': '/api/v3/analyses/PK/views/ANALYSIS_VIEW_PK/', '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', # GET & POST with replaceable keys 'properties_meters_reading': '/api/v3/properties/PK/meters/METER_PK/readings/', } @@ -216,6 +219,9 @@ def _check_response(self, response, *args, **kwargs): elif 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' in response_content_types: # spreadsheet response error = False + elif 'application/pdf' in response_content_types: + # PDF report response + error = False elif 'application/json' not in response_content_types: # get as text if not response.content: @@ -425,11 +431,14 @@ def get(self, pk, endpoint=None, data_name=None, **kwargs): """ url_args = kwargs.pop('url_args', None) + org_id_qp = kwargs.pop('include_org_id_query_param', False) kwargs = self._set_params(kwargs) endpoint = _set_default(self, 'endpoint', endpoint) data_name = _set_default(self, 'data_name', data_name, required=False) url = add_pk(self.urls[endpoint], pk, required=kwargs.pop('required_pk', True), slash=True) url = _replace_url_args(url, url_args) + if org_id_qp: + url += f"?organization_id={self.org_id}" response = super(ReadMixin, self)._get(url=url, **kwargs) self._check_response(response, **kwargs) return self._get_result(response, data_name=data_name, **kwargs) @@ -477,6 +486,7 @@ def put(self, pk, endpoint=None, data_name=None, **kwargs): data_name = _set_default(self, 'data_name', data_name, required=False) url = add_pk(self.urls[endpoint], pk, required=kwargs.pop('required_pk', True), slash=True) url = _replace_url_args(url, url_args) + response = super(UpdateMixin, self)._put(url=url, **kwargs) self._check_response(response, **kwargs) return self._get_result(response, data_name=data_name, **kwargs) diff --git a/setup.cfg b/setup.cfg index d9c908f..8a18908 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name=py-seed -version=0.4.0 +version=0.4.1 description=A Python API client for the SEED Platform author=Nicholas Long, Katherine Fleming, Fable Turas, Paul Munday author_email=nicholas.long@nrel.gov, fable@raintechpdx.com, paul@paulmunday.net diff --git a/setup.py b/setup.py index 5a388ba..0114110 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,12 @@ -#!/usr/bin/env python - # Imports from Third Party Modules +from pathlib import Path from setuptools import setup -setup() +this_directory = Path(__file__).parent +long_description = (this_directory / "README.rst").read_text() + +setup( + name='py-SEED', + long_description=long_description, + long_description_content_type='text/x-rst' +)