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.5.1.dev1.tar.gz (3.2 MB view details)

Uploaded Source

Built Distributions

mountaineer-0.5.1.dev1-cp312-none-win_amd64.whl (17.2 MB view details)

Uploaded CPython 3.12 Windows x86-64

mountaineer-0.5.1.dev1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (20.6 MB view details)

Uploaded CPython 3.12 manylinux: glibc 2.17+ x86-64

mountaineer-0.5.1.dev1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (20.2 MB view details)

Uploaded CPython 3.12 manylinux: glibc 2.17+ ARM64

mountaineer-0.5.1.dev1-cp312-cp312-macosx_11_0_arm64.whl (18.6 MB view details)

Uploaded CPython 3.12 macOS 11.0+ ARM64

mountaineer-0.5.1.dev1-cp312-cp312-macosx_10_12_x86_64.whl (19.4 MB view details)

Uploaded CPython 3.12 macOS 10.12+ x86-64

mountaineer-0.5.1.dev1-cp311-none-win_amd64.whl (17.2 MB view details)

Uploaded CPython 3.11 Windows x86-64

mountaineer-0.5.1.dev1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (20.6 MB view details)

Uploaded CPython 3.11 manylinux: glibc 2.17+ x86-64

mountaineer-0.5.1.dev1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (20.3 MB view details)

Uploaded CPython 3.11 manylinux: glibc 2.17+ ARM64

mountaineer-0.5.1.dev1-cp311-cp311-macosx_11_0_arm64.whl (18.6 MB view details)

Uploaded CPython 3.11 macOS 11.0+ ARM64

mountaineer-0.5.1.dev1-cp311-cp311-macosx_10_12_x86_64.whl (19.4 MB view details)

Uploaded CPython 3.11 macOS 10.12+ x86-64

mountaineer-0.5.1.dev1-cp310-none-win_amd64.whl (17.2 MB view details)

Uploaded CPython 3.10 Windows x86-64

mountaineer-0.5.1.dev1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (20.6 MB view details)

Uploaded CPython 3.10 manylinux: glibc 2.17+ x86-64

mountaineer-0.5.1.dev1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (20.3 MB view details)

Uploaded CPython 3.10 manylinux: glibc 2.17+ ARM64

mountaineer-0.5.1.dev1-cp310-cp310-macosx_11_0_arm64.whl (18.6 MB view details)

Uploaded CPython 3.10 macOS 11.0+ ARM64

mountaineer-0.5.1.dev1-cp310-cp310-macosx_10_12_x86_64.whl (19.4 MB view details)

Uploaded CPython 3.10 macOS 10.12+ x86-64

File details

Details for the file mountaineer-0.5.1.dev1.tar.gz.

File metadata

  • Download URL: mountaineer-0.5.1.dev1.tar.gz
  • Upload date:
  • Size: 3.2 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/5.1.0 CPython/3.12.4

File hashes

Hashes for mountaineer-0.5.1.dev1.tar.gz
Algorithm Hash digest
SHA256 80ed57cec7c1df13bf3948f50ac79ce4eef0b69e95ce1f4e6c8f21bb7c958292
MD5 2ef927013532260da551f04f3276f396
BLAKE2b-256 10f754d56d5a54a9009cea310a9ae510cc5afe11485ab158d12d86abf30e8e98

See more details on using hashes here.

File details

Details for the file mountaineer-0.5.1.dev1-cp312-none-win_amd64.whl.

File metadata

File hashes

Hashes for mountaineer-0.5.1.dev1-cp312-none-win_amd64.whl
Algorithm Hash digest
SHA256 ed19c334b853a144723dfc8897c606bc8b77e3cf64f696cb24a5fd93d5d59fb2
MD5 6fcfdb6414d9fa12199528c1717efc67
BLAKE2b-256 f4dc6a5fe29a10c135a60040eaa15078f4a23fe06b1cc2ba67017b1888ea0398

See more details on using hashes here.

File details

Details for the file mountaineer-0.5.1.dev1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for mountaineer-0.5.1.dev1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 074e14f8c09a5ade23d10f0e13eddba2ea1eb4cf8b1c63588c189a10cdd225b2
MD5 17ac980a9e8009fd2462c2bfe8f72a91
BLAKE2b-256 d31d6016012ebc82032e5b0bdcf41eb38f83a993133b47c65cc14d1a7db95e79

See more details on using hashes here.

File details

Details for the file mountaineer-0.5.1.dev1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for mountaineer-0.5.1.dev1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 250dd072282eda6508f7464c24a43c914271b158a38c5c1f1e09777947ca2876
MD5 0f9bbf5f813d9dcd7b8aba51bdcbc0ea
BLAKE2b-256 a275e394d86b4f14c05788b3fe3611b500900dcb5262ee054deaeccb2ce52423

See more details on using hashes here.

File details

Details for the file mountaineer-0.5.1.dev1-cp312-cp312-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for mountaineer-0.5.1.dev1-cp312-cp312-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 16b2f9dfb896acdb2a7c78a267de2254175515e965ff68a67d9a00c07b199767
MD5 18e70d764a52f9cabaf0588a7941c2be
BLAKE2b-256 02abcc0b17e4bcdb887fbe9a73eb880cdc7dbb609b4bd7347ba6da31a0c8d3f2

See more details on using hashes here.

File details

