jueves, 31 de octubre de 2024

Fast HTML (IV). Creando un menú anticuado

1. Introducción

Para crear un menú desplegable usando CSSS y FastHTML, primeramente se crea un menú con html y luego se convierte a FastHtml.

Hay que tener en cuent que el código FastHTML requiere incluior los ficheros estáticos (CSS) tal como se vió en el post anterior.

Para ver como se hacía he tomado como referencia estas dos urls:

phppot de Vincy

w3schools 

La información de los menús a mostrar está en un fichero con formato YAML (static/conf/menu.yml) y será esta:

menus:
  - { id: 1    , description: 'Menu 1'    , action: null        }
  - { id: 11   , description: 'Menu 11'   , action: 'action11'  }
  - { id: 2    , description: 'Menu 2'    , action: 'action2'   }
  - { id: 3    , description: 'Menu 3'    , action: null        }
  - { id: 31   , description: 'Menu 31'   , action: 'action31'  }
  - { id: 32   , description: 'Menu 32'   , action: 'action32'  }
  - { id: 321  , description: 'Menu 321'  , action: 'action321' }
  - { id: 322  , description: 'Menu 322'  , action: null        }
  - { id: 323  , description: 'Menu 323'  , action: 'action323'}
  - { id: 324  , description: 'Menu 324'  , action: 'action324'}
  - { id: 3221 , description: 'Menu 3221' , action: 'action3221'}
  - { id: 3222 , description: 'Menu 3222' , action: 'action3222'}
  - { id: 3223 , description: 'Menu 3223' , action: 'action3223'}
  - { id: 3224 , description: 'Menu 3224' , action: 'action3224'}
  - { id: 3225 , description: 'Menu 3225' , action: 'action3225'}
  

En este fichero se han tomado estos criterios:

Los menús de la cabecera tienen un id con un solo dígito: Por ejemplo los ids: 1,2,3 

Los hijos del menú de cabecera tendrán 2 dígitos en el id, el dígito de la decena corresponde al id del menú padre. Por ejemplo el el id 11 es hijo del id 1, los ids 31 y 32 son hijos del menú con id 3

Análogamente si dividimos por 10 un id, el cociente nos dará el id del menú padre.


Al final nos queda este árbol de menus donde se muestra solamente los ids:

1
     11
2

     31
     32
           321
           322
                  3221
                  3222
                  3223
                  3224
                  3225
           323
           324  

El funcionamioento es el siguiente:

Definimos la clase Menu,  en el header defiimos las rutas de los css y librerías , defoinimos los menus después de leer el fichero yaml, creamos el menú y cuando seejecuta una cción en el post demomento modificamos el contenido del final de la página diciendo que acción se ha ejecutado

 2. Código python (menu.py)

#1. Imports
from fastapi.staticfiles import StaticFiles
from fasthtml import common as fh
import yaml


#2. Defining Menu class
class Menu():
	# Constructor 
	def __init__(self, id, description, action):
		self.id = id
		self.description = description
		self.action = action
		self.parentId=self.parentId=None if self.id is None or self.id<10 else int(str(id)[0:-1])
		self.childrenIds=[]

	# Alternative constructor from a dioctionary using @classmethod
	@classmethod
	def fromDict(cls, d:dict):
		return cls(d['id'], d['description'], d['action'])
		
	# Setter for parentId
	def setParendId(self):
		self.parentId=self.parentId=None if self.id is None or self.id<10 else int(str(id)[0:-1])
		

#3. Load menus from conf/menus.yml as a list of dict
def getMenus(menuFilePath: str='static/conf/menus.yml')->dict:
	with open(menuFilePath) as f: 
		mnuDict=yaml.safe_load(f)	
		mnuLst=[Menu.fromDict(m) for m in mnuDict['menus']]
		return mnuLst
	
#4. Convert the list of menus into a dict whose key is the id
#   and get the children menus
def getMenuDict(menus:list[Menu]):
	menuDict={}
	for i, m in enumerate(menus):
		m.childrenIds=[elem.id for elem in filter(lambda x:x.parentId==m.id,menus)] # Set childrenIds
		menuDict[m.id] = m
	return menuDict	
	
