I will preface this by saying this is going to be a bit long. I have a few code piece to post, but I will try and only post the relevant pieces unless someone requests all of it.
I am trying to build a small UI for updating some settings and then displaying some intake data on a raspberrypi. Python version is 3.9.2. The most logical way to save the settings I thought would be building JSON and then saving it to a file.
I am struggling to get the json_data dictionary to update and retain the settings. I got rid of the __init__ for some tries using @classmethod with cls instead of self. I tried @staticmethod, but that didn't seem right based on what I think I understand about that decorator. I tried using args, **args, **kwargs into the function.
The Settings.update_settings() is called from multiple classes in different .py files at different times to update the settings. I have been able to get the json_data to update, but the problem is after each call to update_settings the json_data is reset when using classmethod/staticmethod. When using self it is, which I think is expected, throwing a missing self argument when called outside the Settings class.
I have not used classes in pretty much any of my previous projects because it was a single use script for a very specific purpose generally related to moving data around which only had a few small functions. I am trying to expand my abilities, but I am really struggling to understand and fix this issue. I have spent way too much time on this and have to go to the next hard part of using threading to display the data and then log it at certain intervals. So I am looking for some help to get this JSON to not reset every time I try and add to it.
Here is the relevant portions of the MainApp where they input the first updates for the JSON in the Settings class.
from guizero import App, Box, info, Picture, PushButton, Text, TextBox, Window, yesno
from datetime import datetime as dt
import subprocess
import sys
from data_output_setup_page import LoggerSetupPage
from logging_data_display import DataDisplayPage
import logging as log
from utilities import Settings
class MainApp:
def __init__(self):
self.submit_site_text = 'Submit'
self.app = App('Epic One Water Pulse Logging',
width=800, height=480, bg='#050f2b')
# Maximize the app window
#self.app.tk.attributes('-fullscreen', True)
........
def run(self):
self.app.display()
def get_settings(self):
self.settings = Settings.retrieve_settings()
print(self.settings)
if not isinstance(self.settings, type(None)):
load_settings = yesno('Load Settings', 'Settings file found. Load settings?')
if load_settings:
self.site_name.value = self.settings['Site Name']
self.logger_setup.import_settings(self.settings)
elif isinstance(self.settings, type(None)):
info('Config', 'No settings file found. Please configure settings.')
def check_json(self):
self.local_settings = Settings.check_json()
print(self.local_settings)
if self.local_settings:
info('Config', 'Settings ready for save.')
self.sv_stg_to_file.show()
else:
self.sv_stg_to_file.hide()
def site_lock(self):
if self.submit_site_text == 'Submit':
self.site_name.disable()
self.submit_site_text = 'Alter Site Name'
Settings.update_settings({
'Settings':
{'Site Name': self.site_name.value}
})
self.get_settings()
self.open_ds_button.show()
self.open_db_button.show()
# Add a log statement
log.info('Site name updated to {0}'.format(self.site_name.value))
else:
self.site_name.enable()
self.submit_site_text = 'Submit'
self.open_ds_button.hide()
self.open_db_button.hide()
# Add a log statement
log.info('Site name updated to {0}'.format(self.site_name.value))
self.submit_site.text = self.submit_site_text
LoggerSetupPage class is next, it is long because I think it is important to understand what I am doing with it. Basically I have created a window where I create and destroy widgets after they are used to make the entries. This is why I need to iteratively update the json_data in Settings. I will remove as much as I can that is not relevant, but if you want the entire code I can link it from GitHub.
from base64 import b64encode as be
from base64 import b64decode as bd
from datetime import datetime as dt
from guizero import Box, Combo, info, Picture, ListBox, PushButton, Text, TextBox, Window
from utilities import Settings
class LoggerSetupPage:
def __init__(self, parent, main_app):
self.parent = parent
self.main_app = main_app
self.current_row = 0
self.settings_dict = {}
self.widgets_to_destroy = []
...
# Middle box
self.mid_box = Box(self.parent, layout='grid')
self.config_selection = Combo(
self.mid_box,
options=[ 'Data Output Config', 'Sensor Config'],
command=self.check_selection,
grid=[0, 0]
)
self.config_selection.text_color = 'white'
self.config_selection.text_size = 16
# Bottom box for buttons
self.bottom_box = Box(self.parent, layout='grid', align='bottom')
self.return_button = PushButton(self.bottom_box,
text='Return to Main Page',
command=self.return_to_main, align='bottom', grid=[0, 2])
self.return_button.text_color = 'white'
def return_to_main(self):
self.main_app.app.show()
self.main_app.check_json()
self.parent.destroy()
def create_input_list(self):
if self.config_selection.value == 'Data Output Config':
self.data_output_choice_label = Text(self.mid_box, text='Data Output:',
grid=[0, 0])
self.data_output_choice_label.text_color = 'white'
self.data_output_choice = Combo(self.mid_box,
options=['local', 's3', 'ftp'], command=self.check_sub_selection,
grid=[1, 0])
self.data_output_choice.text_color = 'white'
self.current_row += 1
self.widgets_to_destroy.extend([
self.data_output_choice_label,
self.data_output_choice
])
def create_inputs(self):
if self.config_selection.value == 'Sensor Config':
self.sn_label = Text(self.mid_box, text='Sensor Name:',
align='left', grid=[0, 1])
self.sn_label.text_color = 'white'
self.sn_input = TextBox(self.mid_box, grid=[1, 1], width=30,
align='left',)
self.sn_input.text_color = 'white'
self.current_row += 1
self.kf_label = Text(self.mid_box, text='K Factor:',
align='left', grid=[0, 2])
self.kf_label.text_color = 'white'
self.kf_input = TextBox(self.mid_box, grid=[1, 2], width=10,
align='left',)
self.kf_input.text_color = 'white'
self.current_row += 1
self.su_label = Text(self.mid_box, text='Sensor Units:',
align='left', grid=[0, 3])
self.su_label.text_color = 'white'
self.su_input = TextBox(self.mid_box, grid=[1, 3], width=10,
align='left',)
self.su_input.text_color = 'white'
self.current_row += 1
self.du_label = Text(self.mid_box, text='Desired Units:', grid=[0, 4])
self.du_label.text_color = 'white'
self.du_input = TextBox(self.mid_box, grid=[1, 4], width=10,
align='left',)
self.du_input.text_color = 'white'
self.current_row += 1
self.widgets_to_destroy.extend([
self.sn_label,
self.sn_input,
self.kf_label,
self.kf_input,
self.su_label,
self.su_input,
self.du_label,
self.du_input
])
elif self.data_output_choice.value == 's3':
self.l_spacer = Text(self.mid_box, text='', grid=[0, 1], width = 'fill')
self.current_row += 1
self.s3_bucket_label = Text(self.mid_box, text='S3 Bucket:',
grid=[0, 2], align='left')
self.s3_bucket_label.text_color = 'white'
self.s3_bucket_input = TextBox(self.mid_box, grid=[1, 2], width=30,
align='left')
self.s3_bucket_input.text_color = 'white'
self.current_row += 1
self.s3_prefix_label = Text(self.mid_box, text='S3 Folder:',
grid=[0, 3], align='left')
self.s3_prefix_label.text_color = 'white'
self.s3_prefix_input = TextBox(self.mid_box, grid=[1, 3], width=30,
align='left')
self.s3_prefix_input.text_color = 'white'
self.current_row += 1
self.s3_key_label = Text(self.mid_box, text='S3 Filename:',
grid=[0, 4], align='left')
self.s3_key_label.text_color = 'white'
self.s3_key_input = TextBox(self.mid_box, grid=[1, 4], width=30,
align='left')
self.s3_key_input.text_color = 'white'
self.current_row += 1
self.s3_ak_label = Text(self.mid_box, text='User Access Key:',
grid=[0, 5], align='left')
self.s3_ak_label.text_color = 'white'
self.s3_ak_input = TextBox(self.mid_box, grid=[1, 5], width=30,
align='left')
self.s3_ak_input.text_color = 'white'
self.current_row += 1
self.s3_sk_label = Text(self.mid_box, text='User Secret Key:',
grid=[0, 6], align='left')
self.s3_sk_label.text_color = 'white'
self.s3_sk_input = TextBox(self.mid_box, grid=[1, 6], width=30,
align='left')
self.s3_sk_input.text_color = 'white'
self.current_row += 1
self.s3_role_label = Text(self.mid_box, text='Role to Assume:',
grid=[0, 7], align='left')
self.s3_role_label.text_color = 'white'
self.s3_role_input = TextBox(self.mid_box, grid=[1, 7], width=30,
align='left')
self.s3_role_input.text_color = 'white'
self.current_row += 1
self.widgets_to_destroy.extend([
self.s3_bucket_label,
self.s3_bucket_input,
self.s3_prefix_label,
self.s3_prefix_input,
self.s3_key_label,
self.s3_key_input,
self.s3_ak_label,
self.s3_ak_input,
self.s3_sk_label,
self.s3_sk_input,
self.s3_role_label,
self.s3_role_input,
self.l_spacer
])
elif self.data_output_choice.value == 'local':
self.l_spacer = Text(self.mid_box, text='', grid=[0, 1], width = 'fill')
self.email_address_label = Text(self.mid_box, text='Email Address:',
grid=[0, 2], align='left')
self.email_address_label.text_color = 'white'
self.email_address_input = TextBox(self.mid_box, grid=[1, 2], width=40,
align='left')
self.email_address_input.text_color = 'white'
self.current_row += 1
self.widgets_to_destroy.extend([
self.email_address_label,
self.email_address_input,
self.l_spacer
])
# Create a button to return the ListBox to visible
self.show_list_btn = PushButton(self.bottom_box, text='Back to List',
command=self.show_config, grid=[0, self.current_row+1],
align='bottom')
self.show_list_btn.text_color = 'white'
self.save_settings_btn = PushButton(self.bottom_box, text='Save Settings',
command=self.save_settings, grid=[1, self.current_row+1], align='bottom')
self.save_settings_btn.text_color = 'white'
self.widgets_to_destroy.extend([
self.show_list_btn,
self.save_settings_btn
])
def import_settings(self, kwargs):
if kwargs['Location'] == 's3':
self.data_output_choice.value = 's3'
self.s3_bucket_input.value = kwargs['Settings']['Data Output']['Bucket']
self.s3_prefix_input.value = kwargs['Settings']['Data Output']['Prefix']
self.s3_key_input.value = kwargs['Settings']['Data Output']['Key']
self.s3_ak_input.value = bd(kwargs['Settings']['Data Output']\
['Auth']['Access Key']).decode('utf-8')
self.s3_sk_input.value = bd(kwargs['Settings']['Data Output']\
['Auth']['Secret Key']).decode('utf-8')
self.s3_role_input.value = kwargs['Settings']['Data Output']\
['Auth']['Role']
elif kwargs['Location'] == 'ftp':
self.data_output_choice.value = 'ftp'
self.ftp_host_input.value = kwargs['Settings']['Data Output']['Host']
self.ftp_port_input.value = kwargs['Settings']['Data Output']['Port']
self.ftp_un_input.value = bd(kwargs['Settings']['Data Output']\
['Auth']['Username']).decode('utf-8')
self.ftp_pwd_input.value = bd(kwargs['Settings']['Data Output']\
['Auth']['Password']).decode('utf-8')
self.ftp_dir_input.value = kwargs['Settings']['Data Output']['Directory']
else:
self.data_output_choice.value = 'local'
self.email_input.value = kwargs['Email Address']
self.sn_input.value = kwargs['Settings']['Sensor']['Name']
self.kf_input.value = kwargs['Settings']['Sensor']['K Factor']
self.su_input.value = kwargs['Settings']['Sensor']['Standard Unit']
self.du_input.value = kwargs['Settings']['Sensor']['Desired Unit']
def save_settings(self):
if self.config_selection.value == 'Data Output Config':
if self.data_output_choice.value == 's3':
self.settings_dict.update({
'Settings': {
'Data Ouput': {
'Location': self.data_output_choice.value,
'Bucket': self.s3_bucket_input.value,
'Prefeix': self.s3_prefix_input.value,
'Key': self.s3_key_input.value,
'Access Key': be(self.s3_ak_input.value.encode('utf-8')),
'Secret Key': be(self.s3_sk_input.value.encode('utf-8')),
'Role': self.s3_role_input.value
}
}
})
elif self.data_output_choice.value == 'ftp':
self.settings_dict.update({
'Settings': {
'Data Ouput': {
'Location': self.data_output_choice.value,
'Host': self.ftp_host_input.value,
'Port': self.ftp_port_input.value,
'Username': be(self.ftp_un_input.value.encode('utf-8')),
'Password': be(self.ftp_pwd_input.value.encode('utf-8')),
'Directory': self.ftp_dir_input.value
}
}
})
else:
self.settings_dict.update({
'Settings': {
'Data Ouput': {
'Location': self.data_output_choice.value,
'Email Address': self.email_address_input.value
}
}
})
elif self.config_selection.value == 'Sensor Config':
self.settings_dict.update({
'Settings': {
'Sensor': {
'Name': self.sn_input.value,
'K Factor': self.kf_input.value,
'Standard Unit': self.su_input.value,
'Desired Unit': self.du_input.value
}
}
})
Settings.update_settings(self.settings_dict)
info('success', 'settings staged.')
self.return_to_main()
def check_selection(self):
if self.config_selection.value == 'Data Output Config':
# Hide the ListBox
self.config_selection.hide()
self.return_button.hide()
# Create input widgets
self.create_input_list()
self.create_inputs()
elif self.config_selection.value == 'Sensor Config':
# Hide the ListBox
self.config_selection.hide()
self.return_button.hide()
# Create input widgets
self.create_inputs()
def check_sub_selection(self):
if self.data_output_choice.value in ['ftp', 's3'] \
and self.config_selection.visible == False:
# Destroy input widgets and the "Show List" button
self.destroy_widgets()
# Create input widgets
self.create_inputs()
def destroy_widgets(self):
# Destroy existing input widgets if there are any
for widget in self.widgets_to_destroy:
widget.destroy()
self.widgets_to_destroy = [] # Clear the list
def show_config(self):
# Destroy input widgets and the "Show List" button
self.destroy_widgets()
# Show the ListBox
self.config_selection.show()
self.return_button.show()
Finally, the relevant pieces of the current iteration of my Settings class
from botocore.client import Config
import boto3
import collections.abc
import ftplib
import json
import logging
from base64 import b64decode as bd
from datetime import datetime as dt
class Settings:
settings_directory = '/home/ect-one-user/Desktop/One_Water_Pulse_Logger/config/'
settings_filename = '_logger_config.json'
json_data = {
'Settings': {
'Site Name': None,
'Sensor': {
},
'Data Output': {
},
'Email Address': {
}
}
}
@staticmethod
def update_settings(d):
Settings.json_data.update(d)
@staticmethod
def check_json():
print(Settings.json_data)
try:
if Settings.json_data['Settings']['Site Name'] is not None \
and Settings.json_data['Settings']['Sensor']['Name'] is not None \
and Settings.json_data['Settings']['Data Output']['Location'] is not None:
return True
except:
return False
I have tried multiple ways of updating the json_data as well - .update(), setattr, d | json_data. So far .update is the only one that has done anything, but it isn't quite right. With this current iteration of my Settings class and the other two classes which I posted here is the outcome
On Start up - check_json {'Settings': {'Site Name': None, 'Sensor': {}, 'Data Output': {}, 'Email Address': {}}}
after save settings button on LoggerSetupPage with local chosen for the Data Output - check_json {'Settings': {'Data Ouput': {'Location': 'local', 'Email Address': ''}}}
after save settings button on LoggerSetupPage post Data Output when I entered sensor setup - check_json {'Settings': {'Sensor': {'Name': '123', 'K Factor': '123', 'Standard Unit': '2512', 'Desired Unit': '441'}}}
I think this has something to do with different instances of the class perhaps, but I can't figure out how to just use the same class instance for each additional piece of the dictionary.
modified 26-Aug-23 18:47pm.
|