Don't Over Complicate It
Oftentimes it seems that as programmers, or anything else for that matter, we get caught up in a particular pattern for doing things. One that I see quite often is the use of SQL databases in various projects. The "kids" these days are introduced to SQL as a primer for programming and can't think how to handle persistence without it. To show that life can exist without a formal database, HangarControl is being written using some very simple mechanisms for creating, reading, and updating information.
The User Record
In a future episode, I will discuss the Flask-Login module. Flask-Login is a set of helper routines to streamline user session management. As it is written in Python, it expects that your users can be accessed as "User" objects. One might be tempted to just fire up SQLAlchemy (SQLAlchemy - The Database Toolkit for Python ) and start creating a database. Instead, we're going to create a class that provides everything needed without the overhead of a database manager.
According to the documentation, Flask-Login requires the following properties and methods:
is_authenticated | This property should return True if the user is authenticated, i.e. they have provided valid credentials. (Only authenticated users will fulfill the criteria of login_required .) |
is_active | This property should return True if this is an active user - in addition to being authenticated, they also have activated their account, not been suspended, or any condition your application has for rejecting an account. Inactive accounts may not log in (without being forced of course). |
is_anonymous | This property should return True if this is an anonymous user. (Actual users should return False instead.) |
get_id() | This method must return a unicode that uniquely identifies this user, and can be used to load the user from the user_loader callback. Note that this must be a unicode - if the ID is natively an int or some other type, you will need to convert it to unicode . |
Define a User Record
We can easily provide this directly from our favorite text editor. I have broken up the various portions for discussion purposes. Trust that they are all that makes up the 'user.py' file.
# Get a template object for our User record. from flask_login import UserMixin class User(UserMixin): # This is an array that will be shared across all instances of this class users = [] # This method is automatically called when a new instance of User is created. # Notice at the end where the *class* User appends this new instance to our # users list. Just think "SQL insert". def __init__(self, username, acctnum, password=None, fullname=None, active=True): self.username = username self.acctnum = acctnum self.fullname = fullname self.password = password self.active = active User.users.append(self)
Here is something that I recommend you do that just makes your (debugging) liffe so much easier: Create a method for rendering your object in a human readable form!
# Any class that you create should implement __repr__. This provides a # convenient method to display a human readable representation of your object. def __repr__(self): return "<User username:%s acctnum:%s password:%s fullname:%s>" % \ (self.username, self.acctnum, self.password, self.fullname)
If you've done any work with Python, the above pattern is pretty familiar. Next we implement the methods that Flask-Login is expecting from us.
# These are required by the Flask-Login module def is_active(self): return self.active def is_anonymous(self): return False def is_authenticated(self): return True def get_id(self): return self.username
The final portion of our User class is the piece that makes all this "what's a database anyway?" talk complete. Here we are simply defining a mechanism or language, if you will, to query User records in a structured way. Ooh, see what I did there? (Okay, "... structured ... query ... language ...") That's okay, my kids didn't think it was funny either.
# The @classmethod decorator (think "modifier") makes the method definition # available by this syntax: "User.find_by_attr(...)". The important concept # is that this method isn't used by individual 'user records', rather the # collection of all 'user' records. @classmethod def find_by_attr(cls,key,target): for user in User.users: if getattr(user,key) == target: return user break else: return None @classmethod def find_by_username(cls,target): return cls.find_by_attr('username',target)
Working With User Records
Now that we have an implementation of a User, let's take a look at how it will be utilized.
$ python >>> from lib.user import User >>> User('admin', 0, 'pilot', 'Administrator') <User username:admin acctnum:0 password:pilot fullname:Administrator> >>> User('pilot', 142, 'secret', 'Ima Pilot') <User username:pilot acctnum:142 password:secret fullname:Ima Pilot> >>> User.users [ <User username:admin acctnum:0 password:pilot fullname:Administrator>, <User username:pilot acctnum:142 password:secret fullname:Ima Pilot>] >>> User.find_by_username('admin') <User username:admin acctnum:0 password:pilot fullname:Administrator> >>> User.find_by_username('whodat') >>> User.find_by_username('whodat') == None True
Users, Nice and Tidy
In just a couple dozen lines of code, we have implemented everything that we need for our user management. Understandably, it does not handle the complexities of a frequently changing population. But for the sake of this project, the user population is very stable and the creation and management of pilots is handled by another application and HangarControl only gets a list of pilots when there has been a change.
I hope you have found this useful and, in the future, aren't afraid to "go naked" and skip the SQL database!
Rick