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.1.tar.gz (21.8 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.1-py3-none-any.whl (23.2 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: saython-0.1.1.tar.gz
  • Upload date:
  • Size: 21.8 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.1.tar.gz
Algorithm Hash digest
SHA256 e9e0640e2894b2de61d4d40bce3a9879097e7942471934526a1d05bf9b3cdf45
MD5 ea7f1fa17527372114a901c69f6b1269
BLAKE2b-256 bade3fccd766877e277668c713a3a40a0915496ef28b4e4e9065471dd31d47ac

See more details on using hashes here.

File details

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

File metadata

  • Download URL: saython-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 23.2 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.1-py3-none-any.whl
Algorithm Hash digest
SHA256 2b0d59558bf52c8dc3867d96d8415c38f8c755f4a31619faaae73c5c351ff8ff
MD5 2350928f7f081ec3dade2b16378b307e
BLAKE2b-256 802be60012770fc42db22213949497a24f5554ac78ec2fc6190d2157358fff08

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