martes, 17 de septiembre de 2024

Fast HTML (I). Convert html to fasthtml. Convert html to md. Conflicto entorno virtual.@no_type_check decorator

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 decorator
Y las línea 7 añade un único comentario (# type ignore) que hace lo mismo sin imports 

1
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:

  1. Como @app.route('/ruta01', methods=['get','post']) más la función que devuelve el html de esta ruta
  2. Como @app.get('/ruta02') más la función que devuelve el html
  3. 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

Nos mostraria esta pantalla:



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_ )
#3. Create a FastHTML app
@no_type_check
app, rt, todos, Todo = fast_app( 'todos.db', live=True, render=todoRender, id= int, title=str, done=bool, pk='id' ) #4. Define the input element def mk_input(): return Input( placeholder="What needs to be done?", id='title', hx_swap_oob='true', ) #5. Main form @rt("/") def get(): frm= Form( Group( mk_input(), Button("Add") ), hx_post='/', target_id='todo-list', hx_swap='beforeend' ) return Titled( 'Todos', Card( Ul(*todos(), id='todo-list'), header=frm ) ) #6. Toggle element @rt("/toggle/{tid}") def get(tid:int): todo=todos[tid] todo.done = not todo.done return todos.update(todo) #7. Delete element @rt("/{tid}") def delete(tid:int): todos.delete(tid) #8. Add element @rt("/") def post(todo:Todo): return todos.insert(todo), mk_input() #9. Run the application on the server serve()

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 respuestal 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()






lunes, 16 de septiembre de 2024

GitLab (I) .Instalar Gitlab CE en debian

1. Instalación inicial 

Seguimos a https://www.linuxtechi.com/how-to-install-gitlab-on-debian/?utm_content=cmp-true


# 1) Update the System
$ sudo apt update

# 1) Install GitLab dependencies
$ sudo apt install wget ca-certificates curl apt-transport-https gnupg2 -y #3) Install Gitlab on Debian 12 $ curl -s https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.deb.sh | sudo bash $ sudo apt install gitlab-ce -y

Comprobar que funciona y apuntarse la contraseña de "root". Para ello entrar en 

http://192.168.xxx.xxx 

y dar la contraseña de root y un correo para ello marcar el símbolo mostrado:


Seleccionar Edit profile:




Y ahí comprobamos el email y la contraseña.


2. Obtener un certificado SSL.

Parece ser que gitlab quiere una clave (.key) y un certificado (.crt) cuyo nombre sea el mismo que el dominio de la máquina, por ejemplo "gitlab.municipio.es"

2.1 Opción 1: Partimos de un certificado con extension p12

Para ello, si partimos de un certificado "wildcard" en formato p12 seguiremos las instrucciones de https://www.ssl.com/how-to/export-certificates-private-key-from-pkcs12-file-with-openssl/

Veamos como obtenemos la clave privada (.key) y el certificado (.crt) cuyo nombre sea del dominio


# 1) Get only private key with domain name
$ openssl pkcs12 -in crt_wildcard.p12 -out gitlab.municipio.es.key -nodes -nocerts

# 1) Get only certificate with domain name
$ openssl pkcs12 -in crt_wildcard.p12 -out gitlab.municipio.es.crt -nokeys


2.2 Crear la entidad certificadora CA los certificados:

#1. Generar clave privada encriptada para la CA
#   Nos pide una contraseña que hay que guardar
openssl genrsa -aes256 -out MyRootCA.key 4096

#2. Generar certificado autofirmado para la CA par 5 años 1825 dias
openssl req -x509 -new -nodes -key MyRootCA.key -sha256 -days 1825 -out MyRootCA.crt

#3. Agregar el certificado de la CA a los certificados raíz de confianza de los usuarios y del servidor gitlab !!!!
#   En sistemas Linux:
sudo cp MyRootCA.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates

#   En Windows, instálelo manualmente en Certificados - Usuario actual > Entidades de certificación raíz de confianza.

#4. Generar clave privada para el servidor Gitlab con dominio gitlab.municipio.es
openssl genrsa -out gitlab.municipio.es.key 2048

