Skip to main content

No project description provided

Project description

Mountaineer Header

Move fast. Climb mountains. Don't break things.

Mountaineer 🏔️ is a framework to easily build webapps in Python and React. If you've used either of these languages before for development, we think you'll be right at home.

Main Features

Each framework has its own unique features and tradeoffs. Mountaineer focuses on developer productivity above all else, with production speed a close second.

  • 📝 Typehints up and down the stack: frontend, backend, and database
  • 🎙️ Trivially easy client<->server communication, data binding, and function calling
  • 🌎 Optimized server rendering for better accessibility and SEO
  • 🏹 Static analysis of web pages for strong validation: link validity, data access, etc.
  • 🤩 Skip the API or Node.js server just to serve frontend clients

We built Mountaineer out of a frustration that we were reinventing the webapp wheel time and time again. We love Python for backend development and the interactivity of React for frontend UX. But they don't work seamlessly together without a fair amount of glue. So: we built the glue. While we were at it, we embedded a V8 engine to provide server-side rendering, added conventions for application configuration, built native Typescript integrations, and more. Our vision is for you to import one slim dependency and you're off to the races.

We're eager for you to give Mountaineer a try, and equally devoted to making you successful if you like it. File an Issue if you see anything unexpected or if there's a steeper learning curve than you expect. There's much more to do - and we're excited to do it together.

~ Pierce

Getting Started

New Project

To get started as quickly as possible, we bundle a project generator that sets up a simple project after a quick Q&A. Make sure you have pipx installed.

$ pipx run create-mountaineer-app

? Project name [my-project]: my_webapp
? Author [Pierce Freeman <pierce@freeman.vc>] Default
? Use poetry for dependency management? [Yes] Yes
? Create stub MVC files? [Yes] Yes
? Use Tailwind CSS? [Yes] Yes
? Add editor configuration? [vscode] vscode

Mountaineer projects all follow a similar structure. After running this CLI you should see a new folder called my_webapp, with folders like the following:

my_webapp
  /controllers
    /home.py
  /models
    /mymodel.py
  /views
    /app
      /home
        /page.tsx
      /layout.tsx
    /package.json
    /tsconfig.json
  /app.py
  /cli.py
pyproject.toml
poetry.lock

Every service file is nested under the my_webapp root package. Views are defined in a disk-based hierarchy (views) where nested routes are in nested folders. This folder acts as your React project and is where you can define requirements and build parameters in package.json and tsconfig.json. Controllers are defined nearby in a flat folder (controllers) where each route is a separate file. Everything else is just standard Python code for you to modify as needed.

Development

If you're starting a new application from scratch, you'll typically want to create your new database tables. Make sure you have postgres running. We bundle a docker-compose file for convenience with create-mountaineer-app.

docker-compose up -d
poetry run createdb

Of course you can also use an existing database instance, simply configure it in the .env file in the project root.

Mountaineer relies on watching your project for changes and doing progressive compilation. We provide a few CLI commands to help with this.

While doing development work, you'll usually want to preview the frontend and automatically build dependent files. You can do this with:

$ poetry run runserver

INFO:     Started server process [93111]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:5006 (Press CTRL+C to quit)

Navigate to http://127.0.0.1:5006 to see your new webapp running.

Or, if you just want to watch the source tree for changes without hosting the server. Watching will allow your frontend to pick up API definitions from your backend controllers:

$ poetry run watch

Both of these CLI commands are specified in your project's cli.py file.

Walkthrough

Below we go through some of the unique aspects of Mountaineer. Let's create a simple Todo list where we can add new items.

For the purposes of this walkthrough we assume your project is generated with create-mountaineer-app and you've skipped MVC stub files. If not, you'll have to delete some of the pre-existing files.

Let's get started by creating the data models that will persist app state to the database. These definitions are effectively Pydantic schemas that will be bridged to the database via SQLModel.

# my_webapp/models/todo.py

from mountaineer.database import SQLModel, Field
from uuid import UUID, uuid4

class TodoItem(SQLModel, table=True):
    id: UUID = Field(default_factory=uuid4, primary_key=True)

    description: str
    completed: bool = False

Update the index file as well:

# my_webapp/models/__init__.py

from .todo import TodoItem # noqa: F401

Make sure you have a Postgres database running. We bundle a docker-compose file for convenience with create-mountaineer-app. Launch it in the background and create the new database tables from these code definitions:

docker-compose up -d
poetry run createdb
poetry run runserver

Great! At this point we have our database tables created and have a basic server running. We next move to creating a new controller, since this will define which data you can push and pull to your frontend.

# my_webapp/controllers/home.py

