Spaces:
Runtime error
Runtime error
deploy at 2024-06-29 16:29:52.130254
Browse files- Dockerfile +10 -0
- README.md +29 -4
- config.ini +5 -0
- favicon.ico +0 -0
- main.py +80 -0
- requirements.txt +1 -0
Dockerfile
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10
|
| 2 |
+
WORKDIR /code
|
| 3 |
+
COPY --link --chown=1000 . .
|
| 4 |
+
RUN mkdir -p /tmp/cache/
|
| 5 |
+
RUN chmod a+rwx -R /tmp/cache/
|
| 6 |
+
ENV HF_HUB_CACHE=HF_HOME
|
| 7 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 8 |
+
|
| 9 |
+
ENV PYTHONUNBUFFERED=1 PORT=7860
|
| 10 |
+
CMD ["python", "main.py"]
|
README.md
CHANGED
|
@@ -1,10 +1,35 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji: 🐨
|
| 4 |
colorFrom: green
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: FastHTML Todos
|
|
|
|
| 3 |
colorFrom: green
|
| 4 |
+
colorTo: green
|
| 5 |
sdk: docker
|
| 6 |
pinned: false
|
| 7 |
+
license: apache-2.0
|
| 8 |
---
|
| 9 |
|
| 10 |
+
# FastHTML on 🤗 Spaces
|
| 11 |
+
|
| 12 |
+
Deploy a FastHTML application to [HuggingFace Spaces](https://huggingface.co/spaces) for free with one command!
|
| 13 |
+
|
| 14 |
+
## Quickstart
|
| 15 |
+
|
| 16 |
+
1. Create a free account on [HuggingFace](https://huggingface.co)
|
| 17 |
+
2. Go to your account settings and create an access token with write access. Keep this token safe and don't share it.
|
| 18 |
+
3. Set the `HF_TOKEN` environment variable to that token
|
| 19 |
+
4. Install the huggingface hub client library (`pip install huggingface-hub`).
|
| 20 |
+
5. Run the `deploy_hf.py` script and pass the name you want to give your space along with your token, e.g. `python deploy_hf.py fasthtml-todos <token>`.
|
| 21 |
+
|
| 22 |
+
By default this will upload a public space. You can make it private with the `--private` flag.
|
| 23 |
+
|
| 24 |
+
## Configuration
|
| 25 |
+
|
| 26 |
+
The space will upload a backup of your database to a [HuggingFace Dataset](https://huggingface.co/datasets). By default it will be private and its name will be `<your-huggingface-id>/todos-backup`. You can change this behavior in the `config.ini` file. In not provided, a default file will be created with the contents (note that the `[DEFAULT]` line is required at the top):
|
| 27 |
+
|
| 28 |
+
```
|
| 29 |
+
[DEFAULT]
|
| 30 |
+
dataset_id = todos-backup
|
| 31 |
+
db_dir = data
|
| 32 |
+
private_backup = True
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
If you so choose, you can disable the automatic backups and use [persistent storage](https://huggingface.co/docs/hub/en/spaces-storage#persistent-storage-specs) instead for $5/month (USD).
|
config.ini
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[DEFAULT]
|
| 2 |
+
dataset_id = todos-backup
|
| 3 |
+
db_dir = data
|
| 4 |
+
private_backup = True
|
| 5 |
+
|
favicon.ico
ADDED
|
|
main.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fasthtml.common import *
|
| 2 |
+
from backup import upload, download
|
| 3 |
+
|
| 4 |
+
db = database("data/utodos.db")
|
| 5 |
+
todos,users = db.t.todos,db.t.users
|
| 6 |
+
if todos not in db.t:
|
| 7 |
+
users.create(name=str, pwd=str, pk='name')
|
| 8 |
+
todos.create(id=int, title=str, done=bool, name=str, pk='id')
|
| 9 |
+
Todo,User = todos.dataclass(),users.dataclass()
|
| 10 |
+
|
| 11 |
+
id_curr = 'current-todo'
|
| 12 |
+
def tid(id): return f'todo-{id}'
|
| 13 |
+
|
| 14 |
+
def lookup_user(u,p):
|
| 15 |
+
try: user = users[u]
|
| 16 |
+
except NotFoundError: user = users.insert(name=u, pwd=p)
|
| 17 |
+
return user.pwd==p
|
| 18 |
+
|
| 19 |
+
css = Style(':root { --pico-font-size: 100%; }')
|
| 20 |
+
authmw = user_pwd_auth(lookup_user, skip=[r'/favicon\.ico', r'/static/.*', r'.*\.css'])
|
| 21 |
+
|
| 22 |
+
def before(auth): todos.xtra(name=auth)
|
| 23 |
+
|
| 24 |
+
app = FastHTML(hdrs=(picolink, css), middleware=authmw, before=before)
|
| 25 |
+
rt = app.route
|
| 26 |
+
|
| 27 |
+
@rt("/{fname:path}.{ext:static}")
|
| 28 |
+
async def get(fname:str, ext:str): return FileResponse(f'{fname}.{ext}')
|
| 29 |
+
|
| 30 |
+
@patch
|
| 31 |
+
def __xt__(self:Todo):
|
| 32 |
+
show = AX(self.title, f'/todos/{self.id}', id_curr)
|
| 33 |
+
edit = AX('edit', f'/edit/{self.id}' , id_curr)
|
| 34 |
+
dt = ' (done)' if self.done else ''
|
| 35 |
+
return Li(show, dt, ' | ', edit, id=tid(self.id))
|
| 36 |
+
|
| 37 |
+
def mk_input(**kw): return Input(id="new-title", name="title", placeholder="New Todo", **kw)
|
| 38 |
+
def clr_details(): return Div(hx_swap_oob='innerHTML', id=id_curr)
|
| 39 |
+
|
| 40 |
+
@rt("/")
|
| 41 |
+
async def get(request, auth):
|
| 42 |
+
add = Form(Group(mk_input(), Button("Add")),
|
| 43 |
+
hx_post="/", target_id='todo-list', hx_swap="beforeend")
|
| 44 |
+
card = Card(Ul(*todos(), id='todo-list'),
|
| 45 |
+
header=add, footer=Div(id=id_curr)),
|
| 46 |
+
title = 'Todo list'
|
| 47 |
+
top = Grid(H1(f"{auth}'s {title}"), Div(A('logout', href=basic_logout(request), target="_blank"), style='text-align: right'))
|
| 48 |
+
return Title(title), Main(top, card, cls='container')
|
| 49 |
+
|
| 50 |
+
@rt("/todos/{id}")
|
| 51 |
+
async def delete(id:int):
|
| 52 |
+
todos.delete(id)
|
| 53 |
+
return clr_details()
|
| 54 |
+
|
| 55 |
+
@rt("/")
|
| 56 |
+
async def post(todo:Todo):
|
| 57 |
+
return todos.insert(todo), mk_input(hx_swap_oob='true')
|
| 58 |
+
|
| 59 |
+
@rt("/edit/{id}")
|
| 60 |
+
async def get(id:int):
|
| 61 |
+
res = Form(Group(Input(id="title"), Button("Save")),
|
| 62 |
+
Hidden(id="id"), Checkbox(id="done", label='Done'),
|
| 63 |
+
hx_put="/", target_id=tid(id), id="edit")
|
| 64 |
+
return fill_form(res, todos[id])
|
| 65 |
+
|
| 66 |
+
@rt("/")
|
| 67 |
+
async def put(todo: Todo):
|
| 68 |
+
return todos.upsert(todo), clr_details()
|
| 69 |
+
|
| 70 |
+
@rt("/todos/{id}")
|
| 71 |
+
async def get(id:int):
|
| 72 |
+
todo = todos[id]
|
| 73 |
+
btn = Button('delete', hx_delete=f'/todos/{todo.id}',
|
| 74 |
+
target_id=tid(todo.id), hx_swap="outerHTML")
|
| 75 |
+
return Div(Div(todo.title), btn)
|
| 76 |
+
|
| 77 |
+
app.on_event("startup")(download)
|
| 78 |
+
app.on_event("shutdown")(upload)
|
| 79 |
+
|
| 80 |
+
run_uv()
|
requirements.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
python-fasthtml
|