Skip to content

Tina4 Python -- Quick Reference

Hot Tips

  • Routes go in src/routes/, templates in src/templates/, static files in src/public/
  • GET routes are public by default; POST/PUT/PATCH/DELETE require a token
  • Return a dict from response() and the framework sets application/json
  • Run tina4 serve to start the dev server on port 7145

Installation

bash
pip install tina4-python
tina4 init my-project
cd my-project
tina4 serve

The CLI scaffolds your project, installs one package, and starts the server. No dependency tree. No version conflicts. Your browser opens to http://localhost:7145 and the welcome page greets you.

More details on project setup and customization.

Static Websites

Put .twig files in ./src/templates and assets in ./src/public. The framework serves them without additional configuration.

twig
<!-- src/templates/index.twig -->
<h1>Hello Static World</h1>

More details on static website routing.

Basic Routing

The @app decorators register routes. Each handler receives request and response. Path parameters arrive as function arguments.

python
@app.get("/")
async def get_home(request, response):
    return response("<h1>Hello Tina4 Python</h1>")

# POST requires a formToken in the body or Bearer auth
@app.post("/api")
async def post_api(request, response):
    return response({"data": request.body})

# Redirect after a POST
@app.post("/register")
async def post_register(request, response):
    return response.redirect("/welcome")

Follow the links for basic routing and dynamic routing with variables.

Middleware

Middleware runs before and after your route handler. Define a class with static methods, then attach it with the @middleware decorator.

python
class RunSomething:

    @staticmethod
    def before_something(request, response):
        response.content += "Before"
        return request, response

    @staticmethod
    def after_something(request, response):
        response.content += "After"
        return request, response

    @staticmethod
    def before_and_after_something(request, response):
        response.content += "[Before / After Something]"
        return request, response

@middleware(RunSomething)
@app.get("/middleware")
async def get_middleware(request, response):
    return response("Route") # Before[Before / After Something]Route[Before / After Something]After

Follow the links for more on Middleware Declaration and Linking to Routes.

Template Rendering

Put .twig files in ./src/templates and assets in ./src/public. The template engine reads your layout, fills in the variables, and delivers clean HTML.

twig
<!-- src/templates/index.twig -->
<h1>Hello {{name}}</h1>
python
@app.get("/")
async def get_home(request, response):
    return response.render("index.twig", {"name": "World!"})

Sessions

The default session handler stores data on the file system. Override TINA4_SESSION_HANDLER in .env to switch backends.

HandlerBackendRequired package
SessionFileHandler (default)File system--
SessionRedisHandlerRedisredis
SessionValkeyHandlerValkeyvalkey
SessionMongoHandlerMongoDBpymongo
bash
TINA4_SESSION_HANDLER=SessionMongoHandler
TINA4_SESSION_MONGO_HOST=localhost
TINA4_SESSION_MONGO_PORT=27017
TINA4_SESSION_MONGO_URI=
TINA4_SESSION_MONGO_USERNAME=
TINA4_SESSION_MONGO_PASSWORD=
TINA4_SESSION_MONGO_DB=tina4_sessions
TINA4_SESSION_MONGO_COLLECTION=sessions
python
@app.get("/session/set")
async def get_session_set(request, response):
    request.session.set("name", "Joe")
    request.session.set("info", {"info": ["one", "two", "three"]})
    return response("Session Set!")


@app.get("/session/get")
async def get_session_get(request, response):
    name = request.session.get("name")
    info = request.session.get("info")
    return response({"name": name, "info": info})


@app.get("/session/clear")
async def get_session_clear(request, response):
    request.session.delete("name")
    return response("Session key removed!")

SCSS Stylesheets

Drop .scss files in ./src/scss. The framework compiles them to ./src/public/css.

scss
// src/scss/main.scss
$primary: #2c3e50;
body {
  background: $primary;
  color: white;
}

More details on css and scss.

Environments

The .env file holds your project configuration. The framework reads it at startup.

TINA4_DEBUG=true
TINA4_PORT=7145
DATABASE_URL=sqlite:///data/app.db
TINA4_LOG_LEVEL=ALL
API_KEY=ABC1234
python
import os

api_key = os.getenv("API_KEY", "ABC1234")

Access env vars programmatically:

python
from tina4_python.dotenv import load_env, get_env, has_env, require_env, is_truthy

load_env()                          # Load .env file (auto on server start)
get_env("DATABASE_URL")             # Get value or None
get_env("PORT", "7145")             # Get value with default
has_env("TINA4_DEBUG")              # True if set
require_env("DATABASE_URL")         # Raises if missing
is_truthy(get_env("TINA4_DEBUG"))   # True for "true", "1", "yes"

