En aquest segon article sobre guillotina construirem una aplicació de xat, seguirem el tutorial que hi ha en la seva documentació.
- 1.0 - Prerequisits i instal·lació del paquet de guillotina
- 2.0 - Configuració
- 3.0 - Creant content types
- 4.0 - Instal·lant addons
- 5.0 - Permissions/Role
- 6.0 - Subscrivint-nos a esdeveniments
- 7.0 - Usuaris
- 8.0 - Serialitzant el contingut
- 9.0 - Serveis
- 10.0 - Utilitats asíncrones
- 11.0 - Websockets
- 12.0 - Arxius estàtics
- 13.0 - Conclusions
1.0 - Prerequisits i instal·lació del paquet de guillotina
Tal com vam explicar en el primer article sobre guillotina, primer de tot necessitem tenir:
- Python >= 3.7
- Docker
- Postman ( o qualsevol altre client per fer peticions al nostre servidor )
Per començar crearem l'entorn virtual, i instal·larem guillotina i cookiecutter.
mkdir guillotina_app_folder
cd guillotina_app_folder
python3 -m venv genv
cd genv
source ./bin/activate
cd ..
pip install guillotina
pip install cookiecutter
Cal recordar que si volem tenir una base de dades PostgreSQL, ho podem fer creant un contenidor de docker.
docker run \
-e POSTGRES_DB=guillotina -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres \
-p 127.0.0.1:5432:5432 \
postgres:15.1
En el primer article sobre guillotina creàvem un arxiu de configuració per poder modificar quina base de dades volem utilitzar. En aquest cas en comptes de crear un sol arxiu de configuració crearem un paquet, usant les plantilles que ja ens proporciona guillotina.
guillotina create --template=application
Quan ens demani el package_name
li posarem guillotina_chat
.
En el mateix directori ens ha creat dos arxius g.db
i g.db.blobs
, els podem eliminar, ja que farem servir postreSQL com a base de dades.
Instal·larem el paquet que acabem de crear.
cd guillotina_chat
pip install -e .
2.0 - Configuració
Modifiquem l'arxiu de configuració config.yaml
per utilitzar la PostgreSQL:
databases:
db:
storage: postgresql
dsn: postgresql://postgres:postgres@localhost:5432/guillotina
read_only: false
En el mòdul de configuració de Guillotina i l'objecte app_settings
podem ampliar qualsevol configuració.
La definició dels content types
, behaviors
, serveis
, etc necessiten configurar-se en el mòdul de configuració. Guillotina llegeix tota la configuració per cada aplicació carregada i la carrega.
L'arxiu app_settings
Guillotina proporciona l'objecte global app_settings
from guillotina import app_settings
Aquest objecte conte tota la configuració del nostre arxiu config.yaml
i tota la configuració addicional.
La prioritat de configuració a guillotina funciona:
- La configuració per defecte de guillotina
- Cada aplicació, en l'ordre que estigui definida, pot sobreescriure la configuració per defecte de guillotina
- Finalment l'arxiu
config.yaml
té la prioritat final sobre tota la configuració.
L'arxiu app_settings
té una configuració extra amb la clau __file__
que conté la ruta de l'arxiu de configuració, permetent que les rutes relatives es puguin utilitzar en la configuració de les aplicacions.
3.0 - Creant content types
Un cop ja tenim la guillotina configurada, podem crear el nostre primer content type
. Per poder intercanviar missatges en el xat necessitem l'entitat de conversa
.
Crearem l'arxiu content.py
en la nostra aplicació. (a la mateixa carpeta on hi ha el fitxer de __init__.py
)
from guillotina import configure, content, schema
from guillotina.directives import index_field
from guillotina.interfaces import IFolder, IItem
class IConversation(IFolder):
index_field("users", type="keyword")
users = schema.List(
value_type=schema.TextLine(),
default=list()
)
@configure.contenttype(
type_name="Conversation",
schema=IConversation,
behaviors=["guillotina.behaviors.dublincore.IDublinCore"],
allowed_types=['Message'])
class Conversation(content.Folder):
pass
class IMessage(IItem):
index_field("text", type="text")
text = schema.Text(required=True)
@configure.contenttype(
type_name="Message",
schema=IMessage,
behaviors=[
"guillotina.behaviors.dublincore.IDublinCore",
"guillotina.behaviors.attachment.IAttachment"
],
globally_addable=False,
)
class Message(content.Item):
pass
NOTA: Quan modifiquem guillotina, els canvis no s'actualitzen en calent, és a dir, hem de parar el servidor i tornar-lo a arrancar per veure els canvis aplicats.
Analitzem una mica el que acabem de fer, hem creat dos nous content types
el Conversation
i el Message
.
Per crear els content types
primer hem de crear una interficie que serà on definim tots els atributs que tindrà aquell tipus. La conversa volem poder afegir-hi elements a dintre, així que la crearem heretant de Folder
en canvi, el missatge volem que sigui un objecte que no s'hi puguin afegir elements a dins, així que el crearem heretant de Item
.
Un cop hem creat les dues interfícies, crearem els content types
, quan els creem li podem definir quins behaviors
volem que tinguin tots els objectes que crearem d'aquell tipus.
La funció index_field
exposa aquest camp perquè pugui ser cercat amb el punt d'entrada @search
.
En la configuració del Message
estem marcant amb el globally_addable=False
amb el qual limitem a què només podem afegir objectes del tipus Message
a dins d'objectes del tipus Conversation
.
Perquè guillotina detecti aquesta nova configuració, hem de dir-li a guillotina que escanegi el nostre arxiu en l'arxiu __init__.py
, d'aquesta manera detectarà aquests nous content types
que acabem de crear.
from guillotina import configure
configure.scan('guillotina_chat.content')
Per comprovar que hem creat correctament els nostres content types
crearem un objecte del tipus Conversation
i a dins un del tipus Message
.
Abans per això haurem de crear el nostre contenidor de l'aplicació.
guillotina serve -c config.yaml
curl --location --request POST 'http://localhost:8080/db/' \
--header 'Authorization: Basic cm9vdDpyb290' \
--header 'Content-Type: application/json' \
--data-raw '{
"id":"container",
"@type":"Container"
}'
Creem la conversa:
curl --location --request POST 'http://localhost:8080/db/container/' \
--header 'Authorization: Basic cm9vdDpyb290' \
--header 'Content-Type: application/json' \
--data-raw '{
"id":"primera_conversa",
"title":"Primera conversa",
"@type":"Conversation"
}'
Creem el missatge:
curl --location --request POST 'http://localhost:8080/db/container/primera_conversa' \
--header 'Authorization: Basic cm9vdDpyb290' \
--header 'Content-Type: application/json' \
--data-raw '{
"id":"primer_missatge",
"title":"Primer missatge",
"text": "Aquest és el text del primer missatge",
"@type":"Message"
}'
Si ara fem una petició GET a l'objecte conversa, podrem veure tota la seva informació i en el camp items
com hi ha també el missatge que acabem de crear. De la mateixa manera també podríem fer un GET al missatge per poder veure la seva informació.
curl --location --request GET 'http://localhost:8080/db/container/primera_conversa' \
--header 'Authorization: Basic cm9vdDpyb290'
4.0 - Instal·lant addons
Guillotina diferencia les aplicacions
dels addons
( complement/extensió )
Una aplicació és un paquet de Python que tu instal·les en el teu entorn i l'afegeixes a la llista d'aplicacions a l'arxiu de configuració.
En canvi, els addons, són per quan volem crear lògica d'instal·lació en el container
.
Per exemple en el nostre cas volem crear una carpeta que ja tingui uns permisos predefinits.
Per definir un addon
utilitzarem el decorador @configure.addon
en l'arxiu install.py
de la nostra aplicació.
# install.py
from guillotina import configure
from guillotina.addons import Addon
from guillotina.content import create_content_in_container
from guillotina.interfaces import IRolePermissionManager
@configure.addon(
name="guillotina_chat",
title="Guillotina server application python project")
class ManageAddon(Addon):
@classmethod
async def install(cls, container, request):
roleperm = IRolePermissionManager(container)
roleperm.grant_permission_to_role_no_inherit(
'guillotina.AccessContent', 'guillotina.Member')
if not await container.async_contains('conversations'):
conversations = await create_content_in_container(
container, 'Folder', 'conversations',
id='conversations', creators=('root',),
contributors=('root',))
roleperm = IRolePermissionManager(conversations)
roleperm.grant_permission_to_role(
'guillotina.AddContent', 'guillotina.Member')
roleperm.grant_permission_to_role(
'guillotina.AccessContent', 'guillotina.Member')
@classmethod
async def uninstall(cls, container, request):
registry = task_vars.registry.get() # noqa
# uninstall logic here...
NOTA: Quan modifiquem guillotina, els canvis no s'actualitzen en calent, és a dir, hem de parar el servidor i tornar-lo a arrancar per veure els canvis aplicats.
Com podem veure, quan instal·lem aquest addon, crearem una carpeta amb la clau conversations
i li donarem permís d'afegir contingut i accés als usuaris del rol guillotina.Member
Per instal·lar l'addon farem una petició POST
al punt d'entrada @addon
.
curl -i -X POST http://localhost:8080/db/container/@addons -H 'Accept: application/json' -H 'Content-Type: application/json' --data-raw '{"id": "guillotina_chat"}' --user root:root
Ara si fem una petició GET al nostre contenidor, podrem veure dues carpetes, primer la que hem creat al pas anterior i la que acabem de crear en instal·lar l'addon.
5.0 - Permissions/Role
Els permisos els definim en el codi de la nostra aplicació. Per la nostra aplicació crearem rols que els usuaris tindran en una conversa.
En l'arxiu __init__.py
afegeix:
# __init__.py
configure.role("guillotina_chat.ConversationParticipant",
"Conversation Participant",
"Users that are part of a conversation", False)
configure.grant(
permission="guillotina.ViewContent",
role="guillotina_chat.ConversationParticipant")
configure.grant(
permission="guillotina.AccessContent",
role="guillotina_chat.ConversationParticipant")
configure.grant(
permission="guillotina.AddContent",
role="guillotina_chat.ConversationParticipant")
Acabem de crear un rol pels usuaris que participaran en una conversa, i estem donant els permisos d'afegir contingut, accedir-hi i veure'l.
6.0 - Subscrivint-nos a esdeveniments
Els esdeveniments a Guillotina estan molt influenciats pels esdeveniments de Zope. Per la nostra aplicació, volem assegurar-nos que tots els usuaris que formen part de la conversa tinguin el permís per afegir nous missatges i veure'n d'altres.
Per fer-ho crearem un subscriptor quan s'afegeix i es modifica l'objecte, on modificarem els permisos. Guillotina ens permet fer accions a varis esdeveniments com pot ser abans de crear un objecte, un cop s'ha creat, modificat, eliminat, etc. Pots veure més informació sobre els esdeveniments en la documentació
Crea un arxiu subscribers.py
a la teva aplicació:
# subscribers.py
from guillotina import configure
from guillotina.interfaces import (IObjectAddedEvent, IObjectModifiedEvent,
IPrincipalRoleManager)
from guillotina.utils import get_authenticated_user_id
from guillotina_chat.content import IConversation
@configure.subscriber(for_=(IConversation, IObjectAddedEvent))
@configure.subscriber(for_=(IConversation, IObjectModifiedEvent))
async def container_changed(conversation, event):
user_id = get_authenticated_user_id()
if user_id not in conversation.users:
conversation.users.append(user_id)
manager = IPrincipalRoleManager(conversation)
for user in conversation.users:
manager.assign_role_to_principal(
'guillotina_chat.ConversationParticipant', user)
Perquè guillotina detecti la nostra configuració, hem d'afegir l'arxiu en els includeme
de l'arxiu inicial __init__.py
from guillotina import configure
configure.scan('guillotina_chat.subscribers')
NOTA: Quan modifiquem guillotina, els canvis no s'actualitzen en calent, és a dir, hem de parar el servidor i tornar-lo a arrancar per veure els canvis aplicats.
Per comprovar-ho podem crear una nova conversa i un nou missatge, i amb el punt d'entrada @sharing
fent una petició GET podrem comprovar quins permisos s'han assignat a l'objecte.
curl --location --request POST 'http://localhost:8080/db/container/' \
--header 'Authorization: Basic cm9vdDpyb290' \
--header 'Content-Type: application/json' \
--data-raw '{
"id":"segona_conversa",
"title": "Segona conversa",
"@type":"Conversation"
}'
curl --location --request POST 'http://localhost:8080/db/container/segona_conversa/' \
--header 'Authorization: Basic cm9vdDpyb290' \
--header 'Content-Type: application/json' \
--data-raw '{
"id":"primer_missatge_segona_conversa",
"title": "Primer missatge segona conversa",
"text": "Aquest es el primer missatge",
"@type":"Message"
}'
curl --location --request GET 'http://localhost:8080/db/container/segona_conversa/@sharing' \
--header 'Authorization: Basic cm9vdDpyb290' \
--data-raw ''
Resposta del @sharing
{
"local": {
"roleperm": {},
"prinperm": {},
"prinrole": {
"root": {
"guillotina.Owner": "Allow",
"guillotina_chat.ConversationParticipant": "Allow"
}
}
},
"inherit": [
{
"@id": "http://localhost:8080/db/container",
"roleperm": {
"guillotina.Member": {
"guillotina.AccessContent": "AllowSingle"
}
},
"prinperm": {},
"prinrole": {
"root": {
"guillotina.ContainerAdmin": "Allow",
"guillotina.Owner": "Allow"
}
}
}
]
}
Aquí podem veure com en el nou objecte hi tenim el permís que hem afegit en el subscriptor de quan creem un objecte.
7.0 - Usuaris
Guillotina ens proporciona un paquet per gestionar usuaris i grups.
Instal·lació
Hem d'afegir l'aplicació guillotina.contrib.dbusers
a la llista d'aplicacions a la configuració en el config.yaml
.
Després d'afegir l'aplicació hem de reiniciar el servidor, i seguidament afegir l'addon de dbusers
mitjançant el punt d'entrada @addons
.
curl -i -X POST http://localhost:8080/db/container/@addons -H 'Accept: application/json' -H 'Content-Type: application/json' --data-raw '{"id": "dbusers"}' --user root:root
Afegir usuaris
Per crear usuaris, només hem d'afegir un objecte del tipus User
curl -i -X POST http://localhost:8080/db/container/users -H 'Accept: application/json' -H 'Content-Type: application/json' --data-raw '{"@type": "User", "email": "bob@domain.io", "password": "secret", "user_roles": ["guillotina.Member"], "username": "bob"}' --user root:root
Un cop hem creat l'usuari podem iniciar sessió amb el punt d'entrada @login
curl -i -X POST http://localhost:8080/db/container/@login -H 'Accept: application/json' -H 'Content-Type: application/json' --data-raw '{"password": "secret", "username": "bob"}' --user root:root
En la resposta de l'inici de sessió, obtenim el token d'accés per poder realitzar les següents crides com a usuari ja ha iniciat la sessio. El token és del tipus JSON web token.
HTTP/1.1 200 OK
Content-Type: application/json
{
"exp": 1532253747,
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1MzIyNTM3NDcsImlkIjoiQm9iIn0.1-JbNe1xNoHJgPEmJ05oULi4I9OMGBsviWFHnFPvm-I"
}
Llavors, en les peticions futures afegirem aquest token en les capçaleres com a Bearer
. Per exemple per crear una conversa farem:
curl -i -X POST http://localhost:8080/db/container/conversations/ -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1MzIyNTM3NDcsImlkIjoiQm9iIn0.1-JbNe1xNoHJgPEmJ05oULi4I9OMGBsviWFHnFPvm-I' --data-raw '{
"@type": "Conversation",
"title": "Nova conversa amb usuaris",
"users": ["usuari1", "usuari2"]
}'
8.0 - Serialitzant el contingut
Guillotina proporciona mecanismes per serialitzar tot el contingut d'un objecte, tant les propietats de l'objecte com els behaviors
, i també serialitzadors per al contingut de resum dels llistats.
Per crear un serialitzador personalitzat per un tipus, necessitem proporcionar un multi adapter
per les interficies IResourceSerializeToJsonSummary
i IResourceSerializeToJson
.
Pel nostre cas, volem assegurar-nos d'incloure la data de creació i altres dades en la serialització del resum de la conversa i del missatge.
Definim un serializador personalitzat
Crearem un nou arxiu anomenat serialize.py
from guillotina import configure
from guillotina.interfaces import IResourceSerializeToJsonSummary
from guillotina.json.serialize_content import DefaultJSONSummarySerializer
from guillotina.utils import get_owners
from guillotina_chat.content import IConversation, IMessage
from zope.interface import Interface
@configure.adapter(
for_=(IConversation, Interface),
provides=IResourceSerializeToJsonSummary)
class ConversationJSONSummarySerializer(DefaultJSONSummarySerializer):
async def __call__(self):
data = await super().__call__()
data.update({
'creation_date': self.context.creation_date,
'title': self.context.title,
'users': self.context.users
})
return data
@configure.adapter(
for_=(IMessage, Interface),
provides=IResourceSerializeToJsonSummary)
class MessageJSONSummarySerializer(DefaultJSONSummarySerializer):
async def __call__(self):
data = await super().__call__()
data.update({
'creation_date': self.context.creation_date,
'text': self.context.text,
'author': get_owners(self.context)[0]
})
return data
Recorda que hem d'afegir l'arxiu perquè guillotina el pugui detectar a __init__.py
configure.scan('guillotina_chat.serialize')
NOTA: Quan modifiquem guillotina, els canvis no s'actualitzen en calent, és a dir, hem de parar el servidor i tornar-lo a arrancar per veure els canvis aplicats.
Ara si fem una petició GET a un objecte del tipus Conversation
podrem veure com en la serialització del resum dels missatges, en el camp items
, hi ha la informació que hem definit en el serialitzador, com el camp author
.
9.0 - Serveis
Els serveis a guillotina són com en els altres frameworks els endpoints
o views
.
En el nostre cas utilitzarem el servei per obtenir les converses i els missatges més recents d'un usuari en una conversa.
Creant els serveis
Crearem els serveis @conversations
i @messages
en l'arxiu services.py
from guillotina import configure
from guillotina.component import get_multi_adapter
from guillotina.interfaces import IContainer, IResourceSerializeToJsonSummary
from guillotina.utils import get_authenticated_user_id
from guillotina_chat.content import IConversation
@configure.service(context=IContainer, name='@conversations',
permission='guillotina.AccessContent')
async def get_conversations(context, request):
results = []
conversations = await context.async_get('conversations')
user_id = get_authenticated_user_id()
async for conversation in conversations.async_values():
if user_id in getattr(conversation, 'users', []):
summary = await get_multi_adapter(
(conversation, request),
IResourceSerializeToJsonSummary)()
results.append(summary)
results = sorted(results, key=lambda conv: conv['creation_date'])
return results
@configure.service(context=IConversation, name='@messages',
permission='guillotina.AccessContent')
async def get_messages(context, request):
results = []
async for message in context.async_values():
summary = await get_multi_adapter(
(message, request),
IResourceSerializeToJsonSummary)()
results.append(summary)
results = sorted(results, key=lambda mes: mes['creation_date'])
return results
NOTA: Quan modifiquem guillotina, els canvis no s'actualitzen en calent, és a dir, hem de parar el servidor i tornar-lo a arrancar per veure els canvis aplicats.
Recorda que hem d'afegir-ho en el fitxer __init__.py
configure.scan('guillotina_chat.services')
Aquests serveis accedeixen a la base de dades i retornen els resultats per tu. Aquí podem veure algunes interaccions amb els objectes i la base de dades amb els serveis, tot i això pots fer el mateix utilitzant el punt d'entrada @search
.
El servei @conversations
hi accedirem des del container
de l'aplicació, en canvi, el servei @messages
hi accedirem des de qualsevol objecte del tiups Conversation
per obtenir tots els missatges d'aquest.
Per utilitzar el punt d'entrada @search
assegura't de tenir l'aplicació guillotina.contrib.catalog.pg
en la configuració en l'arxiu config.yaml
Per exemple pots obtenir totes les converses fent una petició GET amb el punt d'entrada @search
GET @search?type_name=Conversation
i per obtenir tots els missatges
GET @search?type_name=Message
També podem fer cerques per text i filtrant per dates
GET @search?type_name=Message&text=foobar&creation_date__gte=2023-01-13T15:30:27.580369+00:00
10.0 - Utilitats asíncrones
Una utilitat asíncrona és un objecte asíncron que s'executa permanentment a l'event loop d'asyncio
. Són útils per tasques llargues.
Per la nostra aplicació utilitzarem una tasca asíncrona per enviar missatges als usuaris que han iniciat sessió.
Crea l'arxiu utility.py
from guillotina.async_util import IAsyncUtility
from guillotina.component import get_multi_adapter
from guillotina.interfaces import IResourceSerializeToJsonSummary
from guillotina.utils import get_authenticated_user_id, get_current_request
import asyncio
import orjson
import logging
logger = logging.getLogger('guillotina_chat')
class IMessageSender(IAsyncUtility):
pass
class MessageSenderUtility:
def __init__(self, settings=None, loop=None):
self._loop = loop
self._settings = {}
self._webservices = []
self._closed = False
def register_ws(self, ws, request):
ws.user_id = get_authenticated_user_id()
self._webservices.append(ws)
def unregister_ws(self, ws):
self._webservices.remove(ws)
async def send_message(self, message):
summary = await get_multi_adapter(
(message, get_current_request()),
IResourceSerializeToJsonSummary)()
await self._queue.put((message, summary))
async def finalize(self):
self._closed = True
async def initialize(self, app=None):
self._queue = asyncio.Queue()
while not self._closed:
try:
message, summary = await asyncio.wait_for(self._queue.get(), 0.2)
for user_id in message.__parent__.users:
for ws in self._webservices:
if ws.user_id == user_id:
await ws.send_str(orjson.dumps(summary))
except (RuntimeError, asyncio.CancelledError, asyncio.TimeoutError):
pass
except Exception:
logger.warning(
'Error sending message',
exc_info=True)
await asyncio.sleep(1)
Les utilitats asíncrones han d'implementar el mètode initialize
. En el nostre cas estem creant una cua i esperant per processar els missatges.
Cada nou missatge rebut, l'enviarem pels websockets que tinguem registrats amb el mateix usuari.
Tal com hem fet amb els altres mòduls de configuració, afegirem l'arxiu en el __init__.py
A més a més, hem de definir la utilitat en el app_settings
en el mateix arxiu __init__.py
app_settings = {
"load_utilities": {
"guillotina_chat.message_sender": {
"provides": "guillotina_chat.utility.IMessageSender",
"factory": "guillotina_chat.utility.MessageSenderUtility",
"settings": {}
},
}
}
Enviant missatges
Necessitarem afegir un nou subscriptor d'esdeveniments en el nostre 'subscribers.py' per enviar els nous missatges a la utilitat, i processar-los cap als websockets registrats.
Ara el nostre arxiu subscribers.py
quedarà de la següent manera:
from guillotina import configure
from guillotina.component import get_utility
from guillotina.interfaces import IObjectAddedEvent, IObjectModifiedEvent, IPrincipalRoleManager
from guillotina.utils import get_authenticated_user_id, get_current_request
from guillotina_chat.content import IConversation, IMessage
from guillotina_chat.utility import IMessageSender
@configure.subscriber(for_=(IConversation, IObjectAddedEvent))
@configure.subscriber(for_=(IConversation, IObjectModifiedEvent))
async def container_added(conversation, event):
user_id = get_authenticated_user_id()
if user_id not in conversation.users:
conversation.users.append(user_id)
manager = IPrincipalRoleManager(conversation)
for user in conversation.users:
manager.assign_role_to_principal(
'guillotina_chat.ConversationParticipant', user)
@configure.subscriber(for_=(IMessage, IObjectAddedEvent))
async def message_added(message, event):
utility = get_utility(IMessageSender)
await utility.send_message(message)
11.0 - Websockets
Guillotina ja integra suport pels Websockets per defecte. És tan senzill com utilitzar un websocket d'asgi en un servei.
Crea un arxiu anomenat wb.py
from guillotina import configure
from guillotina.component import get_utility
from guillotina.interfaces import IContainer
from guillotina.transactions import get_tm
from guillotina_chat.utility import IMessageSender
import asyncio
import logging
logger = logging.getLogger('guillotina_chat')
@configure.service(
context=IContainer, method='GET',
permission='guillotina.AccessContent', name='@conversate')
async def ws_conversate(context, request):
ws = request.get_ws()
await ws.prepare()
utility = get_utility(IMessageSender)
utility.register_ws(ws, request)
tm = get_tm()
await tm.abort()
try:
async for msg in ws:
# ws does not receive any messages, just sends
pass
finally:
logger.debug('websocket connection closed')
utility.unregister_ws(ws)
return {}
Aquí utilitzarem utility = get_utility(IMessageSender)
per obtenir la nostra utilitat asíncrona que hem definit prèviament, tot seguit registrarem el nostre webservice amb utility.register_ws(ws, request)
.
El nostre servei és simple, ja que no ens cal rebre cap missatge i la utilitat ja els envia.
Utilitzant els websockets
Per utilitzar els websockets primer necessitem obtenir un token. Els tokens dels websockets estan encriptats amb jwk. Guillotina no proporciona per defecte un token jwt, així que n'haurem de generar un:
guillotina gen-key
Un cop el tenim, afegirem el token en la configuració en l'arxiu config.yaml
jwk:
k: Jg2oze4sTlw0QtLAF6vPGpygfUdzFLa_MufKKBWgvXE
kty: oct
Seguidament, utilitzarem el punt d'entrada @wstoken
per obtenir un token per iniciar el websocket.
curl --location --request GET 'http://localhost:8080/db/container/@wstoken' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2NzQyNTE5MDYsImV4cCI6MTY3NDI1NTUwNiwiaWQiOiJib2IiLCJzdWIiOiJib2IifQ.-w1hchfQ1Nd-D4Un4adu6qPyD7vHM_e8c-wNltMv5QQ' \
--data-raw ''
Finalment, utilitzem aquest token per generar la URL (Aquí tenim un exemple amb Javascript):
var url = 'ws://localhost:8080/db/container/@conversate?ws_token=' + ws_token;
SOCKET = new WebSocket(url);
SOCKET.onopen = function(e){
};
SOCKET.onmessage = function(msg){
var data = JSON.parse(msg.data);
};
SOCKET.onclose = function(e){
};
SOCKET.onerror = function(e){
};
12.0 - Arxius estàtics
Guillotina ens permet servir arxius estàtics. Només hem de definir en la configuració quina és la ruta on tenim els arxius a servir, i contra quina ruta es podran consultar.
app_settings = {
...
"static": {
"static_web": "guillotina_chat:static"
},
}
Aquí estem definint que en l'aplicació guillotina_chat
en la carpeta static
hi ha els arxius, i els servirem via la url /static_web
.
En la nostra aplicació crearem una petita aplicació amb javascript.
Copiem els següents arxius en una nova carpeta static
en la nostra aplicació:
Modifica l'objecte app_settings
de l'arxiu __init__.py
app_settings = {
"load_utilities": {
"guillotina_chat.message_sender": {
"provides": "guillotina_chat.utility.IMessageSender",
"factory": "guillotina_chat.utility.MessageSenderUtility",
"settings": {}
},
},
"static": {
"static_web": "guillotina_chat:static"
},
}
Per veure la web podríem accedir a la URL http://localhost:8080/static_web
.
13.0 - Conclusions
Ara ja hem vist com podem crear una aplicació amb guillotina, creant els nostres propis content types
, utility
, utilizant els addons
assignant els permisos i utilitzant-la amb una petita aplicació JavaScript.
Pots veure el codi de l'aplicació en el repositori de Github
En un següent article crearem una interfície gràfica web per poder gestionar tots els objectes de la nostra aplicació.