from mountaineer import sideeffect, ControllerBase, RenderBase
from mountaineer.database import DatabaseDependencies

from fastapi import Request, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from sqlmodel import select

from my_webapp.models.todo import TodoItem

class HomeRender(RenderBase):
    client_ip: str
    todos: list[TodoItem]

class HomeController(ControllerBase):
    url = "/"
    view_path = "/app/home/page.tsx"

    async def render(
        self,
        request: Request,
        session: AsyncSession = Depends(DatabaseDependencies.get_db_session)
    ) -> HomeRender:
        todos = await session.execute(select(TodoItem))

        return HomeRender(
            client_ip=(
                request.client.host
                if request.client
                else "unknown"
            ),
            todos=todos.scalars().all()
        )

The only three requirements of a controller are setting the:

  • URL
  • View path
  • Initial data payload

This render() function is a core building block of Mountaineer. All Controllers need to have one. It defines all the data that your frontend will need to resolve its view. This particular controller retrieves all Todo items from the database, alongside the user's current IP.

[!TIP] render() functions accepts all parameters that FastAPI endpoints do: paths, query parameters, and dependency injected functions. Right now we're just grabbing the Request object to get the client IP.

Note that the database session is provided via dependency injection, which plug-and-plays with FastAPI's Depends syntax. The standard library provides two main dependency providers:

  • mountaineer.CoreDependencies: helper functions for configurations and general dependency injection
  • mountaineer.database.DatabaseDependencies: helper functions for database lifecycle and management

Now that we've newly created this controller, we wire it up to the application. This registers it for display when you load the homepage.

# my_webapp/app.py
from mountaineer.app import AppController
from mountaineer.js_compiler.postcss import PostCSSBundler
from mountaineer.render import LinkAttribute, Metadata

from my_webapp.config import AppConfig
from my_webapp.controllers.home import HomeController

controller = AppController(
    config=AppConfig(),
    global_metadata=Metadata(
        links=[LinkAttribute(rel="stylesheet", href="/static/app_main.css")]
    ),
    custom_builders=[
        PostCSSBundler(),
    ],
)

controller.register(HomeController())

Let's move over to the frontend.

/* my_webapp/views/app/home/page.tsx */

import React from "react";
import { useServer, ServerState } from "./_server/useServer";

const CreateTodo = ({ serverState }: { serverState: ServerState }) => {
  return (
    <div className="flex gap-x-4">
      <input
        type="text"
        className="grow rounded border-2 border-gray-200 px-4 py-2"
      />
      <button className="rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700">
        Create
      </button>
    </div>
  );
};

const Home = () => {
  const serverState = useServer();

  return (
    <div className="mx-auto max-w-2xl space-y-8 p-8 text-2xl">
      <p>
        Hello {serverState.client_ip}, you have {serverState.todos.length} todo
        items.
      </p>
      <CreateTodo serverState={serverState} />
      {
        /* Todo items are exposed as typehinted Typescript interfaces */
        serverState.todos.map((todo) => (
          <div key={todo.id} className="rounded border-2 border-gray-200 p-4">
            <div>{todo.description}</div>
          </div>
        ))
      }
    </div>
  );
};

export default Home;

We define a simple view to show the data coming from the backend. To accomplish this conventionally, we'd need to wire up an API layer, a Node server, or format the page with Jinja templates.

Here instead we use our automatically generated useServer() hook. This hook payload will provide all the HomeRender fields as properties of serverState. And it's available instantly on page load without any roundtrip fetches. Also - if your IDE supports language servers (which most do these days), you should see the fields auto-suggesting for serverState as you type.

IDE Typehints

If you access this in your browser at localhost:5006/ we can see our welcome message, but we can't really do anything with the todos yet. Let's add some interactivity.

[!TIP] Try disabling Javascript in your browser. The page will still render as-is with all variables intact, thanks to our server-side rendering.

Server-side rendering

What good is todo list that doesn't get longer? We define a add_todo function that accepts a pydantic model NewTodoRequest, which defines the required parameters for a new todo item. We then cast this to a database object and add it to the postgres table.

# my_webapp/controllers/home.py

from pydantic import BaseModel

class NewTodoRequest(BaseModel):
    description: str

class HomeController(ControllerBase):
    ...

    @sideeffect
    async def add_todo(
        self,
        payload: NewTodoRequest,
        session: AsyncSession = Depends(DatabaseDependencies.get_db_session)
    ) -> None:
        new_todo =  TodoItem(description=payload.description)
        session.add(new_todo)
        await session.commit()

