emmett Framework Documentation

repository·master·Indexed 22 days ago

https://github.com/emmett-framework/emmett

A full-stack Python web framework (v2.8.1) designed for simplicity and ease of learning. It features an integrated ORM, asynchronous handler support, a flexible application pipeline, and a comprehensive authorization system. The framework supports Python 3.9+ and utilizes semantic versioning, providing tools for module hierarchies, sub-namespace configuration, and customizable authentication workflows.

Tokens
57.6K
Snippets
172
Records
302
Agent score
78%

What's inside emmett

  1. Emmett Compatibility and Versioning

    master

    Emmett is production-ready and requires Python 3.9 or above.

    The project follows semantic versioning ({major}.{minor}.{patch}):

    • Major: Breaking changes.
    • Minor: New features and potential deprecations.
    • Patch: Bug fixes.

    Deprecations are maintained for at least 3 minor versions before removal, and changes are communicated via the official upgrade guide.

  2. How module hierarchies and sub-modules work

    master

    Modules can be nested to create a hierarchy of paths and namespaces. Sub-modules inherit properties from their parents, and their url_prefix values are composed.

    Composition Rules

    1. URL Paths: Prefixes are concatenated. If apis has prefix /apis and v1 has prefix /v1, routes in v1 start with /apis/v1.
    2. Routing Namespaces: Names are namespaced using dots. A route in v1 inside apis becomes apis.v1.route_name.
    3. Pipelines: Sub-module pipelines are added to the parent module's pipeline. If the parent has PipeA and the child has PipeB, the final pipeline is [PipeA, PipeB].
    # Parent module
    apis = app.module(__name__, 'apis', url_prefix='apis')
    apis.pipeline = [ServicePipe('json')]
    
    # Sub-module
    v1_apis = apis.module(__name__, 'v1', url_prefix='v1')
    v1_apis.pipeline = [SomeAuthPipe()]
    
    # Grandchild module
    users = v1_apis.module(__name__, 'users', url_prefix='users')
    
    @users.route('/')
    async def index():
        # Final URL: /apis/v1/users/
        # Final Namespace: apis.v1.users.index
        pass
    apis = app.module(__name__, 'apis', url_prefix='apis')
    v1_apis = apis.module(__name__, 'v1', url_prefix='v1')
    users = v1_apis.module(__name__, 'users', url_prefix='users')
  3. Define routing with variable rules

    master

    Emmett uses the @app.route() decorator for routing. You can include variable parts in a URL using the <type:variable_name> syntax. These variables are passed as keyword arguments to the decorated function.

    Supported variable types:

    • int: integers
    • float: floats in dot notation
    • str: strings
    • date: date strings (YYYY-MM-DD)
    • alpha: strings containing only literals
    • any: any path (including slashes)

    If a URL does not match the specified type (e.g., providing a string for an int rule), Emmett returns a 404 error.

    To make a part of the URL optional, wrap it in parentheses and append a question mark: "/path(/<type:var>)?". If the optional part is missing, the corresponding function parameter will be None.

    @app.route('/user/<str:username>')
    async def user(username):
        return "Hello %s" % username
    
    @app.route('/double/<int:number>')
    async def double(number):
        return "%d * 2 = %d" % (number, number*2)
    
    @app.route("/profile(/<int:user_id>)?")
    async def profile(user_id):
        if user_id:
            # get requested user
        else:
            # load current logged user profile
  4. How the Emmett pipeline works

    master

    The Emmett pipeline manages the request flow through your application using a sequence of Pipe objects. The lifecycle of a request through the pipeline follows these steps:

    1. Open: Emmett calls open() on all pipes in the pipeline.
    2. Pipe (Request Flow): The request flows through each pipe's pipe() method sequentially until it reaches the routed method.
    3. Route Execution: The routed method executes and produces a response.
    4. Pipe (Response Flow): The response flows back through the pipeline in reverse order.
    5. Close: Emmett calls close() on all pipes. This is guaranteed to run even if an exception occurs.

    Key Lifecycle Methods for Custom Pipes:

    • open(): Setup logic before the request flows (e.g., opening a database connection).
    • close(): Cleanup logic after the response is built (e.g., closing a connection).
    • pipe(next_pipe, **kwargs): The core logic that handles the request. It is responsible for calling next_pipe to continue the flow. This method can be used to alter the flow (e.g., aborting a request) or inject data into kwargs.
    • on_pipe_success(): Called when the flow returns successfully.
    • on_pipe_failure(): Called if an exception occurs anywhere in the subsequent pipeline steps.

    Note on Execution Order: The order of open() and close() calls is not guaranteed. If you require strict execution order, implement your logic within the pipe() method instead.

    from emmett import Pipe
    
    class MyPipe(Pipe):
        async def open(self):
            pass
        async def close(self):
            pass
        async def pipe(self, next_pipe, **kwargs):
            return await next_pipe(**kwargs)
        async def on_pipe_success(self):
            pass
        async def on_pipe_failure(self):
            pass
  5. Configure the application pipeline with Session, DB, and Auth

    master

    To enable database access, authorization, and session management in your routes, you must add the corresponding 'pipes' to app.pipeline.

    Common pipeline order:

    1. SessionManager.cookies(secret_key): Enables cookie-based sessions.
    2. db.pipe: Enables ORM access within request context.
    3. auth.pipe: Enables authorization checks and session.auth access.
    from emmett.sessions import SessionManager
    
    app.pipeline = [
        SessionManager.cookies('your_secret_key'),
        db.pipe,
        auth.pipe
    ]
  6. Customize Table Naming

    master

    By default, Emmett pluralizes the class name to create the table name (e.g., Post becomes posts). To use a specific name, set the tablename attribute within your model. This is useful for irregular English plurals (e.g., Mouse -> mice) or specific DBMS requirements.

    class Post(Model):
        tablename = "myposts"
  7. Implement the MVC pattern in Emmett

    master

    Emmett supports the Model-View-Controller (MVC) pattern by using application modules. While Emmett does not provide a dedicated 'Controller' class, you can implement this pattern by organizing your code into controllers and models sub-packages.

    Recommended MVC Structure:

    /myapp
        __init__.py
        /controllers
            __init__.py
            main.py
            api.py
        /models
            __init__.py
            user.py
            article.py
        /templates
            layout.html
            index.html
            login.html
            ...

    Implementation Details:

    1. App Initialization: In __init__.py, initialize the App and Database, define your models, and then import your controllers.
    2. Default Namespace: Setting app.config.url_default_namespace = "main" allows you to use url('index') instead of the fully qualified url('main.index').
    3. Controllers: Use app.module() to create sub-modules for specific functional areas (like an API) and apply url_prefix to group those routes.
    # /myapp/__init__.py
    from emmett import App
    from emmett.orm import Database
    
    app = App(__name__)
    app.config.url_default_namespace = "main"
    
    db = Database()
    
    from .models.user import User
    from .models.article import Post
    db.define_models(User, Post)
    
    from .controllers import main, api
    
    # /myapp/controllers/main.py
    from .. import app
    
    @app.route("/")
    async def index():
        # code
    
    # /myapp/controllers/api.py
    from .. import app
    
    api = app.module(__name__, 'api', url_prefix='api')
    
    @api.route()
    async def a():
        # code
  8. When to use HTML helpers vs Templates

    master

    While the emmett.html helpers are convenient for generating HTML directly in route code, they are noticeably slower than using Emmett's built-in template engine.

    • Use HTML helpers for small, dynamic fragments or simple logic-heavy HTML generation.
    • Use Templates for rendering long, complex, or mostly static HTML content to ensure better performance.
  9. How the @compute watch parameter works

    master

    The watch parameter in the @compute decorator controls when a computation is executed during database operations. This prevents partial updates from leaving computed fields in an inconsistent state.

    Execution Logic:

    • Computations without watch fields: Executed on every operation; if they fail, they are ignored.
    • Computations with watch fields: Executed only when all specified watch fields are present in the operation.
    • Missing watch fields: If an operation involves some but not all watch fields, Emmett will raise an exception and prevent the operation from continuing.
    • Unrelated fields: Operations that do not involve any of the watch fields will not trigger the computation.

    Example Scenario: If total watches ['price', 'quantity']:

    • update(price=10, quantity=2) -> Triggers computation.
    • update(price=10) -> Raises exception (cannot re-compute total without quantity).
    • update(other_field=True) -> Does nothing to the computation.
  10. Use ORM callbacks to automate database operations

    master

    Emmett provides callback decorators that can be used inside your models to execute logic automatically during specific database operations (insert, update, delete, save, destroy, or commit).

    Important Rules:

    • Return Value: All callback methods must return None or False. If a callback returns True, the current database operation will be aborted.
    • Execution Order: Some operations trigger multiple callbacks. For example, save triggers before_save followed by before_insert or before_update. Similarly, destroy triggers before_destroy followed by before_delete.
  11. Implement Model inheritance and subclassing

    master

    The Model class can be subclassed to create meta-classes for common fields, validations, and behaviors. Emmett automatically merges special attributes like default_values and update_values from parent models into child models, preventing the need to rewrite the entire dictionary.

    Key behaviors:

    • Attribute Merging: default_values and update_values are merged. Subclasses can override parent values by redefining them in their own dictionaries.
    • Feature Inheritance: Models inherit all decorated helper functions (like @scope or @rowmethod) and relations from their parent classes.
    • Multiple Inheritance: You can use multiple base classes; Emmett merges properties based on the order of the base classes.

    Important Rules:

    • Every subclass must be a subclass of Model.
    • To override a decorated method from a super model, you must also decorate the method in the subclass using the same decorator and method name.
    class TimeStampModel(Model):
        created_at = Field.datetime()
        updated_at = Field.datetime()
    
        default_values = {
            'created_at': lambda: request.now,
            'updated_at': lambda: request.now
        }
    
        update_values = {
            'updated_at': lambda: request.now
        }
    
    class Post(TimeStampModel):
        title = Field()
        body = Field.text()
        status = Field()
    
        default_values = {
            'status': 'published'
        }
    
    # Post will now have created_at, updated_at, and status with their respective defaults.
  12. How Emmett migrations work

    master

    Emmett uses a revision-based migration system to propagate model changes to the database schema. Unlike automatic migrations (which are discouraged for production), the migration engine uses discrete migration files containing up and down instructions. The current migration status is stored within the database itself, ensuring consistency across multiple machines.

    Key Concepts:

    • Revisions: Unique identifiers for each migration step.
    • up() method: Defines operations to apply the changes.
    • down() method: Defines operations to rollback the changes.
    • revises attribute: Specifies the parent revision this migration builds upon.

    Warning: Do not enable automatic migrations in production environments. They rely on pydal detection and may produce unwanted side effects compared to the formal migration engine.

    class Migration(migrations.Migration):
        revision = 'fe68547ce244'
        revises = None
    
        def up(self):
            # Operations to apply
            pass
    
        def down(self):
            # Operations to rollback
            pass