Guillotina: El marc de referència (framework) de Python amb AsyncIO. (I)

Roger Boixader Güell

13/1/2023

Que és

Guillotina és un framework que ens permet crear aplicacions de servidor fet amb Python i AsyncIO. Veurem quines són les seves principals característiques, com podem instal·lar-ho i navegarem per la seva API (Interfície de programació d'aplicacions) que ens ofereix per defecte. En un següent article profunditzarem una mica més i crearem una petita aplicació de xat seguint el seu tutorial.

Gràcies al fet que ja porta implementat totes les funcionalitats necessàries ens permet poder desenvolupar molt ràpidament qualsevol aplicació, també està dissenyat per solucions d'alt rendiment.

Principals característiques

  • Rendiment: Els servidors web tradicionals fets amb Python limiten el nombre de peticions simultànies al nombre de fils que s'executen al servidor. Amb AsyncIO, podem atendre moltes més sol·licituds simultànies.

  • Amigable amb les aplicacions client: Guillotina està dissenyat per fer feliços els desenvolupadors d'aplicacions amb javascript. Amb coses com la creació automàtica de documentació amb Swagger per tots els punts d'entrada de l'aplicació, configuració de CORS automàtica i websockets. Utilitza JSON per les comunicacions, però es pot adaptar a qualsevol classe de contingut que s'envia o es rep en cos de la petició.

  • AsyncIO: Amb AsyncIO, els websockets són simples. A més AsyncIO és una combinació ideal amb arquitectures de microserveis.

  • Models: Guillotina utilitza un model d'objecte jeràrquic. Aquesta jerarquia d'objectes s'assigna a l'URL i és perfecte per gestionar un gran nombre d'objectes.

  • Seguretat: Guillotina té un sistema de seguretat granular, jeràrquic i multidimensional que ens permetrà gestionar la seguretat del contingut a un nivell no disponible en altres frameworks.

  • Escalabilitat: Amb integracions com Redis, ElasticSearch i Cockroch, tenim totes les eines per escalar.

Si voleu profunditzar més sobre més característiques i motivacions sobre guillotina ho podeu trobar a la seva documentació (https://guillotina.readthedocs.io/en/latest/about.html)

Instal·lant, executant i configurant guillotina.

Ara que ja hem vist que és guillotina i les seves principals característiques, veurem com instal·lar-ho, executar-ho i fer les primeres peticions contra l'API que ens ofereix per defecte.

1.0 - Prerequisits

Per poder crear l'aplicació necessitarem:

  • Python >= 3.7
  • Docker
  • Postman ( o qualsevol altre client per fer peticions al nostre servidor )

2.0 - Instal·lant Guillotina

Per instal·lar guillotina utilitzarem pip i Docker. Assegura't de tenir els dos instal·lats.

És recomanat instal·lar-ho amb un entorn virtual de python

python3 -m venv genv
  cd genv
  source ./bin/activate
pip install guillotina

3.0 - Executant Guillotina

3.1 - Comanda

Un cop tenim instal·lada la guillotina, podem provar d'executar-la. Ho farem amb la comanda guillotina a l'arrel de la carpeta on hem creat l'entorn virtual. Per assegurar que tenim l'entorn virtual, en el prompt de la terminal, a davant de tot hem de veure que hi ha entre parèntesis el nom de l'entorn virtual, en el nostre cas (genv)

guillotina

Un cop hem executat la guillotina hauríem de veure alguna cosa semblant a això:

No configuration file found. Using default settings with DUMMY_FILE db.
INFO:     Started server process [13656]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:8080 (Press CTRL+C to quit)

La comanda guillotina ens permet executar una sèrie de comandes. La comanda per defecte és serve. De la mateixa manera podem executar la guillotina fent:

guillotina serve  

I hauríem de tenir el mateix resultat que abans.

La comanda serve li podem especificar els paràmetres --host i --port per canviar la configuració.

Si vols profunditzar més amb totes les comandes que ofereix guillotina ho pots consultar a la seva documentació

3.2 - Comprovant la instal·lació

Obriu el postman o el client que feu servir, i fes un GET bàsic contra http://localhost:8080 amb les credencials root com a usuari i root com a constrasenya amb basic auth. Per exemple amb CURL seria:

curl --location --request GET 'http://localhost:8080' \
--header 'Authorization: Basic cm9vdDpyb290'

I la resposta és:

{
    "databases": [
        "db"
    ],
    "static_file": [],
    "static_directory": [
        "+admin"
    ],
    "@type": "Application"
}

També pots fer un GET a http://localhost:8080/db obtenint com a resposta:

{
    "containers": [],
    "@type": "Database"
}

En els dos casos hauries de tenir un codi HTTP 200 com a resposta. Més endavant veurem com interpretar les dades que ens retornen aquestes dues peticions, i el significat de les URL on hem fet la petició.

4 - Configuració

Guillotina per defecte s'executa sense configuració. La base de dades que utilitza en aquest cas és un DUMMY_FILE que guardarà el fitxer de base de dades localment.

En aquest apartat veurem com configurar guillotina per executar-la amb una base de dades PostgreSQL

Guillotina proporciona una comanda per crear un fitxer de configuració:

guillotina create --template=configuration

Això crearà un arxiu config.yaml en el directori actual on ho executem. Si obres l'arxiu podràs diferents seccions de configuració que ha creat per defecte.

En aquest enllaç pots veure totes les opcions de configuració que ens permet guillotina.

Nota: Guillotina també suporta arxius de configuració amb JSON.

4.1 - Arrancant la base de dades

Per utilitzar una base de dades PostgreSQL, utilitzarem docker, cal tenir en compte que la següent comanda crea una base de dades no persistent, això vol dir que si parem el contenidor de docker, les dades que hàgim generat es perdran en tornar a arrancar el mateix contenidor.

docker run \
  -e POSTGRES_DB=guillotina -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres \
  -p 127.0.0.1:5432:5432 \
  postgres:15.1

Un cop tenim la base de dades arrancada, haurem de modificar l'arxiu de configuració per posar la configuració correcta de la base de dades.

databases:
  db:
    storage: postgresql
    dsn: postgresql://postgres:postgres@localhost:5432/guillotina
    read_only: false

4.2 - Arxiu de configuració

Per executar la guillotina amb un arxiu de configuració concret farem:

guillotina -c config.yaml

Assegura't que la configuració de la base de dades és la correcta.

4.3 - Instal·lant aplicacions

Les aplicacions de guillotina són paquets o mòduls de Python que podem instal·lar i configurar en la configuració. Per exemple en la configuració per defecte podem veure que tenim instal·lades les aplicacions guillotina.contrib.catalog.pg i guillotina.contrib.swagger.

guillotina.contrib.catalog.pg ens dona tota la configuració per poder indexar el nostre contingut amb Postresql. També podríem utilitzar Elasticsearch per exemple.

guillotina.contrib.swagger ens dona per defecte tota la documentació dels nostres punts d'entrada documentats.

Per exemple si anem a http://localhost:8080/@docs podrem veure l'aplicació de Swagger.

5 - Conceptes principals de guillotina.

5.1 - Objectes i tipus d'objecte

Guillotina estructura els objectes en forma jeràrquica en una estructura d'arbre. Dins de l'estructura hi ha 4 tipus d'objectes principals.

Content types o tipus d'objecte, ens permeten crear esquemes i continguts personalitzats pels nostres serveis.

  • Application: És l'objecte arrel de l'arbre /
  • Database: La base de dades /(db)
  • Container: L'objecte principal per afegir-hi les dades /(db)/(container)
  • Content: Per defecte tenim els tipus Item o Folder. A aquest nivell també podem crear tipus d'objectes personalitzats.

Cada tius d'objecte porta per defecte punts d'entrada ja implementats. Podeu investigar més sobre tots els punts d'entrada de cada tipus en la seva documentació.

5.2 - Behaviors (comportaments)

A part de tenir els content types, hi ha els behaviors, els quals ens permeten oferir funcionalitats a tots els content types. En altres paraules ens permeten utilitzar esquemes específics a qualsevol content type i afegir també funcionalitats específiques.

Per exemple un behavior podria ser el guillotina.behaviors.attachment.IDublinCore que ja porta guillotina, que defineix de cada objecte quan va ser creat, actualitzat i altra informació genèrica que ens pot interessar per qualsevol objecte.

Els behaviors els podem definir estàticament en la definició del content type o dinàmicament a través de l'api de l'objecte, i només per un objecte.

5.3 - Serveis

Els serveis proporcionen respostes a les sol·licituds de punt d'entrada de l'API. Un servei és el mateix que una vista que podeu trobar amb molts altres frameworks.

5.4 - Seguretat

El sistema de seguretat de guillotina està basat en 3 punts. Rols, Permisos i Usuaris/Grups ( anomenats principal a guillotina).

Gràcies a aquest sistema podem dotar de permisos fins a nivell d'objecte. Per exemple tenim el permís de View Content que ens permet poder accedir directament a la informació d'un objecte.

Nosaltres podem definir aquest permís contra un principal en un objecte, o contra un rol. Que un rol seràn un grup de principals.

Els permisos es propaguen en tota l'estructura d'arbre. I podem donar permís per poder accedir a aquell objecte concret, o a aquell objecte i tots els seus fills.

Si voleu profunditzar més sobre la seguretat a guillotina, podeu consultar la seva documentació

6 - Utilitzant l'api de Guillotina

Abans de començar a utilitzar l'API de guillotina crearem dades de test per poder jugar-hi. Usant la comanda testdata omplirem la base de dades amb una mica d'informació de la wikipedia.

pip install aiohttp
guillotina testdata --per-node=5 --depth=2 --container=container
guillotina -c config.yaml

No repassarem tots els punts d'entrada que proporciona guillotina, però si que mirarem alguns i veurem com poder explorar i utilitzar tota l'API.

6.1 - Creant contingut

Per crear contingut farem una petició POST al contenidor o a algun objecte de tipus Folder. Actualment, com que no tenim cap objecte d'aquest tipus creat, el farem contra el nostre contenidor:

Necessitem tenir el permís guillotina.AddContent.

curl -i -X POST http://127.0.0.1:8080/db/container -H 'Accept: application/json' --data-raw '{
    "@type": "Item",
    "id": "foobar5"
}' --user root:root

Aquí podem veure com creem un objecte del tipus Item amb l'identificador foobar5. I com a resposta obtindrem:

HTTP/1.1 201 OK
Content-Type: application/json
Location: http://localhost/db/container/foobar5

{
    "@id": "http://localhost/db/container/foobar5",
    "@name": "foobar5",
    "@type": "Item",
    "@uid": "bf6|f13d2535731443b492102e832eee65df"
}

Com a resposta podem tenir.

  • 200 OK - Correcte
  • 401 Unauthorized - No estàs autoritzat per fer l'acció
  • 404 Not Found - El recurs sobre el qual estàs intentant afegir no existeix.
  • 409 Conflict - Ja existeix un objecte amb aquest identificador.
  • 412 Precondition Failed - Alguna condició en afegir l'objecte no s'ha complert.

6.2 - Afegint behaviors

Per afegir behaviors dinàmicament farem servir el punt d'entrada @behaviors. Hem de fer un PATCH a l'objecte.

Necessitem tenir el permís guillotina.ModifyContent.

curl -i -X PATCH http://127.0.0.1:8080/db/container/foobar5/@behaviors -H 'Accept: application/json' --data-raw '{
    "behavior": "guillotina.behaviors.attachment.IAttachment"
}' --user root:root

