Tina4

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 7146

Installation โ€ข Static Websites โ€ข Routing โ€ข Middleware โ€ข Templates โ€ข Sessions โ€ข SCSS โ€ข Environments โ€ข Authentication โ€ข Forms & Tokens โ€ข AJAX โ€ข OpenAPI โ€ข Databases โ€ข Database Results โ€ข Migrations โ€ข ORM โ€ข CRUD โ€ข REST Client โ€ข Testing โ€ข Services โ€ข Websockets โ€ข Queues โ€ข WSDL โ€ข GraphQL โ€ข Localization โ€ข HTML Builder โ€ข Events โ€ข Logging โ€ข Cache โ€ข Health โ€ข DI Container โ€ข Error Overlay โ€ข Dev Admin โ€ข CLI โ€ข MCP โ€ข FakeData

Installation#

bash
# Install the tina4 CLI once. Windows: irm https://tina4.com/install.ps1 | iexcurl -fsSL https://tina4.com/install.sh | shโ€‹tina4 init python my-appcd my-apptina4 serve

The CLI scaffolds your project, installs the dependencies, and starts the server. No dependency tree. No version conflicts. Your browser opens to http://localhost:7146 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#

Import the route decorators from tina4_python. Each handler receives request and response. Path parameters arrive as function arguments.

python
from tina4_python import get, postโ€‹@get("/")async def get_home(request, response):    return response("<h1>Hello Tina4 Python</h1>")โ€‹# POST requires a formToken in the body or Bearer auth@post("/api")async def post_api(request, response):    return response({"data": request.body})โ€‹# Redirect after a POST@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
from tina4_python import get, middlewareโ€‹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)@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
from tina4_python import getโ€‹@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_BACKEND in .env to switch backends.

The value is the backend name, not a class name, and it is normalised: leading or trailing spaces and capitals are fine, so Redis resolves.

An unrecognised value RAISES at startup, naming the bad value and the valid ones. It used to fall back to the file backend silently, which meant a typo produced a running app writing sessions to local disk while you believed they were in Redis: nothing logged, nothing failed, and the symptom only surfaced later as users being logged out whenever a request landed on another instance. Leaving TINA4_SESSION_BACKEND unset, or setting it to an empty value, still gives you the file default.

ValueBackendRequired package
file (default)File system--
redisRedisredis
valkeyValkeyvalkey
mongodbMongoDBpymongo
databaseYour ORM DB--
bash
TINA4_SESSION_BACKEND=mongodbTINA4_SESSION_MONGO_URI=mongodb://localhost:27017TINA4_SESSION_MONGO_DB=tina4_sessionsTINA4_SESSION_MONGO_COLLECTION=sessions
python
from tina4_python import getโ€‹@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!")โ€‹โ€‹@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})โ€‹โ€‹@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=trueTINA4_PORT=7145TINA4_DATABASE_URL=sqlite:///data/app.dbTINA4_LOG_LEVEL=ALLTINA4_API_KEY=ABC1234
python
import osโ€‹api_key = os.getenv("TINA4_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("TINA4_DATABASE_URL")             # Get value or Noneget_env("PORT", "7145")             # Get value with defaulthas_env("TINA4_DEBUG")              # True if setrequire_env("TINA4_DATABASE_URL")         # Raises if missingis_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 TINA4_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 import get, post, noauth, securedfrom tina4_python.auth import Authโ€‹@post("/login")@noauth()async def login(request, response):    token = Auth.get_token({"user_id": 1, "role": "admin"})    return response({"token": token})โ€‹@get("/protected")@secured()async def secret(request, response):    return response("Welcome!")โ€‹@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 getfrom tina4_python.swagger import descriptionโ€‹@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.sqlCREATE 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
from tina4_python import getโ€‹@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 @websocket decorator, and the framework manages the connection alongside your HTTP routes on the same port.

python
from tina4_python import websocketโ€‹@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, with no separate Producer or Consumer classes.

python
from tina4_python.queue import Queueโ€‹# Produce a messagequeue = Queue(topic="emails")queue.produce("emails", {"to": "alice@example.com", "subject": "Welcome"})โ€‹# Consume messagesfor job in queue.consume("emails"):    print(job.payload)

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

WSDL#

Subclass WSDL and decorate each operation with @wsdl_operation, giving the return shape. Drop the file in src/routes/ and the framework serves the SOAP endpoint plus its generated WSDL.

python
from typing import Listfrom tina4_python.wsdl import WSDL, wsdl_operationโ€‹โ€‹class Calculator(WSDL):โ€‹    @wsdl_operation({"Result": int})    def Add(self, a: int, b: int):        return {"Result": a + b}โ€‹    @wsdl_operation({"Numbers": List[int], "Total": int})    def SumList(self, Numbers: List[int]):        return {"Numbers": Numbers, "Total": sum(Numbers)}

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
from tina4_python import post, noauthโ€‹@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)#

Translation files live in src/locales/ as JSON. Create an I18n instance with a locale directory and a default locale, switch languages at runtime, and translate keys with t().

python
from tina4_python.i18n import I18nโ€‹i18n = I18n(locale_dir="src/locales", default_locale="en")โ€‹i18n.set_locale("af")          # switch languagei18n.t("welcome_message")      # translated string for the active localei18n.t("greeting", name="Ada") # with interpolation

Missing keys fall back to the default locale.

Back to top

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>โ€‹# Nestingpage = HTMLElement("div")(    HTMLElement("h1")("Title"),    HTMLElement("p")("Content"),)โ€‹# Helper functionsadd_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(max_age=120)@get("/api/products")async def products(request, response):    return response(expensive_query())

Health Endpoint#

Built-in at /__health, with /health always registered too. Returns {"status": "ok", "version": "3.13.94", "uptime": 123.4, "framework": "tina4-python"}. Configure the path with TINA4_HEALTH_PATH. It is a liveness probe: it reports on the process, never on a database or cache.

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 projecttina4 serve                  # Start dev servertina4 serve --production     # Production modetina4 doctor                 # Check environmenttina4 env                    # Configure .envtina4 docs                   # Download documentationtina4 generate model User    # Generate scaffoldingtina4 migrate                # Run migrationstina4 test                   # Run teststina4 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

๐Ÿ“• Download the book#

Tina4 for Python Developers (PDF): full reference, printable, with clickable table of contents and PDF outline. Regenerated with every release.