Browse Source

Improved Logging. Automated DB connection and closing. Improved .conf controls for emails and live_google.

Miek Stagl 5 years ago
parent
commit
5dfd904a8f
3 changed files with 67 additions and 46 deletions
  1. 6 6
      CHANGELOG
  2. 54 37
      index.py
  3. 7 3
      landsearch.conf

+ 6 - 6
CHANGELOG

@@ -11,7 +11,7 @@ Search critiera can only be modified in the index.py code by adding criteria to
 index.py broke on multiple instances of the word 'address' on page 2 of the georgiamls listings.  Added fix to
 index.py broke on multiple instances of the word 'address' on page 2 of the georgiamls listings.  Added fix to
 set the address attribute to only the first instance of the word.
 set the address attribute to only the first instance of the word.
 
 
-[0.1.1] Update .1 WIP
+[0.1.1] Update .1
 
 
 applied hotfix 1
 applied hotfix 1
 Added ConfigParser to read landsearch.conf file and gather search criteria from this file.
 Added ConfigParser to read landsearch.conf file and gather search criteria from this file.
@@ -20,11 +20,9 @@ Added checktype to Search class.  This ensures that the word None in landsearch.
 Changed email module.  Forces email in email=True in conf file.  Sends nicely formatted email for empty results.
 Changed email module.  Forces email in email=True in conf file.  Sends nicely formatted email for empty results.
 Added total new properties found in email subject line.
 Added total new properties found in email subject line.
 Improved Logging.
 Improved Logging.
-    TODO - create logging class and check if instance created in each module.
-    This way
-
-FUTURE - allow logging to both logfile AND console
-FUTURE - add .conf section for results.py to aid in results report generation.
+Removed need for user to call connect_db() and close_db() in user code.
+Added live_google to .conf file to allow user to select wether or not to call Google API.
+Applied Update1 - added datestamp to logging output
 
 
 
 
 FUTURE GOALS
 FUTURE GOALS
@@ -40,3 +38,5 @@ FUTURE GOALS
   - Show results within X time/dist to school/work
   - Show results within X time/dist to school/work
   - Show various columns in output
   - Show various columns in output
   - Output results to csv.
   - Output results to csv.
+- Allow logging to both console and logfile
+- Add sectionin .conf file for results.py report generation

+ 54 - 37
index.py

@@ -16,19 +16,12 @@ import logging
 
 
 ### TO DO ###
 ### TO DO ###
 #
 #
-#  Print useful reports (land only, house and land, etc)
 #  Check if db entries no longer appear online (mark expired)
 #  Check if db entries no longer appear online (mark expired)
 #  When checking online from various sites, check if address already exists in db
 #  When checking online from various sites, check if address already exists in db
 #    - if so, warn user and do not add
 #    - if so, warn user and do not add
-#  Add date_added to initial entries
 #  Check results against database for changes
 #  Check results against database for changes
 #   - update and add/change date_modified 
 #   - update and add/change date_modified 
-#  Add argument to run update query when results.py is calles
-#  Add database column to hold parcel number.  Make links to GIS servers 
-#
-#  IDENTIFY NEW PROPERTIES!!
-#
-#  Automate db opening and closing when calling dbinsert()
+#  Add database column to hold parcel number.  Make links to GIS servers
 #
 #
 #############
 #############
 
 
@@ -54,6 +47,7 @@ class Property:
         self.description = description
         self.description = description
         self.link = link
         self.link = link
 
 
+
 class Parameters:
 class Parameters:
     '''Parameters taken from config file'''
     '''Parameters taken from config file'''
 
 
@@ -69,15 +63,18 @@ class Parameters:
         except Exception as err:
         except Exception as err:
             print(err, "Using default search Parameters")
             print(err, "Using default search Parameters")
 
 
+
 class Mylogger:
 class Mylogger:
     ''' Logging tool for this session'''
     ''' Logging tool for this session'''
+
     def __init__(self):
     def __init__(self):
         log_params = Parameters().log_params
         log_params = Parameters().log_params
 
 
-        filename=log_params.get('log_file')
-        level=int(log_params.get('logging_level', str('30')))
-        format='%(asctime)s %(levelname)-8s %(message)s'
-        datefmt='%Y-%m-%d %H:%M:%S'
+        filename = log_params.get('log_file')
+        level = int(log_params.get('logging_level', str('30')))
+        format = '%(asctime)s %(levelname)-8s %(message)s'
+        datefmt = '%Y-%m-%d %H:%M:%S'
+
 
 
 class Search:
 class Search:
     '''Universal Search Criteria'''
     '''Universal Search Criteria'''