A l'objecte que acabem de crear foobar5 li estem afegint el behavior guillotina.behaviors.attachment.IAttachment, que ens permetrà poder afegir arxius a l'objecte. I com a resposta obtindrem:

HTTP/1.1 200 OK
Content-Type: application/json

{}

Com a resposta podem tenir.

  • 200 OK - Correcte, s'ha afegit el behavior
  • 401 Unauthorized - No estàs autoritzat per fer l'acció
  • 404 Not Found - El recurs sobre el qual estàs intentant afegir no existeix.
  • 412 Precondition Failed - El behavior ja està assignat a l'objecte.

6.3 - Pujant arxius

Els arxius els podem pujar amb el punt d'entrada @upload

Necessitem tenir el permís guillotina.ModifyContent

Aquí farem un PATCH contra l'objecte que acabem de crear a @upload/file. Hi afegim file, ja que el behavior que hem afegit abans, té aquesta clau com a identificador de l'arxiu. Si el camp que volem guardar-hi l'arxiu tingués un altre identificador, hauríem de fer @upload/id-camp

curl -i -X PATCH http://127.0.0.1:8080/db/container/foobar5/@upload/file -H 'Accept: application/json' --data-raw '<text data>' --user root:root

Per poder descarregar l'arxiu, utilitzarem el punt d'entrada @download

