Share your repls and programming experiences

← Back to all posts
A Store in Python
FeaturedSpace (13)

Every answer has a question, so inversely, does every question have an answer? Perhaps not, but then again, is something really a question if it has no answer? [insert more philosophical crap here].

Moving on from the above. Today I set about to create a CLI store in Python. It was fun. There were a lot of features that I wanted to pack in.

Features:

  1. Admin mode to alter stock and items
  2. Customer checkout feature
  3. Logins for admins and customers
  4. Persistent inventory across anybody running the REPL.
  5. Persistent carts across logins.

Now for one day, that's not a huge order, but it's still at least a tall one.

I started off by creating two classes:
Item and Session.

The session was going to maintain the cart and the mode (admin/customer). It would also later on hold data about the login so we could communicate with our auth system.

After these two classes were defined (you can find them in this REPL, obviously), I set about creating a basic software control loop that would define the main user storyboard.

I created the method main that looked something like this:

def main():
  # clear the screen
  # handle auth
  # create a new session for auth
  # create a new prompt based on the new session

This main method would essentially handle logins and be the way to call the program again when one user "logs out".

I also created a quick test method for logging in:

def login(session, username, password):
  # login stuff
  if(username == "admin"):
    session.mode = "admin";
  else:
    session.mode = "customer";

This would let us have some basic testing of both the admin and the customer interfaces. And before you cringe, yes I use semicolons with my code. Most languages require it so I'm not going to stop using them just because one language doesn't.

You should also notice that we pass the session before many methods in the main file. This is called functional programming. Yes, I could have put those directly on the Session class, but then the class itself would be bloated. The point of the Session object would be to store data and perform some basic "actions" using said data (namely the mode of the session).

I created a dictionary called "inventory" that would hold items. Its keys would be the name of the item and the value would be an instance of the "Item" class.

inventory = {
  'Coca-Cola 12oz': Item('Coca-Cola 12oz', 1.19, 15),
};

(this example contains 15 20oz cans of coke)

I also created a method that would list out these items with indexes so that they would be easy to select with the command prompt.

It is also worthy to note that I could have been super fancy and used many more python modules to have arrow based selection and multiple choice. The reason I opted to use plain python is very simple: I wanted to keep it low profile. The less code I have from other people the better.

I then proceeded to create various actions for the admin mode. Adding/creating items, removing items from inventory, and updating an item's value in the inventory.

Each of those actions exists as a method on the Session class, as it's directly related to the mode of the session, which it should conditionally check within itself.

Next, I set out to create the same for customers. Customers had some more special things that weren't just data manipulation such as viewing the cart and checking out.

I created some extra functional methods such as display_cart(session, show_nums=False)

Another quaint functional method that isn't related to the mode of the session, so it exists outside of the class.

Whew. That was a lot. But we're here now. We have basic actions, a defined inventory, and easy manipulation.


That was just the beginning though, as now we reach a point where I need to begin working with the REPL Data API. Which luckily for me, has a built-in library module for Python.

I began by creating auth.py and adding some basic methods such as register_account, has_account, etc.

def init():
  if(len(db.prefix("admins")) <= 0):
    # insert code to set your own admin usernames here.
    print("Resetting admins.");

def is_admin(session, username, password):
  return username in db["admins"];

def retrieve_account(session, username, password):
  init();

  key = f'{username}@{password}';
  if(key in db):
    cart = db[key];
  else:
    cart = {};

I initially wanted to have a dictionary within the key "users" in the database, but I ran into issues trying to get it to hash. Thinking back, I probably could have called .items() on the dictionary object to get an immutable collection, but I didn't think of that at the time. Hindsight is 20/20.

So we moved to a system where accounts are stored in this style: {username}@{password}. This way any non-accounts will not have an "@" symbol in their key. An example of this is the "admins" key, which stores a list of account usernames that should have admin permissions.

After playing with the code A LOT, I managed to get logins working. That was a very happy moment. I then had my friend test who immediately figured out that I hadn't properly implemented the inventory/stock integration. That is when you checked out your cart, it wouldn't check if you were trying to buy more than the store had in stock, and after you bought it, the inventory would not change in stock. That was an easy fix, but also a good save because if I had forgotten to implement that until much later on, implementing the synchronized inventory would have been much more difficult.