@@ -140,18 +137,20 @@ class Search:
         ## FOR TESTING, PRINT ALL ATTRIBUTES OF SEARCH ##
         ## FOR TESTING, PRINT ALL ATTRIBUTES OF SEARCH ##
         logging.debug(vars(self))
         logging.debug(vars(self))
 
 
+
 class ImproperSearchError(Exception):
 class ImproperSearchError(Exception):
     def __init__(self, search, message="Improper Search.  Must use instance of Search class"):
     def __init__(self, search, message="Improper Search.  Must use instance of Search class"):
         self.search = search
         self.search = search
         self.message = message
         self.message = message
         super().__init__(self.message)
         super().__init__(self.message)
 
 
+
 class MLSDATA:
 class MLSDATA:
     """Fetches and stores MLS Data
     """Fetches and stores MLS Data
      Currently only supports GeorgiaMLS.com (GMLS)"""
      Currently only supports GeorgiaMLS.com (GMLS)"""
     counties = ['Gwinnett', 'Barrow', 'Hall', 'Jackson', 'Walton']
     counties = ['Gwinnett', 'Barrow', 'Hall', 'Jackson', 'Walton']
     GoogleAPIKey = 'AIzaSyAXAnpBtjv760W8YIPqKZ0dFXpwAaZN7Es'
     GoogleAPIKey = 'AIzaSyAXAnpBtjv760W8YIPqKZ0dFXpwAaZN7Es'
-    live_google = True
+    # live_google = False
 
 
     def __init__(self, mlstype):
     def __init__(self, mlstype):
         self.parameters = Parameters()
         self.parameters = Parameters()
@@ -161,6 +160,7 @@ class MLSDATA:
         self.cnx = ''
         self.cnx = ''
         self.new_listings = []
         self.new_listings = []
         self.email = self.parameters.search_params.getboolean('email')
         self.email = self.parameters.search_params.getboolean('email')
+        self.live_google = self.parameters.search_params.getboolean('live_google')
         print('Email  ' + str(self.email))
         print('Email  ' + str(self.email))
 
 
     def stringbuilder(self, search: Search, county):
     def stringbuilder(self, search: Search, county):
@@ -297,17 +297,25 @@ class MLSDATA:
                 print("Scanning for results in " + county + " using the " + self.mlstype.upper() + " database.")
                 print("Scanning for results in " + county + " using the " + self.mlstype.upper() + " database.")
                 if self.mlstype == 'gmls':
                 if self.mlstype == 'gmls':
                     list = self.gmlsparser(self.stringbuilder(search, county), county)
                     list = self.gmlsparser(self.stringbuilder(search, county), county)
-                logging.info("Completed search in " + county + " county. " + str(len(list)) + " total properties scanned.")
+                logging.info(
+                    "Completed search in " + county + " county. " + str(len(list)) + " total properties scanned.")
                 return list
                 return list
         else:
         else:
             raise ImproperSearchError(search)
             raise ImproperSearchError(search)
 
 
-    def checkdb(self, criteria_dict):
+    def check_db(self, criteria_dict):
         """Check dictionary of critera against database.
         """Check dictionary of critera against database.
        Currently accepts keys: MLS, title, address (street number/name, not city/state/zip).
        Currently accepts keys: MLS, title, address (street number/name, not city/state/zip).
        Returns True if records exists."""
        Returns True if records exists."""
