Skip to content

Tutorials

This section covers how to create Miniappi apps with Python.

Basics

Showing content

To display user some content:

from miniappi import content

@app.on_open()
async def new_user(session):
    cont = content.v0.widgets.InputText()

    # Show the input box
    await cont.show()

Browse the available content types from the content section.

Nesting content

Some fields of some content types can be nested, ie. you can have cards inside a card:

@app.on_open()
async def new_user(session):
    Card = content.v0.cards.Card
    cont = content.v0.layouts.Grid(
        cols=2,
        contents=[
            Card(title="First card"),
            Card(title="Second card"),
            Card(title="Third card"),
            Card(title="Forth card"),
        ]
    )

    await cont.show()

Waiting for user input

Some content can be clicked, typed or otherwise interacted with. To get the user input, you can use wait_input method:

@app.on_open()
async def new_user(session):
    cont = content.v0.widgets.InputText()

    # Waiting for user input
    text = await cont.wait_input()
    print(text)
    # {'id': '...', 'value': '<some text>'}

Not all content is interactive!

If the component or all of its children are not interactive, this will wait forever.

Tip

If you don't want to trigger rendering, you can pass wait_input(show=False) to just wait for input.

Advanced

The remaining sections are advanced. Please familiarize yourself with other topics before going to these topics.

Show content from global state

To show all users the same content, simply call the show method in a function that runs in app scope (ie. on_start):

@app.on_start()
async def start_app():
    ...

    # Wait for some users joining
    await asyncio.sleep(60)

    cont = content.v0.Title(
        text="Game starts!"
    )
    # Show to all users
    await cont.show()

Or you can fetch specific user's session by iterating open sessions:

from miniappi import app_context

@app.on_start()
async def start_app():
    ...

    # Wait for some users joining
    await asyncio.sleep(60)

    for session_id, session in app_context.sessions.items():

        cont = content.v0.Title(
            text="Game starts!"
        )
        # Show for this user
        await cont.show(session)

We will go through using contexts later.

Synchronize multiple users

You might want to show a user content based on other users actions:

@app.on_start()
async def start_app():
    n_users = 0
    @app.on_open()
    async def new_user():
        nonlocal n_users
        n_users += 1
        cont = content.v0.Title(
            text=f"You are {n_users}th user!"
        )
        await cont.show()

Tip

Combine this with wait_input to have interactivity between users:

@app.on_start()
async def start_app():
    messages = []
    @app.on_open()
    async def new_user():
        last_message = messages[-1] if messages else ""
        cont = content.v0.layouts.Column(
            contents=[
                content.v0.Title(
                    text=f"Previous user said: {last_message}"
                ),
                content.v0.widgets.InputText(
                    submitText="Say"
                ),
            ]
        )
        action = await cont.wait_input()
        messages.append(action["value"])

Alternatively, you can use app context to structure your shared data (more later).

Using Contexts

Contexts (in Miniappi's case) are global variables which data depends on the scope.

There are two contexts in Miniappi:

  • app_context: Scoped for the current app
  • user_context: Scoped for the current user

The scope determines where the data inside the context is accessible. Ie. a user scoped context is only accessible from functions decorated with app.on_open(), app.on_message(), app.on_close(),

Less talking and more showing:

from miniappi import app_context, user_context

# This WILL raise an error:
app_context.sessions

@app.on_start()
async def start_app():
    # This won't raise an error:
    app_context.sessions

    # This WILL raise an error
    user_context.request_id

@app.on_open()
async def new_user():
    # These won't raise an error
    app_context.sessions
    user_context.request_id

This means that you can safely create one app/user context and use that all around your app, including in modules, without constantly needing to pass it around. Context is useful for widely used data in your app.

You can also create your own context. It uses Pydantic underneath:

from miniappi import ContextModel

class AppContext(ContextModel):
    n_users: int = 0

class UserContext(ContextModel):
    username: str = ""

app_context = AppContext()
user_context = UserContext()

app = App(
    app_context=app_context,
    user_context=user_context
)

@app.on_start()
async def start_app():

    @app.on_open()
    async def new_user():
        app_context.n_users += 1
        user_context.username = "..."

Tip

Specifying your own context won't override the default contexts. You can use both.

The default contexts have some useful attributes:

miniappi.app_context

Attribute Type Description
app App App object
sessions Dict[str, Session] Mapping of request IDs and their sessions (open connections)
extra Dict Custom data

miniappi.user_context

Attribute Type Description
session Session User conntection object
request_id str ID of the session/connection
extra Dict Custom data

Temporary handling

Sometimes you may want to handle user actions differently based on the state of your app, for example:

  • Put new users to waiting list after game session has started

For such situations you can use temp method which enables you to create callbacks for the duration of the context manager:

import asyncio

@app.on_start()
async def new_user(session):
    with app.temp() as temp:
        @temp.on_open()
        async def new_user():
            ...
        # Sync new users
        ... 
    # From now on, "new_user"
    # won't be run for new users
    ...

Here is an example of a waiting list:

import asyncio

@app.on_start()
async def new_user(session):
    is_started = asyncio.Event()
    players = []
    with app.temp() as temp:
        @temp.on_open()
        async def new_player():
            cont = content.v0.widgets.InputText(
                placeholder="Player name",
                submitText="Join"
            )
            textbox = await cont.wait_input()
            name = textbox["value"]
            players.append(name)
            if len(players) < 5:
                await is_started.wait()
            else:
                is_started.set()
            cont = content.v0.Title(
                text="Game started!"
            )
            await cont.show()
            ... # Play the game

        # Wait till 5th player joins the game
        await is_started.wait()

    # From now on, the function
    # "new_player" won't be run
    # for new users but it
    # continues to run for the existing
    # users.

    # We will show waiting list
    # for new users
    with app.temp() as temp:
        @temp.on_open()
        async def new_users_to_waiting_list():
            cont = content.v0.Title(
                text="You are in a waiting list..."
            )
            await cont.show()
            ...
        ... # Play game