# safety loop to make sure we aren't pulling from invisble inventory
      for item in self.cart.items():
        if item[1] > inventory[item[0].name].quantity:
          prompt(self, "Checkout cancelled. Not enough stock.");
          return;
        
      # then we loop again to actually affect the stock. So we don't have to undo anything
      for item in self.cart.items():
        inventory[item[0].name].quantity -= item[1];

After getting authentication working entirely, I set about creating inv.py, a module that would allow me to manipulate the persistent inventory across running instances of the REPL.

I created some basic methods that were passed a dictionary object that was meant to be an inventory (it would have been redundant to create a class for the inventory as it was basically already just a dictionary).

The problem then arose that the Item class was not hashable into JSON (which is what Python uses for hashing, apparently. Good to know. I got around this by converting it to a dictionary and back with two helper methods: inv_to_dict and dict_to_inv. These also came in handy later on when storing the carts of customers.

def item_to_dict(item):
  if(int(item.quantity) < 0): item.quantity = 0;
  
  obj = {
    "name": item.name,
    "price": item.price,
    "quantity": item.quantity,
  };

  return obj;

def dict_to_item(obj):
  import main;

  return main.Item(
    obj["name"],
    float(obj["price"]),
    int(obj["quantity"]),
  );

There was a lot of trial and error with the inventory sync. I will say that out of every "subsection" of this little project, hashing was the biggest obstacle. The majority of other problems in this are solved simply by good programming practices :)

The final feature that we need to implement is cart-persistency. Out of everything this was the hardest to implement. The main reason for this was that the "cart" object on the Session used Items as keys. Now anything instantiating the Item class is not hashable, due to it's being a class and whatnot. At first, I tried to just convert it to a dictionary, rather than an Item. But this proved fruitless as Dictionaries in Python are also not hashable. I had to seek a different route. The route I ended up finding was to store the cart in a list in the database, and bring it out as a dictionary when sending it off to a session.

def get_items(inventory):
  db_inv = db["inventory"];
  inventory.clear();

  for dict_item in db_inv:
    item = dict_to_item(dict_item);
    inventory[item.name] = item;

def set_items(inventory):
  some_list = [];
  for item in inventory.values():
    some_list.append(item_to_dict(item));
  
  db["inventory"] = some_list;

This was the final obstacle, and after clearing up some code, I was happily whizzing along with some unit tests as well as some simple interaction tests.

I came across this weird bug. Whenever the user would log in upon first running the program, the login page would flicker, reset, and they'd have to log in a second time. Without inconsistency, the second login attempt would always work. There was something weird going on inside the login method.

Turns out, we were retrieving items from the database (which is a costly procedure) and immediately continuing on from the method. I don't quite know what was causing it but I would imagine it had something to do with the retrieval of a large database item. I confirmed this after removing the method, observing that the strange bug had vanished.

def prompt(session, message=None):
  helper.clear();
  # inv.get_items(inventory); # removing this method got rid of the bug
  
  if session.mode == "admin":
    prompt_admin(session);
  else:
    prompt_customer(session);

  if message != None: print(message);
  take_action(session);

I wondered how I could fix this for a while, as I needed to recall the inventory every time we displayed the prompt to the user. The solution I came up with was to pro-actively retrieve the inventory from the database upon the initial running of the program. This seemed to correct the issue, caching the object and making alterations rather than pulling it from the database again.

def main():
  helper.clear();
  session = Session(None);
  
  inv.get_items(inventory);
  # ... more code down here

After a long day's work and no lunch, I came home from school and ate some snacks. Now here I am, writing to you. One thing I definitely learned from this is the concept of combining functional programming with object-oriented programming to create a clean program.

Overall I had fun. I hope you enjoyed this little rundown/experiment. Stay tuned for more <3.

DISCLAIMER: YOU MUST FORK THIS REPL TO USE IT CORRECTLY. SOME ISSUES WITH THE REPL DATA API HAVE MADE THE REPL INACCESSIBLE OTHERWISE. o7

  • Noah
Commentshotnewtop
ZDev1 (654)

WHY YOU HAVE SEMICOLONS IN PYTHON???