#5. Crear un archivo de configuración "gitlab_openssl.cnf" para incluir SAN
#  Copiar este texto al archivo "gitlab_openssl.cnf"
[ req ]
default_bits       = 2048
prompt             = no
default_md         = sha256
distinguished_name = req_distinguished_name
req_extensions     = req_ext

[ req_distinguished_name ]
C  = ES
ST = Valencia
L  = municipio
O  = MiEmpresa
CN = gitlab.municipio.es

[ req_ext ]
subjectAltName = @alt_names

[ alt_names ]
DNS.1 = gitlab.municipio.es


#6. Generar la CSR con CN y SAN
openssl req -new -key gitlab.municipio.es.key -out gitlab.municipio.es.csr -config gitlab_openssl.cnf

#6. Firmar el CSR con tu CA
openssl x509 -req -in gitlab.municipio.es.csr \
-CA MyRootCA.crt -CAkey MyRootCA.key -CAcreateserial \
-out gitlab.municipio.es.crt -days 365 -sha256 \
-extfile gitlab_openssl.cnf -extensions req_ext

#7. Verificar que  el CN y SAN están incluidos en el certificado
openssl x509 -in gitlab.municipio.es.crt -text -noout

#   Deben aparecer estos datos
#   CN = gitlab.municipio.es
#   X509v3 extensions:
#      X509v3 Subject Alternative Name: 
 DNS:gitlab.tavernes.es

#7. Copiar a la carpeta del servidor /etc/gitlab/ssl/ estos archivos:
gitlab.municipio.es.crt
gitlab.municipio.es.key
MyRootCA.crt

#8. Configurar en GitLab (/etc/gitlab/gitlab.rb):
nginx['ssl_certificate'] = "/etc/gitlab/ssl/gitlab.munipio.es.crt"
nginx['ssl_certificate_key'] = "/etc/gitlab/ssl/gitlab.municipio.es.key"

#9. Reiniciar gitlab
sudo gitlab-ctl reconfigure
sudo gitlab-ctl restart

#10. Agregar el certificado MyRootCA.crt a los certificados de CA de chrome (Configuración, provacidad y seguridd, certificados ..)

#11. Borrar caché de Chrome
#    Haz clic en los tres puntos verticales (el menú "Más") en la esquina superior derecha del navegador.
#    Luego "Borrar datos de navegación".



2.2 Opción 2: Nos creamos un certificado self-signed (autofirmado) NO VA!!

Para ello, si partimos de un certificado "wildcard" en formato p12 seguiremos las instrucciones de https://medium.com/@gengchao77977/configure-self-signed-ssl-certificate-on-gitlab-58cc8e8cf3fa


# 1) Create the root CA key
$ sudo openssl genrsa -out ca.key 4096

# 2) Create the root CA certificate
$ sudo openssl req -x509 -new -nodes -key ca.key -sha256 -days 365 -out ca.crt -subj '/CN=gitlab.municipio.es' #3) Create a certificate signing request (CSR) $ sudo openssl req -new -nodes -out gitlab.municipio.es.csr -newkey rsa:4096 -keyout gitlab.municipio.es.key -subj '/CN=gitlab.municipio.es'
#4) Create a certificate configuration file. Create a file named "run.extfile" with this content basicConstraints=critical,CA:TRUE keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment subjectAltName = @alt_names [alt_names] DNS.1 = yourdomain.com #5) Create a Gitlab certificate $ sudo openssl x509 -req -in gitlab.municipio.es.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out gitlab.municipio.es.crt -days 365 -sha256 -extfile run.extfile


3. Instalar el certificado en gitlab.

Modificamos el fichero /etc/gitlab/gitlab.rb y añadimos estas líneas

#----------------------------------------
#6.1 Edit the /etc/gitlab/gitlab.rb as follows
#----------------------------------------
# your GitLab instance URL
external_url "https://yourdomain.com"

# auto http -> https
nginx['redirect_http_to_https'] = true

# please note to disable letsencrypt
letsencrypt['enable'] = false
#----------------------------------------

Crear un directorio por ejemplo ssl cuya ruta absoluta será /etc/gitlab/ssl y copiamos la clave privada (gitlab.municipio.es.key) y el certificado (gitlab.municipio.es.crt) dentro de esta carpeta.