-        if self.cursor:  ## Check if DB is connected
-            for criteria in criteria_dict:
+        if not self.cursor:  ## Check if DB is connected
+            try:
+                self.connect_db()
+                logging.debug("No Database Connection.  Connecting to DB in check_db function.")
+            except Exception as err:
+                print("Could not connect to Database. " + str(err))
+                logging.warning("Could not connect to Database. " + str(err))
+                return 0
+        for criteria in criteria_dict:
                 ## Determine criteria passed, and execute queries for each
                 ## Determine criteria passed, and execute queries for each
                 if criteria == 'MLS':
                 if criteria == 'MLS':
                     self.cursor.execute("SELECT COUNT(*) FROM properties WHERE MLS = %(MLS)s GROUP BY id",
                     self.cursor.execute("SELECT COUNT(*) FROM properties WHERE MLS = %(MLS)s GROUP BY id",
@@ -323,11 +331,9 @@ class MLSDATA:
                     if self.cursor.rowcount > 0: return self.cursor.rowcount  # stop for loop if match already found.
                     if self.cursor.rowcount > 0: return self.cursor.rowcount  # stop for loop if match already found.
                 else:
                 else:
                     print("Cannot search on parameter: " + criteria)
                     print("Cannot search on parameter: " + criteria)
-            return self.cursor.rowcount
-        else:
-            print("Database is not connected or cursor not filled.  Use function 'connectdb()' to establish")
+        return self.cursor.rowcount
 
 
-    def getGoogle(self, property):
+    def get_google(self, property):
         """Supplies date from Google Distance Matrix API to populate
         """Supplies date from Google Distance Matrix API to populate
       distance_to_work
       distance_to_work
       time_to_work
       time_to_work
@@ -362,6 +368,16 @@ class MLSDATA:
 
 
     def insertrecord(self, property, work_address=None, school_address=None):
     def insertrecord(self, property, work_address=None, school_address=None):
         """Inserts record into database.  Takes argument Property class object."""
         """Inserts record into database.  Takes argument Property class object."""
+        if not self.cursor:
+            print("not self.cursor")
+            logging.debug("MYSQL connection not established.  Trying to connect...")
+            try:
+                self.connect_db()
+                logging.debug("Connecting to DB in insertrecord fucntion.")
+            except Exception as err:
+                print("Could not connect to Database. " + str(err))
+                logging.warning("Could not connect to Database. " + str(err))
+                return
         if self.cursor:
         if self.cursor:
             criteria_dict = property.__dict__
             criteria_dict = property.__dict__
             criteria_dict['Date_Added'] = str(datetime.date.today())
             criteria_dict['Date_Added'] = str(datetime.date.today())
@@ -373,7 +389,7 @@ class MLSDATA:
                 self.cursor.execute(qry)
                 self.cursor.execute(qry)
                 self.cnx.commit()
                 self.cnx.commit()
                 print("Inserted " + criteria_dict['MLS'] + " | " + criteria_dict['address'] + " into database.")
                 print("Inserted " + criteria_dict['MLS'] + " | " + criteria_dict['address'] + " into database.")
-                logging.debug("Inserted " + criteria_dict['MLS'] + " | " + criteria_dict['address'] + " into database.")
+                logging.info("Inserted " + criteria_dict['MLS'] + " | " + criteria_dict['address'] + " into database.")
             except Exception as e:
             except Exception as e:
                 print("Could not insert " + criteria_dict['address'] + " into database.  Database connection error.")
                 print("Could not insert " + criteria_dict['address'] + " into database.  Database connection error.")
                 logging.warning("Could not insert " + criteria_dict['address'] + "into database.  Database connection "
                 logging.warning("Could not insert " + criteria_dict['address'] + "into database.  Database connection "
@@ -381,27 +397,33 @@ class MLSDATA:
                 logging.warning(str(e))
                 logging.warning(str(e))
         else:
         else:
             print("Database is not connected or cursor not filled.  Use function 'connectdb()' to establish")
             print("Database is not connected or cursor not filled.  Use function 'connectdb()' to establish")
+            print(str(self.cursor))
 
 
-    def connectdb(self, host='192.168.100.26', user='landsearchuser', password='1234', database='landsearch'):
+    def connect_db(self, host='192.168.100.26', user='landsearchuser', password='1234', database='landsearch'):
         """Connects to database and returns a cursor object"""
         """Connects to database and returns a cursor object"""
         self.cnx = mysql.connector.connect(host=host, user=user, password=password, database=database, buffered=True)
         self.cnx = mysql.connector.connect(host=host, user=user, password=password, database=database, buffered=True)
         self.cursor = self.cnx.cursor()
         self.cursor = self.cnx.cursor()
         return self.cursor
         return self.cursor
 
 
-    def closedb(self):
+    def close_db(self):
         """Cleanly close the db."""
         """Cleanly close the db."""
         self.cursor.close()
         self.cursor.close()
         self.cnx.close()
         self.cnx.close()
 
 
-    def dbinsert(self, properties: list):
+    def db_insert(self, properties: list):
         """Inserts records into database.  Takes list of Property class objects"""
         """Inserts records into database.  Takes list of Property class objects"""
         if not properties == None:
         if not properties == None:
             if not isinstance(properties, list):
             if not isinstance(properties, list):
                 raise TypeError('type list required')
                 raise TypeError('type list required')
             for property in properties:
             for property in properties:
-                if not self.checkdb({'MLS': property.MLS, 'address': property.address}):
-                    if self.live_google: self.getGoogle(
+                if not self.check_db({'MLS': property.MLS, 'address': property.address}):
+                    if self.live_google: self.get_google(
                         property)  ## <- This will populate distance and time fields if set TRUE
                         property)  ## <- This will populate distance and time fields if set TRUE
+                    else:
+                        print("NOT fetching google data.  Suppressed by settings in landsearch.conf")
+                        logging.warning("NOT fetching google data for " + property.address + ".  Suppressed by "
+                                                                                             "settings in "
+                                                                                             "landsearch.conf")
                     self.insertrecord(property)
                     self.insertrecord(property)
                     self.new_listings.append(property)
                     self.new_listings.append(property)
                 else:
                 else:
@@ -414,9 +436,6 @@ class MLSDATA:
         logging.info("Database Update Complete.")
         logging.info("Database Update Complete.")
         logging.info(str(len(self.new_listings)) + " new listings found.")
         logging.info(str(len(self.new_listings)) + " new listings found.")
 
 
-    def alerts(self):
-        pass
-
     def email_results(self):
     def email_results(self):
         global mymail
         global mymail
         sendto = ['stagl.mike@gmail.com', 'M_Stagl@hotmail.com']
         sendto = ['stagl.mike@gmail.com', 'M_Stagl@hotmail.com']
@@ -424,10 +443,9 @@ class MLSDATA:
             ''' Send some kind of email! '''
             ''' Send some kind of email! '''
             # If there are new listings, populate email ##
             # If there are new listings, populate email ##
             if len(self.new_listings) > 0:
             if len(self.new_listings) > 0:
-                logging.debug("email_results" + str(self.email))
                 body = ''
                 body = ''
                 data = []
                 data = []
-                subj = "New Real Estate Listings for " + str(datetime.date.today())
+                subj = str(len(self.new_listings)) + " New Real Estate Listings for " + str(datetime.date.today())
                 for listing in self.new_listings:
                 for listing in self.new_listings:
                     row = []
                     row = []
                     body += listing.MLS + " | " + listing.address + " | " + listing.acres + " | " + listing.price + " | " + listing.link + "\n"
                     body += listing.MLS + " | " + listing.address + " | " + listing.acres + " | " + listing.price + " | " + listing.link + "\n"
@@ -464,6 +482,7 @@ class MLSDATA:
             print("Suppressing email based on landsearch.conf preferences.")
             print("Suppressing email based on landsearch.conf preferences.")
             logging.warning("Suppressing email based on landsearch.conf preferences.")
             logging.warning("Suppressing email based on landsearch.conf preferences.")
 
 
+
 if __name__ == '__main__':
 if __name__ == '__main__':
 
 
     gmls = MLSDATA('GMLS')  # Create MLSDATA object
     gmls = MLSDATA('GMLS')  # Create MLSDATA object
@@ -487,10 +506,8 @@ if __name__ == '__main__':
             for listing in mydata:
             for listing in mydata:
                 myresults.append(listing)
                 myresults.append(listing)
 
 
-    #  print(len(myresults))
-    #  print(myresults[0].address)
-    gmls.connectdb()
-    gmls.dbinsert(myresults)
-    gmls.closedb()
+    #gmls.connectdb()
+    gmls.db_insert(myresults)
+    #gmls.closedb()
 
 
     gmls.email_results()
     gmls.email_results()

+ 7 - 3
landsearch.conf

@@ -1,6 +1,6 @@
 [Search]
 [Search]
 ## Accepts Gwinnett, Ballow, Hall, Jackson, and/or Walton.  Seperate multiple counties with comma.  Default is all 5 counties
 ## Accepts Gwinnett, Ballow, Hall, Jackson, and/or Walton.  Seperate multiple counties with comma.  Default is all 5 counties
-county = Barrow, Hall
+county = Barrow
 
 
 ## Accepts farm, house, and/or land.  Seperate multiples with comman.  Default is all 3
 ## Accepts farm, house, and/or land.  Seperate multiples with comman.  Default is all 3
 ## land typically refers to land-lots
 ## land typically refers to land-lots
@@ -16,9 +16,13 @@ type = farm
 ; upper_bedrooms = 10
 ; upper_bedrooms = 10
 
 
 ## It true, sends email of results (whether or not new listings are found)
 ## It true, sends email of results (whether or not new listings are found)
-email = false
+email = true
+
+## Live_google will fetch actual time-to-school and time-to-work results from Google API.
+## There may be a cost for this service, thus it can be turned off for testing.
+live_google = True
 
 
 [Logging]
 [Logging]
 log_file = landsearch.log
 log_file = landsearch.log
 ## log level accepts 10, 20, 30, 40, 50.  10 - Debug (most verbose) | 50 - Warning (least verbose)
 ## log level accepts 10, 20, 30, 40, 50.  10 - Debug (most verbose) | 50 - Warning (least verbose)
-logging_level = 20
+logging_level = 10