Programming

This is kinda long and I don’t really expect anyone to look at it, but if they do and have any comments, critiques, questions that’d be great. I wrote this little python (3) program to take a csv file dowload from a woocommerce/wordpress store that I’m using for kroshipkin so I could change the prices of a bunch of items (the T-shirts) all at once. It has a GUI using tk/tcl (tkinter). You can search for a subset of items and then change them. Then save it back into a csv file to reupload to the store. I’ll add onto this as needed. I tried to make it like modular or something. That’s why I made the save function a callback that is sent in with the data because maybe some other code would save the result somewhere else.

#! /usr/bin/python

import tkinter as tk
import os, csv, copy
from collections import OrderedDict

_filename = 'temp'

class Product_manager(tk.Frame):

    def __init__(self, root, **kwargs):

        tk.Frame.__init__(self, root)
        self.close_button = tk.Button(root, text="Goodbye", command=root.destroy)
        self.close_button.pack()


class Popup():

    def __init__(self, products=None, save_callback=None):

        self.products = copy.deepcopy(products)
        self.input_products = copy.deepcopy(products)
        self.save_callback = save_callback

        #lay the items out on the popup window
        win = tk.Toplevel()
        win.geometry("700x400")
        win.title("title of popup")

        self.canvas = tk.Canvas(win, width=100, height=200, borderwidth=0,
                                background='#ffffff')
        self.frame = tk.Frame(self.canvas, background='#ffffff')
        self.vsb = tk.Scrollbar(self.canvas, orient='vertical',
                                command=self.canvas.yview)
        self.canvas.configure(yscrollcommand=self.vsb.set)

        tk.Button(win, text='filter', borderwidth='1', relief='solid',
                  command=self.filter_products).pack(side='top', fill='x')
        self.filter_entry = tk.Entry(win, width="50")
        self.filter_entry.pack()
        tk.Button(win, text="Set price for all", borderwidth='1', relief='solid',
                command=self.set_price).pack(side='top', fill='x')
        self.price_entry = tk.Entry(win, width="50")
        self.price_entry.pack()
        tk.Button(win, text="Save", borderwidth='1', relief='solid', 
                command=self.save).pack(side='top', fill='x')
        self.vsb.pack(side='right', fill='y')
        self.canvas.pack(side='left', fill='both', expand=True)
        self.canvas.create_window((4,4), window=self.frame, anchor='nw',
                tags='self.frame')
        self.frame.bind('<Configure>', self.onFrameConfigure)

        self.populate()


    def populate(self):
        row = 0
        for key, product in self.products.items():
            tk.Label(self.frame, text='%s' % row, width=3, borderwidth='1',
                     relief='solid').grid(row=row, column=0)
            self.t = f"Name of product is {product['Name']}"
            tk.Label(self.frame, text=self.t).grid(row=row, column=1, sticky='w')
            self.t = f"Price is {product['Regular price']}"
            tk.Label(self.frame, text=self.t).grid(row=row, column=2, sticky='w')
            row += 1


    def onFrameConfigure(self, event):
        '''Reset the scroll region to encompass the innter frame'''
        self.canvas.configure(scrollregion=self.canvas.bbox('all'))

    
    def set_price(self):
        updated_products = {}
        for key, product in self.products.items():
            product['Regular price'] = self.price_entry.get()
            updated_products[key] = product
        self.products = updated_products
        self.redraw_popup()


    def filter_products(self):
        new_products = {}
        for key, product in self.products.items():
            if self.filter_entry.get() in product['Name']:
                new_products[key] = product
        self.products = new_products
        self.redraw_popup()


    def redraw_popup(self):
        for item in self.frame.winfo_children():
            item.destroy()
        self.populate()


    def save(self):
        self.save_callback(self)