The important part here is the @sideeffect. Once you create a new Todo item, the previous state on the frontend is outdated. It will only show the todos before you created a new one. That's not what we want in an interactive app. This decorator indicates that we want the frontend to refresh its data, since after we update the todo list on the server the client state will be newly outdated.

Mountaineer detects the presence of this sideeffect function and analyzes its signature. It then exposes this to the frontend as a normal async function.

/* my_webapp/views/app/home/page.tsx */

import React, { useState } from "react";
import { useServer } from "./_server/useServer";

/* Replace the existing CreateTodo component definition you have */
const CreateTodo = ({ serverState }: { serverState: ServerState }) => {
  const [newTodo, setNewTodo] = useState("");

  return (
    <div className="flex gap-x-4">
      <input
        type="text"
        className="grow rounded border-2 border-gray-200 px-4 py-2"
        value={newTodo}
        onChange={(e) => setNewTodo(e.target.value)}
      />
      <button
        className="rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700"
        onClick={
          /* Here we call our sideeffect function */
          async () => {
            await serverState.add_todo({
              requestBody: {
                description: newTodo,
              },
            });
            setNewTodo("");
          }
        }
      >
        Create
      </button>
    </div>
  );
};

...

export default Home;

useServer() exposes our add_todo function so we can call our backend directly from our frontend. Also notice that we don't have to read or parse the output value of this function to render the new todo item to the list. Since the function is marked as a sideeffect, the frontend will automatically refresh its data after the function is called.

Go ahead and load it in your browser. If you open up your web tools, you can create a new Todo and see POST requests sending data to the backend and receiving the current server state. The actual data updates and merging happens internally by Mountaineer.

Getting Started Final TODO App

Getting Started Final TODO App

You can use these serverState variables anywhere you'd use dynamic React state variables (useEffect, useCallback, etc). But unlike React state, these variables are automatically updated when a relevant sideeffect is triggered.

And that's it. We've just built a fully interactive web application without having to worry about an explicit API. You specify the data model and actions on the server and the appropriate frontend hooks are generated and updated automatically. It gives you the power of server rendered html and the interactivity of a virtual DOM, without having to compromise on complicated data mutations to keep everything in sync.

Learn More

We have additional documentation that does more of a technical deep dive on different features of Mountaineer. Check out mountaineer.sh.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

mountaineer-0.4.1.tar.gz (3.1 MB view details)

Uploaded Source

Built Distributions

mountaineer-0.4.1-cp312-none-win_amd64.whl (17.0 MB view details)

Uploaded CPython 3.12 Windows x86-64

mountaineer-0.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (24.5 MB view details)

Uploaded CPython 3.12 manylinux: glibc 2.17+ x86-64

mountaineer-0.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (23.7 MB view details)

Uploaded CPython 3.12 manylinux: glibc 2.17+ ARM64

mountaineer-0.4.1-cp312-cp312-macosx_11_0_arm64.whl (19.2 MB view details)

Uploaded CPython 3.12 macOS 11.0+ ARM64

mountaineer-0.4.1-cp312-cp312-macosx_10_12_x86_64.whl (20.1 MB view details)

Uploaded CPython 3.12 macOS 10.12+ x86-64

mountaineer-0.4.1-cp311-none-win_amd64.whl (17.0 MB view details)

Uploaded CPython 3.11 Windows x86-64

mountaineer-0.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (24.5 MB view details)

Uploaded CPython 3.11 manylinux: glibc 2.17+ x86-64

mountaineer-0.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (23.7 MB view details)

Uploaded CPython 3.11 manylinux: glibc 2.17+ ARM64

mountaineer-0.4.1-cp311-cp311-macosx_11_0_arm64.whl (19.2 MB view details)

Uploaded CPython 3.11 macOS 11.0+ ARM64

mountaineer-0.4.1-cp311-cp311-macosx_10_12_x86_64.whl (20.1 MB view details)

Uploaded CPython 3.11 macOS 10.12+ x86-64

File details

Details for the file mountaineer-0.4.1.tar.gz.

File metadata

  • Download URL: mountaineer-0.4.1.tar.gz
  • Upload date:
  • Size: 3.1 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/5.0.0 CPython/3.12.3

File hashes

Hashes for mountaineer-0.4.1.tar.gz
Algorithm Hash digest
SHA256 450291e11930e684ed3d2b5d21d2fa13c33e38fb509f2f488a7d305ee1d47fa3
MD5 1866db112722e6a3e31b314a49b845a1
BLAKE2b-256 a12ef6fa28179c5f483aa4710a111acbee2a9c4f48e2e6d9c252cdfb0d2f04e2

See more details on using hashes here.

File details

