Skip to main content

A beginner-friendly Python web framework for CRUD apps with auth and ORM

Project description

SAYTHON

A beginner-friendly Python web framework focused on CRUD apps — with built-in ORM and authentication.
Zero hard dependencies. Pure Python standard library + optional extras.


Install

pip install saython
# or from source:
git clone <repo>
pip install -e .

Optional extras for better performance:

pip install PyJWT bcrypt          # faster JWT + stronger password hashing
pip install psycopg2-binary       # PostgreSQL support
pip install gunicorn              # production WSGI server

Quickstart

from saython import Saython, Model, Field, auth_required, get_auth_router, Router, Response

app = Saython(__name__)

class Book(Model):
    title  = Field(str, required=True)
    author = Field(str, required=True)
    year   = Field(int, default=2024)

# Mount auth routes
app.include("/auth", get_auth_router())

# Write your own routes
book_router = Router()

@book_router.get("/")
def list_books(req):
    return Book.all_as_dicts()

@book_router.post("/")
@auth_required
def create_book(req):
    book = Book.create(**req.json)
    return Response(book.to_dict(), 201)

@book_router.get("/<id>")
def get_book(req):
    return Book.get(int(req.params["id"])).to_dict()

@book_router.delete("/<id>")
@auth_required
def delete_book(req):
    Book.get(int(req.params["id"])).delete()
    return {"message": "deleted"}

app.include("/books", book_router)

@app.get("/")
def index(req):
    return {"message": "Hello from SAYTHON!"}

app.run(debug=True)
Method Path Description
POST /auth/register Register user
POST /auth/login Login → get token
GET /auth/me Current user (token)
GET /books List all books
POST /books Create (auth)
GET /books/<id> Get one book
DELETE /books/<id> Delete (auth)
GET / Custom endpoint

CLI

saython new my-blog      # scaffold a new project
cd my-blog
python main.py           # run it

saython run              # same, from CLI
saython routes           # print all registered routes

ORM

from saython import Model, Field

class Post(Model):
    title    = Field(str,   required=True)
    content  = Field(str,   required=True)
    views    = Field(int,   default=0)
    published = Field(bool, default=False)

# Every Model gets: id, created_at, updated_at automatically.

CRUD operations

# Create
post = Post.create(title="Hello", content="World")

# Read
post  = Post.get(1)                                 # by id (NotFound if missing)
posts = Post.all()                                  # list all
posts = Post.filter(published=True)                 # equality filter
page  = Post.paginate(page=1, limit=10)             # pagination dict
post  = Post.first(title="Hello")                   # first match or None
count = Post.count()
exists = Post.exists(title="Hello")

# Update
post.title = "Updated"
post.save()

# Delete
post.delete()

# Serialise
post.to_dict()                        # all fields as dict
post.to_dict(exclude=["content"])     # exclude fields

Switch database

# SQLite (default):
app.configure(DATABASE_URL="myapp.db")

# PostgreSQL:
app.configure(DATABASE_URL="postgresql://user:pass@localhost/dbname")

# MySQL:
app.configure(DATABASE_URL="mysql://user:pass@localhost/dbname")

Authentication

Register & Login

POST /auth/register
{ "username": "alice", "email": "alice@example.com", "password": "secret123" }

POST /auth/login
{ "username": "alice", "password": "secret123" }
→ { "token": "eyJ...", "user": { "id": 1, "username": "alice", ... } }

Protect endpoints

from saython import auth_required, admin_required

@app.get("/dashboard")
@auth_required
def dashboard(req):
    return {"hello": req.user.username}

@app.delete("/admin/wipe")
@admin_required         # is_admin=True required
def wipe(req):
    return {"done": True}

Send the token in requests:

Authorization: Bearer eyJ...

User model

from saython.auth import User

user = User.register(username="bob", email="bob@x.com", password="pass123")
user = User.authenticate("bob", "pass123")
token = user.generate_token()
user.to_dict()   # password field automatically excluded

Custom Routing

@app.get("/items")
def list_items(req):
    page  = int(req.query.get("page", 1))
    limit = int(req.query.get("limit", 20))
    return Item.paginate(page=page, limit=limit)

@app.post("/items")
def create_item(req):
    data = req.json          # parsed JSON body
    item = Item.create(**data)
    return item.to_dict(), 201

@app.get("/items/<id>")
def get_item(req):
    return Item.get(int(req.params["id"])).to_dict()

@app.delete("/items/<id>")
@auth_required
def delete_item(req):
    Item.get(int(req.params["id"])).delete()
    return {"deleted": True}

Sub-Routers

from saython import Router

book_router = Router()

@book_router.get("/")
def list_books(req): ...

@book_router.post("/")
def create_book(req): ...

app.include("/books", book_router)

Configuration

app.configure(
    SECRET_KEY="very-secret",          # JWT signing key
    DATABASE_URL="myapp.db",           # DB connection
    DEBUG=True,                        # show tracebacks
    CORS=True,                         # enable CORS headers
    CORS_ORIGINS=["https://myapp.com"],
    TOKEN_EXPIRY_HOURS=48,             # JWT lifetime
    HOST="0.0.0.0",
    PORT=8080,
)

Middleware & Hooks

@app.before_request
def log_request(req):
    print(f"→ {req.method} {req.path}")

@app.after_request
def add_header(req, resp):
    resp.headers["X-Powered-By"] = "SAYTHON"

@app.error_handler(404)
def not_found(req, err):
    return {"error": "Page not found", "path": req.path}, 404

Production

pip install gunicorn
gunicorn main:app --bind 0.0.0.0:8000 --workers 4

Project Structure (generated by saython new)

my-project/
├── main.py          ← app + routes + model registration
├── models.py        ← extra model definitions
├── .env             ← secret key, DB URL
├── requirements.txt
└── README.md

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

saython-0.1.0.tar.gz (21.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

saython-0.1.0-py3-none-any.whl (22.9 kB view details)

Uploaded Python 3

File details

Details for the file saython-0.1.0.tar.gz.

File metadata

  • Download URL: saython-0.1.0.tar.gz
  • Upload date:
  • Size: 21.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.12

File hashes

Hashes for saython-0.1.0.tar.gz
Algorithm Hash digest
SHA256 14e50b1d0b29e0d89b48a8ca5905d3629fecb37aae82a2a4e6b2f6e7eb55889f
MD5 3681b20fa27f1ae989e25922a0dd1ea1
BLAKE2b-256 57ec114515c08afbbbc5c1733045671edbcba76c08f284103acaf21c790f0600

See more details on using hashes here.

File details

Details for the file saython-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: saython-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 22.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.12

File hashes

Hashes for saython-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f88553192852e81dca382a73aca0fb13d119c8c48abbaab18449c202ff691797
MD5 9ff99631169eaa3ed9900c2af8c213ed
BLAKE2b-256 b7b8d16033565677c8c1c9055d44188304b44855b1f90f89737349e9f185c3d2

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page