Prólogo
Olá, Habr! Este artigo é dedicado à análise dos prós e contras do próximo framework Python, que foi lançado há cerca de uma semana.
Então, uma pequena digressão lírica. Durante os eventos mais conhecidos, quando estávamos um pouco isolados, tínhamos um pouco mais de tempo livre. Alguém conseguiu a lista de literatura reservada para leitura, alguém começou a estudar outra língua estrangeira, alguém continuou a pressionar em Dotan e não prestou atenção às mudanças. Mas eu (desculpe, este artigo conterá muito "eu" e estou um pouco envergonhado) decidi e tentei fazer algo útil. No entanto, a utilidade é discutível. As perguntas óbvias que o leitor provavelmente terá em primeiro lugar:“Hum, estrutura Python? Outro? Com licença, mas por quê? Afinal, não somos JavaScript! "
Na verdade, isso é exatamente o que será discutido neste artigo: É necessário? Se necessário, para quem? Qual é a diferença do que já existe? Como pode ser atraente e por que, por exemplo, pode ser enterrado sem esperar o primeiro aniversário. O artigo não planeja muitos códigos - exemplos de como escrever um aplicativo e usar partes individuais podem ser encontrados na documentação (há muito mais código lá;)). Este artigo é mais uma visão geral.
Quem precisa disso?
Uma resposta um tanto egoísta a essa pergunta - em primeiro lugar, é claro, eu mesmo. Tenho alguma experiência na construção de aplicações web usando frameworks existentes e regularmente me pego pensando: “Sim, tudo é legal, mas se ao menos fosse assim…. E aqui está um comercial ... "... A maioria de nós, de uma forma ou de outra, mais cedo ou mais tarde se depara com o fato de que algumas coisas não gostam e gostariam (ou até mesmo deveriam) mudá-las. Tentei juntar o que gosto com as ferramentas que usei. Espero não estar sozinho em minhas preferências e que haja pessoas que vão achar essas ideias próximas. A ideia principal por trás do Crax é que ele não impõe nenhum estilo particular de desenvolvimento tanto quanto possível. Por exemplo, não precisamos de namespaces, não queremos dividir a lógica em aplicativos, queremos implantar rapidamente duas rotas e gerar solicitações e respostas. Ok, neste caso podemos apenas criar um único aplicativo de arquivo e obter o que queremos. Mas a situação oposta também é possível, e isso também não será um problema. A segunda coisa que Crax defende é a simplicidade. Um mínimo de código e um mínimo de documentação de leitura para começar.Se uma pessoa que está começando a aprender Python planeja trabalhar com a estrutura, ela deve ser capaz de superar o limiar de entrada sem dor.
Se você olhar para o número de linhas de código necessárias para passar em todos os testes
TechEmpower (mais sobre isso abaixo), então o Crax em um aplicativo que consiste em um arquivo é mais compacto do que todos os outros participantes e não havia propósito de "encolher" esse arquivo. Não há realmente nada mais para escrever. Resumindo o que foi dito acima, podemos dizer que Crax é adequado para uma gama muito diferente de tarefas e uma gama muito ampla de programadores de vários graus de treinamento.
Por que não usar as ferramentas existentes?
Por que não? Se você sabe exatamente qual ferramenta usar, o que é mais adequado para a sua tarefa atual, além disso, você já trabalhou com essa ferramenta e conhece todas as nuances. Claro, você escolherá o que conhece e se adapta. Não há (e nunca haverá) objetivo de posicionar Crax como o "assassino de% framework_name%". Não haverá tipo de agitação: "Jogue com urgência% framework_name%, reescreva tudo no Crax e aumente imediatamente o
Primeiro, é rápido o suficiente. É escrito usando a interface ASGI (leia a especificação aqui) e é muito mais rápido que Flask ou Django 1. *, 2. *. Mas Crax obviamente não é o único framework Python a usar ASGI, e testes preliminares mostram que ele compete bem com outros frameworks que usam essa tecnologia. Para comparação, usamos os testes de avaliação de desempenho TechEmpower . Infelizmente, Crax, como outros frameworks adicionados no meio da rodada atual, só vai entrar na próxima, e então você pode ver os resultados na edição gráfica. No entanto, após cada solicitação de pull, o Travis executa testes e você pode ver as características comparativas das estruturas no log do Travis. Abaixo do link está um longo rodapé do log do Travis para estruturas Python com nomes em ordem alfabética de A a F Aqui... Você pode tentar ler o log e comparar Crax, por exemplo, com apidaora, ficará muito bom. Abaixo, no gráfico, está o estado atual das coisas na Rodada de 19 testes.
Claro, seremos capazes de ver os resultados reais e os resultados reais apenas na próxima rodada, mas mesmo assim.
Porém, nós, como mencionado acima, não temos ferramentas menos rápidas e já comprovadas.
O mesmo assíncrono, com suporte nativo para websockets e outras alegrias.
Digamos Starlette ou FastApi. Eles são frameworks absolutamente incríveis, com uma grande comunidade interessada em desenvolver esses produtos. É importante notar que Crax é mais semelhante a Starlette ou FastAPI em sua ideologia, e algumas ideias foram
from crax.utils import get_settings_variable
base_url = get_settings_variable('BASE_URL')
Pareceria uma vantagem duvidosa, no entanto, quando o arquivo de configuração começa a ficar sobrecarregado com variáveis e configurações, e gostaríamos de ter acesso a elas, isso se torna importante.
O próximo detalhe importante sobre o qual gostaria de falar é a organização da estrutura do aplicativo. Quando você tem um projeto pequeno, toda a lógica do qual pode ser colocada em um arquivo, isso é uma coisa. Mas quando você escreve algo mais global, você pode querer separar visualizações, modelos, descrições de rotas, etc., de acordo com sua lógica. Neste contexto, grandes projetos Flask ou aplicativos Django vêm à mente. Crax fala sobre namespaces neste sentido. Inicialmente, seu aplicativo deve ser
um conjunto de pacotes Python incluídos no arquivo de projeto principal. A propósito, os namespaces (suas partes do aplicativo) podem ser aninhados recursivamente (hello Flask), e os nomes dos arquivos neles não importam. Por que fazer isso? E o que isso nos dá?
Primeiro, roteamento. Os namespaces criarão uri com base na localização do namespace automaticamente (mas isso, é claro, pode ser controlado). Por exemplo:
from crax.urls import Route, Url, include
url_list = [
Route(Url('/'), Home),
Route(Url('/guest_book'), guest_view_coroutine),
include('second_app.urls'),
include('second_app.nested.urls'),
include('third_app.urls')
]
Substitua os pontos por barras e você obterá o uri do seu namespace (é claro, adicionando um manipulador final). Como já mencionamos o roteamento, vamos nos alongar sobre ele com mais detalhes.
Crax oferece algumas possibilidades interessantes, além do trabalho usual com expressões regulares ou o trabalho via caminho do Django.
# URL defined as regex with one floating (optional) parameter
Url(r"/cabinet/(?P<username>\w{0,30})/(?:(?P<optional>\w+))?", type="re_path")
# General way to define URL
Url("/v1/customer/<customer_id>/<discount_name>/")
No entanto, é possível vincular vários Urls a um manipulador.
from crax.urls import Route, Url
class APIView(TemplateView):
template = "index.html"
urls = [
Route(
urls=(
Url("/"),
Url("/v1/customers"),
Url("/v1/discounts"),
Url("/v1/cart"),
Url("/v1/customer/<customer_id:int>"),
Url("/v1/discount/<discount_id:int>/<optional:str>/"),
),
handler=APIView)
]
Você mesmo pode pensar em onde isso pode ser útil para você. E também, existe um modo de operação do resolvedor no modo "mascaramento". Por exemplo, você deseja apenas distribuir algum tipo de diretório com modelos e não deseja mais nada. Talvez esta seja a documentação do Sphinx ou algo semelhante. Você sempre pode fazer isso:
import os
from crax.urls import Url, Route
class Docs(TemplateView):
template = 'index.html'
scope = os.listdir('docs/templates')
URL_PATTERNS = [
Route(urls=(
Url('/documentation', masquerade=True),
handler=Docs),
]
Ótimo, agora todos os modelos que estão no diretório docs / templates serão renderizados com sucesso usando um manipulador. Um leitor curioso dirá que nenhum python é necessário aqui, e tudo isso pode ser feito apenas com a ajuda do Nginx condicional. Estou absolutamente de acordo, exatamente até que seja necessário, por exemplo, distribuir esses modelos por função ou em algum lugar lateral, não é necessária lógica adicional.
No entanto, de volta aos nossos namespaces
Não há ORM em Crax. E não é suposto. De qualquer forma, até SQLAlchemy oferece soluções assíncronas. Porém, o trabalho com bancos de dados (Postgres, MySQL e SQLite) é declarado. Isso significa que é possível escrever seus próprios modelos com base no Crax BaseTable . Por baixo do capô, este é um invólucro muito fino sobre o SQLAlchemy Core Table e pode fazer tudo o que o Core Table pode fazer . Para o que pode ser necessário. Talvez para fazer algo semelhante.
from crax.database.model import BaseTable
import sqlalchemy as sa
class BaseModelOne(BaseTable):
# This model just passes it's fields to the child
# Will not be created in database because the abstract is defined
parent_one = sa.Column(sa.String(length=50), nullable=False)
class Meta:
abstract = True
class BaseModelTwo(BaseTable):
# Also passes it's fields to the child
# Will be created in database
parent_two = sa.Column(sa.String(length=50), nullable=False)
class MyModel(BaseModelOne, BaseModelTwo):
name = sa.Column(sa.String(length=50), nullable=False)
print([y.name for x in MyModel.metadata.sorted_tables for y in x._columns])
# Let's check our fields ['name', 'id', 'parent_one', 'parent_two']
E para poder trabalhar com migrações. As migrações Crax são um pouco de código em cima do SQLAlchemy Alembic. Como estamos falando sobre namespaces e separação de lógica, então,
obviamente, gostaríamos de armazenar migrações no mesmo pacote que a outra lógica desse namespace. É assim que as migrações Crax funcionam. Todas as migrações serão distribuídas de acordo com seu namespace, e se este namespace implicar em trabalhar com diferentes bancos de dados, então dentro do diretório de migração haverá uma divisão em diretórios dos bancos de dados correspondentes. O mesmo se aplica a migrações offline - todos os arquivos * .sql serão divididos de acordo com o namespace e o banco de dados modelo. Não vou pintar aqui sobre como escrever consultas - está na documentação, direi apenas que você ainda está trabalhando com o SQLAlchemy Core.
Novamente, os namespaces implicam no armazenamento conveniente de modelos (herança e outros recursos do Jinja2 são suportados + algumas comodidades na forma de tokens CSRF prontos ou geração de url). Ou seja, todos os seus modelos são estruturados. Bem, é claro, não estou preso em um 2007 glorioso, entendo que os modelos (mesmo que sejam renderizados de forma assíncrona) terão pouca demanda em 2020. E isso, provavelmente, você tem o prazer de separar a lógica do front-end e do back-end. Crax faz um excelente trabalho nisso, os resultados podem ser vistos no Github.
AquiVueJs é usado como front-end. E como temos algum tipo de API, provavelmente queremos fazer documentação interativa. Crax pode construir documentação OpenAPI (Swagger) fora da caixa com base em suas listas de rotas e suas docstrings de manipulador. Todos os exemplos, é claro, estão na documentação.
Antes de passarmos para a parte mais interessante de nossa breve visão geral, vale a pena falar um pouco sobre quais baterias úteis já são fornecidas com o Crax.
Naturalmente, o modo de depuração é quando o erro e o rastreamento completo podem ser lidos diretamente no navegador, na página onde ocorreu o infortúnio. O modo de depuração pode ser desligado e personalizado com
Logger integrado com a capacidade de gravar simultaneamente no arquivo especificado e enviar logs para o console (ou fazer alguma coisa). Capacidade de atribuir seu próprio registrador em vez do padrão. Suporte para Sentry adicionando duas linhas à configuração (e, se necessário, personalização).
Dois tipos de middleware pré-instalado. O primeiro é processado ANTES da solicitação ser processada pelo aplicativo e o segundo DEPOIS.
Suporte integrado para cabeçalhos CORS. Você só precisa declarar as regras CORS na configuração.
Capacidade de definir métodos disponíveis para cada manipulador diretamente no local. Cada manipulador trabalhará com a lista de métodos HTTP que é especificada (+ HEAD e OPTIONS), ou apenas com GET, HEAD e OPTIONS.
Capacidade de especificar que este manipulador está disponível apenas para usuários autorizados, ou apenas para usuários do grupo Administradores, ou apenas para membros da função de superusuário.
Há autorização para sessões assinadas HMAC, para as quais você não precisa ir ao banco de dados e uma série de ferramentas para criar e gerenciar usuários. Você pode ativar o suporte de back-end de autorização e obter um usuário predefinido e uma série de ferramentas para trabalhar. No entanto, como a maioria das ferramentas Crax, você pode deixá-lo desativado, usá-lo e escrever o seu próprio. Você não pode usar autorização, bancos de dados, modelos, migrações, visualizações e escrever completamente suas próprias soluções personalizadas. Você não precisa fazer nenhum esforço para fazer isso, você não o ligou - não está.
Existem vários tipos de resposta e vários tipos de manipuladores baseados em classe que o ajudarão a escrever aplicativos de forma mais rápida e concisa. Nesse caso, o seu próprio também funcionará, que não herda dos embutidos.
from crax.views import BaseView
# Written your own stuff
class CustomView:
methods = ['GET', 'POST']
def __init__(self, request):
self.request = request
async def __call__(self, scope, receive, send):
if self.request.method == 'GET':
response = TextResponse(self.request, "Hello world")
await response(scope, receive, send)
elif self.request.method == 'POST':
response = JSONResponse(self.request, {"Hello": "world"})
await response(scope, receive, send)
# Crax based stuff
class CustomView(BaseView):
methods = ['GET', 'POST']
async def get(self):
response = TextResponse(self.request, "Hello world")
return response
async def post(self):
response = JSONResponse(self.request, {"Hello": "world"})
return response
class CustomersList(TemplateView):
template = 'second.html'
# No need return anything in case if it is TemplateView.
# Template will be rendered with params
async def get(self):
self.context['params'] = self.request.params
Suporte de proteção CSRF. Gerando tokens, verificando a presença de um token no corpo da solicitação,
desabilitando a verificação para manipuladores específicos.
Suporte para proteção de ClickJacking (políticas Frame, iframe, embed ... render)
Suporte para verificar o tamanho máximo permitido do corpo de uma solicitação ANTES de o aplicativo começar a processá-la.
Suporte para websocket nativo. Vamos pegar um exemplo da documentação e escrever um aplicativo simples que pode enviar mensagens de websocket por broadcast, por grupo de usuários ou mensagens para um usuário específico. Suponha que temos grupos "meninos" e "meninas" (é possível adicionar um grupo "pais"). Podemos escrever algo semelhante para um exemplo (claro, este não é um código de produto).
#app.py
import asyncio
import json
import os
from base64 import b64decode
from functools import reduce
from crax.auth import login
from crax.auth.authentication import create_session_signer
from crax.auth.models import Group, UserGroup
from crax.response_types import JSONResponse
from crax.urls import Route, Url
from crax.views import TemplateView, WsView
from sqlalchemy import and_, select
from websockets import ConnectionClosedOK
BASE_URL = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SECRET_KEY = "SuperSecret"
MIDDLEWARE = [
"crax.auth.middleware.AuthMiddleware",
"crax.auth.middleware.SessionMiddleware",
]
APPLICATIONS = ["ws_app"]
CLIENTS = {'boys': [], 'girls': []}
class Home(TemplateView):
template = "index.html"
login_required = True
class Login(TemplateView):
template = "login.html"
methods = ["GET", "POST"]
async def post(self):
credentials = json.loads(self.request.post)
try:
await login(self.request, **credentials)
if hasattr(self.request.user, "first_name"):
context = {'success': f"Welcome back, {self.request.user.username}"}
status_code = 200
else:
context = {'error': f"User or password wrong"}
status_code = 401
except Exception as e:
context = {'error': str(e)}
status_code = 500
response = JSONResponse(self.request, context)
response.status_code = status_code
return response
class WebSocketsHome(WsView):
def __init__(self, request):
super(WebSocketsHome, self).__init__(request)
self.group_name = None
async def on_connect(self, scope, receive, send):
# This coroutine will be called every time a client connects.
# So at this point we can do some useful things when we find a new connection.
await super(WebSocketsHome, self).on_connect(scope, receive, send)
if self.request.user.username:
cookies = self.request.cookies
# In our example, we want to check a group and store the user in the desired location.
query = select([Group.c.name]).where(
and_(UserGroup.c.user_id == self.request.user.pk, Group.c.id == UserGroup.c.group_id)
)
group = await Group.query.fetch_one(query=query)
self.group_name = group['name']
# We also want to get the username from the user's session key for future access via direct messaging
exists = any(x for x in CLIENTS[self.group_name] if cookies['session_id'] in list(x)[0])
signer, max_age, _, _ = create_session_signer()
session_cookie = b64decode(cookies['session_id'])
user = signer.unsign(session_cookie, max_age=max_age)
user = user.decode("utf-8")
username = user.split(":")[0]
val = {f"{cookies['session_id']}:{cookies['ws_secret']}:{username}": receive.__self__}
# Since we have all the information we need, we can save the user
# The key will be session: ws_cookie: username and the value will be an instance of uvicorn.WebSocketProtocol
if not exists:
CLIENTS[self.group_name].append(val)
else:
# We should clean up our storage to prevent existence of the same clients.
# For example due to page reloading
[
CLIENTS[self.group_name].remove(x) for x in
CLIENTS[self.group_name] if cookies['session_id'] in list(x)[0]
]
CLIENTS[self.group_name].append(val)
async def on_disconnect(self, scope, receive, send):
# This coroutine will be called every time a client disconnects.
# So at this point we can do some useful things when we find a client disconnects.
# We remove the client from the storage
cookies = self.request.cookies
if self.group_name:
try:
[
CLIENTS[self.group_name].remove(x) for x in
CLIENTS[self.group_name] if cookies['session_id'] in list(x)[0]
]
except ValueError:
pass
async def on_receive(self, scope, receive, send):
# This coroutine will be called every time we receive a new incoming websocket message.
# Check the type of message received and send a response according to the message type.
if "text" in self.kwargs:
message = json.loads(self.kwargs["text"])
message_text = message["text"]
clients = []
if message["type"] == 'BroadCast':
clients = reduce(lambda x, y: x + y, CLIENTS.values())
elif message["type"] == 'Group':
clients = CLIENTS[message['group']]
elif message["type"] == 'Direct':
username = message["user_name"]
client_list = reduce(lambda x, y: x + y, CLIENTS.values())
clients = [client for client in client_list if username.lower() in list(client)[0]]
for client in clients:
if isinstance(client, dict):
client = list(client.values())[0]
try:
await client.send(message_text)
except (ConnectionClosedOK, asyncio.streams.IncompleteReadError):
await client.close()
clients.remove(client)
URL_PATTERNS = [Route(Url("/"), Home), Route(Url("/", scheme="websocket"), WebSocketsHome), Route(Url("/login"), Login)]
DATABASES = {
"default": {
"driver": "sqlite",
"name": f"/{BASE_URL}/ws_crax.sqlite",
},
}
app = Crax('ws_app.app')
if __name__ == "__main__":
if sys.argv:
from_shell(sys.argv, app.settings)
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Crax Websockets</title>
</head>
<body>
<div id="wsText"></div>
<form>
<input id="messageText"><br>
<select id="targetGroup">
<option>boys</option>
<option>girls</option>
</select>
<select id="messageType">
<option>BroadCast</option>
<option>Group</option>
<option>Direct</option>
</select>
<select id="userNames">
<option>Greg</option>
<option>Chuck</option>
<option>Mike</option>
<option>Amanda</option>
<option>Lisa</option>
<option>Anny</option>
</select>
</form>
<a href="#" id="sendWs">Send Message</a>
<script>
var wsText = document.getElementById("wsText")
var messageType = document.getElementById("messageType")
var messageText = document.getElementById("messageText")
var targetGroup = document.getElementById("targetGroup")
var userName = document.getElementById("userNames")
var sendButton = document.getElementById("sendWs")
ws = new WebSocket("ws://127.0.0.1:8000")
ws.onmessage = function(e){
wsText.innerHTML+=e.data
}
sendButton.addEventListener("click", function (e) {
e.preventDefault()
var message = {type: messageType.value, text: messageText.value}
var data
if (messageText.value !== "") {
if (messageType.value === "BroadCast"){
// send broadcast message
data = message
}
else if (messageType.value === "Group"){
// send message to group
data = Object.assign(message, {group: targetGroup.value})
}
else if (messageType.value === "Direct"){
// send message to certain user
data = Object.assign(message, {user_name: userName.value})
}
ws.send(JSON.stringify(data))
}
})
</script>
</body>
</html>
<!-- login.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Crax Websockets</title>
</head>
<body>
<form>
<input id="username">
<input id="password" type="password">
</form>
<div id="loginResults"></div>
<a href="#" id="sendLogin">Login</a>
<script>
var loginButton = document.getElementById("sendLogin")
var loginResults = document.getElementById("loginResults")
var username = document.getElementById("username")
var password = document.getElementById("password")
loginButton.addEventListener("click", function (e) {
e.preventDefault()
if (username.value !== "" && password.value !== "") {
var xhr = new XMLHttpRequest()
xhr.overrideMimeType("application/json")
xhr.open("POST", "/login")
xhr.send(JSON.stringify({username: username.value, password: password.value}))
xhr.onload = function () {
var result = JSON.parse(xhr.responseText)
if ("success" in result){
loginResults.innerHTML+="<h5 style='color: green'>"+result.success+ "</h5>"
}
else if ("error" in result) {
loginResults.innerHTML+="<h5 style='color: red'>"+result.error+ "</h5>"
}
}
}
})
</script>
</body>
</html>
O código completo pode ser visto na documentação Crax.
Bem, chegou a hora do mais interessante neste artigo.
Por que isso é desnecessário?
Em primeiro lugar, como mencionado acima, existem vários frameworks que fazem o mesmo e têm uma comunidade já formada. Considerando que Crax é um bebê de uma semana. O exército de um homem é quase uma garantia de que mais cedo ou mais tarde o projeto será abandonado. É triste, mas o fato de trabalhar na mesa, lançando releases e atualizações apenas para você e Vasily do Syktyvkar, é muito mais demorado do que quando a comunidade está trabalhando no projeto. Nesse ínterim, o projeto não tem uma série de recursos que são obrigatórios em 2020. Por exemplo: sem suporte JWT (JOSE). Sem suporte imediato para ferramentas OAuth2. Sem suporte GraphQL. É claro que você mesmo pode escrever isso para o seu projeto, mas Starlette ou FastAPI já tem. Eu só tenho que escrever isso (sim, está nos planos). Haverá um pouco sobre os planos em conclusão.
Os desenvolvedores da Netflix e da Microsoft escrevem sobre FastAPI. Sobre Crax escreve noname, não se sabe onde apareceu, e quem sabe onde é capaz de exatamente o abismo depois de amanhã. Eles
não chamam um vaporizador pelo meu nome idiota.
Minha mãe chora à noite, porque deu à luz uma aberração ...
(c)
Isso é importante. É chamado de reputação e ecossistema. Crax também não. Sem essas coisas importantes, o projeto tem a garantia de ir para o aterro sem nunca nascer.
Vale a pena entender. O que está escrito acima não é uma tentativa de digitar classes e nem o texto de um sem-teto no trem. Esta é uma avaliação sóbria e um aviso de que “soluções prontas para produção” não é apenas o resultado da cobertura do código-fonte por testes, é uma avaliação geral da maturidade das tecnologias, abordagens e soluções utilizadas no projeto.
Se você está apenas começando a se familiarizar com Python e experimentando frameworks, você está em perigo: Provavelmente, você não encontrará respostas para a pergunta no SO, talvez camaradas mais experientes o ajudem, que, infelizmente, podem não estar lá.
Os objetivos
A primeira coisa que pretendo fazer é, é claro, adicionar algumas coisas obrigatórias como suporte a JWT (JOSE), OAuth2 e GraphQL. Isso é o que tornará mais fácil para mim e para as pessoas interessadas trabalhar. E este é, de fato, o principal objetivo de Crax - tornar o trabalho de alguém um pouco mais fácil. Talvez então uma nova rodada na TechEmpower comece e os benchmarks se tornem mais evidentes. É até possível que depois disso haja algum interesse na comunidade.
Existe uma ideia de escrever um CMS baseado em Crax.
Se não estou enganado (se estou enganado - corrija), ainda não temos nenhum CMS assíncrono em Python em nosso kit de ferramentas. Posso mudar de ideia e decidir escrever algum tipo de solução de e-commerce. Mas, obviamente, para evitar que Crax se afogue antes de chegar às bóias, algo interessante precisa ser feito em sua base. Talvez os entusiastas se interessem por isso. Os entusiastas estão quando é grátis. Porque não há dinheiro aqui e, muito provavelmente, não haverá. Crax é totalmente gratuito para todos e eu não ganhei um centavo por este trabalho. Assim, o desenvolvimento está previsto para "longas noites de inverno" e, talvez, no próximo ano, algo interessante irá nascer.
Conclusão
Estava pensando em qual grupo incluir este artigo (aliás, esta é minha primeira publicação sobre o recurso). Talvez tenha valido a pena colocá-lo na etiqueta "I'm PR". O que me fez mudar de ideia: em primeiro lugar, o fato de não ter nenhum caráter publicitário.
Não há nenhuma chamada "Rapazes, inscrevam-se com urgência para solicitações de pull" . Não há ideia de encontrar um patrocinador aqui. Não tem nem ideia de que trouxe algo que você nunca viu (claro, viu). Você pode abstrair da ideia de que sou o autor de ambos os artigos e desta ferramenta e perceber o que foi escrito como uma revisão. E, sim, essa é a melhor maneira. Será um excelente resultado para mim se você apenas tiver em mente que é.
Isso, talvez, seja tudo.
“Então… é hora de pegar as varas de pescar.
- Por quê?
- O boné vermelho de Harris assustou todos os peixes.
(c)
Código na documentação do GitHub