Details for the file mountaineer-0.4.1-cp312-none-win_amd64.whl.

File metadata

File hashes

Hashes for mountaineer-0.4.1-cp312-none-win_amd64.whl
Algorithm Hash digest
SHA256 11ad60b879e97b8de1b060388d803d4f7ef9bc6dbaaced2a32f216e5634a8290
MD5 dd8f2fa6df79052082420a6133297041
BLAKE2b-256 4890435601e7c0a8b89e970c940c05b64dda86696b4d70fc010c2ee4977fbb62

See more details on using hashes here.

File details

Details for the file mountaineer-0.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for mountaineer-0.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 a815651afe0d81a3d304d9fa42aa25ecbe6c7aad1326020f78fc6b737a40a21c
MD5 0e9af8bc1f2625738327964e68f35182
BLAKE2b-256 0ba8b3f406b62d07b9af661d80e257f7250153ca70765acd27dc13c11e66eae5

See more details on using hashes here.

File details

Details for the file mountaineer-0.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for mountaineer-0.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 a9175beff062003c346d3223fea3ce7884a6bbbf708953bed71f2c24a257b324
MD5 7e688769dff726cbcfbdb9663e494914
BLAKE2b-256 d3787a5192c94ba559942cf9bbac7fe32b7edaeafd89768b0e351168d22bebe3

See more details on using hashes here.

File details

Details for the file mountaineer-0.4.1-cp312-cp312-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for mountaineer-0.4.1-cp312-cp312-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 3292e65c58e61f56f0e97ecb9d4ba85acadd8e7f43998db025949ba0dfd0c69d
MD5 372ed3687f91404c2eac551c615cf48b
BLAKE2b-256 0d0a070df8946930009600e567807f371403bdd776f05293d2f0a0b66f32e86c

See more details on using hashes here.

File details

Details for the file mountaineer-0.4.1-cp312-cp312-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for mountaineer-0.4.1-cp312-cp312-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 f549ba08f697fc063f537bed3cbac4b4df09d957c8b862dd552a9a8d1d2e267b
MD5 d17ffba34d091b596b4ceadd47d60b93
BLAKE2b-256 853c6e30aa88292faf15f780202eea426e8b37cd20595a67b657925614cfca27

See more details on using hashes here.

File details

Details for the file mountaineer-0.4.1-cp311-none-win_amd64.whl.

File metadata

File hashes

Hashes for mountaineer-0.4.1-cp311-none-win_amd64.whl
Algorithm Hash digest
SHA256 8f78fff9d0e8eab7ccef1906c143094eb7824508b2d9ff95c572f5a5dc55b9e5
MD5 c827a9d3d66eaf88aaf755fb311c5f84
BLAKE2b-256 d4c6e4a9a50a3ff4a1989e2dfc69cfde8ce35d7ea67415c25b08ae597505f26f

See more details on using hashes here.

File details

Details for the file mountaineer-0.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for mountaineer-0.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 ce3861be8f6b4225fd4729f995ef4d5c9bc0485bcaa3f7a149ad68a0520fb84a
MD5 4134e8f7fd7da420188b2f4a1ba6d3f1
BLAKE2b-256 97e96c7f935c6c7b515b307ffa1ffc735c679d1a37d7c9fd345df0bae5860708

See more details on using hashes here.

File details

Details for the file mountaineer-0.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for mountaineer-0.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 4d8cbef12fbe5b87e757477c0ea28a6227fe33c61fd331ffc826f2205d2e7d35
MD5 3ede9084756a85a98d7f410de0133778
BLAKE2b-256 856d3aa7434e6068bba82c5a2cb0b363d68d587258225bc0264813845662404a

See more details on using hashes here.

File details

Details for the file mountaineer-0.4.1-cp311-cp311-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for mountaineer-0.4.1-cp311-cp311-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 747c0068585a60a602ff51ad5451cdd22f189ba6ba39d2cd676a99d33adbe474
MD5 eb6f46d89f115c3d1cda8e09ead41db8
BLAKE2b-256 44e275dff834ea2f94dd3d22a67527dbdfb277ad8190c7454b9e3deec5b0a83b

See more details on using hashes here.

File details

Details for the file mountaineer-0.4.1-cp311-cp311-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for mountaineer-0.4.1-cp311-cp311-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 38e8ce25e34798ba5e3c5e09f0f8caccbb09ec271e191fcc81f86761fe2816f5
MD5 0b2408ce2dbb074cd57a7ffe7624703b
BLAKE2b-256 a43061e75133d3ddd983f186c3e0bda5fede7eef0b3e4b973a81108e57066f88

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page