Necessitem tenir el permís guillotina.ViewContent

curl -i http://127.0.0.1:8080/db/container/foobar5/@download/file -H 'Accept: application/json' --user root:root

Guillotina també suporta el protocol TUS per poder pujar arxius grans dividits amb parts. Podeu veure més informació a la seva documentació.

6.4 - Modificant permisos

El punt d'entrada @sharing ens permet consultar i modificar els permisos de cada objecte.

Necessitem tenir el permís guillotina.SeePermissions per veure els permisos

Necessitem tenir el permís guillotina.ChangePermissions per modificar els permisos

Per poder veure quins permisos té l'objecte que hem creat abans farem:

curl -i http://127.0.0.1:8080/db/container/foobar5/@sharing -H 'Accept: application/json' --user root:root

I com a resposta obtindrem:

HTTP/1.1 200 OK
Content-Type: application/json

{
    "local": {
        "roleperm": {},
        "prinperm": {},
        "prinrole": {
            "root": {
                "guillotina.Owner": "Allow"
            }
        }
    },
    "inherit": [
        {
            "@id": "http://localhost/db/container",
            "roleperm": {},
            "prinperm": {},
            "prinrole": {
                "root": {
                    "guillotina.ContainerAdmin": "Allow",
                    "guillotina.Owner": "Allow"
                }
            }
        }
    ]
}

