Skip to content

Commit

Permalink
Merge branch 'main' into WolfDWyc-tuples-in-forms
Browse files Browse the repository at this point in the history
  • Loading branch information
samuelcolvin committed Dec 21, 2023
2 parents d4248b9 + 4274a8d commit a5d6871
Show file tree
Hide file tree
Showing 131 changed files with 3,812 additions and 1,800 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ module.exports = {
'plugin:react-hooks/recommended',
'prettier',
],
ignorePatterns: ['node_modules'],
ignorePatterns: ['node_modules', 'dist', 'htmlcov'],
parser: '@typescript-eslint/parser',
plugins: ['react', '@typescript-eslint', 'react-refresh', 'simple-import-sort'],
rules: {
Expand Down
67 changes: 52 additions & 15 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ jobs:
with:
node-version: 18

- run: pip install -r python/requirements/all.txt
- run: pip install -r src/python-fastui/requirements/all.txt
- run: pip install src/python-fastui

- run: npm install

Expand All @@ -34,7 +35,46 @@ jobs:
env:
SKIP: no-commit-to-branch

packages-build:
test:
name: test ${{ matrix.python-version }} on ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu, macos]
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']

runs-on: ${{ matrix.os }}-latest

env:
PYTHON: ${{ matrix.python-version }}
OS: ${{ matrix.os }}

steps:
- uses: actions/checkout@v3

- name: set up python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

- run: pip install -r src/python-fastui/requirements/test.txt
- run: pip install -r src/python-fastui/requirements/pyproject.txt
- run: pip install src/python-fastui

- run: coverage run -m pytest src

# test demo on 3.11 and 3.12, these tests are intentionally omitted from coverage
- if: matrix.python-version == '3.11' || matrix.python-version == '3.12'
run: pytest demo/tests.py

- run: coverage xml

- uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
env_vars: PYTHON,OS

npm-build:
runs-on: ubuntu-latest

steps:
Expand All @@ -46,11 +86,11 @@ jobs:

- run: npm install
- run: npm run build
- run: tree packages
- run: tree src

check: # This job does nothing and is only used for the branch protection
if: always()
needs: [lint, packages-build]
needs: [lint, test, npm-build]
runs-on: ubuntu-latest

steps:
Expand All @@ -72,22 +112,19 @@ jobs:
steps:
- uses: actions/checkout@v3

- name: set up python
uses: actions/setup-python@v4
- uses: actions/setup-python@v4
with:
python-version: '3.11'

- name: install
run: pip install -U build
- run: pip install -U build

- name: check version
id: check-version
- id: check-version
uses: samuelcolvin/[email protected]
with:
version_file_path: 'python/fastui/__init__.py'
version_file_path: 'src/python-fastui/fastui/__init__.py'

- run: python -m build --outdir dist src/python-fastui

- name: build
run: python -m build
- run: ls -lh dist

- name: Upload package to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
- uses: pypa/gh-action-pypi-publish@release/v1
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
/**/*.egg-info

node_modules
dist
Expand All @@ -31,3 +32,5 @@ __pycache__/
/frontend-dist/
/scratch/
/packages-dist/
/.coverage
/users.db
6 changes: 6 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,9 @@ repos:
entry: npm run typecheck
language: system
pass_filenames: false
- id: python-generate-ts
name: python-generate-ts
types_or: [python]
entry: fastui generate fastui:FastUI src/npm-fastui/src/models.d.ts
language: system
pass_filenames: false
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
htmlcov/
26 changes: 14 additions & 12 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,44 +1,46 @@
.DEFAULT_GOAL:=all
paths = python
path = src/python-fastui

.PHONY: install
install:
pip install -U pip pre-commit pip-tools
pip install -r python/requirements/all.txt
pip install -r $(path)/requirements/all.txt
pip install -e $(path)
pre-commit install