#5. Get the code of a sub menu and its nested (children) menus
def getSubmenuCodeFH(id:int, menuDict:dict[int, Menu]):
	menu=menuDict.get(id)
	paramDict={}
	if len(menu.childrenIds)==0:
		paramDict['hx_post']=f"/action/{menu.action}"
		paramDict['hx_target']="#message"
		paramDict['hx_swap']="innerHTML"
		SpanContent=''
	else:
		SpanContent=fh.I(cls='triang fa fa-caret-right')
		href="#"
	tttClas='tooltiptext' if menu.id<10 else 'tooltiptextchild'	
	return fh.Li(
		fh.A(
				menu.description,
				fh.Span(
					SpanContent,
					cls='expand'
				),
				fh.Span(
					menu.description + '-tooltiptext',
					cls=tttClas
				),	
			**paramDict,
			cls='tooltip' 
		),
		fh.Ul(*[getSubmenuCodeFH(k, menuDict) for k in sorted(menu.childrenIds)], cls='child'),
		cls='parent'
	)

	
# Get the codde of the main menu
def getGeneralMenuCodeFH(menuDict:dict[int, Menu]):
	return fh.Body(
    	fh.Div(id='navbar', cls='navbar'),
    	fh.Ul(
			*[getSubmenuCodeFH(k, menuDict) for k in filter(lambda x: x<10,list(menuDict.keys())) ], 
			id='menu'
		),
		fh.Div('qqqqq',id='message', cls='message')
    )



menus=getMenus('static/conf/menus.yml')
menuDict=getMenuDict(menus)

#6. Defining Headers (Styles, Scripts, etc)
myHeaders = [
	fh.Meta(name='viewport', content='width=device-width, initial-scale=1'),
	fh.Link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css'),
	fh.Link(rel='stylesheet', href='../static/css/menu.css')
]

#7. Creating FastAPI App
app = fh.FastHTML(hdrs=myHeaders)

#8. Mounting the static folder for accessing the menu.css
app.mount("/static", StaticFiles(directory="static"), name="static")

#9. Defining the routes
@app.get("/")
def geto1():
	return getGeneralMenuCodeFH(menuDict=menuDict) 

#10. Menu Actions executed in a
@app.post("/action/{actEdu}")
def postAction(actEdu:str):
	return f"Action received: {actEdu}"
	


3. Código CSS (static/css/menu.css)

Aquí hemos tenido en cuento los tool tip text ....


body {font-family: Arial, Helvetica, sans-serif;}
.navbar {overflow: hidden;background-color: black;position:fixed;top: 0;height:46px; width: 100%; z-index:-1}
#menu {padding-left:10px; }