hello1964 (22)

@ZDev1 So you can put multiple lines of code on one line

FeaturedSpace (13)

@ZDev1 "And before you cringe, yes I use semicolons with my code. Most languages require it so I'm not going to stop using them just because one language doesn't."

ZDev1 (654)

@FeaturedSpace but python doesn't support semicolons

FeaturedSpace (13)

@ZDev1 Well it clearly does since the code runs just as it would if they were not there Besides, just because it doesn't need them doesn't mean it isn't good practice to include them. I don't work with Python professionally so I see no point in converting my style any more than it already requires.

Kirit0 (28)

@FeaturedSpace The code isnt running for me tho... line 23 in get__items key error: inventory. kinda lazy to debug it tho ngl

Kirit0 (28)

@FeaturedSpace wait nvm. i read more comments
edit: forked but still same error

FeaturedSpace (13)

@Kirit0 Ahah sorry about that. All this weird REPL DB API errors had me accidentally remove a line in the main function that automatically populates the inventory key if it was not present. See the function in inv.py: debug(), as well as in main.py, the function main calls inv.debug();. That should resolve any issues.

maxyang (77)

wut
Traceback (most recent call last):
File "main.py", line 427, in <module>
main();
File "main.py", line 268, in main
inv.get_items(inventory);
File "/home/runner/gu5j3vdig2/inv.py", line 12, in get_items
db_inv = db["inventory"];
File "/opt/virtualenvs/python3/lib/python3.8/site-packages/replit/database/database.py", line 169, in getitem
raise KeyError(key)
KeyError: 'inventory'

fuzzyastrocat (767)

@maxyang Oh no, I get this error too. This looks really cool though, nice work!

FeaturedSpace (13)

@maxyang if you fork the REPL you will have to populate the database. Which means you will have to change the inv.get_items(); in the "main" method to "inv.set_items()".

I hope I've helped you fix this issue!

fuzzyastrocat (767)

@FeaturedSpace Hmm, I didn't fork the repl though. I'm just running it from your repl... maybe the database isn't shared though?

FeaturedSpace (13)

@fuzzyastrocat it would seem so... Try forking it and doing as instructed below!

fuzzyastrocat (767)

@FeaturedSpace Works when I fork it. That must be the issue, each user has their own database.

FeaturedSpace (13)

@fuzzyastrocat Super glad to hear that! Thanks for helping out!

fuzzyastrocat (767)

@FeaturedSpace No problem! Like I said earlier, great project, it's clear you put a lot of effort into it!

nsturtz (5)

@featurespace How do you add the debug() function?

FeaturedSpace (13)

@nsturtz It should be called by default. (I altered the original repl)

FeaturedSpace (13)

Just wanted to drop a disclaimer in here as well: A lot of people are having a KeyError issue. This error is due to REPL not allowing you guys to access to database assigned to the REPL from this post. The solution is to fork the REPL so that you yourself can have a database. A secondary issue you may encounter is that even after forking you still get a KeyError. This is because the inventory is not present in the database yet and needs to be populated with a default value. There should be a function in inv.py called debug(). Call this function in main.py's main() function. It should already be there by default as I removed the line and then later added it back again.

If you experience any other issues do not hesitate to comment down below and I'll take a look at it.

RahulChoubey1 (23)

It just says KeyError: 'inventory' upon load

FeaturedSpace (13)

@RahulChoubey1 Please read the disclaimer at the bottom of the post. In order for you to test the program, you will have to fork it.

hello1964 (22)

I get this

Traceback (most recent call last):
File "main.py", line 427, in <module>
main();
File "main.py", line 268, in main
inv.get_items(inventory);
File "/home/runner/50wo44tdsbw/inv.py", line 23, in get_items
db_inv = replit.db["inventory"];
File "/opt/virtualenvs/python3/lib/python3.8/site-packages/replit/database/database.py", line 169, in getitem
raise KeyError(key)
KeyError: 'inventory'

FeaturedSpace (13)

@hello1964 Hey! You need to fork the project for it to work.

FeaturedSpace (13)

@ridark Please see the disclaimers at the bottom of the article and in the comments.

FeaturedSpace (13)

@k9chelsea2 consistent coding style is essential for a polyglot.