Directivas Personalizadas¶
Ter directivas embutidas do Lilya é ótimo, pois oferece muitas facilidades para o seu projecto, mas ter directivas personalizadas é o que realmente potencializa a sua aplicação e a eleva.
Importante¶
Antes de ler esta secção, deve familiarizar-se com as formas como o Lilya lida com a descoberta das aplicações.
Os seguintes exemplos e explicações irão utilizar a abordagem --app e variáveis de ambiente, mas a descoberta automática é igualmente válida e funciona da mesma forma.
O que é uma directiva personalizada?¶
Antes de entrarmos nisso, vamos voltar às raízes do Python.
O Python era e ainda é amplamente utilizado como uma linguagem de script. Os scripts são pedaços isolados de código e lógica que podem ser executados em qualquer máquina que tenha o Python instalado e executar sem muitos problemas ou obstáculos.
Bastante simples, certo?
Então, o que é isso tem a ver com directivas? Bem, as directivas seguem o mesmo princípio, mas aplicado ao seu próprio projecto. E se pudesse criar os seus próprios scripts estruturados dentro do seu projecto diretamente? E se pudesse construir pedaços de lógica dependentes ou independentes que pudessem ser executados usando os recursos da sua própria aplicação Lilya?
Isso é o que é uma directiva.
Tip
Se está familiarizado com os comandos de gestão do Django, as directivas do Lilya seguem o mesmo princípio. Há um excelente artigo sobre isso se se quiser familiarizar.
Exemplos¶
Imagine que precisa criar uma base de dados que conterá todas as informações sobre acessos de utilizadores específicos e gerirá as funções da sua aplicação.
Agora, uma vez que essa base de dados é criada com sua aplicação, geralmente precisaria se conectar ao seu servidor de produção e configurar manualmente um utilizador ou executar um script ou comando específico para criar o mesmo superutilizador. Isso pode ser demorado e propenso a erros, certo?
Pode usar uma directiva para fazer esse mesmo trabalho por si.
Ou e se precisar criar operações específicas para serem executadas em segundo plano por algumas operações que não requerem APIs, por exemplo, atualizar a função de um utilizador? As directivas resolvem esse problema também.
Há um mundo de possibilidades do que se pode fazer com as directivas.
Directiva¶
Esta é a classe principal para cada directiva personalizada que deseja implementar. Este é um objecto especial com algumas configurações padrão que pode usar.
Parâmetros¶
- --directive - O nome da directiva (o ficheiro onde a Directiva foi criada). Verifique listar todas as directivas para obter mais detalhes sobre como obter os nomes.
Como executar¶
A sintaxe é muito simples para uma directiva personalizada:
Com o parâmetro --app
$ lilya --app <LOCALIZAÇÃO> run --directive <DIRECTIVE-NAME> <OPTIONS>
Exemplo:
lilya --app myproject.main:app run --directive mydirective --name lilya
Com a variável de ambiente LILYA_DEFAULT_APP definida
$ export LILYA_DEFAULT_APP=myproject.main:app
$ lilya run --directive <DIRECTIVE-NAME> <OPTIONS>
Exemplo:
$ export LILYA_DEFAULT_APP=myproject.main:app
$ lilya run --directive mydirective --name lilya
O run --directive
está sempre à espera do nome do ficheiro da directiva.
Por exemplo, criou um ficheiro createsuperuser.py
com a lógica da Directiva
. O parâmetro --directive
será run --directive createsuperuser
.
Exemplo:
$ export LILYA_DEFAULT_APP=myproject.main:app
$ lilya run --directive createsuperuser --email example@lilya.dev
Como criar uma directiva¶
Para criar uma directiva, deve herdar da classe BaseDirective e o nome do objecto deve-se chamar Directive
.
from lilya.cli.base import BaseDirective
Crie a classe Directiva
import argparse
from typing import Any, Type
from lilya.cli.base import BaseDirective
class Directive(BaseDirective):
def add_arguments(self, parser: Type["argparse.ArgumentParser"]) -> Any:
# Add argments
...
Todas as directivas personalizadas criadas devem ser chamadas de Directive e deve herdar da classe
BaseDirective
.
Internamente, o lilya
procura por um objecto Directive
e verifica se é uma subclasse de BaseDirective
.
Se uma dessas condições falhar, lançará um DirectiveError
.
Onde as directivas devem ser colocadas?¶
Todas as directivas personalizadas criadas devem estar dentro de um módulo directives/operations
para
serem descobertas.
O local para as directives/operations
pode ser em qualquer lugar da aplicação e também pode ter mais do que uma.
Exemplo:
.
├── Makefile
└── myproject
├── __init__.py
├── apps
│ ├── accounts
│ │ ├── directives
│ │ │ ├── __init__.py
│ │ │ └── operations
│ │ │ ├── createsuperuser.py
│ │ │ └── __init__.py
│ ├── payroll
│ │ ├── directives
│ │ │ ├── __init__.py
│ │ │ └── operations
│ │ │ ├── run_payroll.py
│ │ │ └── __init__.py
│ ├── products
│ │ ├── directives
│ │ │ ├── __init__.py
│ │ │ └── operations
│ │ │ ├── createproduct.py
│ │ │ └── __init__.py
├── configs
│ ├── __init__.py
│ ├── development
│ │ ├── __init__.py
│ │ └── settings.py
│ ├── settings.py
│ └── testing
│ ├── __init__.py
│ └── settings.py
├── directives
│ ├── __init__.py
│ └── operations
│ ├── db_shell.py
│ └── __init__.py
├── main.py
├── serve.py
├── tests
│ ├── __init__.py
│ └── test_app.py
└── urls.py
Como pode ver no exemplo anterior, temos quatro directivas:
- createsuperuser - Dentro de
accounts/directives/operations
. - run_payroll - Dentro de
payroll/directives/operations
. - createproduct - Dentro de
products/directives/operations
. - db_shell - Dentro de
./directives/operations
.
Todas elas, não importa onde coloque a directiva, estão dentro de um directives/operations onde o lilya vai sempre procurar.
Funções da directiva¶
handle()¶
A lógica da Directiva
é implementada dentro de uma função handle
que pode ser síncrona
ou
assíncrona
.
Ao chamar uma Directiva
, o lilya
executará o handle()
e executará toda a lógica.
import argparse
from typing import Any, Type
from lilya.cli.base import BaseDirective
from lilya.cli.terminal import Print
printer = Print()
class Directive(BaseDirective):
def add_arguments(self, parser: Type["argparse.ArgumentParser"]) -> Any:
# Add argments
...
def handle(self, *args: Any, **options: Any) -> Any:
# Runs the handle logic in sync mode
printer.write_success("Sync mode handle run with success!")
import argparse
from typing import Any, Type
from lilya.cli.base import BaseDirective
from lilya.cli.terminal import Print
printer = Print()
class Directive(BaseDirective):
def add_arguments(self, parser: Type["argparse.ArgumentParser"]) -> Any:
# Add argments
...
async def handle(self, *args: Any, **options: Any) -> Any:
# Runs the handle logic in async mode
printer.write_success("Async mode handle run with success!")
Como pode ver, as Directivas do Lilya também permitem funções assíncronas
e síncronas
. Isto pode ser
particularmente útil quando precisa executar tarefas específicas no modo assíncrono, por exemplo.
add_arguments()¶
Este é o local onde adiciona qualquer argumento necessário para executar sua directiva personalizada. Os argumentos
são argumentos relacionados ao argparse
, portanto, a sintaxe deve ser familiar.
import argparse
from typing import Any, Type
from lilya.cli.base import BaseDirective
from lilya.cli.terminal import Print
printer = Print()
class Directive(BaseDirective):
def add_arguments(self, parser: Type["argparse.ArgumentParser"]) -> Any:
"""Arguments needed to create a user"""
parser.add_argument("--first-name", dest="first_name", type=str, required=True)
parser.add_argument("--last-name", dest="last_name", type=str, required=True)
parser.add_argument("--username", dest="username", type=str, required=True)
parser.add_argument("--email", dest="email", type=str, required=True)
parser.add_argument("--password", dest="password", type=str, required=True)
async def handle(self, *args: Any, **options: Any) -> Any:
# Runs the handle logic in async mode
...
Como pode ver, a Directiva tem cinco parâmetros e todos eles são obrigatórios.
lilya --app teste.main:app run --directive mydirective --first-name Lilya --last-name Toolkit --email example@lilya.dev --username lilya --password lilya
Ajuda¶
Existem duas opções de ajuda para as directivas. Uma quando executa o executor do lilya (run) e
outra para a directive
em si.
--help¶
Este comando é usado apenas para a ajuda do executor, por exemplo:
$ lilya run --help
-h¶
Esta opção é usada para acessar a ajuda da directive
e não do run
.
$ lilya run --directive mydirective -h
Observações¶
A única maneira de ver a ajuda de uma directiva é através de -h
.
Se o --help
for usado, ele mostrará apenas a ajuda do run
e não da directive
em si.
Ordem de prioridade¶
Isso é muito importante perceber.
O que acontece se tivermos duas directivas personalizadas com o mesmo nome?
Vamos usar a seguinte estrutura como exemplo:
.
├── Makefile
└── myproject
├── __init__.py
├── apps
│ ├── accounts
│ │ ├── directives
│ │ │ ├── __init__.py
│ │ │ └── operations
│ │ │ ├── createsuperuser.py
│ │ │ └── __init__.py
│ │ ├── __init__.py
│ │ ├── models.py
│ │ ├── tests.py
│ │ └── v1
│ │ ├── __init__.py
│ │ ├── schemas.py
│ │ ├── urls.py
│ │ └── controllers.py
├── configs
│ ├── __init__.py
│ ├── development
│ │ ├── __init__.py
│ │ └── settings.py
│ ├── settings.py
│ └── testing
│ ├── __init__.py
│ └── settings.py
├── directives
│ ├── __init__.py
│ └── operations
│ ├── createsuperuser.py
│ └── __init__.py
├── main.py
├── serve.py
├── tests
│ ├── __init__.py
│ └── test_app.py
└── urls.py
Este exemplo está a simular uma estrutura de um projecto Lilya com duas directivas personalizadas com o mesmo nome.
A primeira directiva está dentro de ./directives/operations/
e a segunda dentro de
./apps/accounts/directives/operations
.
As directivas do Lilya funcionam com base no princípio de Primeiro Encontrado, Primeiro Executado, o que significa que se tiver duas directivas personalizadas com o mesmo nome, o lilya irá executar a primeira directiva encontrada com aquele nome específico.
Noutras palavras, se quiser executar o createsuperuser
do accounts
, a primeira directiva encontrada
dentro de ./directives/operations/
deve ter um nome diferente ou então será executada
em vez da pretendida em accounts
.
Execução¶
As directivas do Lilya usam os mesmos eventos passados na aplicação.
Por exemplo, se deseja executar operações de base de dados e as conecções de base de dados devem ser estabelecidas antecipadamente, pode fazer de duas maneiras:
- Usar os eventos de Lifespan e as directivas os utilizarão.
- Estabelecer as conecções (abrir e fechar) dentro da Directiva diretamente.
O exemplo prático usa os eventos de lifespan para executar as operações. Desta forma, só precisa de um lugar para gerir os eventos da aplicação necessários.
Um exemplo prático¶
Vamos executar um exemplo de uma directiva personalizada para a sua aplicação. Como mencionamos o
createsuperuser
com frequência, vamos criar essa mesma directiva e aplicá-la à nossa aplicação Lilya.
Para este exemplo, usaremos o Saffier, pois isso permitirá fazer uma directiva completa de ponta à ponta
usando a abordagem assíncrona
.
Este exemplo é muito simples.
Para produção, deve ter seus modelos num local dedicado e suas configurações de registry
e database
nalgum lugar do seu settings
onde possa aceder em qualquer lugar do código por meio
das configurações do lilya, por exemplo.
P.S.: Para a estratégia de registro e base de dados com saffier, é bom ler as dicas e truques com saffier.
O design fica ao seu critério.
O que vamos criar:
- myproject/main/main.py - O ponto de entrada para nossa aplicação Lilya
- createsuperuser - Nossa directiva.
No final, simplesmente executamos a directiva.
Também usaremos o Saffier para os modelos do base de dados, pois isso tornará o exemplo mais simples.
O ponto de entrada da aplicação¶
from typing import Any
import saffier
from saffier import Database, Registry
from lilya.apps import Lilya
database = Database("postgres://postgres:password@localhost:5432/my_db")
registry = Registry(database=database)
class User(saffier.Model):
"""
Base model used for a custom user of any application.
"""
first_name = saffier.CharField(max_length=150)
last_name = saffier.CharField(max_length=150)
username = saffier.CharField(max_length=150, unique=True)
email = saffier.EmailField(max_length=120, unique=True)
password = saffier.CharField(max_length=128)
last_login = saffier.DateTimeField(null=True)
is_active = saffier.BooleanField(default=True)
is_staff = saffier.BooleanField(default=False)
is_superuser = saffier.BooleanField(default=False)
class Meta:
registry = registry
@classmethod
async def create_superuser(
cls,
username: str,
email: str,
password: str,
**extra_fields: Any,
):
extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("is_superuser", True)
return await cls._create_user(username, email, password, **extra_fields)
@classmethod
async def _create_user(cls, username: str, email: str, password: str, **extra_fields: Any):
"""
Create and save a user with the given username, email, and password.
"""
if not username:
raise ValueError("The given username must be set")
user = await cls.query.create(
username=username, email=email, password=password, **extra_fields
)
return user
def get_application():
"""
This is optional. The function is only used for organisation purposes.
"""
app = Lilya(
routes=[],
on_startup=[database.connect],
on_shutdown=[database.disconnect],
)
return app
app = get_application()
A string de conecção deve ser substituída pelo que for adequado para si.
O createsuperuser¶
Agora é hora de criar a directiva createsuperuser
. Como mencionado acima,
a directiva deve estar dentro de um módulo directives/operations
.
import argparse
import random
import string
from typing import Any, Type
from asyncpg.exceptions import UniqueViolationError
from lilya.cli.base import BaseDirective
from lilya.cli.terminal import Print
from ..main import User
printer = Print()
class Directive(BaseDirective):
help: str = "Creates a superuser"
def add_arguments(self, parser: Type["argparse.ArgumentParser"]) -> Any:
parser.add_argument("--first-name", dest="first_name", type=str, required=True)
parser.add_argument("--last-name", dest="last_name", type=str, required=True)
parser.add_argument("--username", dest="username", type=str, required=True)
parser.add_argument("--email", dest="email", type=str, required=True)
parser.add_argument("--password", dest="password", type=str, required=True)
def get_random_string(self, length=10):
letters = string.ascii_lowercase
result_str = "".join(random.choice(letters) for i in range(length))
return result_str
async def handle(self, *args: Any, **options: Any) -> Any:
"""
Generates a superuser
"""
first_name = options["first_name"]
last_name = options["last_name"]
username = options["username"]
email = options["email"]
password = options["password"]
try:
user = await User.query.create_superuser(
first_name=first_name,
last_name=last_name,
username=username,
email=email,
password=password,
)
except UniqueViolationError:
printer.write_error(f"User with email {email} already exists.")
return
printer.write_success(f"Superuser {user.email} created successfully.")
E isso deve ser tudo. Agora temos um createsuperuser
e uma aplicação e agora podemos executar no terminal:
Usando a descoberta automática
$ lilya run --directive createsuperuser --first-name Lilya --last-name Framework --email example@lilya.dev --username lilya --password lilya
Usando o --app ou LILYA_DEFAULT_APP
$ lilya --app myproject.main:app run --directive createsuperuser --first-name Lilya --last-name Framework --email example@lilya.dev --username lilya --password lilya
Ou
$ export LILYA_DEFAULT_APP=myproject.main:app
$ lilya run --directive createsuperuser --first-name Lilya --last-name Framework --email example@lilya.dev --username lilya --password lilya
Após a execução do comando, deverá ver o superutilizador criado na sua base de dados.