.parent {display: block;position: relative;float: left;line-height: 30px;background-color: black;border-right:#CCC 1px solid;}	
.parent a{margin: 10px;color: #FFFFFF;text-decoration: none;}
.parent:hover > ul {display:block;position:absolute;}
.parent li:hover {background-color: #F0F0F0;}

.child {display: none;padding-top: 10px;box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);}
.child li {background-color: #f9f9f9;line-height: 30px;border-right:#CCC 1px solid; width:100%;}
.child li a{color: #000000;}
.child .child{padding-top: 0px;}

ul{list-style: none;margin: 0;padding: 0px; min-width:10em;}
ul ul ul{left: 100%;top: 0;margin-left:1px;}

li:hover {background-color: red;}

.expand{float:right;margin-right:5px;}
.triang{margin-top:6px;}
.message {position: fixed; bottom:50px;color: green;background-color: white;border-right:#CCC 1px solid;}	

.tooltip {}
.tooltip .tooltiptext {	
	visibility: hidden;	width: 150px; font-size:85%;
	color: black; background-color:gold;
	text-align: center;	border-radius: 6px;	padding: 5px 0;
	position: absolute;	z-index: 1;	top: -5px;	left: 105%;
}
.tooltip:hover .tooltiptext {visibility: visible;}


.tooltip .tooltiptextchild {	
	visibility: hidden;	width: 150px; font-size:85%;
	color: black; background-color:gold;
	text-align: center;	border-radius: 6px;	padding: 5px 0;
	position: absolute;	z-index: 1;	top: -5px;	left: 105%;
}
.tooltip:hover .tooltiptextchild {visibility: visible;}	


4. Ejecución (uvicorn menu:app)

En el terminal ejecutmos uvicorn menu:app  



martes, 29 de octubre de 2024

Fast HTML (III). Referenciando ficheros locales de recursos (js, css, imágenes ..)

Se explica en https://fastapi.tiangolo.com/tutorial/static-files/

Supongamos que tenemos esta estructura de ficheros

/proyecto
        main.py
        /static
                /css
                        menu.css

y desde main.py queremos referenciar a menú css


Para ello tenemos este código en main.py, dondew se muestra como se genera código html cons FastHTML y directamente, y como hacer las referencias relativas en ambos, que se hace de la misma manera


 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
from fastapi.staticfiles import StaticFiles
from fasthtml import common as fh

fh.FastHTML(static_path='./static/css')
app = fh.FastHTML()

#Montamos la carpeta "static"
app.mount("/static", StaticFiles(directory="static"), name="static")

#Creamos el código HTML con FastHTML
def getCodeFH(cssPath:str):
	return fh.Html(
		fh.Head(
			fh.Link(rel='stylesheet', href=cssPath),
		),
		fh.Body(
			fh.Div(
				fh.A('Home', href='#home'),
				fh.A('News', href='#news'),
				cls='navbar'
			),
		)
	)

#Creamos el mismo código HTML pero directamente
def getCodeHTML(cssPath:str):
	return f'''
		<!DOCTYPE html>
		<html>
		  <head>
		    <link rel="stylesheet" href="{cssPath}">
		  </head>

		  <body style="background-color:white;">
            <div class="navbar">
		      <a href="#home">Home</a>
		      <a href="#news">News</a>
		    </div>
		  </body>
		</html>
		'''

# Falla por no meter los .. en la ruta
@app.get("/fallo1/")
def getf1():
	return getCodeFH('/static/css/menu.css')

# Falla por no meter los .. en la ruta
@app.get("/fallo2/")
def getf2():
	return getCodeHTML('/static/css/menu.css')

# Va bien por meter los .. en la ruta
@app.get("/ok1/")
def geto1():
	return getCodeFH('../static/css/menu.css') 

# Va bien por meter los .. en la ruta
@app.get("/ok2/")
def geto1():
	return getCodeHTML('../static/css/menu.css') 


Para referenciar al fichero static/css/menu.css debemos:

  • Línea 8 : Montar la carpeta "/static" para que al hacer un route la reconozca.
  • Líneas 56 y 61: Referenciarlas como "../static/css/menu.css" (No olvidar los 2 puntos)

En las líneas 46 y 51 no encuentra los css por no colocar ".." como prefijo de la  ruta


jueves, 24 de octubre de 2024

Fast HTML (II). Uvicorn. Development and production. nginx. https. Certificates. ssl

1. Uvicorn y SSL

Para mas información ver la página oficial de Uvicorn.

Parece ser que Uvicorn es el servidor http/https

Si vamos a la definción del la funcion serve de fastHtml que está en el fichero core.py tenemos este código:

# %% ../nbs/api/00_core.ipynb
def serve(
        appname=None, # Name of the module
        app='app', # App instance to be served
        host='0.0.0.0', # If host is 0.0.0.0 will convert to localhost
        port=None, # If port is None it will default to 5001 or the PORT environment variable
        reload=True, # Default is to reload the app upon code changes
        reload_includes:list[str]|str|None=None, # Additional files to watch for changes
        reload_excludes:list[str]|str|None=None # Files to ignore for changes
        ): 
    "Run the app in an async server, with live reload set as the default."
    bk = inspect.currentframe().f_back
    glb = bk.f_globals
    code = bk.f_code
    if not appname:
        if glb.get('__name__')=='__main__': appname = Path(glb.get('__file__', '')).stem
        elif code.co_name=='main' and bk.f_back.f_globals.get('__name__')=='__main__': appname = inspect.getmodule(bk).__name__
    if appname:
        if not port: port=int(os.getenv("PORT", default=5001))
        print(f'Link: http://{"localhost" if host=="0.0.0.0" else host}:{port}')
        uvicorn.run(f'{appname}:{app}', host=host, port=port, reload=reload, reload_includes=reload_includes, reload_excludes=reload_excludes)


Y al final tenemos una llamada a "uvicorn.run" cuyo código está en main.py y és:

def run(
    app: ASGIApplication | Callable[..., Any] | str,
    *,
    host: str = "127.0.0.1",
    port: int = 8000,
    uds: str | None = None,
    fd: int | None = None,
    loop: LoopSetupType = "auto",
    http: type[asyncio.Protocol] | HTTPProtocolType = "auto",
    ws: type[asyncio.Protocol] | WSProtocolType = "auto",
    ws_max_size: int = 16777216,
    ws_max_queue: int = 32,
    ws_ping_interval: float | None = 20.0,
    ws_ping_timeout: float | None = 20.0,
    ws_per_message_deflate: bool = True,
    lifespan: LifespanType = "auto",
    interface: InterfaceType = "auto",
    reload: bool = False,
    reload_dirs: list[str] | str | None = None,
    reload_includes: list[str] | str | None = None,
    reload_excludes: list[str] | str | None = None,
    reload_delay: float = 0.25,
    workers: int | None = None,
    env_file: str | os.PathLike[str] | None = None,
    log_config: dict[str, Any] | str | RawConfigParser | IO[Any] | None = LOGGING_CONFIG,
    log_level: str | int | None = None,
    access_log: bool = True,
    proxy_headers: bool = True,
    server_header: bool = True,
    date_header: bool = True,
    forwarded_allow_ips: list[str] | str | None = None,
    root_path: str = "",
    limit_concurrency: int | None = None,
    backlog: int = 2048,
    limit_max_requests: int | None = None,
    timeout_keep_alive: int = 5,
    timeout_graceful_shutdown: int | None = None,
    ssl_keyfile: str | os.PathLike[str] | None = None,
    ssl_certfile: str | os.PathLike[str] | None = None,
    ssl_keyfile_password: str | None = None,
    ssl_version: int = SSL_PROTOCOL_VERSION,
    ssl_cert_reqs: int = ssl.CERT_NONE,
    ssl_ca_certs: str | None = None,
    ssl_ciphers: str = "TLSv1",
    headers: list[tuple[str, str]] | None = None,
    use_colors: bool | None = None,
    app_dir: str | None = None,
    factory: bool = False,
    h11_max_incomplete_event_size: int | None = None,
) -> None:
    if app_dir is not None:
        sys.path.insert(0, app_dir)

    config = Config(
        app,
        host=host,
        port=port,
        uds=uds,
        fd=fd,
        loop=loop,
        http=http,
        ws=ws,
        ws_max_size=ws_max_size,
        ws_max_queue=ws_max_queue,
        ws_ping_interval=ws_ping_interval,
        ws_ping_timeout=ws_ping_timeout,
        ws_per_message_deflate=ws_per_message_deflate,
        lifespan=lifespan,
        interface=interface,
        reload=reload,
        reload_dirs=reload_dirs,
        reload_includes=reload_includes,
        reload_excludes=reload_excludes,
        reload_delay=reload_delay,
        workers=workers,
        env_file=env_file,
        log_config=log_config,
        log_level=log_level,
        access_log=access_log,
        proxy_headers=proxy_headers,
        server_header=server_header,
        date_header=date_header,
        forwarded_allow_ips=forwarded_allow_ips,
        root_path=root_path,
        limit_concurrency=limit_concurrency,
        backlog=backlog,
        limit_max_requests=limit_max_requests,
        timeout_keep_alive=timeout_keep_alive,
        timeout_graceful_shutdown=timeout_graceful_shutdown,
        ssl_keyfile=ssl_keyfile,
        ssl_certfile=ssl_certfile,
        ssl_keyfile_password=ssl_keyfile_password,
        ssl_version=ssl_version,
        ssl_cert_reqs=ssl_cert_reqs,
        ssl_ca_certs=ssl_ca_certs,
        ssl_ciphers=ssl_ciphers,
        headers=headers,
        use_colors=use_colors,
        factory=factory,
        h11_max_incomplete_event_size=h11_max_incomplete_event_size,
    )
    server = Server(config=config)

    if (config.reload or config.workers > 1) and not isinstance(app, str):
        logger = logging.getLogger("uvicorn.error")
        logger.warning("You must pass the application as an import string to enable 'reload' or " "'workers'.")
        sys.exit(1)

    try:
        if config.should_reload:
            sock = config.bind_socket()
            ChangeReload(config, target=server.run, sockets=[sock]).run()
        elif config.workers > 1:
            sock = config.bind_socket()
            Multiprocess(config, target=server.run, sockets=[sock]).run()
        else:
            server.run()
    except KeyboardInterrupt:
        pass  # pragma: full coverage
    finally:
        if config.uds and os.path.exists(config.uds):
            os.remove(config.uds)  # pragma: py-win32

    if not server.started and not config.should_reload and config.workers == 1:
        sys.exit(STARTUP_FAILURE)


Y hay que destacar que podemos trabajar con https si le damos la información del certificado de servidor que marcamos en amarillo (también hay que adecuar otros parámetros como los puertos, versión del ssl , etc)

2. Fasthtml y SSL

Para ejecutar SSL en FastHML podemos hacerlo de varias maneras

2.1 Copiando la función serve de fastHTML y modificándola para que admita también como parámetros de entrada los parámetros de certificado:

  • ssl_keyfile
  • ssl_certfile
  • ssl_keyfile_password
  • ssl_version
  • ssl_cert_reqs
  • ssl_ca_certs
  • ssl_ciphers

Podemos tener nuestra función serve a la que podemos añadir las opciones del certificado, copiando la función serve de fastHTML y añadiéndole los parámetros de configuración del SSL que hemos detectado.

def serve(
        appname=None, # Name of the module
        app='app', # App instance to be served
        host='0.0.0.0', # If host is 0.0.0.0 will convert to localhost
        port=None, # If port is None it will default to 5001 or the PORT environment variable
        reload=True, # Default is to reload the app upon code changes
        reload_includes:list[str]|str|None=None, # Additional files to watch for changes
        reload_excludes:list[str]|str|None=None # Files to ignore for changes
        ssl_keyfile: str | os.PathLike[str] | None = None,
        ssl_certfile: str | os.PathLike[str] | None = None,
        ssl_keyfile_password: str | None = None,
        ssl_version: int = SSL_PROTOCOL_VERSION,
        ssl_cert_reqs: int = ssl.CERT_NONE,
        ssl_ca_certs: str | None = None,
        ssl_ciphers: str = "TLSv1",
        ): 
    "Run the app in an async server, with live reload set as the default."
    bk = inspect.currentframe().f_back
    glb = bk.f_globals
    code = bk.f_code
    if not appname:
        if glb.get('__name__')=='__main__': appname = Path(glb.get('__file__', '')).stem
        elif code.co_name=='main' and bk.f_back.f_globals.get('__name__')=='__main__': appname = inspect.getmodule(bk).__name__
    if appname:
        if not port: port=int(os.getenv("PORT", default=5001))
        print(f'Link: http://{"localhost" if host=="0.0.0.0" else host}:{port}')
        uvicorn.run(f'{appname}:{app}', host=host, port=port, reload=reload, reload_includes=reload_includes, reload_excludes=reload_excludes, 
            ssl_keyfile=ssl_keyfile, ssl_certfile= ssl_certfile, ssl_keyfile_password=ssl_keyfile_password, 
            ssl_version=ssl_version, ssl_cert_reqs=ssl_cert_reqs, ssl_ca_certs= ssl_ca_certs, ssl_ciphers=ssl_ciphers)


2.2 Ejecutando uvicorn directamente y pasándole los parametros de certificado

Este ejemplo se ha copiado de uvicorn.

import uvicorn

class App:
    ...

app = App()

if __name__ == "__main__":
    uvicorn.run("main:app", host="127.0.0.1", port=443, log_level="info",
                ssl_keyfile='ruta1', ssl_certfile='ruta2, ssl_keyfile_password='mipaswword')


3. Uvicorn y nginx

Podemos consultar la misma página. y podemos crear una configuración de nginx (que hay que instalarlo a parte)

http {
  server {
    listen 80;
    client_max_body_size 4G;

    server_name example.com;

    location / {
      proxy_set_header Host $http_host;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection $connection_upgrade;
      proxy_redirect off;
      proxy_buffering off;
      proxy_pass http://uvicorn;
    }

    location /static {
      # path for static files
      root /path/to/app/static;
    }
  }

  map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
  }

  upstream uvicorn {
    server unix:/tmp/uvicorn.sock;
  }

}