.PHONY: update-lockfiles
update-lockfiles:
@echo "Updating requirements files using pip-compile"
pip-compile -q --strip-extras -o python/requirements/lint.txt python/requirements/lint.in
pip-compile -q --strip-extras -o python/requirements/pyproject.txt pyproject.toml --extra=fastapi
pip install --dry-run -r python/requirements/all.txt
pip-compile -q --strip-extras -o $(path)/requirements/lint.txt $(path)/requirements/lint.in
pip-compile -q --strip-extras -o $(path)/requirements/pyproject.txt -c $(path)/requirements/lint.txt $(path)/pyproject.toml --extra=fastapi
pip-compile -q --strip-extras -o $(path)/requirements/test.txt -c $(path)/requirements/lint.txt -c $(path)/requirements/pyproject.txt $(path)/requirements/test.in
pip install --dry-run -r $(path)/requirements/all.txt

.PHONY: format
format:
ruff check --fix-only $(paths)
ruff format $(paths)
ruff check --fix-only $(path) demo
ruff format $(path) demo

.PHONY: lint
lint:
ruff check $(paths)
ruff format --check $(paths)
ruff check $(path) demo
ruff format --check $(path) demo

.PHONY: typecheck
typecheck:
pyright python/fastui
pyright

.PHONY: test
test:
coverage run -m pytest tests
coverage run -m pytest

.PHONY: testcov
testcov: test
coverage html

.PHONY: dev
dev:
uvicorn python.demo:app --reload
uvicorn demo:app --reload --reload-dir .

.PHONY: all
all: testcov lint
58 changes: 17 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# FastUI