Details for the file mountaineer-0.5.1.dev1-cp312-cp312-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for mountaineer-0.5.1.dev1-cp312-cp312-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 78f10dcf3ff1b0bbf81404805e8bad3ce67e3b8756c5a228aa4a7104e7124f62
MD5 5478e294ce38893c9f33adf46a3a8fd9
BLAKE2b-256 7d3e2a349d9cf4146a3603fa7dd4cd421fc6c353af2f2e206e4589c286c2faa1

See more details on using hashes here.

File details

Details for the file mountaineer-0.5.1.dev1-cp311-none-win_amd64.whl.

File metadata

File hashes

Hashes for mountaineer-0.5.1.dev1-cp311-none-win_amd64.whl
Algorithm Hash digest
SHA256 ffeae8c6a9bd3a7610932b04b0c86a01ca09dcbca0adf925a326cb26501e5091
MD5 518620d7dd4257b48bc299adece10cac
BLAKE2b-256 44168b16717a7bba635163adbe710ff325f2c88ae6040c1882895f39c87ceee1

See more details on using hashes here.

File details

Details for the file mountaineer-0.5.1.dev1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for mountaineer-0.5.1.dev1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 9b4e7418a5e31708abda5d793cba6bee7b63ff09dd75ead4e1ed8118be3423a4
MD5 2725b2f4cbe7a45130a5ecfb71c03edc
BLAKE2b-256 b123e94e314062b98761e0786841019ab0e4c38e4f02c76c3bb3faec5fd3e250

See more details on using hashes here.

File details

Details for the file mountaineer-0.5.1.dev1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for mountaineer-0.5.1.dev1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 d79943e4fe3495fd26ed286c0cfd2e55e54f5b890f8afa6bd415df5cd206763f
MD5 cae79789a8325d5c78e7fe3de0a3d12f
BLAKE2b-256 6e1bcb96695e50ee686771125abe89f6c417b53d541c8a518eae5cc74c670492

See more details on using hashes here.

File details

Details for the file mountaineer-0.5.1.dev1-cp311-cp311-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for mountaineer-0.5.1.dev1-cp311-cp311-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 0acbee486afb1a4ef73a50e5594636ae8a98f34a3a0c8d969b64f2c0aa4d4ae6
MD5 9e09cef7e1b060ddf03457aad2a9e45a
BLAKE2b-256 593d34d43788d3a35dc74d1d2f1981e51d9792045e4d6d2e8fb2935e3a3379e6

See more details on using hashes here.

File details

Details for the file mountaineer-0.5.1.dev1-cp311-cp311-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for mountaineer-0.5.1.dev1-cp311-cp311-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 13ea8396e610a080e3720cf0c2b45b8d4bb362310f6d39c1fe1e42d27274787f
MD5 af6c00030ad2b9042061b0e3b145ee0a
BLAKE2b-256 fd6adaa422863040fa474670b418479d7563641e9d4880fd21916e8f27e68c76

See more details on using hashes here.

File details

Details for the file mountaineer-0.5.1.dev1-cp310-none-win_amd64.whl.

File metadata

File hashes

Hashes for mountaineer-0.5.1.dev1-cp310-none-win_amd64.whl
Algorithm Hash digest
SHA256 7a2dd363915558b8e2b17d44215756c4eea18555b629c196e8f7d4b2cbf371b1
MD5 a5151c66ff726645f6a628c96e293f89
BLAKE2b-256 e1bada9f5aa462dfc59fce8cc28cc642e4986d86d117b927a7c35e79bdb6c478

See more details on using hashes here.

File details

Details for the file mountaineer-0.5.1.dev1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for mountaineer-0.5.1.dev1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 ea40959075ea34768f1b66661cef178ccbaf0d40d6bff2e441c653fccbf8d643
MD5 a042ca229dd8f0f22768f4a52375148b
BLAKE2b-256 b3d20a8aac5053cc61792c9fd265b092a881ebc464f622e26eea349282e01341

See more details on using hashes here.

File details

Details for the file mountaineer-0.5.1.dev1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for mountaineer-0.5.1.dev1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 084590be2ba651cb216f6dbe633dda334c21ef33abf1f0c27d6833a1b45ed697
MD5 5c67d323bf0541b806030851e4510f62
BLAKE2b-256 e5ac7c0fc331d3a450f12582f81bd99ca261b24ab7206dd6bcdf1bd2ebc44169

See more details on using hashes here.

File details

Details for the file mountaineer-0.5.1.dev1-cp310-cp310-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for mountaineer-0.5.1.dev1-cp310-cp310-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 76f3e81c25fd690045ca1e49789ee9dec7e9c1ad8245ddd7c32f4f492d772838
MD5 67038a5985e92356cd47742d626a869a
BLAKE2b-256 95fc839c74158d8bbc1b25ebee730655688d8298e7da536d7bee946f6c72696c

See more details on using hashes here.

File details

Details for the file mountaineer-0.5.1.dev1-cp310-cp310-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for mountaineer-0.5.1.dev1-cp310-cp310-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 4dc3bb9c32ebf050c0f3292d754c4ff4040e5aa17d237ee8a1b88820187ebb12
MD5 0099defb162cd35877c7782ff3eb572f
BLAKE2b-256 99629c356531e7d575a167bb38c5f914b1cad38c2869a9e49662d15e44b665a2

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