From c84a547f1e17e9e79a3fe788270480022ce511f6 Mon Sep 17 00:00:00 2001 From: Hannes Hapke Date: Sat, 30 May 2020 12:39:50 -0700 Subject: [PATCH 1/6] Added new attributes --- pyzillow/pyzillow.py | 68 +++++++++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 30 deletions(-) diff --git a/pyzillow/pyzillow.py b/pyzillow/pyzillow.py index 1d79a29..c440844 100644 --- a/pyzillow/pyzillow.py +++ b/pyzillow/pyzillow.py @@ -315,8 +315,12 @@ class GetUpdatedPropertyDetails(ZillowResults): ``.num_rooms`` ``.parking_type`` ``.photo_gallery`` + ``.posting_agent`` + ``.posting_last_update`` + ``.posting_mls`` ``.posting_status`` ``.posting_type`` + ``.price`` ``.property_size`` ``.roof`` ``.rooms`` @@ -331,48 +335,52 @@ class GetUpdatedPropertyDetails(ZillowResults): attribute_mapping = { # attributes in common with GetDeepSearchResults - "zillow_id": "zpid", - "home_type": "editedFacts/useCode", - "home_detail_link": "links/homeDetails", + "bathrooms": "editedFacts/bathrooms", + "bedrooms": "editedFacts/bedrooms", "graph_data_link": "", - "map_this_home_link": "", + "home_detail_link": "links/homeDetails", + "home_size": "editedFacts/finishedSqFt", + "home_type": "editedFacts/useCode", + "last_sold_date": "", + "last_sold_price": "", "latitude": "address/latitude", "longitude": "address/longitude", - "tax_year": "", + "map_this_home_link": "", + "property_size": "editedFacts/lotSizeSqFt", "tax_value": "", + "tax_year": "", "year_built": "editedFacts/yearBuilt", - "property_size": "editedFacts/lotSizeSqFt", - "home_size": "editedFacts/finishedSqFt", - "bathrooms": "editedFacts/bathrooms", - "bedrooms": "editedFacts/bedrooms", - "last_sold_date": "", - "last_sold_price": "", + "zillow_id": "zpid", # new attributes in GetUpdatedPropertyDetails - "photo_gallery": "links/photoGallery", - "home_info": "links/homeInfo", - "year_updated": "editedFacts/yearUpdated", - "floor_material": "editedFacts/floorCovering", - "num_floors": "editedFacts/numFloors", + "agent_name": "posting/agentName", + "agent_profile_url": "posting/agentProfileUrl", + "appliances": "editedFacts/appliances", "basement": "editedFacts/basement", - "roof": "editedFacts/roof", - "view": "editedFacts/view", - "parking_type": "editedFacts/parkingType", + "brokerage": "posting/brokerage", + "elementary_school": "elementarySchool", + "floor_material": "editedFacts/floorCovering", "heating_sources": "editedFacts/heatingSources", "heating_system": "editedFacts/heatingSystem", - "rooms": "editedFacts/rooms", - "num_rooms": "editedFacts/numRooms", - "appliances": "editedFacts/appliances", - "neighborhood": "neighborhood", - "school_district": "schoolDistrict", - "elementary_school": "elementarySchool", - "middle_school": "middleSchool", - "school_district": "schoolDistrict", "home_description": "homeDescription", + "home_info": "links/homeInfo", + "middle_school": "middleSchool", + "neighborhood": "neighborhood", + "num_floors": "editedFacts/numFloors", + "num_rooms": "editedFacts/numRooms", + "parking_type": "editedFacts/parkingType", + "photo_gallery": "links/photoGallery", + "posting_agent": "posting/agentName", + "posting_last_update": "posting/lastUpdatedDate", + "posting_mls": "posting/mls", "posting_status": "posting/status", "posting_type": "posting/type", - "agent_name": "posting/agentName", - "agent_profile_url": "posting/agentProfileUrl", - "brokerage": "posting/brokerage", + "price": "price", + "roof": "editedFacts/roof", + "rooms": "editedFacts/rooms", + "school_district": "schoolDistrict", + "school_district": "schoolDistrict", + "view": "editedFacts/view", + "year_updated": "editedFacts/yearUpdated", } def __init__(self, data, *args, **kwargs): From eae48d8dccaa3124ab96c81ca3996ae1fa37800e Mon Sep 17 00:00:00 2001 From: Hannes Hapke Date: Sun, 31 May 2020 13:43:50 -0700 Subject: [PATCH 2/6] Support API error 6 (#29) * handling error 6 * linter --- pyzillow/pyzillowerrors.py | 1 + test/api_responses.py | 1 + test/test_pyzillow.py | 20 +++++++++++++++++++ .../error_6_account_not_authorized.xml | 13 ++++++++++++ 4 files changed, 35 insertions(+) create mode 100644 test/xml_payloads/error_6_account_not_authorized.xml diff --git a/pyzillow/pyzillowerrors.py b/pyzillow/pyzillowerrors.py index 9797180..dfb9c1b 100644 --- a/pyzillow/pyzillowerrors.py +++ b/pyzillow/pyzillowerrors.py @@ -34,6 +34,7 @@ class ZillowError(Exception): + "The Zillow Web Service is currently not available. " + "Please come back later and try again.", ), + (6, "This account is not authorized to execute this API call."), (7, "Too many requests. \n" + "Daily requests exceeded.",), ( 500, diff --git a/test/api_responses.py b/test/api_responses.py index 106e422..bf99f1b 100644 --- a/test/api_responses.py +++ b/test/api_responses.py @@ -9,6 +9,7 @@ "get_deep_search_200_ok": "get_deep_search_200_ok.xml", "updated_property_details_200_ok": "updated_property_details_200_ok.xml", "error_2_zwsid_missing": "error_2_zwsid_missing.xml", + "error_6_account_not_authorized": "error_6_account_not_authorized.xml", "error_500_no_address_provided": "error_500.xml", "error_501_no_city_state": "error_501.xml", "error_508_invalid_address": "error_508_invalid_address.xml", diff --git a/test/test_pyzillow.py b/test/test_pyzillow.py index ede3c7c..59e4f16 100755 --- a/test/test_pyzillow.py +++ b/test/test_pyzillow.py @@ -166,6 +166,26 @@ def test_zillow_error_no_coverage(self): error_msg = "Status 508: No exact match found for input address." assert error_msg in str(excinfo.value) + @responses.activate + def test_zillow_error_account_not_authorized(self): + """ + This test checks for account not authorized error + Expected error code: 6 + """ + + set_get_deep_search_response( + self.api_response_obj.get("error_6_account_not_authorized") + ) + + zillow_data = ZillowWrapper(self.ZILLOW_API_KEY) + + with pytest.raises(ZillowError) as excinfo: + zillow_data.get_deep_search_results( + address=self.address, zipcode=self.zipcode + ) + error_msg = "Status 6: This account is not authorized to execute this API call" + assert error_msg in str(excinfo.value) + @responses.activate def test_deep_search_results(self): """ diff --git a/test/xml_payloads/error_6_account_not_authorized.xml b/test/xml_payloads/error_6_account_not_authorized.xml new file mode 100644 index 0000000..ca8d77a --- /dev/null +++ b/test/xml_payloads/error_6_account_not_authorized.xml @@ -0,0 +1,13 @@ + + + +
2114 Bigelow Ave Seattle, WA
+ 98109 +
+ + Error: this account is not authorized to execute this API call + 6 + +
From 54d9d25e91d918b328ccd3e1b729a30c4d6cb758 Mon Sep 17 00:00:00 2001 From: Timo Cornelius Metzger Date: Wed, 3 Jun 2020 10:22:48 -0700 Subject: [PATCH 3/6] Fix mapping (#30) * Updated mapping (incl. PR #10, #16) * Updated docs to reflect limitations of GetUpdatedPropertyDetails * Fixed issues with whitespaces * lower cased use_Code Co-authored-by: Hannes Hapke --- docs/getting_started.rst | 29 +- pyzillow/pyzillow.py | 802 ++++++++++++++++++++------------------- 2 files changed, 430 insertions(+), 401 deletions(-) diff --git a/docs/getting_started.rst b/docs/getting_started.rst index fee0fd4..2ac11b9 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -30,6 +30,8 @@ To query the GetDeepSearchResults API: An instance of ``GetDeepSearchResults`` has the following attributes: ``.bathrooms`` ``.bedrooms`` +``.city`` +``.fips_county`` ``.graph_data_link`` ``.home_detail_link`` ``.home_size`` @@ -45,8 +47,12 @@ An instance of ``GetDeepSearchResults`` has the following attributes: ``.rentzestimate_valuation_range_high`` ``.rentzestimate_valuation_range_low`` ``.rentzestimate_value_change`` +``.state`` +``.street`` ``.tax_value`` ``.tax_year`` +``.total_rooms`` +``.use_code`` ``.year_built`` ``.zestimate_amount`` ``.zestimate_last_updated`` @@ -55,6 +61,7 @@ An instance of ``GetDeepSearchResults`` has the following attributes: ``.zestimate_valuation_range_low`` ``.zestimate_value_change`` ``.zillow_id`` +``.zipcode`` Access the information by reading the ``GetDeepSearchResults`` object's attributes. For example: @@ -67,7 +74,8 @@ Accessing the GetUpdatedPropertyDetails API ******************************************* The GetUpdatedPropertyDetails API endpoint requires a Zillow Property ID (ZPID) as an argument. To find this identifier, you can read the attribute ``.zillow_id`` of a GetDeepSearchResults object. -Compared to the GetDeepSearchResults API endpoint described above, the GetUpdatedPropertyDetails API endpoint delivers more details about the object, such as ``.heating_system`` or ``.school_district``. However, it does not include Zestimate or Rentzestimate information. +Compared to the GetDeepSearchResults API endpoint described above, the GetUpdatedPropertyDetails API endpoint delivers more details about the object, such as ``.heating_system`` or ``.school_district``. +However, GetUpdatedPropertyDetails data is not available for all valid Zillow Property IDs. To query the GetUpdatedPropertyDetails API: @@ -84,39 +92,46 @@ An instance of ``GetDeepSearchResults`` has the following attributes: ``.bathrooms`` ``.bedrooms`` ``.brokerage`` +``.city`` +``.cooling_system`` ``.elementary_school`` +``.exterior_material`` ``.floor_material`` -``.graph_data_link`` ``.heating_sources`` ``.heating_system`` +``.high_school`` ``.home_description`` ``.home_detail_link`` ``.home_info`` ``.home_size`` ``.home_type`` -``.last_sold_date`` -``.last_sold_price`` ``.latitude`` ``.longitude`` -``.map_this_home_link`` ``.middle_school`` ``.neighborhood`` ``.num_floors`` ``.num_rooms`` +``.page_view_count_this_month`` +``.page_view_count_total`` ``.parking_type`` ``.photo_gallery`` +``.posting_agent`` +``.posting_last_update`` +``.posting_mls`` ``.posting_status`` ``.posting_type`` +``.price`` ``.property_size`` ``.roof`` ``.rooms`` ``.school_district`` -``.tax_value`` -``.tax_year`` +``.state`` +``.street`` ``.view`` ``.year_built`` ``.year_updated`` ``.zillow_id`` +``.zipcode`` Access the information by reading the ``GetUpdatedPropertyDetails`` object's attributes. For example: diff --git a/pyzillow/pyzillow.py b/pyzillow/pyzillow.py index c440844..bc100ed 100644 --- a/pyzillow/pyzillow.py +++ b/pyzillow/pyzillow.py @@ -1,394 +1,408 @@ -import requests - -from xml.etree import cElementTree as ElementTree # for zillow API - -from .pyzillowerrors import ZillowError, ZillowFail, ZillowNoResults -from . import __version__ - - -class ZillowWrapper(object): - """This class provides an interface into the Zillow API. An API key is required to - create an instance of this class: - - >>> from pyzillow.pyzillow import ZillowWrapper - >>> zillow_data = ZillowWrapper(YOUR_ZILLOW_API_KEY) - - To request data from Zillow, you can choose between: - - 1. The GetDeepSearchResults API endpoint (:class:`pyzillow.pyzillow.GetDeepSearchResults`) - which requires the following arguments: - - * A street address (e.g. ``'2114 Bigelow Ave'``) - * A ZIP code or city and state combination (e.g. ``'98109'`` or ``'Seattle, WA'``) - * Optional: Enabling or disabling Zillow Rentzestimate information in - API results (``True``/``False``) - - Example: - - >>> from pyzillow.pyzillow import ZillowWrapper, GetDeepSearchResults - >>> zillow_data = ZillowWrapper(YOUR_ZILLOW_API_KEY) - >>> deep_search_response = zillow_data.get_deep_search_results(address, - zipcode, - rentzestimate) - >>> result = GetDeepSearchResults(deep_search_response) - - 2. The GetUpdatedPropertyDetails API endpoint - (:class:`pyzillow.pyzillow.GetUpdatedPropertyDetails`) which requires a - Zillow Property ID (ZPID) as an argument. You can acquire this identifier by - accessing ``.zillow_id`` from a :class:`pyzillow.pyzillow.GetDeepSearchResults` - object. - - Example: - - >>> from pyzillow.pyzillow import ZillowWrapper, GetUpdatedPropertyDetails - >>> zillow_data = ZillowWrapper(YOUR_ZILLOW_API_KEY) - >>> updated_property_details_response = \ - zillow_data.get_updated_property_details(zillow_id) - >>> result = GetUpdatedPropertyDetails(updated_property_details_response) - """ - - def __init__(self, api_key: str = None): - """Constructor method - """ - self.api_key = api_key - - def get_deep_search_results( - self, address: str, zipcode: str, rentzestimate: bool = False - ): - """This method provides results from the GetDeepSearchResults API endpoint - as an XML object. - - :param address: Street address to look up - :type address: str - :param zipcode: ZIP code to look up - :type zipcode: str - :param rentzestimate: Add Rent Zestimate information to result (True/False), - defaults to False - :type rentzestimate: bool, optional - :return: Result from API query - :rtype: xml.etree.ElementTree.Element - """ - url = "http://www.zillow.com/webservice/GetDeepSearchResults.htm" - - params = { - "address": address, - "citystatezip": zipcode, - "rentzestimate": str(rentzestimate).lower(), - "zws-id": self.api_key, - } - return self.get_data(url, params) - - def get_updated_property_details(self, zpid: str): - """This method provides results from the GetUpdatedPropertyDetails API endpoint - as an XML object. - - :param zpid: Zillow Web Service Identifier - :type zpid: str - :return: Result from API query - :rtype: xml.etree.ElementTree.Element - """ - url = "http://www.zillow.com/webservice/GetUpdatedPropertyDetails.htm" - - params = {"zpid": zpid, "zws-id": self.api_key} - return self.get_data(url, params) - - def get_data(self, url: str, params: dict): - """This method requests data from the API endpoint specified in the url argument. - It uses parameters from the params argument. - - :param url: URL of API endpoint - :type url: str - :param params: Parameters for API query - :type params: dict - :raises ZillowFail: The API endpoint could not be reached or the request - did not return valid XML - :raises ZillowError: The API endpoint responded with an error code - :raises ZillowNoResults: The request did not return any results - :return: Result from API query - :rtype: xml.etree.ElementTree.Element - """ - try: - request = requests.get( - url=url, - params=params, - headers={ - "User-Agent": "".join(["pyzillow/", __version__, " (Python)"]) - }, - ) - except ( - requests.exceptions.ConnectionError, - requests.exceptions.TooManyRedirects, - requests.exceptions.Timeout, - ): - raise ZillowFail - - try: - request.raise_for_status() - except requests.exceptions.HTTPError: - raise ZillowFail - - try: - response = ElementTree.fromstring(request.text) - except ElementTree.ParseError: - print("Zillow response is not a valid XML ({})".format(params["address"])) - raise ZillowFail - - if response.findall("message/code")[0].text != "0": - raise ZillowError(int(response.findall("message/code")[0].text)) - else: - if not response.findall("response"): - print("Zillow returned no results for ({})".format(params["address"])) - raise ZillowNoResults - return response - - -class ZillowResults(object): - """Base class for :class:`pyzillow.pyzillow.GetDeepSearchResults` - and :class:`pyzillow.pyzillow.GetUpdatedPropertyDetails`. - """ - - attribute_mapping = {} - - def get_attr(self, attr): - """ - """ - try: - return self.data.find(self.attribute_mapping[attr]).text - except AttributeError: - return None - - def __str__(self): - return self.zillow_id - - @property - def area_unit(self): - """ - lotSizeSqFt - """ - return u"SqFt" - - @property - def last_sold_price_currency(self): - """ - lastSoldPrice currency - """ - return self.data.find(self.attribute_mapping["last_sold_price"]).attrib[ - "currency" - ] - - -class GetDeepSearchResults(ZillowResults): - """Maps results from the XML data array into attributes of an instance of - GetDeepSearchResults. - - An instance of ``GetDeepSearchResults`` has the following attributes: - ``.bathrooms`` - ``.bedrooms`` - ``.graph_data_link`` - ``.home_detail_link`` - ``.home_size`` - ``.home_type`` - ``.last_sold_date`` - ``.last_sold_price`` - ``.latitude`` - ``.longitude`` - ``.map_this_home_link`` - ``.property_size`` - ``.rentzestimate_amount`` - ``.rentzestimate_last_updated`` - ``.rentzestimate_valuation_range_high`` - ``.rentzestimate_valuation_range_low`` - ``.rentzestimate_value_change`` - ``.tax_value`` - ``.tax_year`` - ``.year_built`` - ``.zestimate_amount`` - ``.zestimate_last_updated`` - ``.zestimate_percentile`` - ``.zestimate_valuation_range_high`` - ``.zestimate_valuation_range_low`` - ``.zestimate_value_change`` - ``.zillow_id`` - """ - - attribute_mapping = { - "zillow_id": "result/zpid", - "home_type": "result/useCode", - "home_detail_link": "result/links/homedetails", - "graph_data_link": "result/links/graphsanddata", - "map_this_home_link": "result/links/mapthishome", - "latitude": "result/address/latitude", - "longitude": "result/address/longitude", - "tax_year": "result/taxAssessmentYear", - "tax_value": "result/taxAssessment", - "year_built": "result/yearBuilt", - "property_size": "result/lotSizeSqFt", - "home_size": "result/finishedSqFt", - "bathrooms": "result/bathrooms", - "bedrooms": "result/bedrooms", - "last_sold_date": "result/lastSoldDate", - "last_sold_price": "result/lastSoldPrice", - "zestimate_amount": "result/zestimate/amount", - "zestimate_last_updated": "result/zestimate/last-updated", - "zestimate_value_change": "result/zestimate/valueChange", - "zestimate_valuation_range_high": "result/zestimate/valuationRange/high", - "zestimate_valuation_range_low": "result/zestimate/valuationRange/low", - "zestimate_percentile": "result/zestimate/percentile", - "rentzestimate_amount": "result/rentzestimate/amount", - "rentzestimate_last_updated": "result/rentzestimate/last-updated", - "rentzestimate_value_change": "result/rentzestimate/valueChange", - "rentzestimate_valuation_range_high": "result/rentzestimate/valuationRange/high", - "rentzestimate_valuation_range_low": "result/rentzestimate/valuationRange/low", - } - - def __init__(self, data, *args, **kwargs): - """Constructor method - """ - self.data = data.findall("response/results")[0] - for attr in self.attribute_mapping.__iter__(): - try: - self.__setattr__(attr, self.get_attr(attr)) - except AttributeError: - print("AttributeError with {}".format(attr)) - - @property - def region_name(self): - """ - region name - """ - try: - return self.data.find("result/localRealEstate/region").attrib["name"] - except AttributeError: - return None - - @property - def region_id(self): - """ - region id - """ - try: - return self.data.find("result/localRealEstate/region").attrib["id"] - except AttributeError: - return None - - @property - def region_type(self): - """ - region type - """ - try: - return self.data.find("result/localRealEstate/region").attrib["type"] - except AttributeError: - return None - - -class GetUpdatedPropertyDetails(ZillowResults): - """Maps results from the XML data array into attributes of an instance of - GetUpdatedPropertyDetails. - - An instance of ``GetDeepSearchResults`` has the following attributes: - ``.agent_name`` - ``.agent_profile_url`` - ``.appliances`` - ``.basement`` - ``.bathrooms`` - ``.bedrooms`` - ``.brokerage`` - ``.elementary_school`` - ``.floor_material`` - ``.graph_data_link`` - ``.heating_sources`` - ``.heating_system`` - ``.home_description`` - ``.home_detail_link`` - ``.home_info`` - ``.home_size`` - ``.home_type`` - ``.last_sold_date`` - ``.last_sold_price`` - ``.latitude`` - ``.longitude`` - ``.map_this_home_link`` - ``.middle_school`` - ``.neighborhood`` - ``.num_floors`` - ``.num_rooms`` - ``.parking_type`` - ``.photo_gallery`` - ``.posting_agent`` - ``.posting_last_update`` - ``.posting_mls`` - ``.posting_status`` - ``.posting_type`` - ``.price`` - ``.property_size`` - ``.roof`` - ``.rooms`` - ``.school_district`` - ``.tax_value`` - ``.tax_year`` - ``.view`` - ``.year_built`` - ``.year_updated`` - ``.zillow_id`` - """ - - attribute_mapping = { - # attributes in common with GetDeepSearchResults - "bathrooms": "editedFacts/bathrooms", - "bedrooms": "editedFacts/bedrooms", - "graph_data_link": "", - "home_detail_link": "links/homeDetails", - "home_size": "editedFacts/finishedSqFt", - "home_type": "editedFacts/useCode", - "last_sold_date": "", - "last_sold_price": "", - "latitude": "address/latitude", - "longitude": "address/longitude", - "map_this_home_link": "", - "property_size": "editedFacts/lotSizeSqFt", - "tax_value": "", - "tax_year": "", - "year_built": "editedFacts/yearBuilt", - "zillow_id": "zpid", - # new attributes in GetUpdatedPropertyDetails - "agent_name": "posting/agentName", - "agent_profile_url": "posting/agentProfileUrl", - "appliances": "editedFacts/appliances", - "basement": "editedFacts/basement", - "brokerage": "posting/brokerage", - "elementary_school": "elementarySchool", - "floor_material": "editedFacts/floorCovering", - "heating_sources": "editedFacts/heatingSources", - "heating_system": "editedFacts/heatingSystem", - "home_description": "homeDescription", - "home_info": "links/homeInfo", - "middle_school": "middleSchool", - "neighborhood": "neighborhood", - "num_floors": "editedFacts/numFloors", - "num_rooms": "editedFacts/numRooms", - "parking_type": "editedFacts/parkingType", - "photo_gallery": "links/photoGallery", - "posting_agent": "posting/agentName", - "posting_last_update": "posting/lastUpdatedDate", - "posting_mls": "posting/mls", - "posting_status": "posting/status", - "posting_type": "posting/type", - "price": "price", - "roof": "editedFacts/roof", - "rooms": "editedFacts/rooms", - "school_district": "schoolDistrict", - "school_district": "schoolDistrict", - "view": "editedFacts/view", - "year_updated": "editedFacts/yearUpdated", - } - - def __init__(self, data, *args, **kwargs): - """Constructor method - """ - self.data = data.findall("response")[0] - for attr in self.attribute_mapping.__iter__(): - try: - self.__setattr__(attr, self.get_attr(attr)) - except AttributeError: - print("AttributeError with {}".format(attr)) +import requests + +from xml.etree import cElementTree as ElementTree # for zillow API + +from .pyzillowerrors import ZillowError, ZillowFail, ZillowNoResults +from . import __version__ + + +class ZillowWrapper(object): + """This class provides an interface into the Zillow API. An API key is required to create an instance of this class: + + >>> from pyzillow.pyzillow import ZillowWrapper + >>> zillow_data = ZillowWrapper(YOUR_ZILLOW_API_KEY) + + To request data from Zillow, you can choose between: + + 1. The GetDeepSearchResults API endpoint (:class:`pyzillow.pyzillow.GetDeepSearchResults`) + which requires the following arguments: + + * A street address (e.g. ``'2114 Bigelow Ave'``) + * A ZIP code or city and state combination (e.g. ``'98109'`` or ``'Seattle, WA'``) + * Optional: Enabling or disabling Zillow Rentzestimate information in + API results (``True``/``False``) + + Example: + + >>> from pyzillow.pyzillow import ZillowWrapper, GetDeepSearchResults + >>> zillow_data = ZillowWrapper(YOUR_ZILLOW_API_KEY) + >>> deep_search_response = zillow_data.get_deep_search_results(address, + zipcode, + rentzestimate) + >>> result = GetDeepSearchResults(deep_search_response) + + 2. The GetUpdatedPropertyDetails API endpoint + (:class:`pyzillow.pyzillow.GetUpdatedPropertyDetails`) which requires a + Zillow Property ID (ZPID) as an argument. You can acquire this identifier by + accessing ``.zillow_id`` from a :class:`pyzillow.pyzillow.GetDeepSearchResults` + object. GetUpdatedPropertyDetails data is not available for all valid Zillow IDs. + + Example: + + >>> from pyzillow.pyzillow import ZillowWrapper, GetUpdatedPropertyDetails + >>> zillow_data = ZillowWrapper(YOUR_ZILLOW_API_KEY) + >>> updated_property_details_response = \ + zillow_data.get_updated_property_details(zillow_id) + >>> result = GetUpdatedPropertyDetails(updated_property_details_response) + """ + + def __init__(self, api_key: str = None): + """Constructor method + """ + self.api_key = api_key + + def get_deep_search_results( + self, address: str, zipcode: str, rentzestimate: bool = False + ): + """This method provides results from the GetDeepSearchResults API endpoint as an XML object. + + :param address: Street address to look up + :type address: str + :param zipcode: ZIP code to look up + :type zipcode: str + :param rentzestimate: Add Rent Zestimate information to result (True/False), + defaults to False + :type rentzestimate: bool, optional + :return: Result from API query + :rtype: xml.etree.ElementTree.Element + """ + url = "http://www.zillow.com/webservice/GetDeepSearchResults.htm" + + params = { + "address": address, + "citystatezip": zipcode, + "rentzestimate": str(rentzestimate).lower(), + "zws-id": self.api_key, + } + return self.get_data(url, params) + + def get_updated_property_details(self, zpid: str): + """This method provides results from the GetUpdatedPropertyDetails API endpoint as an XML object. + + :param zpid: Zillow Web Service Identifier + :type zpid: str + :return: Result from API query + :rtype: xml.etree.ElementTree.Element + """ + url = "http://www.zillow.com/webservice/GetUpdatedPropertyDetails.htm" + + params = {"zpid": zpid, "zws-id": self.api_key} + return self.get_data(url, params) + + def get_data(self, url: str, params: dict): + """This method requests data from the API endpoint specified in the url argument. It uses parameters from the params argument. + + :param url: URL of API endpoint + :type url: str + :param params: Parameters for API query + :type params: dict + :raises ZillowFail: The API endpoint could not be reached or the request + did not return valid XML + :raises ZillowError: The API endpoint responded with an error code + :raises ZillowNoResults: The request did not return any results + :return: Result from API query + :rtype: xml.etree.ElementTree.Element + """ + try: + request = requests.get( + url=url, + params=params, + headers={ + "User-Agent": "".join(["pyzillow/", __version__, " (Python)"]) + }, + ) + except ( + requests.exceptions.ConnectionError, + requests.exceptions.TooManyRedirects, + requests.exceptions.Timeout, + ): + raise ZillowFail + + try: + request.raise_for_status() + except requests.exceptions.HTTPError: + raise ZillowFail + + try: + response = ElementTree.fromstring(request.text) + except ElementTree.ParseError: + print("Zillow response is not a valid XML ({})".format(params["address"])) + raise ZillowFail + + if response.findall("message/code")[0].text != "0": + raise ZillowError(int(response.findall("message/code")[0].text)) + else: + if not response.findall("response"): + print("Zillow returned no results for ({})".format(params["address"])) + raise ZillowNoResults + return response + + +class ZillowResults(object): + """Base class for :class:`pyzillow.pyzillow.GetDeepSearchResults` + and :class:`pyzillow.pyzillow.GetUpdatedPropertyDetails`. + """ + + attribute_mapping = {} + + def get_attr(self, attr): + """ + """ + try: + return self.data.find(self.attribute_mapping[attr]).text + except AttributeError: + return None + + def __str__(self): + return self.zillow_id + + @property + def area_unit(self): + """ + lotSizeSqFt + """ + return u"SqFt" + + @property + def last_sold_price_currency(self): + """ + lastSoldPrice currency + """ + return self.data.find(self.attribute_mapping["last_sold_price"]).attrib[ + "currency" + ] + + +class GetDeepSearchResults(ZillowResults): + """Maps results from the XML data array into attributes of an instance of GetDeepSearchResults. + + An instance of ``GetDeepSearchResults`` has the following attributes: + ``.bathrooms`` + ``.bedrooms`` + ``.city`` + ``.fips_county`` + ``.graph_data_link`` + ``.home_detail_link`` + ``.home_size`` + ``.home_type`` + ``.last_sold_date`` + ``.last_sold_price`` + ``.latitude`` + ``.longitude`` + ``.map_this_home_link`` + ``.property_size`` + ``.rentzestimate_amount`` + ``.rentzestimate_last_updated`` + ``.rentzestimate_valuation_range_high`` + ``.rentzestimate_valuation_range_low`` + ``.rentzestimate_value_change`` + ``.state`` + ``.street`` + ``.tax_value`` + ``.tax_year`` + ``.total_rooms`` + ``.use_code`` + ``.year_built`` + ``.zestimate_amount`` + ``.zestimate_last_updated`` + ``.zestimate_percentile`` + ``.zestimate_valuation_range_high`` + ``.zestimate_valuation_range_low`` + ``.zestimate_value_change`` + ``.zillow_id`` + ``.zipcode`` + """ + + attribute_mapping = { + "bathrooms": "result/bathrooms", + "bedrooms": "result/bedrooms", + "city": "result/address/city", + "fips_county": "result/FIPScounty", + "graph_data_link": "result/links/graphsanddata", + "home_detail_link": "result/links/homedetails", + "home_size": "result/finishedSqFt", + "home_type": "result/useCode", + "last_sold_date": "result/lastSoldDate", + "last_sold_price": "result/lastSoldPrice", + "latitude": "result/address/latitude", + "longitude": "result/address/longitude", + "map_this_home_link": "result/links/mapthishome", + "property_size": "result/lotSizeSqFt", + "rentzestimate_amount": "result/rentzestimate/amount", + "rentzestimate_last_updated": "result/rentzestimate/last-updated", + "rentzestimate_valuation_range_high": "result/rentzestimate/valuationRange/high", + "rentzestimate_valuation_range_low": "result/rentzestimate/valuationRange/low", + "rentzestimate_value_change": "result/rentzestimate/valueChange", + "state": "result/address/state", + "street": "result/address/street", + "tax_value": "result/taxAssessment", + "tax_year": "result/taxAssessmentYear", + "total_rooms": "result/totalRooms", + "use_code": "result/useCode", + "year_built": "result/yearBuilt", + "zestimate_amount": "result/zestimate/amount", + "zestimate_last_updated": "result/zestimate/last-updated", + "zestimate_percentile": "result/zestimate/percentile", + "zestimate_valuation_range_high": "result/zestimate/valuationRange/high", + "zestimate_valuation_range_low": "result/zestimate/valuationRange/low", + "zestimate_value_change": "result/zestimate/valueChange", + "zillow_id": "result/zpid", + "zipcode": "result/address/zipcode", + } + + def __init__(self, data, *args, **kwargs): + """Constructor method + """ + self.data = data.findall("response/results")[0] + for attr in self.attribute_mapping.__iter__(): + try: + self.__setattr__(attr, self.get_attr(attr)) + except AttributeError: + print("AttributeError with {}".format(attr)) + + @property + def region_name(self): + """ + region name + """ + try: + return self.data.find("result/localRealEstate/region").attrib["name"] + except AttributeError: + return None + + @property + def region_id(self): + """ + region id + """ + try: + return self.data.find("result/localRealEstate/region").attrib["id"] + except AttributeError: + return None + + @property + def region_type(self): + """ + region type + """ + try: + return self.data.find("result/localRealEstate/region").attrib["type"] + except AttributeError: + return None + + +class GetUpdatedPropertyDetails(ZillowResults): + """Maps results from the XML data array into attributes of an instance of GetUpdatedPropertyDetails. + + An instance of ``GetUpdatedPropertyDetails`` has the following attributes: + ``.agent_name`` + ``.agent_profile_url`` + ``.appliances`` + ``.basement`` + ``.bathrooms`` + ``.bedrooms`` + ``.brokerage`` + ``.city`` + ``.cooling_system`` + ``.elementary_school`` + ``.exterior_material`` + ``.floor_material`` + ``.heating_sources`` + ``.heating_system`` + ``.high_school`` + ``.home_description`` + ``.home_detail_link`` + ``.home_info`` + ``.home_size`` + ``.home_type`` + ``.latitude`` + ``.longitude`` + ``.middle_school`` + ``.neighborhood`` + ``.num_floors`` + ``.num_rooms`` + ``.page_view_count_this_month`` + ``.page_view_count_total`` + ``.parking_type`` + ``.photo_gallery`` + ``.posting_agent`` + ``.posting_last_update`` + ``.posting_mls`` + ``.posting_status`` + ``.posting_type`` + ``.price`` + ``.property_size`` + ``.roof`` + ``.rooms`` + ``.school_district`` + ``.state`` + ``.street`` + ``.view`` + ``.year_built`` + ``.year_updated`` + ``.zillow_id`` + ``.zipcode`` + """ + + attribute_mapping = { + # attributes in common with GetDeepSearchResults + "bathrooms": "editedFacts/bathrooms", + "bedrooms": "editedFacts/bedrooms", + "city": "result/address/city", + "home_detail_link": "links/homeDetails", + "home_size": "editedFacts/finishedSqFt", + "home_type": "editedFacts/useCode", + "latitude": "address/latitude", + "longitude": "address/longitude", + "property_size": "editedFacts/lotSizeSqFt", + "state": "result/address/state", + "street": "result/address/street", + "year_built": "editedFacts/yearBuilt", + "zillow_id": "zpid", + "zipcode": "result/address/zipcode", + # new attributes in GetUpdatedPropertyDetails + "agent_name": "posting/agentName", + "agent_profile_url": "posting/agentProfileUrl", + "appliances": "editedFacts/appliances", + "basement": "editedFacts/basement", + "brokerage": "posting/brokerage", + "cooling_system": "editedFacts/coolingSystem", + "elementary_school": "elementarySchool", + "exterior_material": "editedFacts/exteriorMaterial", + "floor_material": "editedFacts/floorCovering", + "heating_sources": "editedFacts/heatingSources", + "heating_system": "editedFacts/heatingSystem", + "high_school": "highSchool", + "home_description": "homeDescription", + "home_info": "links/homeInfo", + "middle_school": "middleSchool", + "neighborhood": "neighborhood", + "num_floors": "editedFacts/numFloors", + "num_rooms": "editedFacts/numRooms", + "page_view_count_this_month": "pageViewCount/currentMonth", + "page_view_count_total": "pageViewCount/total", + "parking_type": "editedFacts/parkingType", + "photo_gallery": "links/photoGallery", + "photo_gallery": "links/photoGallery", + "posting_agent": "posting/agentName", + "posting_last_update": "posting/lastUpdatedDate", + "posting_mls": "posting/mls", + "posting_status": "posting/status", + "posting_type": "posting/type", + "price": "price", + "roof": "editedFacts/roof", + "rooms": "editedFacts/rooms", + "school_district": "schoolDistrict", + "view": "editedFacts/view", + "year_updated": "editedFacts/yearUpdated", + } + + def __init__(self, data, *args, **kwargs): + """Constructor method + """ + self.data = data.findall("response")[0] + for attr in self.attribute_mapping.__iter__(): + try: + self.__setattr__(attr, self.get_attr(attr)) + except AttributeError: + print("AttributeError with {}".format(attr)) From e468d2e7e2103179dab78d2a55fcda24a602af27 Mon Sep 17 00:00:00 2001 From: Timo Cornelius Metzger Date: Wed, 3 Jun 2020 10:33:14 -0700 Subject: [PATCH 4/6] Update docs (#31) * Updated guidelines for contributing * Added shields and maintainers * Fixed positioning of shields * Removed trailing whitespace * Fixed minor formatting issues * Added virtualenv reference Co-authored-by: Hannes Hapke --- CONTRIBUTING.rst | 55 ++++++++++++++++++++++++++---------------------- README.md | 23 +++++++++++++++++--- README.rst | 25 ++++++++++++++++++++++ 3 files changed, 75 insertions(+), 28 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index d1786a1..fb7c46f 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -22,7 +22,7 @@ If you are reporting a bug, please include: Fixing bugs ~~~~~~~~~~~ -Look through the `GitHub issues `_ for bugs. Anything tagged with "bug" +Fixes are always welcome! Look through the `GitHub issues `_ for bugs. Anything tagged with "bug" is open to whoever wants to fix it. Implementing features @@ -51,55 +51,60 @@ If you are proposing a feature: are welcome :) Getting started --------------- +--------------- Ready to contribute? Here's how to set up PyZillow for local development. -1. Fork_ the `pyzillow` repo on GitHub. -2. Clone your fork locally:: +Fork_ the `pyzillow` repo on GitHub. - $ git clone git@github.com:hanneshapke/pyzillow.git +Clone your fork locally:: -3. Create a branch for local development:: + $ git clone git@github.com:hanneshapke/pyzillow.git - $ git checkout -b name-of-your-bugfix-or-feature +Create a branch for local development:: + + $ git checkout -b name-of-your-bugfix-or-feature + +Create a virtualenv to separate your Python dependencies: + + $ virtualenv .pyzillow-env && source .pyzillow-env/bin/activate + +Configure development requirements:: + + $ make develop Now you can make your changes locally. -4. When you're done making changes, use ``tox`` to check that your changes pass style and unit - tests, including testing other Python versions:: +When you're done making changes, use `Pytest `_ to check that your changes pass style and unit tests, including testing other Python versions. You can run all tests by running pytest:: + + $ pytest - $ tox +Please lint your code before committing your changes:: -To get tox, just pip install it. + $ make lint -5. Commit your changes and push your branch to GitHub:: +Commit your changes and push your branch to GitHub:: $ git add . $ git commit -m "Your detailed description of your changes." $ git push origin name-of-your-bugfix-or-feature -6. Submit a pull request through the GitHub website. - -.. _Fork: https://github.com/hanneshapke/pyzillow/fork - Submitting a pull request ------------------------- Check that your pull request meets these guidelines before you submit it: 1. The pull request should include tests. -2. If the pull request adds functionality, the docs have to be updated. Put - your new functionality into a function with a docstring, and add the - feature to the list in README.rst. -3. The pull request should work with Python 3.6, 3.7 and 3.8. - Check https://travis-ci.org/hanneshapke/pyzillow - under pull requests for active pull requests or run the ``tox`` command and - make sure that the tests pass for all supported Python versions. +2. If the pull request adds functionality, the docs need to be updated. Include + docstrings with your new functionality (`Sphinx `_ reStructuredText) and check if you + need to update the information in the /docs/ folder. +3. The pull request should work with Python 3.6, 3.7 and 3.8. Make sure that + all tests run by pytest pass. Running a subset of tests ------------------------- -Use pytest in case you want to run a subset of tests:: +Use pytest in combination with a substring in case you want to run only specific tests instead of all available tests. +pytest will only run tests with names matching the substring:: - $ pytest test/test_pyzillow.py + $ pytest -k -v diff --git a/README.md b/README.md index 14508ef..927133c 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,31 @@ PyZillow 0.6 ============ -PyZillow is a Python wrapper for Zillow's API (http://www.zillow.com/howto/api/APIOverview.htm). With PyZillow, you can use a physical address or a Zillow ID to access real estate data from the Zillow database. +![PyPI - Downloads](https://img.shields.io/pypi/dm/pyzillow) +[![Build Status](https://travis-ci.com/hanneshapke/pyzillow.svg?branch=master)](https://travis-ci.com/hanneshapke/pyzillow) +[![Documentation Status](https://readthedocs.org/projects/pyzillow/badge/?version=latest)](https://pyzillow.readthedocs.io/en/latest/?badge=latest) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + +PyZillow is a Python wrapper for Zillow's API (). With PyZillow, you can use a physical address or a Zillow ID to access real estate data from the Zillow database. Currently, PyZillow supports the **GetDeepSearchResults** and **GetUpdatedPropertyDetails** API endpoints. +Status +------ + +PyZillow is now being actively updated again. Please allow us some time +to work through existing pull requests and issues. + Documentation ------------- -The PyZillow documentation is available here: http://pyzillow.readthedocs.org/en/latest/ +The PyZillow documentation is available here: + +Maintainers +----------- + +* Timo Cornelius Metzger +* Hannes Hapke Contributors ------------ @@ -19,4 +36,4 @@ Contributors * Miguel Paolino * Timo Cornelius Metzger -PyZillow was originally developed by Hannes Hapke and Miguel Paolino for [renooble.com](http://www.renooble.com). +PyZillow was originally developed by Hannes Hapke and Miguel Paolino for [renooble.com](). diff --git a/README.rst b/README.rst index f8adcb7..69a44ef 100644 --- a/README.rst +++ b/README.rst @@ -1,15 +1,40 @@ PyZillow 0.6 ============ +.. image:: https://img.shields.io/pypi/dm/pyzillow.svg + :target: https://pypistats.org/packages/pyzillow + :alt: PyPI - Downloads +.. image:: https://travis-ci.com/hanneshapke/pyzillow.svg?branch=master + :target: https://travis-ci.com/hanneshapke/pyzillow + :alt: Travis CI Build Status +.. image:: https://readthedocs.org/projects/pyzillow/badge/?version=latest + :target: https://pyzillow.readthedocs.io/en/latest/?badge=latest + :alt: Documentation Status +.. image:: https://img.shields.io/badge/License-MIT-yellow.svg + :target: https://opensource.org/licenses/MIT + :alt: License: MIT + PyZillow is a Python wrapper for `Zillow's API `_. With PyZillow, you can use a physical address or a Zillow ID to access real estate data from the Zillow database. Currently, PyZillow supports the **GetDeepSearchResults** and **GetUpdatedPropertyDetails** API endpoints. +Status +------ + +PyZillow is now being actively updated again. Please allow us some time +to work through existing pull requests and issues. + Documentation ------------- The PyZillow documentation is available here: http://pyzillow.readthedocs.org/en/latest/ +Maintainers +----------- + +* Timo Cornelius Metzger +* Hannes Hapke + Contributors ------------ From 94162e8cda0d1ce58022df85622bbf1715c4dc6e Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Wed, 3 Jun 2020 19:41:24 +0200 Subject: [PATCH 5/6] Update coverage from 4.5.4 to 5.1 (#28) Co-authored-by: Hannes Hapke --- requirements/test_requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/test_requirements.txt b/requirements/test_requirements.txt index c74629b..af98bf2 100644 --- a/requirements/test_requirements.txt +++ b/requirements/test_requirements.txt @@ -1,5 +1,5 @@ cov-core==1.15.0 -coverage==4.5.4 +coverage==5.1 pytest==5.4.3 pytest-cov==2.9.0 pytest-flakes==4.0.0 From fd20179e0277800d08bbfabb8fff481f20c0b49f Mon Sep 17 00:00:00 2001 From: Hannes Hapke Date: Wed, 3 Jun 2020 10:48:17 -0700 Subject: [PATCH 6/6] bump version number to 0.7 --- HISTORY.rst | 11 +++++++---- README.md | 2 +- README.rst | 2 +- pyzillow/__init__.py | 2 +- setup.py | 2 +- tox.ini | 2 +- 6 files changed, 12 insertions(+), 9 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 7e94dd7..665efe6 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,12 +7,15 @@ Version history 0.7.0 (2020-05-30) ++++++++++++++++++ -* * Updated error handling, too many request error, Github issue 18 -* Pinned python-coveralls to latest version 2.9.3 #27 -* Added `zestimate_valuation_range_low` as an attribute GetDeepSearchResults. `zestimate_valuationRange_low` was deprecated. () +* Updated error handling, too many request error, Github issue 18 +* Updated error handling, error 6 (Github issue 6) +* Pinned python-coveralls to latest version 2.9.3 (#27) * Added posting details to GetUpdatedPropertyDetails result (#10) +* Updated pytest version (#32) +* Updated coverage version (#28) +* Added support for additional API fields (#16) -Thanks to Alexandra M. Chace, Marilyn Chace, Evan Pete Walsh, Stephen Holsapple +Thanks to Alexandra M. Chace (#16), Marilyn Chace, Evan Pete Walsh (#11), Stephen Holsapple (#10), ZAD-Man (Issue #6) 0.6.0 (2020-05-28) diff --git a/README.md b/README.md index 927133c..6a1f032 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -PyZillow 0.6 +PyZillow 0.7 ============ ![PyPI - Downloads](https://img.shields.io/pypi/dm/pyzillow) diff --git a/README.rst b/README.rst index 69a44ef..6c80bef 100644 --- a/README.rst +++ b/README.rst @@ -1,4 +1,4 @@ -PyZillow 0.6 +PyZillow 0.7 ============ .. image:: https://img.shields.io/pypi/dm/pyzillow.svg diff --git a/pyzillow/__init__.py b/pyzillow/__init__.py index 53daf28..f2f6b1d 100644 --- a/pyzillow/__init__.py +++ b/pyzillow/__init__.py @@ -1,3 +1,3 @@ __author__ = "Hannes Hapke" __email__ = "hannes.hapke@gmail.com" -__version__ = "0.6.1" +__version__ = "0.7.0" diff --git a/setup.py b/setup.py index e815f89..a20a458 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ setup( name="pyzillow", - version="0.6.1", + version="0.7.0", description="Python API wrapper for Zillow's API", long_description=readme + "\n\n" + doclink + "\n\n" + history, author="Hannes Hapke", diff --git a/tox.ini b/tox.ini index 2e00b33..a81ea4f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py26, py27, py33, style, docs +envlist = py36, py37, py38, style, docs [testenv] setenv =