Prologue
Bonjour, Habr! Cet article est consacré à l'analyse des avantages et des inconvénients du prochain framework Python, sorti il y a environ une semaine.
Alors, une petite digression lyrique. Lors des événements bien connus, lorsque nous étions un peu isolés, nous avions un peu plus de temps libre. Quelqu'un est arrivé à la liste de littérature réservée à la lecture, quelqu'un a commencé à étudier une autre langue étrangère, quelqu'un a continué à appuyer sur Dotan et n'a pas prêté attention aux changements. Mais j'ai (désolé, cet article contiendra beaucoup de «je», et j'ai un peu honte) décidé et essayé de faire quelque chose d'utile. Cependant, l'utilité est discutable. Les questions évidentes que le lecteur se posera probablement en premier lieu:«Euh, cadre Python? Un autre? Excusez-moi, mais pourquoi? Nous ne sommes pas JavaScript, après tout! "
En fait, c'est exactement ce qui sera discuté dans cet article: est-ce nécessaire? Si nécessaire, à qui? Quelle est la différence par rapport à ce qui existe déjà? Comment il peut être attrayant et pourquoi, par exemple, il peut être enterré sans attendre le premier anniversaire. L'article ne prévoit pas beaucoup de code - des exemples d'écriture d'une application et d'utilisation de parties individuelles peuvent être trouvés dans la documentation (il y a beaucoup plus de code là-bas;)). Cet article est plutôt un article de synthèse.
Qui en a besoin?
Une réponse quelque peu égoïste à cette question - tout d'abord, bien sûr, moi-même. J'ai une certaine expérience dans la création d'applications Web en utilisant des frameworks existants et je me surprends régulièrement à penser: «Oui, tout est cool, mais si seulement c'était comme ça…. Et voici une publicité ... "... La plupart d'entre nous, d'une manière ou d'une autre, se rendent compte tôt ou tard que certaines choses n'aiment pas et que nous aimerions (ou même devons) les changer. J'ai essayé de rassembler ce que j'aime à partir des outils que j'ai utilisés. J'espère que je ne suis pas seul dans mes préférences, et qu'il y a des gens qui trouveront ces idées proches. L'idée principale de Crax est qu'il n'impose pas autant que possible un style de développement particulier. Par exemple, nous n'avons pas besoin d'espaces de noms, nous ne voulons pas diviser la logique en applications, nous voulons déployer rapidement deux routes et générer des requêtes et des réponses. Ok, dans ce cas, nous pouvons simplement créer une application de fichier unique et obtenir ce que nous voulons. Mais la situation inverse est également possible, et ce ne sera pas non plus un problème. La deuxième chose que Crax préconise est la simplicité. Code minimum et lecture de documentation minimum pour commencer.Si une personne qui commence tout juste à apprendre Python envisage de travailler avec le framework, elle devrait être en mesure de franchir sans douleur le seuil d'entrée.
Si vous regardez le nombre de lignes de code nécessaires pour passer tous les tests
TechEmpower (plus à ce sujet ci-dessous), alors Crax dans une application composée d'un fichier est plus compacte que tous les autres participants, et il n'y avait aucun but de "réduire" ce fichier. Il n'y a vraiment plus rien à écrire. Pour résumer ce qui précède, nous pouvons dire que Crax convient à une gamme très différente de tâches et à un très large éventail de programmeurs de divers degrés de formation.
Pourquoi ne pas utiliser les outils existants?
Pourquoi pas? Si vous savez exactement quel outil utiliser, ce qui convient le mieux à votre tâche actuelle, de plus, vous avez travaillé avec cet outil et en connaissez toutes les nuances. Bien sûr, vous choisirez ce que vous savez et ce que vous conviendrez. Il n'y a aucun objectif (et ne le sera jamais) de positionner Crax comme le "% framework_name% killer". Il n'y aura pas de type d'agitation: "Lancer de toute urgence% framework_name%, tout réécrire sur Crax et augmenter immédiatement le
Tout d'abord, c'est assez rapide. Il est écrit à l'aide de l'interface ASGI (lire la spécification ici) et est beaucoup plus rapide que Flask ou Django 1. *, 2. *. Mais Crax n'est bien sûr pas le seul framework Python à utiliser ASGI, et des tests préliminaires montrent qu'il rivalise bien avec d'autres frameworks utilisant cette technologie. À titre de comparaison, nous avons utilisé les tests d' évaluation des performances de TechEmpower . Malheureusement, Crax, comme d'autres frameworks ajoutés au milieu de la ronde en cours, n'entrera que dans le prochain, et vous pourrez ensuite voir les résultats dans le problème graphique. Cependant, après chaque pull request, Travis exécute des tests et vous pouvez voir les caractéristiques comparatives des frameworks dans le journal Travis. Sous le lien se trouve une longue toile de pied du journal Travis pour les frameworks Python avec les noms par ordre alphabétique de A à F Ici... Vous pouvez essayer de lire le journal et de comparer Crax, par exemple, avec apidaora, cela s'avérera plutôt bien. Ci-dessous, sur le graphique, l'état actuel des choses dans la série de 19 tests.
Bien sûr, nous ne pourrons voir les vrais résultats et les vrais résultats qu'au prochain tour, mais néanmoins.
Cependant, comme mentionné ci-dessus, nous ne disposons pas d'outils moins rapides et déjà éprouvés.
Le même asynchrone, avec un support natif pour les websockets et autres joies.
Disons Starlette ou FastApi. Ce sont des cadres absolument incroyables avec une grande communauté intéressée par le développement de ces produits. Il est à noter que Crax est le plus similaire à Starlette ou FastAPI dans son idéologie, et certaines idées ont été
from crax.utils import get_settings_variable
base_url = get_settings_variable('BASE_URL')
Cela semble un avantage douteux, cependant, lorsque le fichier de configuration commence à devenir envahi par les variables et les paramètres, et que nous aimerions y avoir accès, cela devient important.
Le prochain détail important dont je voudrais parler est l'organisation de la structure de l'application. Lorsque vous avez un petit projet, dont toute la logique peut être placée dans un seul fichier, c'est une chose. Mais lorsque vous écrivez quelque chose de plus global, vous souhaiterez peut-être séparer les vues, les modèles, les descriptions d'itinéraire, etc., en fonction de leur logique. Dans ce contexte, de superbes plans Flask ou applications Django viennent à l'esprit. Crax parle d'espaces de noms dans ce sens. Au départ, votre application est destinée à être
un ensemble de packages python inclus dans le fichier de projet principal. À propos, les espaces de noms (vos parties de l'application) peuvent être imbriqués de manière récursive (bonjour, Flask), et les noms des fichiers qu'ils contiennent n'ont pas d'importance. Pourquoi faire ça? Et qu'est-ce que cela nous donne?
Tout d'abord, le routage. Les espaces de noms créeront automatiquement des uri en fonction de l'emplacement de l'espace de noms (mais cela, bien sûr, peut être contrôlé). Par exemple:
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')
]
Remplacez les points par des barres obliques et vous obtiendrez l'URI dans votre espace de noms (bien sûr, en ajoutant un gestionnaire final). Puisque nous avons déjà évoqué le routage, nous y reviendrons plus en détail.
Crax offre quelques possibilités intéressantes, outre le travail habituel avec les expressions régulières ou le travail via le chemin 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>/")
Cependant, il est possible de lier plusieurs URL à un seul gestionnaire.
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)
]
Vous pouvez vous-même penser aux endroits où cela peut vous être utile. Et aussi, il existe un mode de fonctionnement du résolveur en mode "masquerading". Par exemple, vous souhaitez simplement distribuer une sorte de répertoire avec des modèles, et vous ne voulez rien d'autre. C'est peut-être la documentation Sphinx, ou quelque chose de similaire. Vous pouvez toujours faire ceci:
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),
]
Très bien, maintenant tous les modèles qui se trouvent dans le répertoire docs / templates seront rendus avec succès à l'aide d'un seul gestionnaire. Un lecteur curieux dira qu'aucun python n'est nécessaire ici, et tout cela ne peut être fait qu'avec l'aide de Nginx conditionnel. Je suis tout à fait d'accord, jusqu'à ce qu'il soit nécessaire, par exemple, de distribuer ces modèles par rôle ou quelque part sur le côté, aucune logique supplémentaire n'est requise.
Cependant, revenons à nos espaces de noms
Il n'y a pas d'ORM à Crax. Et ce n'est pas censé. Quoi qu'il en soit, jusqu'à ce que SQLAlchemy propose des solutions asynchrones. Cependant, le travail avec des bases de données (Postgres, MySQL et SQLite) est déclaré. Cela signifie qu'il est possible d'écrire vos propres modèles basés sur Crax BaseTable . Sous le capot, il s'agit d'un wrapper très fin sur SQLAlchemy Core Table , et il peut faire tout ce que Core Table peut faire . Pour ce dont il peut être nécessaire. Peut-être pour faire quelque chose de similaire.
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']
Et pour pouvoir travailler avec les migrations. Les migrations Crax sont un peu de code au-dessus de SQLAlchemy Alembic. Puisque nous parlons d'espaces de noms et de séparation de la logique, alors,
évidemment, nous aimerions stocker les migrations dans le même package que l'autre logique de cet espace de noms. C'est ainsi que fonctionnent les migrations Crax. Toutes les migrations seront distribuées en fonction de leur espace de noms, et si cet espace de noms implique de travailler avec différentes bases de données, alors dans le répertoire de migration, il y aura une division en répertoires des bases de données correspondantes. La même chose s'applique aux migrations hors ligne - tous les fichiers * .sql seront divisés en fonction de l'espace de noms et de la base de données du modèle. Je ne vais pas peindre ici sur l'écriture de requêtes - c'est dans la documentation, je dirai seulement que vous travaillez toujours avec SQLAlchemy Core.
Encore une fois, les espaces de noms impliquent un stockage pratique des modèles (l'héritage et d'autres fonctionnalités Jinja2 sont pris en charge + quelques équipements sous la forme de jetons CSRF prêts à l'emploi ou de génération d'URL). Autrement dit, tous vos modèles sont structurés. Eh bien, bien sûr, je ne suis pas coincé dans une glorieuse 2007, je comprends que les modèles (même s'ils sont rendus de manière asynchrone) seront peu demandés en 2020. Et que, très probablement, vous êtes heureux de séparer la logique du frontend et du backend. Crax fait un excellent travail, les résultats peuvent être consultés sur Github.
IciVueJs est utilisé comme interface. Et comme nous avons une sorte d'API, nous voulons probablement créer une documentation interactive. Crax peut créer une documentation OpenAPI (Swagger) prête à l'emploi en fonction de vos listes de routes et de vos docstrings de gestionnaire. Tous les exemples, bien sûr, sont dans la documentation.
Avant de passer à la partie la plus intéressante de notre bref aperçu, il convient de parler un peu des batteries utiles déjà fournies avec le Crax.
Naturellement, le mode débogage est lorsque l'erreur et la trace complète peuvent être lues directement dans le navigateur, sur la page où le malheur s'est produit. Le mode de débogage peut être désactivé et personnalisé avec
Enregistreur intégré avec la possibilité d'écrire simultanément dans le fichier spécifié et d'envoyer des journaux à la console (ou de faire une chose). Possibilité d'attribuer votre propre enregistreur au lieu de celui par défaut. Prise en charge de Sentry en ajoutant deux lignes à la configuration (et, si nécessaire, personnalisation).
Deux types de middleware préinstallés. Le premier est traité AVANT que la demande soit traitée par l'application, et le second APRÈS.
Prise en charge intégrée des en-têtes CORS. Il vous suffit de déclarer les règles CORS dans la configuration.
Possibilité de définir les méthodes disponibles pour chaque gestionnaire directement sur site. Chaque gestionnaire fonctionnera avec la liste des méthodes HTTP spécifiées (+ HEAD et OPTIONS), ou uniquement avec GET, HEAD et OPTIONS.
Possibilité de spécifier que ce gestionnaire est disponible uniquement pour les utilisateurs autorisés, ou uniquement pour les utilisateurs du groupe Administrateurs, ou uniquement pour les membres du rôle de superutilisateur.
Il existe une autorisation pour les sessions signées HMAC, pour lesquelles vous n'avez pas besoin d'accéder à la base de données et à un certain nombre d'outils de création et de gestion des utilisateurs. Vous pouvez activer la prise en charge du backend d'autorisation et obtenir un utilisateur prédéfini et un certain nombre d'outils avec lesquels travailler. Cependant, comme la plupart des outils Crax, vous pouvez le laisser de côté, l'utiliser et écrire le vôtre. Vous ne pouvez pas utiliser l'autorisation, les bases de données, les modèles, les migrations, les vues et écrire complètement vos propres solutions personnalisées. Vous n'avez pas besoin de faire d'efforts pour le faire, vous ne l'avez pas allumé - ce n'est pas le cas.
Il existe plusieurs types de réponse et plusieurs types de gestionnaires basés sur les classes qui vous aideront à écrire des applications plus rapidement et de manière plus concise. Dans ce cas, le vôtre fonctionnera également, ce qui n'hérite pas de ceux intégrés.
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
Prise en charge de la protection CSRF. Génération de jetons, vérification de la présence d'un jeton dans le corps de la requête,
désactivation de la vérification pour des gestionnaires spécifiques.
Prise en charge de la protection ClickJacking (politiques de rendu Frame, iframe, embed ...)
Prise en charge de la vérification de la taille maximale autorisée d'une requête AVANT que l'application ne commence à la traiter.
Prise en charge native de websocket. Prenons un exemple de la documentation et écrivons une application simple qui peut envoyer des messages websocket par diffusion, par groupe d'utilisateurs ou des messages à un utilisateur spécifique. Supposons que nous ayons des groupes «garçons» et «filles» (il est possible d'ajouter un groupe «parents»). Nous pouvons écrire quelque chose de similaire pour un exemple (bien sûr, ce n'est pas un code produit).
#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>
Le code complet peut être consulté dans la documentation Crax.
Eh bien, le moment est venu pour le plus intéressant de cet article.
Pourquoi est-ce inutile?
Premièrement, comme mentionné ci-dessus, il existe plusieurs cadres qui font de même et qui ont une communauté déjà formée. Alors que Crax est un bébé âgé d'une semaine. L'armée d'un seul homme est presque une garantie que tôt ou tard le projet sera abandonné. C'est triste, mais le fait de travailler sur la table, de publier des versions et des mises à jour uniquement pour vous et Vasily de Syktyvkar, est beaucoup plus long que lorsque la communauté travaille sur le projet. En attendant, le projet ne dispose pas d'un certain nombre de fonctionnalités indispensables en 2020. Par exemple: pas de support JWT (JOSE). Il n'y a pas de support prêt à l'emploi pour OAuth2. Pas de support GraphQL. Il est clair que vous pouvez l'écrire vous-même pour votre projet, mais Starlette ou FastAPI l'a déjà. Je dois juste écrire ceci (oui, c'est dans les plans). Il y aura un peu sur les plans en conclusion.
Les développeurs de Netflix et Microsoft écrivent sur FastAPI. A propos de Crax écrit noname, on ne sait pas où il est apparu, et qui sait où est capable d'exactement après-demain l'abîme. Ils
n'appelleront pas un bateau à vapeur par mon nom idiot
Ma mère pleure la nuit, car elle a donné naissance à un monstre ...
(c)
C'est important. Cela s'appelle la réputation et l'écosystème. Crax n'a pas non plus. Sans ces éléments importants, le projet est assuré d'aller à la décharge sans jamais naître.
Cela vaut la peine d'être compris. Ce qui est écrit ci-dessus n'est pas une tentative de taper des cours ni le texte d'une personne sans domicile fixe dans le train. Il s'agit d'un bilan sobre et d'un avertissement que «les solutions prêtes à la production» ne sont pas seulement les résultats de la couverture du code source par des tests, c'est une évaluation générale de la maturité des technologies, de l'approche et des solutions utilisées dans le projet.
Si vous commencez à peine à vous familiariser avec Python et à essayer des frameworks, vous êtes en danger: vous ne trouverez probablement pas de réponses à la question sur SO, peut-être que des camarades plus expérimentés vous aideront, qui, malheureusement, ne sont peut-être pas là.
Les buts
La première chose que je prévois de faire est, bien sûr, d'ajouter des éléments indispensables comme le support de JWT (JOSE), OAuth2 et GraphQL. C'est ce qui facilitera le travail pour moi et pour les personnes intéressées. Et c'est, en fait, l'objectif principal de Crax: rendre le travail de quelqu'un un peu plus facile. Peut-être que d'ici là, un nouveau cycle à TechEmpower commencera et les repères deviendront plus évidents. Il est même possible qu'après cela, il y ait un certain intérêt pour la communauté.
Il y a une idée d'écrire un CMS basé sur Crax.
Si je ne me trompe pas (si je me trompe - corrigez-le), nous n'avons pas encore de CMS asynchrone en Python dans notre boîte à outils. Je pourrais changer d'avis et décider d'écrire une sorte de solution de commerce électronique. Mais, évidemment, afin d'éviter que Crax ne se noie avant d'atteindre les bouées, quelque chose d'intéressant doit être fait sur sa base. Peut-être que les passionnés seront intéressés par cela. Les amateurs sont quand c'est gratuit. Parce qu'il n'y a pas d'argent ici et, très probablement, il n'y en aura pas. Crax est totalement gratuit pour tout le monde et je n'ai pas reçu un sou pour ce travail. Ainsi, le développement est prévu pour de "longues soirées d'hiver" et, peut-être, dans l'année à venir, quelque chose d'intéressant va naître.
Conclusion
Je réfléchissais à quel groupe inclure cet article (au fait, c'est ma première publication sur la ressource). Peut-être que cela valait même la peine de le placer sous l'étiquette "I'm PR". Ce qui m'a fait changer d'avis: tout d'abord, le fait qu'elle n'ait un caractère publicitaire de rien.
Il n'y a pas d'appel "Garçons, inscrivez-vous d'urgence pour les demandes de tirage" . Il n'y a aucune idée de trouver un sponsor ici. Il n'y a même pas une idée que je vous ai apporté quelque chose que vous n'avez jamais vu (bien sûr, vu). Vous pouvez faire abstraction de l'idée que je suis l'auteur des deux articles et de cet outil, et percevoir ce qui a été écrit comme une critique. Et oui, c'est la meilleure façon. Ce sera un excellent résultat pour moi si vous gardez à l'esprit que c'est le cas.
Sur ce, peut-être, tout.
«Alors… il est temps de prendre les cannes à pêche.
- Pourquoi?
- Le bonnet rouge de Harris a fait peur à tous les poissons.
(c)
Code sur la documentation GitHub