Authentication

POST, PUT, PATCH, and DELETE routes require a Bearer token by default. Pass Authorization: Bearer API_KEY in the request header. Use @noauth to open a route to everyone. Use @secured to lock a GET route behind authentication.

python
from tina4_python.Auth import Auth

@app.post("/login")
@noauth
async def login(request, response):
    token = Auth.get_token({"user_id": 1, "role": "admin"})
    return response({"token": token})

@app.get("/protected")
@secured
async def secret(request, response):
    return response("Welcome!")

@app.get("/verify")
async def verify(request, response):
    token = request.headers.get("Authorization", "").replace("Bearer ", "")
    payload = Auth.valid_token(token)
    return response({"valid": payload is not None})

HTML Forms and Tokens

twig
<form method="POST" action="/register">
    {{ ("Register" ~ RANDOM()) | form_token }}
    <input name="email">
    <button>Save</button>
</form>

More details on posting form data, basic form handling, how to generate form tokens, dealing with file uploads, returning errors, disabling route auth and a full login example.

AJAX and frond.js

Tina4 ships with frond.js, a small zero-dependency JavaScript library for AJAX calls, form submissions, and real-time WebSocket connections.

More details on available features.

OpenAPI and Swagger UI

Visit http://localhost:7145/swagger. Decorated routes appear in the Swagger UI without manual annotation.

python
from tina4_python import description

@app.get("/users")
@description("Get all users")
async def users(request, response):
    return response(User().select("*"))

Follow the links for more on Configuration, Usage and Decorators.

Databases

python
from tina4_python.Database import Database

# dba = Database("<driver>:<hostname>/<port>:database_name", username, password)
dba = Database("sqlite3:data.db")

The adapter speaks PostgreSQL, MySQL, and SQLite. It translates your queries into whichever dialect the database understands.

Follow the links for more on Available Connections, Core Methods, Usage and Full transaction control.

Database Results

python
result = dba.fetch("select * from test_record order by id", limit=3, offset=1)

array = result.to_array()
paginated = result.to_paginate()
csv_data = result.to_csv()
json_data = result.to_json()

Looking at detailed Usage will deepen your understanding.

Migrations

bash
tina4 migrate:create create_users_table
sql
-- migrations/00001_create_users_table.sql
CREATE TABLE users
(
    id   INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT
);
bash
tina4 migrate

Migrations have limitations worth knowing before you use them at scale.

ORM

python
from tina4_python.ORM import ORM, IntegerField, StringField

class User(ORM):
    id   = IntegerField(primary_key=True, auto_increment=True)
    name = StringField()

User({"name": "Alice"}).save()

user = User()
user.load("id = ?", [1])

ORM covers more ground than this snippet shows. Study the Advanced Detail to get the full value.

CRUD

python
@app.get("/users/dashboard")
async def dashboard(request, response):
    users = User().select("id, name, email")
    return response.render("users/dashboard.twig", {"crud": users.to_crud(request)})
twig
{{ crud }}

More details on how CRUD generates its files and where they live.

Consuming REST APIs

python
from tina4_python import Api

api = Api("https://api.example.com", auth_header="Bearer xyz")
result = api.get("/users/42")
print(result["body"])

More details on sending POST data, authorization headers, and other controls for outbound API requests.

Inline Testing

python
from tina4_python import tests


@tests(
    assert_equal((7, 7), 1),
    assert_equal((-1, 1), -1),
    assert_raises(ZeroDivisionError, (5, 0)),
)
def divide(a: int, b: int) -> float:
    if b == 0:
        raise ZeroDivisionError("division by zero")
    return a / b

Run: tina4 test

Services

Due to the nature of Python, services are not necessary.

Websockets

WebSocket support is built in. No extra dependencies. Define a handler with the @app.websocket decorator, and the framework manages the connection alongside your HTTP routes on the same port.

python
@app.websocket("/ws/chat")
async def chat_ws(connection, event, data):
    if event == "message":
        await connection.send(f"Echo: {data}")

Have a look at the PubSub example under Websockets.

Queues

Supports litequeue (default/SQLite), RabbitMQ, Kafka, and MongoDB backends. The queue system uses produce() and consume() directly -- no separate Producer or Consumer classes.

python
from tina4_python.Queue import Queue

# Produce a message
queue = Queue(topic="emails")
queue.produce("emails", {"to": "alice@example.com", "subject": "Welcome"})

# Consume messages
for job in queue.consume("emails"):
    print(job.payload)