[![CI](https://github.com/samuelcolvin/FastUI/actions/workflows/ci.yml/badge.svg)](https://github.com/samuelcolvin/FastUI/actions?query=event%3Apush+branch%3Amain+workflow%3ACI)
[![CI](https://github.com/pydantic/FastUI/actions/workflows/ci.yml/badge.svg)](https://github.com/pydantic/FastUI/actions?query=event%3Apush+branch%3Amain+workflow%3ACI)
[![pypi](https://img.shields.io/pypi/v/fastui.svg)](https://pypi.python.org/pypi/fastui)
[![versions](https://img.shields.io/pypi/pyversions/fastui.svg)](https://github.com/samuelcolvin/FastUI)
[![license](https://img.shields.io/github/license/samuelcolvin/FastUI.svg)](https://github.com/samuelcolvin/FastUI/blob/main/LICENSE)
[![versions](https://img.shields.io/pypi/pyversions/fastui.svg)](https://github.com/pydantic/FastUI)
[![license](https://img.shields.io/github/license/pydantic/FastUI.svg)](https://github.com/pydantic/FastUI/blob/main/LICENSE)

**Please note:** FastUI is still an active work in progress, do not expect it to be complete.

Expand All @@ -26,7 +26,7 @@ At its heart, FastUI is a set of matching [Pydantic](https://docs.pydantic.dev)
FastUI is made up of 4 things:

- [`fastui` PyPI package](https://pypi.python.org/pypi/fastui) — Pydantic models for UI components, and some utilities. While it works well with [FastAPI](https://fastapi.tiangolo.com) it doesn't depend on FastAPI, and most of it could be used with any python web framework.
- [`@pydantic/fastui` npm package](https://www.npmjs.com/package/@pydantic/fastui) — a React TypeScript package that let's you reuse the machinery and types of FastUI while implementing your own components
- [`@pydantic/fastui` npm package](https://www.npmjs.com/package/@pydantic/fastui) — a React TypeScript package that lets you reuse the machinery and types of FastUI while implementing your own components
- [`@pydantic/fastui-bootstrap` npm package](https://www.npmjs.com/package/@pydantic/fastui-bootstrap) — implementation/customisation of all FastUI components using [Bootstrap](https://getbootstrap.com)
- [`@pydantic/fastui-prebuilt` npm package](https://www.jsdelivr.com/package/npm/@pydantic/fastui-prebuilt) (available on [jsdelivr.com CDN](https://www.jsdelivr.com/package/npm/@pydantic/fastui-prebuilt)) providing a pre-built version of the FastUI React app so you can use it without installing any npm packages or building anything yourself. The Python package provides a simple HTML page to serve this app.

Expand Down Expand Up @@ -64,7 +64,7 @@ users = [
def users_table() -> list[AnyComponent]:
"""
Show a table of four users, `/api` is the endpoint the frontend will connect to
when a user fixes `/` to fetch components to render.
when a user visits `/` to fetch components to render.
"""
return [
c.Page( # Page provides a basic container for components
Expand Down Expand Up @@ -113,54 +113,30 @@ async def html_landing() -> HTMLResponse:

Which renders like this:

![screenshot](https://raw.githubusercontent.com/samuelcolvin/FastUI/main/screenshot.png)
![screenshot](https://raw.githubusercontent.com/pydantic/FastUI/main/screenshot.png)

Of course, that's a very simple application, the [full demo](https://fastui-demo.onrender.com) is more complete.

### Components

FastUI already defines the following components, all are shown in the [demo app](https://fastui-demo.onrender.com):

- `Text` — renders a string
- `Paragraph` — renders a string as a paragraph
- `PageTitle` — renders nothing, sets the HTML page title
- `Div` — renders a `<div>` with arbitrary components inside
- `Page` — a container for components
- `Heading` — renders a heading `<h1>` to `<h6>`
- `Markdown` — renders markdown, [example](https://fastui-demo.onrender.com)
- `Code` — renders code with highlighting in a `<pre>`
- `Button` — renders a `<button>`
- `Link` — renders a link `<a>`
- `LinkList` — renders a list of links
- `Navbar` — renders a navbar `<nav>`
- `Modal` — renders a modal dialog that opens triggered by an event
- `ServerLoad` — render components fetched from the server, also provides SSE mode to update components based on server sent events
- `Table` — renders a table
- `Details` — renders a table of key/value pairs as a `<dl>`
- `Display` — renders a value based on a display mode
- `Table` — renders a table from a list of Pydantic models
- `Pagination` — renders a pagination component
- `FormFieldInput` — renders a form field using `<input>`
- `FormFieldCheckbox` — renders a form field for a boolean using `<input type="checkbox">`
- `FormFieldSelect` — renders a form field using `<select>` or [react-select](https://react-select.com)
- `FormFieldSelectSearch` — renders a form field using [react-select](https://react-select.com) with options updated from the server on search
- `Form` — renders a form using a list of `FormField` components
- `ModelForm` — renders a form based on a Pydantic model; the model's JSON Schema is used to build a list of `FormField` components
FastUI already defines a rich set of components.

All components are listed in the [demo app](https://fastui-demo.onrender.com).

## The Principle (long version)

FastUI is an implementation of the RESTful principle; but not as it's usually understood, instead I mean the principle defined in the original [PhD dissertation](https://ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm) by Roy Fielding, and excellently summarised in [this essay on htmx.org](https://htmx.org/essays/how-did-rest-come-to-mean-the-opposite-of-rest/) (HTMX people, I'm sorry to use your article to promote React which I know you despise 🙏).

The RESTful principle as described in the HTMX article is that the frontend doesn't need to (and shouldn't) know anything about the application your building. Instead, it should just provide all the components you need to construct the interface, the backend can then tell the frontend what to do.
The RESTful principle as described in the HTMX article is that the frontend doesn't need to (and shouldn't) know anything about the application you're building. Instead, it should just provide all the components you need to construct the interface, the backend can then tell the frontend what to do.

Think of your frontend as a puppet, and the backend as the hand within it — the puppet doesn't need to know what to say, that's kind of the point.

Building an application this way has a number of significant advantages:

- you only need to write code in one place to build a new feature — add a new view, change the behavior of an existing view or alter the URL structure
- deploying the front and backend can be completely decoupled, provided the frontend knows how to render all the components the backend is going to ask it to use, you're good to go
- You only need to write code in one place to build a new feature — add a new view, change the behavior of an existing view or alter the URL structure
- Deploying the front and backend can be completely decoupled, provided the frontend knows how to render all the components the backend is going to ask it to use, you're good to go
- You should be able to reuse a rich set of opensource components, they should end up being better tested and more reliable than anything you could build yourself, this is possible because the components need no context about how they're going to be used (note: since FastUI is brand new, this isn't true yet, hopefully we get there)
- We can use Pydantic, TypeScript and JSON Schema to provide guarantees that the two sides are communicating with an agreed schema (note: this is not complete yet, see [#18](https://github.com/samuelcolvin/FastUI/issues/18))
- We can use Pydantic, TypeScript and JSON Schema to provide guarantees that the two sides are communicating with an agreed schema (note: this is not complete yet, see [#18](https://github.com/pydantic/FastUI/issues/18))

In the abstract, FastUI is like the opposite of GraphQL but with the same goal — GraphQL lets frontend developers extend an application without any new backend development; FastUI lets backend developers extend an application without any new frontend development.

Expand All @@ -170,7 +146,7 @@ Of course, this principle shouldn't be limited to Python and React applications

This could mean:

- implementing web a frontend using another JS framework like Vue — lots of work, limited value IMHO
- implementing web a frontend using an edge server, so the browser just sees HTML — lots of work but very valuable
- implementing frontends for other platforms like mobile or IOT — lots of work, no idea if it's actually a good idea?
- implementing the component models in another language like Rust or Go — since there's actually not that much code in the backend, so this would be a relatively small and mechanical task
- Implementing a web frontend using another JS framework like Vue — lots of work, limited value IMHO
- Implementing a web frontend using an edge server, so the browser just sees HTML — lots of work but very valuable
- Implementing frontends for other platforms like mobile or IOT — lots of work, no idea if it's actually a good idea?
- Implementing the component models in another language like Rust or Go — since there's actually not that much code in the backend, so this would be a relatively small and mechanical task
29 changes: 29 additions & 0 deletions demo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# FastUI Demo

This a simple demo app for FastUI, it's deployed at [fastui-demo.onrender.com](https://fastui-demo.onrender.com).

## Running

To run the demo app, execute the following commands from the FastUI repo root

```bash
# create a virtual env
python3.11 -m venv env311
# activate the env
. env311/bin/activate
# install deps
make install
# run the demo server
make dev
```

Then navigate to [http://localhost:8000](http://localhost:8000)

If you want to run the dev version of the React frontend, run

```bash
npm install
npm run dev
```

This will run at [http://localhost:3000](http://localhost:3000), and connect to the backend running at `localhost:3000`.
6 changes: 6 additions & 0 deletions python/demo/__init__.py → demo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,18 @@
from fastui.dev import dev_fastapi_app
from httpx import AsyncClient

from .auth import router as auth_router
from .components_list import router as components_router
from .db import create_db
from .forms import router as forms_router
from .main import router as main_router
from .sse import router as sse_router
from .tables import router as table_router


@asynccontextmanager
async def lifespan(app_: FastAPI):
await create_db()
async with AsyncClient() as client:
app_.state.httpx_client = client
yield
Expand All @@ -30,8 +34,10 @@ async def lifespan(app_: FastAPI):
app = FastAPI(lifespan=lifespan)

app.include_router(components_router, prefix='/api/components')
app.include_router(sse_router, prefix='/api/components')
app.include_router(table_router, prefix='/api/table')
app.include_router(forms_router, prefix='/api/forms')
app.include_router(auth_router, prefix='/api/auth')
app.include_router(main_router, prefix='/api')


Expand Down
Loading

0 comments on commit a5d6871

Please sign in to comment.