def save_callback(Products_obj):
    '''    
    I will probably add tags for whether I'm replacing or updating
    the whole file and perhaps do that in other functions, but for
    now this is all the functionality I need
    '''
    #save/replace file

    #construct the replacement dictionary
    replacement_products = []
    for key, product in Products_obj.input_products.items():
        this_product = {}
        try:
            this_product = copy.deepcopy(Products_obj.products[key])
        except:
            this_product = copy.deepcopy(Products_obj.input_products[key])
        replacement_products.append(this_product)

    #save it as the file
    with open(_filename, 'w', encoding='UTF-8-sig') as csvfile:
        writer = csv.DictWriter(csvfile, replacement_products[0].keys())
        writer.writeheader()
        for p in replacement_products:
            writer.writerow(p)


def option_callback(*args):
    #open the popup to make changes to the selected file
    global _filename
    _filename = option.get()
    products = get_products(_filename)
    Popup(products, save_callback)


def get_products(filename):
    with open(filename, 'r', encoding='UTF-8-sig') as csvfile:
        dict_reader = csv.DictReader(csvfile)
        products = {}
        for row in dict_reader:
            product = OrderedDict()
            for key, val in row.items():
                product[key] = val
            products[int(product['ID'])] = product
    return products


if __name__ == '__main__':
    root = tk.Tk()
    root.geometry("400x200")
    root.title("Kroshopkin Product Manager")

    #get a list of csv files in the directory
    file_list = os.listdir('.')
    csv_list = []
    for file in file_list:
        file_name, file_extension = os.path.splitext(file)
        if file_extension == '.csv':
            csv_list.append(file)

    option = tk.StringVar(root)
    option.set(csv_list[0])
    x = tk.OptionMenu(root, option, *csv_list)
    x.pack()

    #execute callback upon selection of file
    option.trace("w", option_callback)

    #open root window
    Product_manager(root, csv_list=csv_list)

    root.mainloop()

Here’s a sample of what the csv file looks like

ID,Type,SKU,Name,Published,Is featured?,Visibility in catalog,Short description,Description,Date sale price starts,Date sale price ends,Tax status,Tax class,In stock?,Stock,Low stock amount,Backorders allowed?,Sold individually?,Weight (oz),Length (in),Width (in),Height (in),Allow customer reviews?,Purchase note,Sale price,Regular price,Categories,Tags,Shipping class,Images,Download limit,Download expiry days,Parent,Grouped products,Upsells,Cross-sells,External URL,Button text,Position,Meta: _wcsquare_disable_sync,Attribute 1 name,Attribute 1 value(s),Attribute 1 visible,Attribute 1 global,Attribute 2 name,Attribute 2 value(s),Attribute 2 visible,Attribute 2 global

10,simple,,Gently Used Bridge,1,0,visible,"This is an older bridge.  It is in good condition, but sale is As-Is with no guarantee.  Shipping is not included.",Used bridge.,,,taxable,,0,0,,0,0,,,,,1,,,99.99,Infrastructure,,,http://kroshopkin.com/wp-content/uploads/2019/11/bridge.jpg,,,,,,,,,0,no,,,,,,,,

32,simple,,Solar Panel,1,0,visible,"Solar Panel 140watts (used)
\n
\nThis item is for sale by a private party, not a worker cooperative as there is no real worker here.  At least there's no boss, so close enough for a demo item.  This is here as a test, but the item is really for sale!","Product is located in the Los Angeles area (South Bay) to be picked up or possibly will be delivered if convenient.
\n
\n<img class=""alignnone size-full wp-image-34"" src=""http://kroshopkin.com/wp-content/uploads/2019/12/solarnameplate.jpg"" alt="""" width=""600"" height=""800"" />
\n
\n&nbsp;",,,none,zero-rate,1,,,0,0,,,,,1,,,44.44,Electronics,,,http://kroshopkin.com/wp-content/uploads/2019/12/solarpanel.jpg,,,,,,,,,0,no,,,,,,,,

