Virgo is a minimal, batteries-included web framework written in Python.
Built for learning — inspired by Django, but simplified for clarity.
- WSGI-compatible dev server
- Gunicorn(Linux/MacOS) and Waitress(Windows) Ready
- CLI for starting new apps
- App-based Structure
- Dynamic Routing
- Per-app templates and static files
- Jinja2-powered Templating engine
- Context Passing Support
- SQLite Database
- SQLAlchemy-powered ORM
- Query Helper
Virgo Framework is open source under the MIT License.
git clone --depth=1 https://github.com/DaleStack/Virgo.git .
#bash
py virgo.py lightstart blog
apps/
blog/
__init__.py
models.py
routes.py
templates/
static/
#apps/blog/routes.py
from virgo.core.routing import routes
from virgo.core.response import Response, redirect
from virgo.core.template import render
def sample(request):
return Response("Welcome to Virgo!")
routes["/sample"] = sample
--
#virgo.py
import apps.blog.routes
#bash
py virgo.py lightserve
http://127.0.0.1:8000/sample
#apps/blog/routes.py
from virgo.core.routing import routes
from virgo.core.response import Response, redirect
from virgo.core.template import render
def sample(request):
return Response("Welcome to Virgo!")
routes["/sample"] = sample
# Define new function
def new_function(request):
return Response("This is a new function")
routes["/"] = new_function
# routes["/"] is the Route Path
# new_function is the name of the function
#virgo.py
import apps.blog.routes
#bash
py virgo.py lightserve
http://127.0.0.1:8000/
def profile_view(request, name):
return Response(f"This is {name}'s Profile")
routes["/profile/<name>"] = profile_view
#bash
py virgo.py lightserve
http://127.0.0.1:8000/profile/JohnDoe
This is JohnDoe's Profile
apps/
example_app/
__init__.py
models.py
routes.py
templates/
static/
def example(request):
return render("home.html", app="example_app")
routes["/example"] = example
# "home.html" is the name of the template
# app="example_app" is the name of the app
apps/
example_app/ # app="example_app" is referring to this
__init__.py
models.py
routes.py
templates/
home.html #Your Template
static/
<!--example_app/templates/home.html-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Example Template</title>
</head>
<body>
<h1>This is my template</h1>
</body>
</html>
#virgo.py
import apps.example_app.routes
#bash
py virgo.py lightserve
http://127.0.0.1:8000/example
apps/
example_app/
__init__.py
models.py
routes.py
templates/
home.html
static/
style.css # Your stylesheet
/** example_app/static/style.css */
h1 {
background-color: chocolate;
}
<!--example_app/templates/home.html-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Example Template</title>
<link rel="stylesheet" href="static/example_app/style.css"></link>
<!-- This is how you should link: static/<app_name>/<stylesheet name> -->
</head>
<body>
<h1>This is my template</h1>
</body>
</html>
#bash
py virgo.py lightserve
http://127.0.0.1:8000/example
and you should see the styles working.
def example(request):
context = {
"name":"John Doe"
"age": 30
}
return render("home.html", context, app="example_app") # Context should be in the middle
routes["/example"] = example
def example(request):
name = "John Doe"
age = 30
return render("home.html", {"name":name, "age":age}, app="example_app")
routes["/example"] = example
<!--example_app/templates/home.html-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Example Template</title>
</head>
<body>
<h1>Hello my name is {{ name }}</h1>
<h3>I am {{ age }} years old</h3>
</body>
</html>
Inside your app's models.py, create a simple model:
# apps/post/models.py
from sqlalchemy import Column, Integer, String
from virgo.core.database import Base
from virgo.core.mixins import BaseModelMixin
class Post(Base, BaseModelMixin):
__tablename__ = "posts"
id = Column(Integer, primary_key=True)
title = Column(String)
content = Column(String)
py virgo.py lightmigrate
This command migrates of all the model and automatically creates a table.
You should see a virgo.db created at a project-level. (If it does not exist yet)
# apps/post/routes.py
from virgo.core.routing import routes
from virgo.core.response import Response, redirect
from virgo.core.template import render
from .models import Post # import your model
def post_create(request):
if request.method == "POST":
data = request.POST
title = data.get("title")
content = data.get("content")
Post.create(title=title, content=content)
# .create() is used to create a data in the model
return redirect("/") # Go back to post list after submitting
return render("post_create.html", app=post)
routes["/create"] = post_create
Creating data in the template:
<!-- apps/post/templates/post_create.html -->
<h1>Create Post</h1>
<form method="POST"> <!-- it should be a POST method -->
<input type="text" name="title" placeholder="Title"/>
<textarea name="content" placeholder="Content"></textarea>
<button type="submit">Create Post</button>
</form>
# apps/post/routes.py
from virgo.core.routing import routes
from virgo.core.response import Response, redirect
from virgo.core.template import render
from .models import Post
def post_list(request):
posts = Post.all()
# .all() is used to fetch all the data in the model
return render("post_list.html", {"posts":posts}, app=post)
routes["/"] = post_list
Looping through the data in the template:
<!-- apps/post/templates/post_list.html -->
<h1>Post List</h1>
{% for post in posts %}
<p>{{ post.title }}</p>
<p>{{ post.content }}</p>
{% endfor %}
# apps/post/routes.py
from virgo.core.routing import routes
from virgo.core.response import Response, redirect
from virgo.core.template import render
from .models import Post
def post_update(request, id):
post = Post.get(id)
if not post:
return Response("Post not found", status=404)
if request.method == "POST":
data = request.POST
title = data.get("title")
content = data.get("cpntent")
post.update(title=title, content=content)
# post is the instance
# .update() is used for updating a data in the model
return redirect("/")
return render("post_update.html", {"post":post}, app=post)
routes["/update/<id>"] = post_update
Updating data in the template:
<!-- apps/post/templates/post_update.html -->
<h1>Update Post</h1>
<form method="POST"> <!-- it should be a POST method -->
<input type="text" name="title" value="{{ post.title }}"/>
<textarea name="content" placeholder="Content">{{ post.content }}</textarea>
<button type="submit">Update Post</button>
</form>
# apps/post/routes.py
from virgo.core.routing import routes
from virgo.core.response import Response, redirect
from virgo.core.template import render
from .models import Post
def post_delete(request, id):
post = Post.get(id)
if not post:
return Response("Post not found", status=404)
post.delete()
# .delete() is used to remove an instance in the database
return redirect("/")
return render("post_delete.html", app=post)
routes["/delete/<id>"] = post_delete
Using the functon in the template:
<!-- apps/post/templates/post_list.html -->
<h1>Post List</h1>
{% for post in posts %}
<p>{{ post.title }}</p>
<p>{{ post.content }}</p>
<a href="/delete/{{ post.id }}">Delete</a> <!-- Deleting -->
<a href="/update/{{ post.id }}">Edit</a>
{% endfor %}
# apps/user/models.py
from sqlalchemy import Column, Integer, String
from virgo.core.database import Base
from virgo.core.mixins import BaseModelMixin
from virgo.core.auth import UserModel # import built-in User Model
class User(UserModel):
pass
Run migrate in terminal:
py virgo.py lightmigrate
this should put a users table in the database.
# apps/user/routes.py
from virgo.core.routing import routes
from virgo.core.response import Response, redirect
from virgo.core.template import render
from virgo.core.auth import UserAlreadyExists # import this Exception
from .models import User # import your User model
def register_view(request):
if request.method == "POST":
data = request.POST
username = data.get("username")
password = data.get("password")
try:
User.register(username, password) # .register() is used to register a user
return User.authenticate(request, username, password)
except UserAlreadyExists:
error = "Username already taken."
return render("register.html", {"error":error}, app="user")
return render("register.html", app="user")
routes["/register"] = register_view
Registration template view:
<!-- apps/user/templates/register.html -->
<h1>Register User</h1>
{% if error %}
<p style="color: red;">{{ error }}</p>
{% endif %}
<form action="" method="POST">
<input type="text" name="username" placeholder="username">
<input type="password" name="password" placeholder="password">
<button type="submit">Register</button>
</form>
# apps/user/routes.py
from virgo.core.routing import routes
from virgo.core.response import Response, redirect
from virgo.core.template import render
from virgo.core.auth import UserAlreadyExists
from .models import User
def login_view(request):
if request.method == "POST":
data = request.POST
username = data.get("username")
password = data.get("password")
user = User.first_by(username=username)
if not user or not user.check_password(password):
error = "Invalid username or password"
return render("login.html", {"error":error}, app="user")
return User.authenticate(request, username, password)
return render("login.html", app="user")
routes["/login"] = login_view
Login template view:
<!-- apps/user/templates/login.html -->
<h1>Login User</h1>
{% if error %}
<p style="color: red;">{{ error }}</p>
{% endif %}
<form action="" method="POST">
<input type="text" name="username" placeholder="username">
<input type="password" name="password" placeholder="password">
<button type="submit">Login</button>
</form>
Open up your settings.py:
# settings.py
LOGIN_REDIRECT_ROUTE="/example"
# Usage: What /<route> do you want users to be redirected to, after authenticating
LOGIN_ROUTE="/login" # This is your login page route
# This will be used for redirecting users who are unauthorized and accessing protected routes
# We'll get back to this in a minute...
LOGOUT_REDIRECT_ROUTE="/"
# Used for redirecting users after logging out
# apps/user/routes.py
from virgo.core.routing import routes
from virgo.core.response import Response, redirect
from virgo.core.template import render
from virgo.core.auth import UserAlreadyExists
from .models import User
from virgo.core.decorators import login_required # import this login_required decorator
@login_required(User) # pass your user model as the parameter
def dashboard(request):
user = request.user # request.user is used to fetch the currently logged-in user
return render("dashboard.html", {"user":user}, app="user")
routes["/dashboard"] = dashboard
Dashboard template view:
<!-- apps/user/templates/dashboard.html -->
<h1>Hello, {{ user.username }}!</h1>
This is also where LOGIN_ROUTE comes to play:
If a user who is not logged-in tries to access "/dashboard" (a protected route), They will be redirected to the login page, preventing them from accessing protected data.
# apps/user/routes.py
from virgo.core.routing import routes
from virgo.core.response import Response, redirect
from virgo.core.template import render
from virgo.core.auth import UserAlreadyExists
from .models import User
from virgo.core.decorators import login_required
@login_required(User)
def logout_view(request):
user = request.user
if user:
return user.logout(request) # .logout is used to clear session
# LOGOUT_REDIRECT_ROUTE will be executed upon logout
routes["/logout"] = logout_view
Dashboard template view:
<!-- apps/user/templates/dashboard.html -->
<h1>Hello, {{ user.username }}!</h1>
<a href="/logout">Logout</a>
Go to virgo/core/auth.py
# virgo/core/auth.py
class UserModel(Base, BaseModelMixin):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
username = Column(String, unique=True, nullable=False)
password = Column(String, nullable=False)
role = Column(String, nullable=False, default="student") # add a role column eg. student or teacher
# make sure it's also called exactly "role" because virgo has a built-in decorator
Run migrate:
py virgo.py lightmigrate
For role to be accepted in registration:
# virgo/core/auth.py
@classmethod
def register(cls, username, password, role): # pass the role as a parameter
if cls.first_by(username=username):
raise UserAlreadyExists("Username already taken")
hashed = cls.hash_password(password)
return cls.create(username=username, password=hashed, role=role) # also pass here for the role to be created
# apps/user/routes.py
def register_view(request):
if request.method == "POST":
data = request.POST
username = data.get("username")
password = data.get("password")
role = data.get("role")
try:
user = User.register(username, password, role)
return User.authenticate(request, username, password)
except UserAlreadyExists:
error = "Username already taken"
return render("register.html", {"error": error}, app="post")
return render("register.html", app="post")
routes["/register"] = register_view
<h1>Register User</h1>
<form action="" method="POST">
{% if error %}
<p>{{ error }}</p>
{% endif %}
<input type="text" name="username" placeholder="username">
<input type="password" name="password" placeholder="password">
<select name="role" id="">
<option value="student">Student</option>
<option value="teacher">Teacher</option>
</select>
<button type="submit">Register</button>
</form>
If there are two or more roles, how can I get the LOGIN_REDIRECT_ROUTE to work if it only takes 1 route? you may ask. Here's how:
# settings.py
LOGIN_REDIRECT_ROUTE="/dashboard"
LOGIN_ROUTE="/login"
LOGOUT_REDIRECT_ROUTE="/"
# Virgo has this
ROLE_ROUTES = {
"student": "/student/dashboard", # role first, then route
"teacher": "/teacher/dashboard"
}
# If your system have no roles, leave ROLE_ROUTES empty, your fall back will be LOGIN_REDIRECT_ROUTE
FORBIDDEN_REDIRECT_ROUTE="/forbidden"
# This will be your role checker fall back, if a student tried to access "/teacher/dashboard" (vice versa)
# They will be redirected to this route
# apps/user/routes.py
from virgo.core.routing import routes
from virgo.core.response import Response, redirect
from virgo.core.template import render
from virgo.core.auth import UserAlreadyExists
from .models import User
from virgo.core.decorators import login_required, role_required # import role_required from decorators
@login_required(User)
@role_required("student")
def student_dashboard(request):
user = request.user
return render("student_dashboard.html", {"user":user}, app="user")
routes["/student/dashboard"] = student_dashboard # route should be the same as the one in the ROLE_ROUTES
@login_required(User)
@role_required("teacher")
def teacher_dashboard(request):
user = request.user
return render("teacher_dashboard.html", {"user":user}, app="user")
routes["/teacher/dashboard"] = teacher_dashboard
def forbidden(request):
return render("forbidden.html", app="user")
routes["/forbidden"] = forbidden # this will be your FORBIDDEN_REDIRECT_ROUTE
We will try to make an application with proper file structure that's why we will create two apps
py virgo.py lightstart post
py virgo.py lightstart user
Navigate to your post app's models.py
# apps/post/models.py
from sqlalchemy import Column, Integer, String, ForeignKey # Import ForeignKey
from sqlalchemy.orm import relationship # Import relationship
from virgo.core.database import Base
from virgo.core.mixins import BaseModelMixin
from virgo.core.auth import UserModel # Import Base UserModel
class Post(Base, BaseModelMixin):
__tablename__ = "posts"
id = Column(Integer, primary_key=True)
title = Column(String)
user_id = Column(Integer, ForeignKey("users.id")) # Connect Post model to ForeignKey
author = relationship("UserModel", back_populates="posts") # Create relationship to Base UserModel
Navigate to virgo/core/auth.py
# virgo/core/auth.py
from sqlalchemy import Column, Integer, String
from virgo.core.database import Base
from virgo.core.mixins import BaseModelMixin
from virgo.core.session import create_session, get_session, destroy_session
from virgo.core.response import Response, redirect
from settings import LOGIN_REDIRECT_ROUTE, LOGOUT_REDIRECT_ROUTE, ROLE_ROUTES
import bcrypt
from sqlalchemy.orm import relationship # Import relationship
class UserModel(Base, BaseModelMixin):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
username = Column(String, unique=True, nullable=False)
password = Column(String, nullable=False)
posts = relationship("Post", back_populates="author") # Create relationship to Post Model
This alone won't be migrated in the database, that's why we need the user app.
# apps/user/models.py
from sqlalchemy import Column, Integer, String
from virgo.core.database import Base
from virgo.core.mixins import BaseModelMixin
from virgo.core.auth import UserModel # Import UserModel
class User(UserModel): # Pass UserModel as a parameter
pass
Creating Login and Register function will just be the same.
# apps/post/routes.py
from virgo.core.routing import routes
from virgo.core.response import Response, redirect
from virgo.core.template import render
from virgo.core.decorators import login_required
from apps.user.models import User
from .models import Post
@login_required(User)
def create_post(request):
user = request.user
if request.method == "POST":
data = request.POST
title = data.get("title")
Post.create(title=title, user_id=user.id) # pass the data, as well as the currently logged-in user's id
return redirect("/")
return render("create_post.html", app="post")
routes["/create-post"] = create_post
# apps/post/routes.py
from virgo.core.routing import routes
from virgo.core.response import Response, redirect
from virgo.core.template import render
from virgo.core.decorators import login_required
from apps.user.models import User
from .models import Post
@login_required(User)
def list_post(request):
user = request.user
posts = Post.filter_by(user_id=user.id, load=["author"])
return render("list_post.html", {"posts":posts, "user":user}, app="post")
routes["/"] = list_post
This is a simple One-to-Many Relationship
Soon
Soon
We will tackle all of the built-in query helpers available in Virgo
Let's say we have a Note model with only a title field
.create()
# apps/note/routes.py
from .models import Note
def create_note(request):
if request.method == "POST":
data = request.POST
title = data.get("title")
Note.create(title=title)
return redirect("/")
return render("create_note.html", app="note")
routes["/create_note"] = create_note
.all() Can load relation
# apps/note/routes.py
from .models import Note
def list_post(request):
user = request.user
posts = Post.filter_by(user_id=user.id, load=["author"])
return render("list_post.html", {"posts":posts, "user":user}, app="post")
routes["/"] = list_post
.get() Note: get() is jsut a wrapper of get_by_id() so this will only work for id
# apps/note/routes.py
from .models import Note
def get_note(request, id):
note = Note.get(id)
if not note:
return Response("Note not found!", status=404)
return render("get_note.html", {"note":note}, app="note")
routes["/get_note/<id>"] = get_note
.filter_by() Can load relation
# apps/note/routes.py
from .models import Note
def filtered_note(request):
notes = Note.filter_by(title="hello")
if not notes:
return Response("No notes were found!", status=404)
return render("filtered_note.html", {"notes":notes}, app="note")
routes["/filtered_note"] = filtered_note
first_by()
# apps/note/routes.py
from .models import Note
def filtered_note(request):
note = Note.first_by(title="hello")
if not note:
return Response("No notes were found!", status=404)
return render("first_note.html", {"note":note}, app="note")
routes["/first_note"] = first_note
order_by() Can load relation
# apps/note/routes.py
from .models import Note
def ordered_note(request):
notes = Note.order_by("title") # asc by default, add "desc" to make it descending ("title", "desc")
if not notes:
return Response("No notes were found!", status=404)
return render("order_note.html", {"notes":notes}, app="note")
routes["/ordered_note"] = ordered_note
filter_and_order_by() Can load relation
# apps/note/routes.py
from .models import Note
def filtered_and_ordered_note(request):
user = request.user
notes = Note.filter_and_order_by(user_id=user.id, order_field="title", direction="desc", load=["author"])
if not notes:
return Response("No notes were found!", status=404)
return render("filter_order_note.html", {"notes":notes}, app="note")
routes["/filtered_and_ordered_note"] = filtered_and_ordered_note
.update()
# apps/note/routes.py
from .models import Note
def update_note(request, id):
note = Note.get(id)
if not note:
return Response("Note not found!", status=404)
if request.method == "POST":
data = request.POST
title = data.get("title")
note.update(title=title)
return redirect("/")
return render("update_note.html", {"note":note}, app="note")
routes["/update_note/<id>"] = update_note
.delete()
# apps/note/routes.py
from .models import Note
def delete_note(request, id):
note = Note.get(id)
if not note:
return Response("Note not found!", status=404)
note.delete()
return redirect("/")
return render("delete_note.html", app="note")
routes["/delete_note/<id>"] = delete_note