roper

A Python framework for writing web applications properly.

$ uvx proper_new myapp

Type this command, and a new project will appear (requires uv)

myapp/models/article.py
# importsimport peewee as pw from proper.rich_text import HasRichText, RichTextField from .base import BaseModel, scope from .attachment import Attachment from .user import User
class Article(BaseModel, HasRichText): title = pw.CharField() content = RichTextField(Attachment, null=True) cover_image = pw.ForeignKeyField(Attachment, null=True) is_draft = pw.BooleanField(default=True) author = pw.ForeignKeyField( model=User, backref="articles", default=lambda: current.user ) @scope def published(query): return query.where(Article.is_draft==False) @scope def recent(query): return ( query .order_by(Article.created_at.desc()) .limit(25) ) def byline(self): return ( f"Written by {self.author.name} " f"on {self.created_at.strftime('%b %d, %Y')}") def publish(self): self.is_draft = False

Peewee ORM make modelling easy.

Proper uses Peewee as its ORM, and adds some extra features like scopes, rich text fields, and attachment support.

myapp/controllers/controller_article.py
# importsfrom proper.errors import NotFound from ..forms.article import ArticleForm from ..models import Article from ..router import router from .app_controller import AppController
@router.resource("articles") class ArticleController(AppController): before = [ {"do":"set_article", "exclude":["index","new","create"]}, {"do":"set_form", "exclude":["index","show","delete"]}, {"do":"validate_form", "only":["create","update"]}, ] def index(self): self.articles = Article.select().published().recent() def show(self): if self.article.is_draft: raise NotFound def new(self): pass def edit(self): pass def create(self): article = self.form.save() self.response.redirect_to("Article.show", article) # Private def set_article(self): article_id = self.params.get("article_id", "") self.article = Article.get_or_none(id=int(article_id)) if not self.article: raise NotFound def set_form(self): obj = getattr(self, "article", None) self.form = ArticleForm(self.params, object=obj)

Controllers handle all requests.

Proper controllers are simple classes with methods for each action. They have before/after hooks, and can render templates or redirect.

myapp/forms/article.py
# importsfrom proper import forms as f from ..models import Article, Attachment
class ArticleForm(f.Form): class Meta: orm_cls = Article title = f.TextField() content = f.RichTextField(required=False) cover_image = f.AttachmentField(Attachment, required=False) is_draft = f.BooleanField(default=False)

Forms are declarative and easy to use.

Proper forms are declarative, and integrate with Peewee models. They also have built-in validation, and can be rendered in templates.

myapp/views/article/show.jx
{#import "layouts/app.jx" as Layout #}
{#def article #}

<Layout title="Room">
  <h1>{{ article.title }}</h1>
  <img src="{{ article.cover_image.url }}"
       alt="{{ article.title }}">
  <p>{{ article.content }}</p>

  {% if current.user.is_admin -%}
    <a href="{{ url_for('Article.edit', article) }}">Edit</a>
  {% endif %}
</Layout>

Component Views.

Proper views are components-based templates. They can have props, slots, and can be nested.

$ proper g resource Post title:str body:text create tests/test_post.py skipped demo/router.py create demo/forms/post.py create demo/controllers/post_controller.py append demo/controllers/__init__.py create demo/views/post/ create demo/views/post/form.jx create demo/views/post/index.jx create demo/views/post/new.jx create demo/views/post/edit.jx create demo/views/post/show.jx append demo/models/__init__.py create demo/models/post.py

Scaffolds,
not snippets

Copy-paste is a tax on attention. Proper's generators produce complete, idiomatic, pre-tested files - so you read, not reassemble.

One command turns a resource into a model, controller, views, tests, and routes - wired together and ready to run.

- Almost suspicious, really.

Opinionated
by design

These are the ideas the framework will not compromise on - the rules that everything else is built around.

- 01

Humans read code. Machines just run it.

Writing code can - and should - be automated. But making that code easy to read and understand is the framework's reason for being.

- 02

Convention over configuration

You already know where things go. A Post model lives at models/post.py. Its controller is PostController in controllers/post_controller.py. Its views are at views/post/. This is not a suggestion - it is a feature.

- 03

CRUD, always

Every controller maps URLs to the same seven actions - index, new, create, show, edit, update, delete. Remove those you don't need. Create another resource if you need more.

- 04

Sync above, async below

The runtime is ASGI. The code you write is not. await is a hazard we've hidden behind a clean interface - you get concurrency without the colour.

For AI Agents

Build a Proper app, side by side with AI.

Drop the Proper skill into your skills folder and your AI agent will scaffold, edit, and refactor your app the Proper way: convention-aware, generator-first, idiomatic from the start.

Installed in your home folder, skills apply across every AI agent session.

# Step one: download the skill. $ mkdir -p ~/.claude/skills $ curl -LsSf https://properproject/skill.zip -o ~/.claude/skills/ # Step two: open your project. $ cd myapp # Step three: ask, plainly, for a thing. $ claude ─────────────── add an OAuth login # Step four: You are the one in charge