FastAPI for Django Programmers: Commands and creating users
On my journey to building an app with Django like components, more specifically, an authentication system, I’ve come across the need to run commands on the command line, to create users, and of course have view that generate html.
Commands
For the commands I need to run, what I really need is access to the ORM. With Django, most of the code around the commands, is linked to working with the django framework. For our commands, none of that is needed.
So to run a command, we need to link to the ORM, first and foremost, and we want to ensure that we link to the same ORM setup as our application. Since Tortoise is independent of FastAPI, we can run different functions to link to the ORM, than we do with FastAPI and we get the same result.
What we do need to have though , is the same database URL and modules parameters, that are used in our main.py file. To manage this, I moved both to variables in main.py, so that I can import them.
import os
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from tortoise.contrib.fastapi import register_tortoise
from dotenv import find_dotenv, dotenv_values
from routes.auth import auth
file_path = os.path.dirname(__file__)
static_dir = os.path.join(file_path, "static")
app = FastAPI()
app.mount("/static", StaticFiles(directory=static_dir), name="static")
settings = dotenv_values(find_dotenv(".ide_settings"))
secret_key = settings.get("secret_key", "aVhMbVFvclczRjYxSm1oTzhBcVlGN0NnT2VoWDlVbE9uc3dYX0xsTnAxRT0=")
db_user = settings.get("db_user", "clusteride")
db_password = settings.get("db_password", "clusteride")
db_host = settings.get("db_host", "localhost")
db_port = settings.get("db_port", "5432")
db_name = settings.get("db_name", "clusteride")
db_url = f"asyncpg://{db_user}:{db_password}@{db_host}:{db_port}/{db_name}"
db_modules = {"models": ["models"]}
register_tortoise(
app,
db_url=db_url,
modules=db_modules,
generate_schemas=True,
add_exception_handlers=True,
)
app.include_router(auth)I then created a commands directory and a core.py file and put in the simple code to initiate a database connection
from tortoise import Tortoise
from main import db_url, db_modules
async def init_db():
await Tortoise.init(
db_url=db_url,
modules=db_modules,
)
await Tortoise.generate_schemas()Note that Tortoise.init() is creating the connection, not register_tortoise. Also note that the function is a coroutine, so this will need to be called as such. Lucky for us, Tortoise comes with a nice function that creates a thread and runs a function called run_async, though you could go through creating and starting a task instead.
Here is a simple command to print a list of users.
from tortoise import run_async
from models import Users
from .core import init_db
async def main():
await init_db()
users = await Users.all()
for user in users:
print(f"{user.username} - {user.email}")
if __name__ == "__main__":
run_asynTo run this I just use.
python -m commands.listusersThat’s it. Nice and simple. I point I will make here, is that I have come across many instances where the features in Django, are not really features, but work arounds. With no framework controlling the layout of our projects and adding modules in, we don’t need a lot of these features.
Now, I have an example of a script to add users. This is a little more complex, as we have to deal with passwords and of course, we need to encrypt them.
I created a new directory called contrib so we can stay in line with Django terms and in there I added auth.py. In here, I added functions that I will use for creating and storing user records.
from models import Users
from main import secret_key
from base64 import b64decode
from cryptography.fernet import Fernet
import datetime
async def authenticate(username, password):
user = await Users.get_or_none(username=username)
if not user:
return None
else:
pwd = hash_password(password)
if pwd == user.password:
return user
else:
return None
async def get_key():
return b64decode(secret_key.encode())
async def create_user(username:str, password: str, email:str) -> Users:
pwd = await hash_password(password)
if email:
user = await Users.create(username=username, password=pwd, email=email, last_login=datetime.datetime.now())
else:
user = await Users.create(username=username, password=pwd, email="Not stated", last_login=datetime.datetime.now())
return user
async def hash_password(password:str):
f = Fernet(await get_key())
return f.encrypt(password.encode()).decode()The first thing to discuss here is the use of the cryptography library and Fernet encryption. This is a reasonably strong encryption system, and an easy to use library. Passwords are encrypted and decrypted using a secret key, that will be pulled in, in main.py.
A point to note here, is that you need to ensure that your secret key is secret. You can’t hard code this into main.py. What I did, is have the key being pulled in using dotenv, but I also encoded the key using the base64 library. Base64 is not high level encryption, but it does add a small extra layer for security.
In commands I then added a createuser.py file and coded the following.
from tortoise import run_async
from commands.core import init_db
from contrib.auth import create_user
import getpass
from models import Users
async def validate_password(password):
if len(password) < 8:
print("Error: Password must be at least 8 characters long.")
return False
elif len(set(password)) < 6:
print("Error: Your password must contain at least 6 different characters.")
return False
elif not any(char.isupper() for char in password):
print("Error: Your password must contain at least 1 uppercase letter.")
return False
elif not any(char.islower() for char in password):
print("Error: Your password must contain at least 1 lowercase letter.")
return False
return True
async def main():
await init_db()
while True:
username = input("Please enter a username: ")
if len(username) < 8:
print("Error: Username must be at least 8 characters long")
continue
if await Users.filter(username=username).exists():
print("Username already exists. Please try again.")
continue
password = getpass.getpass("Please enter a password: ")
if await validate_password(password):
confirm_password = getpass.getpass("Please verify the password: ")
if password == confirm_password:
print("Password confirmed.")
break
else:
print("Passwords do not match. Please try again.")
continue
else:
print("Your password does not meet requirements")
print("Requirements: Min 8 chars long, Uppercase letters, Lowercase letters, 6 differnt characters.")
continue
email = input("Please enter your email address (Optional): ")
user = await create_user(username, password, email)
print(f"User {user.username} created successfully!")
if __name__ == "__main__":
run_async(main())This command is run in a similar way to above
python -m command.createuserThat’s it. A nice simple way to add command like commands for your various tasks.
