SACA Simple Address Checking Application perherman

by Hermansson P

Description

'' #TEST form = tk.Frame() label = tk.Label(form, text='Name') name_input = tk.Entry(form) label.grid(row=0, column=0) name_input.grid(row=1, column=0) ''' from datetime import datetime import os import csv import tkinter as tk from tkinter import ttk from tkinter import messagebox from tkinter import scrolledtext from decimal import Decimal, InvalidOperation import requests import api import pandas as pd import googletrans from googletrans import Translator api_key = '' ''' FAIR USE DISCLAIMEr Since I am new to programming and only learnt basic Python I have taken the basic code for handling fields and labels (the classes) from the book: Chapter 1-4 from Python GUI Programming with Tkinter, by Alan D. Moore, 2018 (Packt Publishing) The DataRecordForm class and Application class have been totally re-written and adapted in order to fit the requirements of the Simple address checking application. ''' ################## # Widget Classes # ################## class ValidatedMixin: """Adds a validation functionality to an input widget""" def __init__(self, *args, error_var=None, **kwargs): self.error = error_var or tk.StringVar() super().__init__(*args, **kwargs) vcmd = self.register(self._validate) invcmd = self.register(self._invalid) self.config( validate='all', validatecommand=(vcmd, '%P', '%s', '%S', '%V', '%i', '%d'), invalidcommand=(invcmd, '%P', '%s', '%S', '%V', '%i', '%d') ) def _toggle_error(self, on=False): self.config(foreground=('red' if on else 'black')) def _validate(self, proposed, current, char, event, index, action): """The validation method. Don't override this, override _key_validate, and _focus_validate """ self._toggle_error(False) self.error.set('') valid = True if event == 'focusout': valid = self._focusout_validate(event=event) elif event == 'key': valid = self._key_validate( proposed=proposed, current=current, char=char, event=event, index=index, action=action ) return valid def _focusout_validate(self, **kwargs): return True def _key_validate(self, **kwargs): return True def _invalid(self, proposed, current, char, event, index, action): if event == 'focusout': self._focusout_invalid(event=event) elif event == 'key': self._key_invalid( proposed=proposed, current=current, char=char, event=event, index=index, action=action ) def _focusout_invalid(self, **kwargs): """Handle invalid data on a focus event""" self._toggle_error(True) def _key_invalid(self, **kwargs): """Handle invalid data on a key event. By default we want to do nothing """ pass def trigger_focusout_validation(self): valid = self._validate('', '', '', 'focusout', '', '') if not valid: self._focusout_invalid(event='focusout') return valid class DateEntry(ValidatedMixin, ttk.Entry): def _key_validate(self, action, index, char, **kwargs): valid = True if action == '0': # This is a delete action valid = True elif index in ('0', '1', '2', '3', '5', '6', '8', '9'): valid = char.isdigit() elif index in ('4', '7'): valid = char == '-' else: valid = False return valid def _focusout_validate(self, event): valid = True if not self.get(): self.error.set('A value is required') valid = False try: datetime.strptime(self.get(), '%Y-%m-%d') except ValueError: self.error.set('Invalid date') valid = False return valid class RequiredEntry(ValidatedMixin, ttk.Entry): def _focusout_validate(self, event): valid = True if not self.get(): valid = False self.error.set('A value is required') return valid class ValidatedCombobox(ValidatedMixin, ttk.Combobox): def _key_validate(self, proposed, action, **kwargs): valid = True # if the user tries to delete, # just clear the field if action == '0': self.set('') return True # get our values list values = self.cget('values') # Do a case-insensitve match against the entered text matching = [ x for x in values if x.lower().startswith(proposed.lower()) ] if len(matching) == 0: valid = False elif len(matching) == 1: self.set(matching[0]) self.icursor(tk.END) valid = False return valid def _focusout_validate(self, **kwargs): valid = True if not self.get(): valid = False self.error.set('A value is required') return valid class ValidatedSpinbox(ValidatedMixin, tk.Spinbox): def __init__(self, *args, min_var=None, max_var=None, focus_update_var=None, from_='-Infinity', to='Infinity', **kwargs): super().__init__(*args, from_=from_, to=to, **kwargs) self.resolution = Decimal(str(kwargs.get('increment', '1.0'))) self.precision = self.resolution.normalize().as_tuple().exponent # there should always be a variable, # or some of our code will fail self.variable = kwargs.get('textvariable') or tk.DoubleVar() if min_var: self.min_var = min_var self.min_var.trace('w', self._set_minimum) if max_var: self.max_var = max_var self.max_var.trace('w', self._set_maximum) self.focus_update_var = focus_update_var self.bind('', self._set_focus_update_var) def _set_focus_update_var(self, event): value = self.get() if self.focus_update_var and not self.error.get(): self.focus_update_var.set(value) def _set_minimum(self, *args): current = self.get() try: new_min = self.min_var.get() self.config(from_=new_min) except (tk.TclError, ValueError): pass if not current: self.delete(0, tk.END) else: self.variable.set(current) self.trigger_focusout_validation() def _set_maximum(self, *args): current = self.get() try: new_max = self.max_var.get() self.config(to=new_max) except (tk.TclError, ValueError): pass if not current: self.delete(0, tk.END) else: self.variable.set(current) self.trigger_focusout_validation() def _key_validate(self, char, index, current, proposed, action, **kwargs): valid = True min_val = self.cget('from') max_val = self.cget('to') no_negative = min_val >= 0 no_decimal = self.precision >= 0 if action == '0': return True # First, filter out obviously invalid keystrokes if any([ (char not in ('-1234567890.')), (char == '-' and (no_negative or index != '0')), (char == '.' and (no_decimal or '.' in current)) ]): return False # At this point, proposed is either '-', '.', '-.', # or a valid Decimal string if proposed in '-.': return True # Proposed is a valid Decimal string # convert to Decimal and check more: proposed = Decimal(proposed) proposed_precision = proposed.as_tuple().exponent if any([ (proposed > max_val), (proposed_precision < self.precision) ]): return False return valid def _focusout_validate(self, **kwargs): valid = True value = self.get() min_val = self.cget('from') max_val = self.cget('to') try: value = Decimal(value) except InvalidOperation: self.error.set('Invalid number string: {}'.format(value)) return False if value < min_val: self.error.set('Value is too low (min {})'.format(min_val)) valid = False if value > max_val: self.error.set('Value is too high (max {})'.format(max_val)) return valid ################## # Module Classes # ################## class LabelInput(tk.Frame): """A widget containing a label and input together.""" def __init__(self, parent, label='', input_class=ttk.Entry, input_var=None, input_args=None, label_args=None, **kwargs): super().__init__(parent, **kwargs) input_args = input_args or {} label_args = label_args or {} self.variable = input_var if input_class in (ttk.Checkbutton, ttk.Button, ttk.Radiobutton): input_args["text"] = label input_args["variable"] = input_var else: self.label = ttk.Label(self, text=label, **label_args) self.label.grid(row=0, column=0, sticky=(tk.W + tk.E)) input_args["textvariable"] = input_var self.input = input_class(self, **input_args) self.input.grid(row=1, column=0, sticky=(tk.W + tk.E)) self.columnconfigure(0, weight=1) self.error = getattr(self.input, 'error', tk.StringVar()) self.error_label = ttk.Label(self, textvariable=self.error) self.error_label.grid(row=2, column=0, sticky=(tk.W + tk.E)) def grid(self, sticky=(tk.E + tk.W), **kwargs): super().grid(sticky=sticky, **kwargs) def get(self): if self.variable: return self.variable.get() elif type(self.input) == tk.Text: return self.input.get('1.0', tk.END) else: return self.input.get() def set(self, value, *args, **kwargs): if type(self.variable) == tk.BooleanVar: self.variable.set(bool(value)) elif self.variable: self.variable.set(value, *args, **kwargs) elif type(self.input).__name__.endswith('button'): if value: self.input.select() else: self.input.deselect() elif type(self.input) == tk.Text: self.input.delete('1.0', tk.END) self.input.insert('1.0', value) else: self.input.delete(0, tk.END) self.input.insert(0, value) class DataRecordForm(tk.Frame): """The input form for our widgets""" def __init__(self, parent, *args, **kwargs): super().__init__(parent, *args, **kwargs) # A dict to keep track of input widgets self.inputs = {} # Build the form # recordinfo section recordinfo = tk.LabelFrame(self, text="Address Information") # line 1 self.inputs['countrycode'] = LabelInput( recordinfo, "Country Code", input_class=ValidatedCombobox, input_var=tk.StringVar(), input_args = {"values": ["SE", "NO", "DK", "FI"]} ) self.inputs['countrycode'].grid(row=0, column=0) self.inputs['street'] = LabelInput( recordinfo, "Postal Address", input_class=RequiredEntry, input_var=tk.StringVar() ) self.inputs['street'].grid(row=0, column=1,sticky="we") # line 2 self.inputs['postalcode'] = LabelInput( recordinfo, "Postal Code", input_class=ValidatedCombobox, ##dropdown list of zip codes, but it is slow! input_var=tk.StringVar(), input_args={"values": [str(x) for x in range(10000, 99999, 1)]} #input_class = ValidatedSpinbox, #input_var = tk.IntVar(), #input_args = {"from_": '10000', "to": '99999', "increment": '1'} ) self.inputs['postalcode'].grid(row=1, column=0) self.inputs['locality'] = LabelInput( recordinfo, "Locality", input_class=RequiredEntry, input_var=tk.StringVar() ) self.inputs['locality'].grid(row=1, column=1) recordinfo.grid(row=1, column=0, sticky="we") # default the form self.reset() def get(self): """Retrieve data from form as a dict""" # We need to retrieve the data from Tkinter variables # and place it in regular Python objects data = {} for key, widget in self.inputs.items(): data[key] = widget.get() return data def reset(self): """Resets the form entries""" # gather the default entered value c_code = self.inputs['countrycode'].get() # clear all values for widget in self.inputs.values(): widget.set('') self.inputs['countrycode'].input.focus() if c_code not in ('',): self.inputs['countrycode'].set(c_code) self.inputs['street'].input.focus() def get_errors(self): """Get a list of field errors in the form""" errors = {} for key, widget in self.inputs.items(): if hasattr(widget.input, 'trigger_focusout_validation'): widget.input.trigger_focusout_validation() if widget.error.get(): errors[key] = widget.error.get() return errors class Application(tk.Tk): """Application root window""" #modified and adapted to work with checking postal addresses at geposit.se def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.title("SACA - Simple Address Checking Application") self.resizable(width=True, height=True) ttk.Label(self, text="SACA - Simple Address Checking Application", font=("TkDefaultFont", 16)).grid(row=0) self.records_saved = 0 self.records_checked = 0 self.record_correct = tk.StringVar() self.record_correct.set('disabled') # parameter in order to check if record is correct before saving. self.recordform = DataRecordForm(self) self.recordform.grid(row=1, padx=20) self.checkbutton = ttk.Button(self, text="Check", command=self.on_check) self.checkbutton.grid(sticky="e",row=2, column=0, padx=10, pady=5) button_state = self.record_correct.get() # print(button_state + 'after') #testing if the button_state has changed after check button self.savebutton = ttk.Button(self, text="Save", state = button_state, command=self.on_save) self.savebutton.grid(sticky="e", row=2, column=1, padx=10, pady=5) self.samplesbutton =ttk.Button(self, text="Show Samples", command=self.on_show_samples) self.samplesbutton.grid(sticky="e",row=2, column=2, padx=10, pady=15) self.savedbutton =ttk.Button(self, text="Show Saved", command=self.on_show_saved) self.savedbutton.grid(sticky="e",row=2, column=3, padx=10, pady=15) # status bar self.status = tk.StringVar() self.statusbar = ttk.Label(self, textvariable=self.status) self.statusbar.grid(sticky="w", row=3, padx=10) def on_check(self): '''Checks if errors in fields, takes data and appends definition of format string=json, and appends string with API-key''' errors = self.recordform.get_errors() if errors: self.status.set( "Cannot check, error in fields: {}" .format(', '.join(errors.keys())) ) return False data = self.recordform.get() #print(data) # print to test function during development #fetch api_key from file # Milestone 1b: hash the key api_key = api.key # append format (json) and API-key to data in order to get the call to succeed. data.update({'response_format': 'json' , 'api_key': api_key}) #add country code from form in order to put it at the end of the URL in request.post() c_code = data['countrycode'] c_code = c_code.lower() #print(c_code) test lower #delete counctrycode element from data del data['countrycode'] #print(data) #test to see if it appends correctly self.records_checked += 1 #add country code to URL #use data from record to check address with geposit.se response = requests.post('https://valid.geposit.se/1.7/validate/address/'+c_code, data=data) response.raise_for_status() #receive data back from geposit.se and assign it to data data = response.json() if ((int)(data['response']['is_valid']) == 1): #print("Address is correct") # testing self.status.set("Address is correct. {} records checked this session".format(self.records_checked)) # print(data) testing self.savebutton['state'] = tk.NORMAL else: #print("Address is incorrect") # print to test function during development self.savebutton['state'] = tk.DISABLED # print(data) #testing error=str(data['response']['errors']) translator = Translator() translated = translator.translate(text=error, src='sv') self.status.set("Address is incorrect, Error: " + translated.text + "\n" + "{} records checked this session".format(self.records_checked)) #print("Errors in address") # print to test function during development #print(data['response']['errors'])# print to test function during development suggestions = data['response']['suggestions'] suggest = suggestions[0] # take out dictionary from list # print(suggestions) # testing #print(suggest) #testing street = suggest.get('street') + ' ' + suggest.get('street_number') + '' + suggest.get( 'extra_number') + '' + suggest.get('letter') postalcode = suggest.get('postalcode') locality = suggest.get('locality') suggested_address = street + ' ' + postalcode + ' ' + locality #print(street) #testing #print(postalcode) # testing #print(locality) # testing messagebox.showinfo("Try the following address:", suggested_address) #print("Suggestion(s) to use instead:") #print(data['response']['suggestions']) # this I want to Display on separate window if possible def on_save(self): """Handles save button clicks""" # Check for errors first errors = self.recordform.get_errors() if errors: self.status.set( "Cannot save, error in fields: {}" .format(', '.join(errors.keys())) ) return False # save to a hardcoded filename with a datestring. # If it doesnt' exist, create it, # otherwise just append to the existing file datestring = datetime.today().strftime("%Y-%m-%d") filename = "addresses_{}.csv".format(datestring) newfile = not os.path.exists(filename) data = self.recordform.get() #print(data) # print to test function during development with open(filename, 'a') as fh: csvwriter = csv.DictWriter(fh, fieldnames=data.keys()) if newfile: csvwriter.writeheader() csvwriter.writerow(data) self.records_saved += 1 self.status.set( "{} records saved this session".format(self.records_saved)) self.recordform.reset() self.savebutton['state'] = tk.DISABLED def on_show_saved(self): '''opens text widget to show saved correct address''' window = tk.Tk() window.title("Saved correct addresses") window.geometry('500x400+805+50') txt = scrolledtext.ScrolledText(window, width=100, height=100) txt.grid(column=1, row=0) datestring = datetime.today().strftime("%Y-%m-%d") filename = "addresses_{}.csv".format(datestring) saved_txt = pd.read_csv(filename, delimiter=",", encoding="ISO-8859-1") txt.insert('insert', saved_txt) def on_show_samples(self): '''opens text widget to show samples of address to use for testing''' window = tk.Tk() window.title("A sample of Swedish addresses (correct and incorrect)") window.geometry('500x400+300+350') txt = scrolledtext.ScrolledText(window, width=100, height=100) txt.grid(column=1, row=0) sample_txt = pd.read_csv("sample_addresses.csv", delimiter=",", encoding="ISO-8859-1") txt.insert('insert', sample_txt) if __name__ == "__main__": app = Application() app.mainloop() I have solved a real life problem that I have: checking the validity of a Swedish address against the internet based, official database of Swedish addresses. The problem is that the National Postal Services often change the postal codes (Zip codes) and people often give the wrong postal codes when giving their adresses, so before adding them to a CRM-Database it is good to check the validity of the address. I enclose the main file: project.py, and api.py (containing the API-authentification string), sampleaddresses. csv (containing a number of sample adresses for testing purposes for people who don´t have Swedish addresses), a readme.rst file, and a specification project.rst file. I couldn´t upload the files, so I just put the project.py in the code window. contents of api.key: key = '3bb5596dd455959defeb3cd2085c871e' Converted to exe-program: https://github.com/perherman/project_SACA/blob/master/dist/project.rar