Full details on backend configuration, batching, multi-queue consumers, and error handling.

WSDL

python
from tina4_python.WSDL import WSDL, wsdl_operation
from typing import List


class Calculator(WSDL):
    SERVICE_URL = "http://localhost:7145/calculator"

    def Add(self, a: int, b: int):
        return {"Result": a + b}

    def SumList(self, Numbers: List[int]):
        return {
            "Numbers": Numbers,
            "Total": sum(Numbers),
            "Error": None
        }


@wsdl("/calculator")
async def wsdl_cis(request, response):
    return response.wsdl(Calculator(request))

More Details on WSDL configuration and usage.

GraphQL

python
from tina4_python.graphql import GraphQL

schema = """
type Query {
    hello(name: String!): String
    users: [User]
}

type User {
    id: Int
    name: String
    email: String
}
"""

resolvers = {
    "hello": lambda info, name: f"Hello, {name}!",
    "users": lambda info: db.fetch("SELECT * FROM users").records,
}

graphql = GraphQL(schema, resolvers)

Register the endpoint:

python
@post("/graphql")
@noauth()
async def handle_graphql(request, response):
    result = graphql.execute(request.body.get("query", ""))
    return response(result)

GraphiQL UI available at /__dev/graphql in debug mode.

Localization (i18n)

Set TINA4_LANGUAGE in .env to change the framework language. Supported: en, fr, af.

python
from tina4_python.Localization import localize

_ = localize()
print(_("Server stopped."))  # "Bediener gestop." (af)

Translations use Python's gettext module. The framework falls back to English for unsupported languages.

python
from tina4_python.Localization import AVAILABLE_LANGUAGES
# ['en', 'fr', 'af']

HTML Builder

python
from tina4_python.HtmlElement import HTMLElement, add_html_helpers

el = HTMLElement("div", {"class": "card"}, ["Hello"])
str(el)  # <div class="card">Hello</div>

# Nesting
page = HTMLElement("div")(
    HTMLElement("h1")("Title"),
    HTMLElement("p")("Content"),
)

# Helper functions
add_html_helpers(globals())
html = _div({"class": "card"},
    _h1("Title"),
    _p("Description"),
    _a({"href": "/more"}, "Read more"),
)

Events

python
from tina4_python.core.events import on, emit, once, off

@on("user.created")
def send_welcome(user):
    print(f"Welcome {user['name']}!")

@once("app.ready")
def on_ready():
    print("Started!")

emit("user.created", {"name": "Alice"})

Logging

python
from tina4_python.debug import Log

Log.info("Server started")
Log.debug("Request received", path="/api/users")
Log.warning("Slow query", duration_ms=450)
Log.error("Connection failed", host="db.example.com")

Set TINA4_LOG_LEVEL in .env: ALL, DEBUG, INFO, WARNING, ERROR.

Response Cache

python
from tina4_python.core.router import get, cached

@cached(True, max_age=120)
@get("/api/products")
async def products(request, response):
    return response(expensive_query())

Health Endpoint

Built-in at /health. Returns {"status": "ok", "uptime": 123.4}. Configure with TINA4_HEALTH_PATH env var.

DI Container

python
from tina4_python.container import Container

container = Container()
container.singleton("db", lambda: Database("sqlite:///app.db"))
container.register("mailer", lambda: MailService())
db = container.get("db")

Error Overlay

Automatic in debug mode. Shows syntax-highlighted stack trace with source context. Set TINA4_DEBUG=true in .env.

Dev Admin

Available at /__dev in debug mode. Includes route inspector, database tab, request capture, metrics bubble chart, gallery examples, dev mailbox.

CLI Commands

bash
tina4 init python my-app    # Scaffold project
tina4 serve                  # Start dev server
tina4 serve --production     # Production mode
tina4 doctor                 # Check environment
tina4 env                    # Configure .env
tina4 docs                   # Download documentation
tina4 generate model User    # Generate scaffolding
tina4 migrate                # Run migrations
tina4 test                   # Run tests
tina4 ai                     # Install AI context

MCP Server

Auto-starts on /__mcp in debug mode. Exposes 24 dev tools via JSON-RPC 2.0 over SSE. Works with Claude Code, Cursor, and other MCP clients.

FakeData

python
from tina4_python.seeder import FakeData

fake = FakeData()
fake.name()      # "Alice Johnson"
fake.email()     # "alice@example.com"
fake.phone()     # "+1-555-0123"
fake.sentence()  # "The quick brown fox..."
fake.integer()   # 4821

Sponsored with 🩵 by Code InfinityCode Infinity