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 lightservehttp://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 lightservehttp://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 lightservehttp://127.0.0.1:8000/profile/JohnDoeThis is JohnDoe's Profileapps/
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 appapps/
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 lightservehttp://127.0.0.1:8000/exampleapps/
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 lightservehttp://127.0.0.1:8000/exampleand 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"] = exampledef 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 lightmigrateThis 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_createCreating 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_listLooping 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_updateUpdating 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_deleteUsing 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):
passRun migrate in terminal:
py virgo.py lightmigratethis 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_viewRegistration 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_viewLogin 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"] = dashboardDashboard 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_viewDashboard 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 decoratorRun migrate:
py virgo.py lightmigrateFor 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_ROUTEWe will try to make an application with proper file structure that's why we will create two apps
py virgo.py lightstart postpy virgo.py lightstart userNavigate 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 UserModelNavigate 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 ModelThis 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
passCreating 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_postThis 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_notefirst_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_noteorder_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_notefilter_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