35,simple,,Well wishes,1,0,visible,"One well wish, intended for testing.","Purchase this and we at Kroshopkin.com will wish you well.  That's it.  I'm really setting this up as a test, but if you do go ahead and purchase these wishes, that's what you'll get.  No promises that wishes will come true.",,,taxable,,1,,,0,0,,,,,1,,,0.50,Uncategorized,,,,,,,,,,,,0,no,,,,,,,,

1 Like

I’ve been applying for stuff on indeed, low-key as you kids say. Interview on Wednesday. First interview in 20 years.

5 Likes

Good luck!

1 Like

I did not know you could even build a GUI in python

:slight_smile:

I really haven’t figured out even what case and underscoring I prefer, but seemed like class getting a capital letter seemed right and the rest underscored python style.

Not much useful to say other than:

file_list = os.listdir('.')
csv_list = []
for file in file_list:
    file_name, file_extension = os.path.splitext(file)
    if file_extension == '.csv':
        csv_list.append(file)

I think is the same as:

csv_list = [
    filename for filename in os.listdir(".")
    if os.path.splitext(filename)[-1] == ".csv"
]

or even:
csv_list = filter(lambda path: os.path.splitext(path)[-1] == ".csv", os.listdir(".")

and:

replacement_products = []
for key, product in Products_obj.input_products.items():
    this_product = {}
    try:
        this_product = copy.deepcopy(Products_obj.products[key])
    except:
        this_product = copy.deepcopy(Products_obj.input_products[key])
    replacement_products.append(this_product)

is:

replacement_products = map(
    lambda key: Products_obj.products.get(key)
                or Products_obj.input_products.get(key),
    Products_obj.input_products.keys()
)

Since all you’re doing is writing replacement_products to a file, I don’t think you need to copy/deepcopy the source.

EDIT: For the second one, this is probably better:

replacement_products = [
	Products_obj.products.get(key, original_product)
	for key, original_product in Products_obj.input_products.items()
]
1 Like

That’s good. Thanks!

fuck all of your underscores imo

I think I did that in COBOL…

(Looking at that now I see “imports should usually be on a separate line”)

At a higher level, it seems like the class design could be reworked:

  • You have two classes: Product_manager (not important, but PEP8 style is ProductManager) and Popup.

  • AFAICT, Product_manager…creates a frame in the window that is passed in as an argument, then creates a close button. The class name seems misleading, and the set of operations is not all that coherent–some of the setup is done in main(), then a few things are handled in Product_manager.

  • Then Popup does everything else, including laying out the GUI and managing the data. This method is tellingly obscure:

    def save(self):
    self.save_callback(self)

save_callback is actually class data and not a method at all, which is why it’s explicitly being passed itself as an argument–it is both responsible for figuring out when to call the save operation and also being the data to be saved!

(Assuming you had your heart set on the Popup class as-is, it seems less cryptic to handle the callback like this:

def __init__(self, products=None, save_callback=None):

    class BagOfData: pass
    my_bag = BagOfData() # or use types.SimpleNamespace class
    my_bag.input_products = copy.deepcopy(products)
    my_bag.products = self.products = products
    
    def save_closure():
        save_callback(my_bag)
        
    # ...
    
    tk.Button(win, text="Save", borderwidth='1', relief='solid', 
            command=save_closure).pack(side='top', fill='x')

)

But, why not define a separate PriceList class:

class PriceList():
    def __init__(self, products):
        self.original_products = copy.deepcopy(products)
        self._reset()

    def _reset(self):
        self.products = copy.deepcopy(self.original_products)
        
    def filter_by_product_name(self, filter_string = ""):
        return filter(lambda product: filter_string in product["Name"],
                      self.products)
        
    def set_prices_by_filter(self, new_price, filter_string):
        for product in self.products:
            if filter_string in product["Name"]:
                product["Regular price"] = price
        
    def iter_over_name_and_price(self):
        for product in self.products:
            yield (product["Name"], product["Current price"])

and then call the appropriate interface methods from the GUI class (for example):

def populate(self):
    row = 0
    for product_name, price in self.products.iter_over_name_and_price():
        tk.Label(self.frame, text=row, width=3, borderwidth='1',
                 relief='solid')
                .grid(row=row, column=0)
        tk.Label(self.frame, text=f"Name of product is {product_name}")
                .grid(row=row, column=1, sticky='w')
        tk.Label(self.frame, text=f"Price is {price}")
                .grid(row=row, column=2, sticky='w')
        row += 1

But is the GUI class really needed? It’s not giving you any kind of useful abstraction (it has no public methods and no instance of it is ever even stored to a variable!) What if you instead had this as a free function:

def refresh_product_list(window, new_list):
    for item in window.winfo_children():
        item.destroy()
    row = 0
    for product_name, price in new_list:
        tk.Label(window, text=row, width=3, borderwidth='1', relief='solid')
                .grid(row=row, column=0)
        tk.Label(window, text=f"Name of product is {product_name}")
                .grid(row=row, column=1, sticky='w')
        tk.Label(window, text=f"Price is {price}")
                .grid(row=row, column=2, sticky='w')
        row += 1

Then you could just bind refresh_product_list(root, price_list.iter_over_name_and_price()) to the relevant buttons. (Maybe you need to modify PriceList so that it combines the iterator with the filter?) Everything that’s in Popup.init() could just be the body of your option_callback function.

1 Like

Thanks a lot Bob.

I’m looking at your post carefully and will likely implement a bunch of it. I will say that I was thinking Project_manager, now ProjectManager, might have more to do in the future. It spawns one popup to do this one thing, but I was thinking there might be other things. If it turns out that other things are closely related to what the popup does and that functionality belongs in that window then from a UI perspective it wouldn’t make sense to even have both. But, if it ends up being kind of a dashboard, then it would. At first anyway, I’m thinking of it as a dashboard.

I’m gonna think about that more when I add another feature as needed. Thinking about what that’s likely to be, I think the main window will end up having like radio button choices that will determine what’s in the popup. I can see it makes sense to separate the windowing from the price editing and “price” might end up being something more generic as other fields would be editable in the same way.

I was thinking about this a bit more over lunch (anything is better than doing my real job!). It might be overkill, but a return-self pattern for the PriceList interface would be kind of sexy, so the client can chain filters and resets and set_prices in any combination they need. (And implement the iterator magic methods so you don’t have to worry about ever returning the data as a list.)

Guido seems to think it’s generally unPythonic to chain side-effects. Not sure if that would apply inside an app where context would make things more clear.

https://mail.python.org/pipermail/python-dev/2003-October/038855.html

This is specifically what I hate most about Python. map(f, iter) is (at least to me) much less readable than iter.map(f).

Let alone reduce(f3, filter(f2, map(f1, list)))…

Also, isn’t y = x.rstrip("\n").split(":").lower() a syntax error? Split returns a list.

lower acts on a string and split returns a list
x = “whaTever”
y = x.rstrip("\n").split(":")[0].lower()

isn’t a syntax error

I’ve done more React than Python lately and am more used to the latter, but I don’t really care about stuff like that - or aboutCasing. I’m happy to let Python be Python and Javascript be Javascript. I do generally like using more lines and doing things one step at a time more than making things compact unless there’s a performance reason, but that’s probably my dumbness/inexperience.

I haven’t been looking long or hard, but it seems relatively hard to get side work at more than like $25/hr or something pretty low. But, I’m sure it varies a ton. The one good side job I’ve had, the company was owned by the brother of a friend and one of my closest friends was their top tech guy (no titles there really). Most likely if I get anything now it will be because my closest friends are all in the business and have been for like 30 years.

1 Like

I don’t know, but there are a bunch of websites for gigs. This is probably lame, but something where I could do little tiny scripts for like $100 or whatever might be better than doing the same thing for no money.