Aquí podem veure com en local és a dir, a nivell d'objecte, tenim que els principals que tinguin el rol guillotina.Owner podran fer totes les accions associades a aquest rol. En l'objecte inherit podrem veure els permisos i rols heretats dels objectes superiors.

Per modificar els permisos de l'objecte farem:

curl -i -X POST http://127.0.0.1:8080/db/container/foobar5/@sharing -H 'Accept: application/json' --data-raw '{
    "prinperm": [
        {
            "principal": "foobar",
            "permission": "guillotina.ModifyContent",
            "setting": "Allow"
        }
    ]
}' --user root:root

Aquí el que estaríem dient, és que el principal foobar tindrà el permís per modificar el contingut d'aquest objecte. I com a resposta tindrem:

HTTP/1.1 200 OK
Content-Type: application/json

{}

Hi ha 3 tipus de configuració de permisos que pots modificar:

  • prinperm: pincipal + permís
  • prinrole: principal + rol
  • roleperm: rol + permís

Per cada un d'aquests canvis hi podem configurar:

  • Allow: Ho assignem a l'objecte i als seus fills que ho hereten.
  • Deny: Ho assignem a l'objecte i als seus fills que ho hereten.
  • AllowSingle: Ho assignem a l'objecte però no als seus fills.
  • Unset: Eliminem la configuració.

6.4 - Explorant l'API amb OpenAPI

En passos anteriors hem configurat guillotina.contrib.swagger. Amb OpenAPI podem inspeccionar qualsevol context i la seva API.

Ves a l'enllaç de la documentació generada per guillotina. http://localhost:8080/@docs

Fes clic al boto Authorize

Com a usuari posa root i contrasenya root també.

En la pròpia url de la barra superior veurem el context el qual estem explorant. Per exemple si anem a /db/container/foobar5 podrem veure tota l'API que exposa l'objecte que acabem de crear. Diferents content types tenen diferents serveis disponibles.

7.0 - Següents passos

Ara ja hem vist que és guillotina i hem fet una pinzellada a les parts més importants que hem de conèixer. Podeu profunditzar més conceptes a la seva documentació.

En futurs articles, seguirem el tutorial de la pròpia documentació per crear una primera aplicació amb guillotina.