0. Solucionar conflictos con el entorno virtual
El principal conflicto surge cuando copiamos una capeta de un proyecto. En este aso se copia también el entrono virtual, pero cuando se instalan librerías parece ser que se guardan las rutas absolutas, entonces surgen problemas.
Para ello cuando se copia un proyecto no hay que copiar el entorno virtual, y en su caso si se ha copiado hay que borrarlo y volverlo a crear.
Para que no de problemas, hay que crear un entorno virtual que no comparta con nadie, pues si se aprovecha un entorno virtual existente, algunas librerías pueden dar problemas de compatibilidad.
Como comprobaciones tenemos:
Comprobar en un terminal que estamos en el entorno virtual y ejecutamos desde el terminal:
- pip list : Muestra las librerías intaladas
- pip freeze: Muestra tambien las librerías insaladas
- pip freeze > requirements.txt : Crea el ficheo para instalar los requerimeientos
Pero también podemos utilizar código python para hacer comprobaciones:
#1. Para ver los módulos instalados en el entorno virtual help("modules") #2. Para ver la ruta al ejecutable python import sys print(sys.executable)
Uno de los errores que detecte al hacer print(sys.executable) es que la ruta del entorno virtual no era la misma y por tanto al hacer help("modules") nlo salían las mismas librerías instaladas.
@no_type_check decorator o # type: ignore o Variable not allowed in type expression
El error producido es :
Variable not allowed in type expression
Podemos utilizar este decorador @no_type_check o simplemente este comentario # type: ignore
En este caso las líneas 1 y 3 sirven para imporar y usuar el @no_type_check decorator1 2 3 4 5 6 7 8 | from typing import no_type_check @no_type_check def __ft1__(self:Todo): ..... def __ft2__(self:Todo): # type: ignore ..... |
1. Webs útiles para convertir html a otros formatos
web2md
Convierte código html a md. Muy bueno para hacer la documentación de github
Convert HTML to FT
Convierte codigo html a FastHtml (python)
2.Introducción
A partir de quí, se siguen las instrucciones de https://docs.fastht.ml/tutorials/quickstart_for_web_devs.html
Hay que instalar en el entorno virtual la librería
pip install python-fasthtml
Crear una aplicación y servirla: Hace falta hacer el import de fasthtml, hacer un get, devolver código html y "servirla"
from fasthtml.common import FastHTML, serve app = FastHTML() @app.get("/") def home(): return "<h1>Hello, World</h1>" serve()
Crear código html: FastHtml tiene funciones para crear "div", "p" etc, y además puede utilizar marcas del framework css "pico". Solo se muestra como construir el html
page = Html( Head(Title('Some page')), Body(Div('Some text, ', A('A link', href='https://example.com'), Img(src="https://placehold.co/200"), cls='myclass'))) print(to_xml(page))
return page
Routing: Se definen de 3 maneras:
- Como @app.route('/ruta01', methods=['get','post']) más la función que devuelve el html de esta ruta
- Como @app.get('/ruta02') más la función que devuelve el html
- Como @rt('/ruta03') mas una función que se llama igual que el método (get(), post(), delete() ...
@app.route("/", methods='get') def home(): return H1('Hello, World') @app.get("/") def my_function(): return "Hello World from a GET request" rt = app.route @rt("/") def post(): return "Hello World from a POST request" client.post("/").text
Styling: Para que todos los "headers" tengan el mismo estilo, se añade la opción "hdrs" cuando llamamos a FastHTML(). También podemos añadir otras librerías como Flexbox
from fasthtml.common import * css = Style(':root {--pico-font-size:90%,--pico-font-family: Pacifico, cursive;}')
flexbotlink = Link(rel="stylesheet", href="https://cdnjs.cloudflare.com/ajax/libs/flexboxgrid/6.3.1/flexboxgrid.min.css", type="text/css") app = FastHTML(hdrs=(picolink, css, flexbotlink))
Testing: Usar scarlette.testclient para ver el html generado
from starlette.testclient import TestClient client = TestClient(app) r = client.get("/") print(r.text)
3. htmx
OJO: Sin htmx devolvemos tuplas de html, pero con htmx se devolverán todos el código!!!!
Veamos este ejemplo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | app = FastHTML() count = 0 @app.get("/") def home(): return Title("Count Demo"), Main( H1("Count Demo"), P(f"Count is set to {count}", id="count"), Button("Increment", hx_post="/increment", hx_target="#count", hx_swap="innerHTML") ) @app.post("/increment") def increment(): print("incrementing") global count count += 1 return f"Count is set to {count}" serve() |
Línea 10: El botón hace una petición tipo post (hx_post) a "/increment", cuyo resultado se aplica al elemento (hx_target) con id="count" (que en este caso es el elemento "p" de la línea anterior (9). Pero con hx_swap="innerHTML", lo que hacemos es substituir el html interno del elemento por el contenido que se devuelve en la llamada por post
Línea 13 a18: Fúnción de llamada a post que devuelve el contenido a asignar al elemento "p" de la línea 10.
4. Todo app
Veamos el código:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | #1. Import fasthtml from fasthtml.common import *from typing import no_type_check # To avoid "Variable not allowed in a type expression"#2. Define the todo element's render function def todoRender(todo): tid_=f'todo-{todo.id}' toggle = A( Mark('Toggle'), hx_get=f'/toggle/{todo.id}', target_id=tid_) delete = A( Mark('Delete'), hx_delete=f'/{todo.id}', hx_swap="outerHTML", target_id=tid_ ) return Li( toggle, ' ', delete, ' ', ' '+todo.title+ ' '+str(todo.id)+(' ✅' if todo.done else ''), id=tid_ ) |
Línea 27: En vez de llamar a FastHTML() utilizamos fast_app() y devolvemos:
- app, rt: para routing
- todo: que es una "matriz" de elementos (no confundir con tupla, dictionary o list)
- Todo: que es el tipo de elemento. OJO: VS se queja diciendo "Variable not allowed in a type expression", però al executar no dona cap problema. El motiu és que "pylance" no puede averiguar ese tipo pues se genera dinámicamente.
Los parámetros de llamada son:
- 'todos.db' que es el nombre de una base de datos sqlite que se guarda en la misma carpeta. Se podría poner 'data/todo.db' para que lo gardara en la subacarpeta data
- live=True para que recargue el navegador la página cuando hay un cambio. Solo para desarrollo y no para producción
- render=todoRender que es la función de renderizado de cada elemento "todo"
- id,title,done,pk: son elementos a tener en cuenta a la hora de renderizar.
Línea 5: Función de renderizado, a la que se indican vínculos a acciones (get para toggle y delete para delete) que se aplican al target_id. Se crea una lista cuyos elementos tienen un "id" que utilian los vínculos para actuar sobre ellos.
Para obtener el símbolo ✅ buscamos en google "check button emoji" que nos lleva a https://emojipedia.org/check-mark-button donde podemos copiar el elemento
Línea 68: Cuidado con el manejo de la matriz "todos". No podemos hacer:
todos[tid].done = not todos[tid].done
Linea 38: Cremos una función que la llamamos para construir cada vez el "input" del título de elemento todo, así aseguramos que borramos su contenido cuando hacemos una alta (botón add)
Línea 42: hx_swap_oob='true' indica que se cambiara todo el elemento indicado en el id
Línea 55: hx_swap='beforeend' indica que se añade la respuestaal final
Línea 55: hx_swap='outerHTML' indica que cambia todo el elemento con la respuesta
5. Usando Flexbox
Vemos un ejemplo de como se utiliza:
#1. Import flexbox #2. Create a parent Div with 3 children:
# the first has the 12 cells and the next 2 children with 6 columns each grid = Html( Link(rel="stylesheet", href="https://cdnjs.cloudflare.com/ajax/libs/flexboxgrid/6.3.1/flexboxgrid.min.css", type="text/css"), Div( Div(Div("This takes up the full width", cls="box", style="background-color: #800000;"), cls="col-xs-12"), Div(Div("This takes up half", cls="box", style="background-color: #008000;"), cls="col-xs-6"),
Div(Div("This takes up half", cls="box", style="background-color: #0000B0;"), cls="col-xs-6"), cls="row", style="color: #fff;" ) ) show(grid)
6. Trabajando routing y request parameters: Sesión, path parameters, regex (reg_re_param fuction), types and enums, @no_type_check, casting Path, integers with default value, boolean values, dates, dataclasses, web sockets, cookies..
El parámetro "session" del enrutamiento es un pequeño diccionario donde guardar datos
from fasthtml.common import * from starlette.testclient import TestClient app = FastHTML() cli = TestClient(app) #------------------------------- # 0. Session #------------------------------- import uuid @app.get("/") def get(session): if 'session_id' not in session: session['session_id'] = str(uuid.uuid4()) return H1(f"Session ID: {session['session_id']}") print(cli.get('/').text) #------------------------------- # 1. Path parameters #------------------------------- @app.get('/user/{nm}') def _(nm:str): return f"Good day to you, {nm}!" print(cli.get('/user/jph').text) #------------------------------- # 2. Regex # Registering a new URL converter # with a specified name and regular expression pattern. #------------------------------- reg_re_param("imgext", "ico|gif|jpg|jpeg|webm") # {fn} is the basename and {ext} is the extension @app.get(r'/static/{path:path}{fn}.{ext:imgext}') def get_img(fn:str, path:str, ext:str): return f"Getting {fn}.{ext} from /{path}" print(cli.get('/static/foo/jph.ico').text) #------------------------------- # 3. Using enum #------------------------------- # str_enum is a helper function that returns an enum type ModelName = str_enum('ModelName', "alexnet", "resnet", "lenet") from typing import no_type_check @app.get("/models/{nm}") @no_type_check # Avoid "Variable not allowed in type expression" # Variable not allowed in type expression Pylancereport InvalidTypeForm def model(nm:ModelName): return nm print(cli.get('/models/alexnet').text) #------------------------------- # 4. Casting to a Path: #------------------------------- @app.get("/files/{path}") def txt(path: Path): return path.with_suffix('.txt') print(cli.get('/files/foo').text) #------------------------------- # 5. An integer with a default value: #------------------------------- fake_db = [{"name": "Foo"}, {"name": "Bar"}] @app.get("/items/") def read_item(idx:int|None = 0): return fake_db[idx] print(cli.get('/items/?idx=1').text) #------------------------------- # 6. Boolean values (takes anything “truthy” or “falsy”): #------------------------------- @app.get("/booly/") def booly(coming:bool=True): return 'Coming' if coming else 'Not coming' print(cli.get('/booly/?coming=true').text) print(cli.get('/booly/?coming=no').text) #------------------------------- # 7.Getting dates: #------------------------------- @app.get("/datie/") def datie(d:parsed_date): return d date_str = "17th of May, 2024, 2p" print(cli.get(f'/datie/?d={date_str}').text) #------------------------------- # 8. Matching a dataclass: # @dataclass decorator add a __init__ method internally # @ see https://docs.python.org/3/library/dataclasses.html # assdict converts a class to a dictionary #------------------------------- from dataclasses import dataclass, asdict @dataclass class Bodie: a:int;b:str @app.route("/bodie/{nm}") def post(nm:str, data:Bodie): res = asdict(data) # convert to dict res['nm'] = nm # add a key and value to the dict return res print(cli.post('/bodie/me', data=dict(a=1, b='foo')).text) #------------------------------- # 9. Cookies. Set values #------------------------------- from datetime import datetime @app.get("/setcookie") def setc(req): now = datetime.now() res = Response(f'Set to {now}') res.set_cookie('now_cookie', str(now)) return res print(cli.get('/setcookie').text) #------------------------------- # 10. Cookies. Get values #------------------------------- @app.get("/getcookie") def getc(now_cookie:parsed_date): return f'Cookie (now_cookie) was set at time {now_cookie.time()}' print(cli.get('/getcookie').text) #------------------------------- # 11. User Agent #------------------------------- @app.get("/ua") #async def ua(user_agent:str): return user_agent async def ua(user_agentin:str): return user_agentin print(cli.get('/ua', headers={'User-Agent':'FastHTML', 'User-Agentin':'Yo mismo'}).text) #------------------------------- #12. Scarlett Requests #------------------------------- @app.get("/form") async def form(request:Request): form_data = await request.form() a = form_data.get('a') @app.get("/redirect") def redirect(): return RedirectResponse(url="/") #------------------------------- # 13. Static files # For images, CSS, etc. #------------------------------- @app.get("/{fname:path}.{ext:static}") def static(fname: str, ext: str): return FileResponse(f'{fname}.{ext}') #------------------------------- # 14. Web Sockets #------------------------------- from asyncio import sleep app = FastHTML(ws_hdr=True) rt = app.route def mk_inp(): return Div('Introduir'),Input(id='msg') @rt('/') async def get(request): cts = Div( Div('Comencem',id='notifications'), Form(mk_inp(), id='form', ws_send=True), hx_ext='ws', ws_connect='/ws') return Titled('Websocket Test', cts) #@app.ws('/ws') #async def ws(msg:str, send): # await send(Div('Hello ' + msg, id="notifications")) # await sleep(2) # return Div('Goodbye ' + msg, id="notifications"), mk_inp() # -----more websocket async def on_connect(send): print('Connected!') await send(Div('Hello, you have connected', id="notifications")) async def on_disconnect(ws): print('Disconnected!') @app.ws('/ws', conn=on_connect, disconn=on_disconnect) async def ws(msg:str, send): await send(Div('Hello ' + msg, id="notifications")) await sleep(2) return Div('Goodbye ' + msg, id="notifications"), mk_inp() #------------------------------- serve()