Cambiamos los permisos de la carpeta y reconfiguramos gitlab. A veces puede requerirse que se vuelva a rearrancar el servicio e incluso a puede ser necesario hcer un "reboot" del servidor

Opcionalmente podemos ver que nos dice del certificado

#6.2 Create the folder in etc/gitlab and copy the certificate
$ sudo mkdir -p /etc/gitlab/ssl
$ sudo chmod 755 /etc/gitlab/ssl
$ sudo cp -f cert_file /etc/gitlab/ssl/
$ sudo gitlab-ctl reconfigure

#6.2.1 Optional: Sometimes it is requiered to restart the gitlab service
$ sudo gitlab-ctl restart
#6.2.2 Optional: Sometimes it is requiered to resboot the server
$ sudo reboot
#6.3 Verify the added certificate $ sudo openssl s_client -showcerts -verify 3 -connect yourdomain.com:443


4. Comprobar que funciona

Con el navegador apuntamos a https://gitlab.municipio.es 


5. Crear un nuevo proyecto y repositorio

Hacer click en el simbolo "+" de la parte superior izquierda y  continuación seleccionmos "New project/repository"



Le damos a"Create blank project"


Damos nombre, namespace/group (root) y "project slug" opcional, y le damos los permisos "Private" y creamos el proyecto


Y nos sale


NO USAR ESTO QUE VIENE A CONTINUACIÓN!!!

6. Guardar (Push) una carpeta existente en el repositorio

Como se muestra en la imagen anterior, nos mentemos en la carpeta a guardar y ejecutamos

cd my-python-library
git config --global user.name "Administrator"
git config --global user.email "axxxxxxxx.org" init --initial-branch=main
git remote add origin git@gitlab.xxxx-xx:root/my-python-library.git
git add .
git commit -m "Initial commit"
git push --set-upstream origin main

Ahora ya hemos guardado nuestro proyecto en el gitlab


























miércoles, 28 de agosto de 2024

FreeCAD (I) Instalación. Crear un documento. Crear una pieza por extrusión. Modos de trabajo del ratón

1. Instalación en Linux Mint

Se aconseja utilizar flatpack. Para ello teclear

flatpak install flathub org.freecadweb.FreeCAD

2. Crear un nuevo documento

Menu Archivo -> Nuevo

3. Crear un cuerpo y scketch (croquis o boceto)

Menu Part Design -> Crear Cuerpo y luego crear sketch 


Seleccionar por ejemplo el plano XY y darle al boton OK


Ahora podemos:
  1. Crear arcos de circunferencia (señalado en rojo)
  2. Crear líneas (señalado en azul)
  3. Acotar diámetros o segmentos (en verde) una vez dibujado (constraint diameter)
Importante: Cuando se dibuja aparecen símbolos que indican que es paralela la línea o que el final del segmento coincide con el comienzo del arco trazado


Ahora debe aparecer un mensaje verde que diga "Totalmente restringido. Se ven también las acotaciones hechas. Y le damos al botón Close (en verde)



4. Extrusionar un sketch (boceto, croquis)

Se ha dibujado un semicírculo. Ahora queremos darle volumen por extrusión: Le damos al icono de extrusión



Y nos pide la altura entre otros datos y le damos al botón OK



Y vemos lo que hay


Y ahora toca cambiar el nombre de la pieza (botón derecho sobre el diagrama de la izquierda (Cuerpo es el nombre de la pieza) y le damos F2 

Análogamente en Menú Archivo -> Guardar Como cambiamos el nombre del documento.


5. Modos de trabajo del ratón

En la parte inferir izquierda, por opmisión tengo seleccionado CAD, pero puede haber más modos que se pueden seleccionar con el triangulito de al lado

Y pasando por encima (hovering) podemos ver que hace cada tecla

3. Restricciones

Se entiende por resticciones (constrains) el acotar un lado, establecer longitudes y anchuras, marcar la distancia (horizontal y vertical) entre 2 puntos





2.

miércoles, 7 de agosto de 2024

Python (IX) Crear ejecutables en windows

1. Crear una carpeta de trabajo

2. En VS abrir dicha carpeta

3. Crear el entorno virtual usando el terminal de VS (comprobando que estamos en dicha carpeta) y activándolo

pip install virtualenv 
python -m venv myenv
myenv\Scripts\activate.bat

4. Crear el fichero "farmacies_excel2csv.py" y copiar el contenido siguiente:

import pandas
import os
import sys


def mainProva():
	FILE_PATH= os.path.dirname(os.path.abspath(sys.argv[1])) 
	excel=pandas.ExcelFile(sys.argv[1])
	for sheetName in excel.sheet_names:
		sheet=pandas.read_excel(excel, sheetName).to_csv(sep=',')
		longLine=""
		separator=""
		for lin in sheet.splitlines():
			fields=lin.split(",")
			a=fields[2].strip()
			try:
				a=int(a)
			except Exception as e: 
				a='????????'
			else:
				longLine+=separator+fields[4]+'"'
				separator=","	

		with open(FILE_PATH + os.sep+sheetName, 'w') as f:
			f.write(longLine)
			f.close()
			
	

if __name__ == "__main__":
	mainProva()	

5. Habran errores que se solucionaran instalando las dependencias que pide que pueden ser:

pip install pandas
pip install xlrd
pip install openpyxl

6. Instalar pyinstaller

pip install -U pyinstaller

7. Crear ejecutable. Puede dar problemas para encontrar la ruta de pyinstaller.
Si da problemas se busca la ruta y se añade al path

Cuando se instala en windows 11 se instala pyinstaller dentro de la carpeta del usuario. Por tanto debemos incluir en el path del usuario 


En windows tenemos que añadir a las variables de entorno de usuario estas carpetas tal como se muestra:

C:\Users\eduard\AppData\Local\Programs\Python\Python312\Scripts

C:\Users\eduard\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0\LocalCache\local-packages\Python312\Scripts







Creamos el ejecutable


# En windows
Pyinstaller --onefile -w farmacies_excel2csv.py
# En windows, si falla
python -m Pyinstaller --onefile -w farmacies_excel2csv.py

# En Linux
pyinstaller --onefile farmacies_excel2csv.py

martes, 23 de julio de 2024

Python (VIII): Accediendo a BD Access en Linux

 Damos créditos a pbdany

1. Instalar la libreria JayDeBeApi en el entorno virtual

# Activamos el entorno virtual (en mi caso es .venv)
source .venv/bin/activate

# Instalamos la librería
pip install JayDeBeApi 


2. Descargar la librería UCanAccess 

Para ello vamos a sourceforge y la descargamos en una carpeta. En mi caso la he descargado en la carpeta "jar" dentro del proyecto de python. Hay que tener cuidado a la hora de definir las rutas a dichas librerias

En mi caso se ha descargado así


3. Descargarse Java y crear la variable JAVA_PATH

Nos descargamos Java Open JDK por ejemplo de OpenLogic  ( y yo personalmente he descargado la version 17 en "/home/ximo/jdk/jdk17/") 

En mi caso el ejecutable java es "/home/ximo/jdk/jdk17/bin/java"

y creamos el JAVA_PATH a dicha localización 

# Creamos la variable de entorno JAVA_PATH
export JAVA_PATH=/home/ximo/jdk/jdk17/bin

# Comprobamos su valor
echo $JAVA_PATH


4. Código fuente del programa python

Se va a convertir un fichero ACCESS a CSV.

Hay que tener en cuenta lo siguiente:

  1. Comprobar todas las rutas a los jars. si hay algún error en alguna de ellas, dará un error como que no encuentra alguna librería para abrir dicho fichero de BD
  2. Dar correctamente los nombres de los ficheros de entrada (tipo mdb) como los de salida (tipo csv)
  3. Dar el nombre de tabla o vista correcto al pasar la sentencia SQL al cursor.
  4. Configurar el csv.writer para definirle el delimitador (en mi caso ";") y si queremos entrecomillar los campos de caracteres.
  5. Utilizar convenientement el csv.writer para escribir títulos y filas

import jaydebeapi
import sys
import os
import csv


# Access DB file
DBFile='/home/ximo/Baixades/EMCO_09_2024_0000000008.mdb'

# CSV output file
CSVFile='/home/ximo/Baixades/DATOS_PERS.csv'


# Initiate Java runtime file location
UCanVer='5.0.1'
lang3Ver='3.8.1'
logVer='1.2'
hsqlVer='2.5.0'
jackVer='3.0.1'

# Cpnfigure path for jar files
configPath = sys.path[0] + os.sep+".."+os.sep+'jars' + os.sep + 'UCanAccess-'+ UCanVer +os.sep
ucanaccess_jars = [
configPath+"ucanaccess-" +UCanVer +".jar",
configPath+"lib/commons-lang3-" +lang3Ver+".jar",
configPath+"lib/commons-logging-"+logVer +".jar",
configPath+"lib/hsqldb-" +hsqlVer +".jar",
configPath+"lib/jackcess-" +jackVer +".jar",
]

# classpath = $PATH$ parameters for Java runtime file location
classpath = ":".join(ucanaccess_jars)

# Initate connection to MS Access files
cnxn = jaydebeapi.connect(
"net.ucanaccess.jdbc.UcanaccessDriver",
"jdbc:ucanaccess://"+DBFile,
["", ""],
classpath
)

# From connection initiate cursor
cursor = cnxn.cursor()

# Run Query
cursor.execute("SELECT * FROM DATOS_PERS")
fieldNames= ",".join([i[0] for i in cursor.description])
with open(CSVFile,'w') as f:
writer = csv.writer(f,dialect='excel',delimiter=';',doublequote=True,quoting=csv.QUOTE_NONNUMERIC)
# Generamos cabecera de nombres de campos
    writer.writerow([i[0] for i in cursor.description])
# Generamos las filas de datos
writer.writerows(row for row in cursor.fetchall())

# Fetch Query result
#for row in cursor.fetchall():
# print(row)

# Close cursor
cursor.close()
# Close connection
cnxn.close()





jueves, 11 de julio de 2024

Python (VII): sqlite con regexp. Definición de funciones en SQLITE

1. Regexp con SQLITE 

Según aGueGu en stackoverflow podemos hacer búsquedas en sqlite y Python así


import sqlite3
import re

# Definimos una función con 2 parámetros 
def match(expr, item):
    return re.match(expr, item) is not None

conn = sqlite3.connect(':memory:')

#Le pasamos esta función y le indicamos que tiene 2 parámetros
conn.create_function("MATCHES", 2, match)
cursor = conn.cursor()
cursor.execute("SELECT MATCHES('^b', 'busy');")
print (cursor.fetchone()[0])

cursor.close()
conn.close()


2. Definición de funciones en SQLITE

En el apartado anterior hemos definido la función MATCHES, que usa regexp, ahora vamos a definir la función split de python donde le indicamos el delimitador. Pero a pesar que en geeksforgeeks.org dice que funciona, a mi no me funciona pues no deja que un campo sea tipo list, tupla o array!!!!!


import sqlite3

# Definimos una función con 2 parámetros 
def split_str(delimitedText, delimiter=','):
    return delimitedText.split(delimiter)


#Nos conectamos a sqlite, en este caso en memoria (sin fichero)
conn = sqlite3.connect(':memory:') #Le pasamos esta función y le indicamos que tiene 2 parámetros conn.create_function("SPLIT", 2, split_str)
cursor = conn.cursor() cursor.execute("SELECT SPLIT('Pepe;Juan;Ignacio', ';');")
print (cursor.fetchone()[0]) #FALLA!!!! cursor.close() conn.close()




viernes, 21 de junio de 2024

Python (VI) NO VA BIEN!!! Robocorp (I) Install rcc. Correr el primer ejemplo en el browser

 1. Instalar rcc

En https://github.com/robocorp/rcc?tab=readme-ov-file#installing-rcc-from-the-command-line dice literalmente:

Windows

  1. Open the command prompt
  2. Download: curl -o rcc.exe https://downloads.robocorp.com/rcc/releases/latest/windows64/rcc.exe
  3. Add to system path: Open Start -> Edit the system environment variables
  4. Test: rcc

macOS

Brew cask from Robocorp tap

  1. Update brew: brew update
  2. Install: brew install robocorp/tools/rcc
  3. Test: rcc

Upgrading: brew upgrade rcc

Linux

  1. Open the terminal
  2. Download: curl -o rcc https://downloads.robocorp.com/rcc/releases/latest/linux64/rcc
  3. Make the downloaded file executable: chmod a+x rcc
  4. Add to path: sudo mv rcc /usr/local/bin/
  5. Test: rcc

===================

2. Ejecutar el módulo de ejemplo del principiante

En https://robocorp.com/docs/courses/beginners-course-python/11-creating-pdf hay un fichero tasks.py que copiamos aquí.

from robocorp.tasks import task
from robocorp import browser

from RPA.HTTP import HTTP
from RPA.Excel.Files import Files
from RPA.PDF import PDF

@task
def robot_spare_bin_python():
    """Insert the sales data for the week and export it as a PDF"""
    browser.configure(
        slowmo=100,
    )
    open_the_intranet_website()
    log_in()
    download_excel_file()
    fill_form_with_excel_data()
    collect_results()
    export_as_pdf()
    log_out()

def open_the_intranet_website():
    """Navigates to the given URL"""
    browser.goto("https://robotsparebinindustries.com/")

def log_in():
    """Fills in the login form and clicks the 'Log in' button"""
    page = browser.page()
    page.fill("#username", "maria")
    page.fill("#password", "thoushallnotpass")
    page.click("button:text('Log in')")

def fill_and_submit_sales_form(sales_rep):
    """Fills in the sales data and click the 'Submit' button"""
    page = browser.page()

    page.fill("#firstname", sales_rep["First Name"])
    page.fill("#lastname", sales_rep["Last Name"])
    page.select_option("#salestarget", str(sales_rep["Sales Target"]))
    page.fill("#salesresult", str(sales_rep["Sales"]))
    page.click("text=Submit")

def download_excel_file():
    """Downloads excel file from the given URL"""
    http = HTTP()
    http.download(url="https://robotsparebinindustries.com/SalesData.xlsx", overwrite=True)

def fill_form_with_excel_data():
    """Read data from excel and fill in the sales form"""
    excel = Files()
    excel.open_workbook("SalesData.xlsx")
    worksheet = excel.read_worksheet_as_table("data", header=True)
    excel.close_workbook()

    for row in worksheet:
        fill_and_submit_sales_form(row)

def collect_results():
    """Take a screenshot of the page"""
    page = browser.page()
    page.screenshot(path="output/sales_summary.png")

def export_as_pdf():
    """Export the data to a pdf file"""
    page = browser.page()
    sales_results_html = page.locator("#sales-results").inner_html()

    pdf = PDF()
    pdf.html_to_pdf(sales_results_html, "output/sales_results.pdf")

def log_out():
    """Presses the 'Log out' button"""
    page = browser.page()
    page.click("text=Log out")

Para ello creamos una carpeta por ejemplo ~/MyPython/prova06_rcc

Copiamos el fichero anterior (tasks.py) en dicha carpeta

Copiamos el siguiente fichero conda.yaml que sale si hemos completado el tutorial indicado en dicha página

# For more details on the format and content:
# https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-in-condayaml
# Tip: Adding a link to the release notes of the packages helps maintenance and security.

channels:
- conda-forge

dependencies:
- python=3.10.12 # https://pyreadiness.org/3.10
- pip=23.2.1 # https://pip.pypa.io/en/stable/news
- robocorp-truststore=0.8.0 # https://pypi.org/project/robocorp-truststore/
- pip:
- rpaframework==28.0.0 # https://rpaframework.org/releasenotes.html
- robocorp==1.4.0 # https://pypi.org/project/robocorp
- robocorp-browser==2.2.1 # https://pypi.org/project/robocorp-browser

Opcionalmente podemos copiar los ficheros LICENSE y README.md

Copiamos el fichero robot.yaml

# For more details on the format and content:
# https://github.com/robocorp/rcc/blob/master/docs/recipes.md#what-is-in-robotyaml

tasks:
Run Task:
shell: python -m robocorp.tasks run tasks.py

environmentConfigs:
- environment_windows_amd64_freeze.yaml
- environment_linux_amd64_freeze.yaml
- environment_darwin_amd64_freeze.yaml
- conda.yaml

artifactsDir: output

PATH:
- .
PYTHONPATH:
- .
ignoreFiles:
- .gitignore

Nos situamos en dichar carpeta y ejecutamos el programa mediante "rcc run"

# Nos situamos y ejecutamos rcc

cd ~/MyPython/prova06_rcc
rcc run

Y ejecuta todo el proceso