Vous êtes-vous déjà demandé où en était votre long traitement et quand se terminerait-il ? Appuyez-vous généralement sur RETURN
plusieurs fois pour vous assurer qu'il ne plante pas ou que la connexion SSH ne se bloque pas ? Avez-vous déjà pensé que ce serait génial de pouvoir suspendre un traitement sans problème, revenir à l'invite Python pour corriger manuellement certains éléments, puis le reprendre de manière transparente ? Je l'ai fait...
J'ai commencé cette nouvelle barre de progression en pensant à tout ça, voilà la progression vivante ! ?
Présentation du nouveau concept de barres de progression pour Python ! alive-progress
est dans une classe à part, avec une gamme de fonctionnalités intéressantes qui le distinguent. Voici quelques faits saillants :
check()
super puissant qui vous aide à concevoir vos propres animations ! Vous pouvez voir à quoi ressembleront les images et les cycles d'animation générés, explosés sur votre écran, et même les voir vivants avant l'installation alive-progress
! C'est l'outil le plus cool au monde ! Libérez votre créativité! Ce README est en constante évolution, alors jetez-y un coup d'œil plus complet de temps en temps... Vous trouverez peut-être de nouveaux détails intéressants dans d'autres sections ! ?
alive-progress
bar()
Après environ un an de stabilité rassurante, le nouveau alive-progress
est enfin arrivé !
Les principales fonctionnalités et améliorations sont :
bar()
avec 0
et même -N
pour le faire reculer ! Utile lorsque vous ne parvenez pas à progresser dans une itération ou que vous devez annuler quelque chose !Et bien plus encore !
enrich_offset
personnalisé à utiliser pour les messages imprimés ou enregistrés, vous permettant de commencer avec on 1:
ou de continuer là où vous en étiez dans les calculs précédents ! Une mise à jour très cool ici ! En plus de peaufiner les choses et d'améliorer la prise en charge des terminaux, alive-progress
prend désormais en charge la reprise des calculs !
Lorsque vous traitez d'énormes ensembles de données ou des choses qui prennent beaucoup de temps, vous pouvez soit utiliser des lots, soit mettre en cache des résultats partiels. Ensuite, au cas où il s'arrêterait et serait redémarré, vous finirez par sauter très rapidement tous ces éléments déjà effectués, ce qui fait croire à alive_bar
que vous traitez des milliers d'éléments par seconde, ce qui à son tour ruine complètement l'ETA... Mais plus maintenant. ! Dites simplement bar()
que vous avez sauté des éléments... ?
Vous pouvez l'utiliser de deux manières :
1. Si vous savez où vous vous êtes arrêté :
with alive_bar ( 120000 ) as bar :
bar ( 60000 , skipped = True )
for i in range ( 60000 , 120000 ):
# process item
bar ()
Oui, appelez simplement bar(N, skipped=True)
une fois, avec le nombre d'éléments.
2. Si vous ne savez pas ou si les objets sont dispersés :
with alive_bar ( 120000 ) as bar :
for i in range ( 120000 ):
if done ( i ):
bar ( skipped = True )
continue
# process item
bar ()
Oui, c'est aussi simple que ça ! Appelez simplement bar(skipped=True)
lorsqu'un élément est déjà terminé, ou bar()
comme d'habitude sinon. Vous pouvez également partager un seul appel bar(skipped=?)
à la fin, avec un booléen indiquant si vous avez ignoré cet élément ou non. Cool, hein ?
Egalement dans cette version :
max_cols
, le nombre de colonnes à utiliser s'il n'est pas possible de le récupérer, comme dans jupyter et d'autres plates-formes qui ne prennent pas en charge la tailleOui, je pourrais enfin sortir cette version ! Voici les nouveaux goodies :
B
, bytes
ou même °C
!sys.stderr
et d'autres fichiers au lieu de sys.stdout
!Correctifs très attendus :
TypeError: unhashable type: 'types.SimpleNamespace'
.RotatingFileHandler
s ! Oui, demander de l'aide est ici.Et enfin, une mise en page plus soignée pour que vous puissiez profiter de votre progression !
Désormais, alive_bar
prend en charge le mode texte Dual Line !
Si vous avez toujours voulu inclure des messages situationnels plus longs dans la barre, vous vous êtes probablement senti coincé sur une seule ligne. Il fallait réduire la barre joliment animée ou, pire encore, supprimer des widgets (!) pour pouvoir voir ce dont vous aviez besoin...
Pas plus!! Vous pouvez maintenant créer la barre Dual Line et mettre du texte en dessous !
Oui, il y a un message sous toute la barre, et tous les autres messages d'impression/journalisation défilent au-dessus !
letters = [ chr ( ord ( 'A' ) + x ) for x in range ( 26 )]
with alive_bar ( 26 , dual_line = True , title = 'Alphabet' ) as bar :
for c in letters :
bar . text = f'-> Teaching the letter: { c } , please wait...'
if c in 'HKWZ' :
print ( f'fail " { c } ", retry later' )
time . sleep ( 0.3 )
bar ()
Sortir:
on 7: fail "H", retry later
on 10: fail "K", retry later
Alphabet |███████████████████████████▊ | ▃▅▇ 18/26 [69%] in 6s (3.2/s, eta: 3s)
-> Teaching the letter: S, please wait...
Il existe également un nouveau paramètre de fonction finalize
dans alive_it
qui vous permet de définir le titre et/ou le texte du reçu final, ainsi qu'une prise en charge améliorée de la journalisation qui détecte les enregistreurs personnalisés.
Tout est question de personnalisation ; les widgets principaux peuvent maintenant être modifiés :
monitor
, elapsed
et stats
pour leur donner l'apparence que vous souhaitez !C'est incroyable que ces chaînes prennent en charge toutes les fonctionnalités du format Python, vous pouvez donc par exemple
{percent:.1%}
.
Ils peuvent être personnalisés davantage sur le reçu final !
monitor_end
, elapsed_end
et stats_end
, avec des formats dynamiques hérités des standards !Si vous avez déjà masqué certains widgets, juste pour qu'ils n'apparaissent pas sur le reçu, vous pouvez désormais les voir dans toute leur splendeur et masquer uniquement ceux du reçu ! Ou l'inverse ?
Un autre ajout, désormais alive-progress
rend magnifiquement sa réception finale à chaque fois qu'il est arrêté, même si vous faites CTRL+C prématurément ! Je ne sais pas pourquoi je n'y ai pas pensé avant...
Download |██████████████████︎ | (!) 45/100 [45%] in 4.8s (9.43/s)
Et enfin, vous pouvez choisir de désactiver complètement CTRL+C ! La valeur par défaut est la valeur plus sûre ctrl_c=True
, ce qui permet à CTRL-C de fonctionner comme d'habitude.
Désactivez-le ctrl_c=False
, pour rendre votre alive_bar
interactive beaucoup plus fluide à utiliser (il n'y a pas de traces de pile si vous l'arrêtez), et/ou si elle se trouve au niveau supérieur de votre programme !
Attention : s'il se trouve par exemple à l'intérieur d'une boucle for, il continuera simplement à l'itération suivante, ce qui peut ou non être ce que vous voulez...
for i in range ( 10 ):
with alive_bar ( 100 , ctrl_c = False , title = f'Download { i } ' ) as bar :
for i in range ( 100 ):
time . sleep ( 0.02 )
bar ()
Sortir:
Download 0 |████████▊︎ | (!) 22/100 [22%] in 0.6s (36.40/s)
Download 1 |████████████████▊︎ | (!) 42/100 [42%] in 1.0s (41.43/s)
Download 2 |██████▍︎ | (!) 16/100 [16%] in 0.4s (39.29/s)
Download 3 |█████▋︎ | (!) 14/100 [14%] in 0.4s (33.68/s)
Download 4 |█████████████▎︎ | (!) 33/100 [33%] in 0.8s (39.48/s)
Download 5 |███████▎︎ | (!) 18/100 [18%] in 0.5s (37.69/s)
Download 6 |█████▎︎ | (!) 13/100 [13%] in 0.3s (37.28/s)
Download 7 |████████████︎ | (!) 30/100 [30%] in 0.8s (38.43/s)
Download 8 |██████︎ | (!) 15/100 [15%] in 0.4s (36.26/s)
...
Des nouveautés majeures, souvent demandées, sont enfin arrivées !
click.echo()
OUI! alive-progress
prend désormais en charge les notebooks Jupyter et inclut également un état désactivé ! Les deux étaient très recherchés et ont enfin atterri !
Et mieux, j'ai implémenté un mécanisme de détection automatique pour les notebooks Jupyter, donc cela fonctionne, prêt à l'emploi, sans aucune modification dans votre code !!
Voyez par vous-même :
Cela semble très bien fonctionner, mais pour le moment, cela devrait être considéré comme expérimental .
Il y a eu des cas dans lesquels des problèmes visuels sont apparus, comme deux actualisationsalive_bar
concaténées ensemble plutôt que l'une sur l'autre... Et c'est quelque chose que je pense que je ne peux pas contourner : il semble que Jupyter actualise parfois le canevas à des moments étranges, ce qui lui fait perdre certaines données. S'il vous plaît laissez-moi savoir sur les problèmes si quelque chose de plus drôle survient.
Il s’agit d’une avancée majeure dans le alive-progress
!
J'ai mis 1 an à le développer et je suis très fier de ce que j'ai accompli o/
.check()
puissants et raffinés qui compilent et restituent magnifiquement toutes les images de tous les cycles d'animation des spinners et des barres ! ils peuvent même inclure des données d'image complètes, des points de code internes et même leurs animations ! ?alive-progress
!alive_it
, qui accepte un itérable et appelle bar()
pour vous !Puisqu’il s’agit d’un changement de version majeur, la compatibilité descendante directe n’est pas garantie. Si quelque chose ne fonctionne pas au début, vérifiez simplement les nouvelles importations et signatures de fonctions, et vous devriez être prêt à partir. Toutes les fonctionnalités précédentes devraient toujours fonctionner ici ! ?
alive-progress
Installez simplement avec pip :
❯ pip install alive-progress
Si vous vous demandez quels styles sont intégrés, c'est showtime
! ;)
from alive_progress . styles import showtime
showtime ()
Remarque : Veuillez ne pas tenir compte du chemin indiqué dans le gif animé ci-dessous, le bon se trouve ci-dessus. La génération de ces longs gifs prend beaucoup de temps, je ne peux donc pas en créer un autre à chaque modification. Merci de votre compréhension.
J'ai créé ces styles juste pour essayer toutes les usines d'animation que j'ai créées, mais je pense que certaines d'entre elles se sont révélées très, très cool ! Utilisez-les à volonté et mélangez-les à votre guise !
Voulez-vous voir de véritables barres alive-progress
fonctionner glorieusement dans votre système avant de les essayer vous-même ?
❯ python -m alive_progress.tools.demo
Cool, hein ?? Entrez maintenant un REPL ipython
et essayez ceci :
from alive_progress import alive_bar
import time
for x in 1000 , 1500 , 700 , 0 :
with alive_bar ( x ) as bar :
for i in range ( 1000 ):
time . sleep ( .005 )
bar ()
Vous verrez quelque chose comme ceci, avec des animations sympas tout au long du processus ? :
|████████████████████████████████████████| 1000/1000 [100%] in 5.8s (171.62/s)
|██████████████████████████▋︎ | (!) 1000/1500 [67%] in 5.8s (172.62/s)
|████████████████████████████████████████✗︎ (!) 1000/700 [143%] in 5.8s (172.06/s)
|████████████████████████████████████████| 1000 in 5.8s (172.45/s)
Bien, hein ? Vous avez adoré ? Je savais que tu le ferais, merci ?.
Pour l'utiliser réellement, enveloppez simplement votre boucle normale dans un gestionnaire de contexte alive_bar
comme celui-ci :
with alive_bar ( total ) as bar : # declare your expected total
for item in items : # <<-- your original loop
print ( item ) # process each item
bar () # call `bar()` at the end
Et c'est vivant ! ?
Donc, en un mot : récupérez les éléments comme toujours, entrez dans le gestionnaire de contexte alive_bar
avec le nombre d'éléments, puis itérez/traitez ces éléments, en appelant bar()
à la fin ! C'est aussi simple que ça ! :)
items
peuvent être n'importe quel itérable, comme par exemple un ensemble de requêtes ;alive_bar
est le total attendu, comme qs.count()
pour les ensembles de requêtes, len(items)
pour les itérables avec une longueur, ou même un nombre statique ;bar()
est ce qui fait avancer la barre — vous l'appelez généralement à chaque itération, juste après avoir terminé un élément ;bar()
(ou trop peu à la fin), la barre restituera graphiquement cet écart par rapport au total
attendu, ce qui permettra de remarquer très facilement les débordements et les sous-débordements ;bar.current
.Vous pouvez faire preuve de créativité ! Puisque la barre n'avance que lorsque vous appelez
bar()
, elle est indépendante de la boucle ! Vous pouvez donc l'utiliser pour surveiller tout ce que vous voulez, comme les transactions en attente, les éléments cassés, etc., ou même l'appeler plus d'une fois dans la même itération ! Ainsi, au final, vous saurez combien de ces événements « spéciaux » ont eu lieu, y compris leur pourcentage par rapport au total !
Dans un contexte alive_bar
, vous pouvez afficher sans effort des messages étroitement intégrés à la barre de progression actuelle affichée ! Il ne se cassera en aucun cas et enrichira même votre message !
bar.text('message')
et bar.text = 'message'
définissent un message situationnel directement dans la barre, où vous pouvez afficher quelque chose sur l'élément en cours ou la phase dans laquelle se trouve le traitement ;bar.title('Title')
et bar.title = 'Title'
— mélanger avec title_length
pour empêcher la barre de changer de longueur;print()
habituelle, où alive_bar
nettoie bien la ligne, imprime votre message à côté de la position actuelle de la barre à ce moment-là et continue la barre juste en dessous ;logging
Python standard, y compris les sorties de fichiers, est également enrichi exactement comme le précédent ;click.echo()
pour imprimer du texte stylisé.Génial, non ? Et tout cela fonctionne de la même manière dans un terminal ou dans un notebook Jupyter !
Vous disposez désormais d’un moyen plus rapide de tout surveiller ! Ici, les articles sont automatiquement suivis pour vous !
Voici l'adaptateur d'itérateur alive_it
=> alive_bar
!
Enveloppez simplement vos articles avec et faites une boucle dessus comme d'habitude !
Le bar fonctionnera tout simplement ; c'est aussi simple que cela !
from alive_progress import alive_it
for item in alive_it ( items ): # <<-- wrapped items
print ( item ) # process each item
C'EST COMMENT COOL ?! ?
Tous les paramètres alive_bar
s'appliquent mais total
, qui est plus intelligent (s'il n'est pas fourni, il sera automatiquement déduit de vos données en utilisant len
ou length_hint
), et manual
qui n'a pas de sens ici.
Notez qu’il n’y a aucune poignée bar
là-dedans. Mais que se passe-t-il si vous le souhaitez, par exemple pour définir des messages texte ou récupérer la progression en cours ?
Vous pouvez interagir avec l' alive_bar
interne en attribuant simplement alive_it
à une variable comme celle-ci :
bar = alive_it ( items ) # <<-- bar with wrapped items
for item in bar : # <<-- iterate on bar
print ( item ) # process each item
bar . text ( f'ok: { item } ' ) # WOW, it works!
Notez qu'il s'agit d'un bar
légèrement spécial, qui ne prend pas en charge bar()
, puisque l'adaptateur d'itérateur suit automatiquement les éléments pour vous. En outre, il prend en charge finalize
, qui vous permet de définir le titre et/ou le texte du reçu final :
alive_it ( items , finalize = lambda bar : bar . text ( 'Success!' ))
...
En un mot:
- l'utilisation complète est toujours
with alive_bar() as bar
, où vous itérez et appelezbar()
quand vous le souhaitez ;- L'utilisation rapide de l'adaptateur
for item in alive_it(items)
, où les éléments sont automatiquement suivis ;- L'utilisation complète de l'adaptateur est
bar = alive_it(items)
, où en plus des éléments suivis automatiquement, vous obtenez unebar
itérable spéciale capable de personnaliser l'alive_progress
interne comme vous le souhaitez.
Les modes par défaut sont auto et inconnu , qui utilisent en interne un compteur pour suivre la progression. Ils comptent le nombre d'éléments traités et l'utilisent pour mettre à jour la barre de progression en conséquence.
L'argument total
est facultatif. Si vous le fournissez, la barre entre en mode automatique . Dans ce mode, la progression de l'opération est automatiquement suivie et tous les widgets alive-progress
a à offrir sont disponibles : barre précise, compteur, pourcentage, compteur, débit et ETA.
Si vous ne fournissez pas total
, la barre passe en mode inconnu . Dans ce mode, la progression est indéterminable, et donc l'ETA, donc toute la barre de progression est animée en permanence. Les widgets disponibles sont : barre animée, spinner, compteur et débit.
Le spinner cool fonctionne de manière complètement indépendante de la barre d'animation, les deux exécutant leurs propres animations simultanément, rendant ainsi un spectacle unique dans votre terminal ! ?
Enfin et surtout, le mode automatique a une capacité unique : marquer les éléments comme ignorés, ce qui rend le débit et l'ETA beaucoup plus précis ! Nous en reparlerons plus tard.
Le mode manuel , activé manuellement par l'argument manual=True
, utilise en interne un pourcentage pour suivre la progression. Il vous permet d’avoir un contrôle total sur la position de la barre. Il est généralement utilisé pour surveiller des processus qui ne vous fournissent qu'un pourcentage d'achèvement ou pour générer des effets spéciaux aléatoires.
Vous pouvez l'utiliser directement avec alive_bar
ou via config_handler
, et il vous permet d'envoyer des pourcentages au gestionnaire bar()
! Par exemple, pour le régler à 15 % d'achèvement, appelez simplement bar(0.15)
- qui est 15/100.
Vous pouvez également fournir total
ici. Si vous le faites, alive-progress
en déduira automatiquement un compteur interne, et pourra ainsi vous proposer tous les mêmes widgets disponibles en mode auto !
Si vous ne fournissez pas total
, vous obtiendrez au moins des versions approximatives du débit et des widgets ETA, calculés respectivement en "%/s" (pourcentage par seconde) et jusqu'à 100 %. Aucun d’eux n’est très précis, mais ils valent mieux que rien.
Lorsque total
est fourni, tout va bien :
mode | comptoir | pourcentage | débit | ETA | débordement/sous-versement |
---|---|---|---|---|---|
auto | ✅ (coche de l'utilisateur) | ✅ (déduit) | ✅ | ✅ | ✅ |
manuel | ✅ (déduit) | ✅ (ensemble utilisateur) | ✅ | ✅ | ✅ |
Dans le cas contraire, certains compromis doivent être faits :
mode | comptoir | pourcentage | débit | ETA | débordement/sous-versement |
---|---|---|---|---|---|
inconnu | ✅ (coche de l'utilisateur) | ✅ | |||
manuel | ✅ (ensemble utilisateur) | ✅ |
Mais c'est en fait simple à comprendre : vous n'avez pas besoin de réfléchir au mode que vous devez utiliser !
total
si vous l’avez et utilisez manual
si vous en avez besoin !C'est ça! Cela fonctionnera du mieux possible ! ? o/
bar()
Les gestionnaires bar()
prennent en charge la sémantique relative ou absolue, selon le mode :
bar()
pour incrémenter le compteur de un, ou envoyer tout autre incrément comme bar(200)
pour incrémenter de 200 à la fois ;ils prennent même en charge
bar(0)
etbar(-5)
pour maintenir ou décrémenter si nécessaire !
bar(0.35)
pour faire passer instantanément la barre à 35 % de progression.Les deux modes vous permettent de faire preuve de créativité ! Puisque vous pouvez simplement faire passer la barre instantanément dans la position de votre choix, vous pouvez :
- faire reculer — par exemple pour afficher graphiquement le délai d'attente de quelque chose ;
- créer des effets spéciaux - par exemple pour imiter une sorte de jauge analogique en temps réel.
Vous pouvez appeler bar()
autant de fois que vous le souhaitez ! Le taux de rafraîchissement du terminal sera toujours calculé de manière asynchrone en fonction du débit et de la progression actuels, vous ne risquerez donc pas de spammer le terminal avec plus de mises à jour que nécessaire.
Dans tous les cas, pour récupérer le compteur/pourcentage actuel, il suffit d'appeler : bar.current
:
Enfin, le gestionnaire bar()
exploite la capacité unique du mode automatique : il suffit d'appeler bar(skipped=True)
ou bar(N, skipped=True)
pour l'utiliser. Lorsque skipped
est défini sur = True
, les éléments associés sont exclus des calculs de débit, empêchant ainsi les éléments ignorés d'affecter de manière inexacte l'ETA.
Maintenir un projet open source est difficile et prend du temps, et j'y ai consacré beaucoup de ❤️ et d'efforts.
Si vous avez apprécié mon travail, vous pouvez me soutenir avec un don ! Merci ?
L'exposition showtime
a un argument facultatif pour choisir quelle émission présenter, Show.SPINNERS
(par défaut), Show.BARS
ou Show.THEMES
, jetez-y un œil ! ;)
from alive_progress . styles import showtime , Show
showtime ( Show . BARS )
showtime ( Show . THEMES )
Remarque : Veuillez ne pas tenir compte du chemin indiqué dans le gif animé ci-dessous, le bon se trouve ci-dessus. La génération de ces longs gifs prend beaucoup de temps, je ne peux donc pas en créer un autre à chaque modification. Merci de votre compréhension.
Et celui des thèmes (? nouveau en 2.0) :
L'exposition showtime
accepte également certaines options de personnalisation :
Par exemple pour obtenir un spectacle marin, vous pouvez showtime(pattern='boat|fish|crab')
:
Vous pouvez également accéder à ces émissions avec les raccourcis
show_bars()
,show_spinners()
etshow_themes()
!
Il existe également un petit utilitaire appelé
print_chars()
, pour vous aider à trouver le caractère sympa à insérer dans vos flèches et barres personnalisées, ou pour déterminer si votre terminal prend en charge les caractères Unicode.
Il existe plusieurs options pour personnaliser à la fois l’apparence et le comportement !
Tous peuvent être définis directement dans alive_bar
ou globalement dans config_handler
!
Voici les options - valeurs par défaut entre parenthèses :
title
: un titre de barre facultatif et toujours visiblelength
: [ 40
] le nombre de colonnes pour afficher la barre de progression animéemax_cols
: [ 80
] le nombre maximum de colonnes à utiliser s'il n'est pas possible de le récupérer, comme dans jupyterspinner
: le style spinner à rendre à côté de la barrebar
: le style de barre à rendre dans les modes connusunknown
: le style de barre à rendre en mode inconnutheme
: [ 'smooth'
] un ensemble de spinner, de barre et d'inconnu correspondantsforce_tty
: [ None
] force les animations à être activées, désactivées ou selon le tty (plus de détails ici)file
: [ sys.stdout
] l'objet fichier à utiliser : sys.stdout
, sys.stderr
ou un TextIOWrapper
similairedisable
: [ False
] si True, désactive complètement toutes les sorties, n'installe pas de hooksmanual
: [ False
] défini pour contrôler manuellement la position de la barreenrich_print
: [ True
] enrichit print() et les messages de journalisation avec la position de la barreenrich_offset
: [ 0
] le décalage à appliquer à enrich_printreceipt
: [ True
] imprime le bon reçu final, désactive si Fauxreceipt_text
: [ False
] défini pour répéter le dernier message texte du reçu finalmonitor
(bool|str): [ True
] configure le widget de moniteur 152/200 [76%]
{count}
, {total}
et {percent}
pour la personnaliserelapsed
(bool|str) : [ True
] configure le widget de temps écoulé in 12s
{elapsed}
pour la personnaliserstats
(bool|str) : [ True
] configure le widget de statistiques (123.4/s, eta: 12s)
{rate}
et {eta}
pour la personnalisermonitor_end
(bool|str) : [ True
] configure le widget de moniteur dans le reçu finalmonitor
elapsed_end
(bool|str) : [ True
] configure le widget de temps écoulé dans le reçu finalelapsed
stats_end
(bool|str) : [ True
] configure le widget de statistiques dans le reçu final{rate}
pour la personnaliser (aucun rapport avec les statistiques)title_length
: [ 0
] fixe la longueur des titres, ou 0 pour illimitéspinner_length
: [ 0
] force la longueur du spinner, ou 0
pour sa longueur naturellerefresh_secs
: [ 0
] force la période de rafraîchissement à cela, 0
est le retour visuel réactifctrl_c
: [ True
] si False, désactive CTRL+C (le capture)dual_line
: [ False
] si True, place le texte sous la barreunit
: tout texte qui étiquette vos entitésscale
: la mise à l'échelle à appliquer aux unités : None
, SI
, IEC
ou SI2
False
ou ''
-> None
, True
-> SI
, 10
ou '10'
-> SI
, 2
ou '2'
-> IEC
precision
: [ 1
] combien de décimales s'affichent lors de la mise à l'échelle Et il y en a aussi un qui ne peut être défini que localement dans le contexte alive_bar
:
calibrate
: débit théorique maximum pour calibrer la vitesse d'animation (plus de détails ici) Pour les définir localement, envoyez-les simplement comme arguments de mots-clés à alive_bar
:
with alive_bar ( total , title = 'Processing' , length = 20 , bar = 'halloween' ) as bar :
...
Pour les utiliser globalement, envoyez-les à config_handler
, et tout alive_bar
créé par la suite inclura ces options ! Et vous pouvez les mélanger et les assortir, les options locales ont toujours priorité sur les options globales :
from alive_progress import config_handler
config_handler . set_global ( length = 20 , spinner = 'wait' )
with alive_bar ( total , bar = 'blocks' , spinner = 'twirls' ) as bar :
# the length is 20, the bar is 'blocks' and the spinner is 'twirls'.
...
Oui, vous pouvez assembler vos propres toupies ! Et c'est facile !
J'ai créé une multitude d'effets spéciaux, vous pouvez donc les mélanger et les assortir comme vous le souhaitez ! Il y a des spinners d'images, de défilement, de rebond, séquentiels, parallèles et retardés ! Soyez créatif ! ?
Les animations des spinners sont conçues par des expressions génératrices très avancées, profondément ancrées dans plusieurs couches de méta-usines, d'usines et de générateurs ?!
alive_bar
et config_handler
;Ces générateurs sont capables de plusieurs cycles d'animation différents en fonction du comportement de la roulette, par exemple une roulette rebondissante peut exécuter un cycle pour amener en douceur un sujet dans la scène, puis le repositionner à plusieurs reprises jusqu'à l'autre côté, puis le faire disparaître en douceur de la scène = > et tout cela n'est qu'un seul cycle ! Ensuite, il peut être suivi d'un autre cycle pour tout refaire mais à l'envers ! Et les filateurs rebondissants acceptent également des motifs différents et alternés dans les directions droite et gauche, ce qui leur permet de générer le produit cartésien de toutes les combinaisons, produisant éventuellement des dizaines de cycles différents jusqu'à ce qu'ils commencent à les répéter !! ?
Et ce n'est pas tout, je pense que c'est l'une des réalisations les plus impressionnantes que j'ai obtenues dans ce système d'animation (outre le compilateur spinner lui-même)... Ils ne produisent plus d'images d'animation que jusqu'à ce que le cycle en cours ne soit pas épuisé, puis ils s'arrêtent ! Eh oui, le prochain cycle ne commence pas encore ! Ce comportement crée des pauses naturelles exactement aux bons endroits, où les animations ne sont pas perturbées, afin que je puisse me connecter en douceur avec n'importe quelle autre animation que je veux !!
Cela a toutes sortes d'implications intéressantes : les cycles peuvent avoir différents nombres d'images, différentes longueurs d'écran, ils n'ont pas besoin d'être synchronisés, ils peuvent créer eux-mêmes de longues séquences différentes, ils peuvent coopérer pour lire des cycles en séquence ou parallèlement, et je peut vous étonner en affichant plusieurs animations totalement distinctes en même temps sans aucune interférence !
C'est presque comme s'ils étaient... vivants !! ?
==> Oui, c'est de là que vient le nom de ce projet !
Désormais, ces générateurs de cycles et de frames sont entièrement consommés en avance par le Spinner Compiler ! Il s'agit d'un nouveau processeur très cool que j'ai créé dans le cadre de l'effort Cell Architecture , pour faire fonctionner toutes ces animations même en présence de caractères larges ou de clusters de graphèmes complexes ! Il était très difficile de faire entrer et sortir ces clusters progressivement, en douceur, tout en les empêchant de casser l'encodage Unicode et surtout de conserver leurs longueurs d'origine dans toutes les frames ! Oui, plusieurs caractères en séquence peuvent représenter un autre symbole complètement différent, ils ne peuvent donc jamais être divisés ! Ils doivent entrer et sortir du cadre toujours ensemble, tous en même temps, sinon le graphème n'apparaîtra pas du tout (un Emoji par exemple) !! Entrez dans le compilateur Spinner ......
Cela a rendu possible des choses incroyables !! Puisque ce compilateur génère au préalable toutes les données du spinner frame :
Ainsi, je peux simplement collecter toutes ces animations prêtes à être lues et en finir avec elles, sans aucune surcharge d'exécution !! ?
De plus, avec les données complètes de l'image compilées et conservées, j'ai pu créer plusieurs commandes pour refactoriser ces données, comme changer de forme, remplacer des caractères, ajouter des pauses visuelles (répétitions d'images), générer des effets de rebond à la demande sur n'importe quel contenu et même transposer des cycles. avec des cadres !!
Mais comment voir ces effets ? L’effet que vous avez créé est-il bon ? Ou est-ce que cela ne fonctionne pas comme vous le pensiez ? OUI, vous pouvez maintenant voir tous les cycles et images générés de manière analytique, dans un très beau rendu !!
J'adore ce que j'ai réalisé ici ?, c'est probablement LE plus bel outil que j'ai jamais créé... Voici l'outil check
!!
C'est génial si je le dis moi-même, n'est-ce pas? Et un logiciel très complexe dont je suis fier, jetez un œil à son code si vous le souhaitez.
Et l'outil check
est beaucoup plus puissant! Par exemple, vous pouvez voir les points de code des cadres !!! Et peut-être avoir un aperçu de la raison pour laquelle cette version était si, si dure et complexe à faire ...
En rouge, vous voyez les grappes Graphème, qui occupent une ou deux "positions logiques", quelles que soient leurs tailles réelles ... ce sont les "cellules" de la nouvelle architecture cellulaire ...
Regardez à quel point un drapeau emoji est génial:
Le drapeau semble se déplacer si bien car il utilise des "demi-caractères"! Puisqu'il s'agit d'un large char, alive-progress
sait qu'il sera rendu avec "deux caractères visibles", et les animations considèrent cela, mais composent avec des espaces, qui n'en occupent qu'un. Quand on utilise des arrière-plans mitigés, la situation est beaucoup plus complexe ...
Les types d'usines que j'ai créés sont:
frames
: dessine n'importe quelle séquence de caractères à volonté, qui sera lue par image en séquence;scrolling
: génère un flux lisse d'un côté à l'autre, se cachant derrière ou enveloppant des bordures invisibles - permet d'utiliser des sujets l'un à la fois, générant plusieurs cycles de caractères distincts;bouncing
: similaire à scrolling
, mais fait rebondir les animations au début, se cacher derrière ou rebondir immédiatement sur les frontières invisibles;sequential
obtient une poignée d'usines et jouez-les une après les autres séquentiellement! permet de les mélanger ou non;alongside
à obtenir une poignée d'usines et à les jouer simultanément, pourquoi choisir quand vous pouvez les avoir tous ?! permet de choisir le pivot de l'animation;delayed
: obtenez n'importe quelle autre usine et copiez-le plusieurs fois, en sautant de plus en plus certaines cadres sur chacun! Des effets très cool sont faits ici!Pour plus de détails, veuillez consulter leurs docstrings, qui sont très complets.
La personnalisation des barres est loin d'être impliquée. Disons qu'ils sont des objets "immédiats" et passifs. Ils ne prennent pas en charge les animations, c'est-à-dire qu'ils généreront toujours la même interprétation compte tenu des mêmes paramètres. N'oubliez pas que les filateurs sont des générateurs infinis, capables de générer des séquences longues et complexes.
Eh bien, les bars ont également une méta-usine, utilisent des fermetures pour stocker les paramètres de style et recevoir des paramètres de fonctionnement supplémentaires, mais l'usine réelle ne peut générer aucun contenu par lui-même. Il a encore besoin d'un paramètre supplémentaire, un nombre de points flottants entre 0 et 1, ce qui est le pourcentage pour se rendre lui-même.
alive_bar
calcule ce pourcentage automatiquement en fonction du comptoir et du total, mais vous pouvez l'envoyer vous-même en modemanual
!
Les bars n'ont pas non plus de compilateur de bar, mais ils fournissent l'outil de contrôle !! ?
Vous pouvez même mélanger et assortir de larges caractères et des caractères normaux comme dans les filateurs! (Et tout reste parfaitement aligné?)
Utilisez les outils de contrôle pour le contenu de votre cœur !! Ils ont encore plus de goodies qui vous attendent, même des animations en temps réel!
Créez les animations les plus folles et les plus cool que vous pouvez et me les envoyer!
Je pense à créer une sorte de packagecontrib
, avec des filateurs et des barres contrôlés par l'utilisateur!
Wow, si vous avez tout lu jusqu'à ici, vous devriez maintenant avoir une saine connaissance de l'utilisation alive-progress
! ?
Mais accumulez-vous parce qu'il y a encore plus, des choses excitantes nous attendent!
Le maintien d'un projet open source est difficile et long, et j'ai mis beaucoup de choses et d'efforts.
Si vous avez apprécié mon travail, vous pouvez me soutenir avec un don! Merci ?
Oh, tu veux le mettre en pause complètement, j'entends? Ceci est un concept roman incroyable, qui ne trouve nulle part Afaik.
Avec cela, vous pouvez agir sur certains articles manuellement , à volonté, en plein milieu d'un traitement continu !!
Oui, vous pouvez revenir à l'invite et réparer, changer, soumettre des choses, et la barre "se souviendra" où elle était ...
Supposons que vous ayez besoin de concilier les transactions de paiement (été là, ce qui est fait). Vous devez les itérer plus de milliers, détecter en quelque sorte ceux qui sont défectueux et les réparer. Ce correctif n'est ni simple ni déterministe, vous devez étudier chacun pour comprendre quoi faire. Ils pourraient manquer un destinataire, ou avoir le mauvais montant, ou ne pas être synchronisés avec le serveur, etc., il est difficile d'imaginer toutes les possibilités.
En règle générale, vous devrez laisser le processus de détection s'exécuter jusqu'à la fin, appuyer sur une liste de chaque incohérence qu'il trouve et attendre, potentiellement longtemps, jusqu'à ce que vous puissiez enfin commencer à les réparer ... vous pouvez bien sûr l'atténuer en traitant en morceaux , ou les imprimer et agir via un autre coquille, etc., mais ceux-ci ont leurs propres lacunes ...?
Maintenant, il y a une meilleure façon! Faites simplement une pause du processus de détection réel pendant un certain temps! Ensuite, il vous suffit d'attendre que la défaillance suivante soit trouvée et d'agir en temps quasi réel!
Pour utiliser le mécanisme de pause, vous n'avez qu'à écrire une fonction, afin que le code puisse yield
les éléments avec lesquels vous souhaitez interagir. Vous en utilisez probablement déjà un dans votre code, mais dans le shell ipython
ou un autre, vous ne le faites probablement pas. Alors, enveloppez simplement votre code de débogage dans une fonction, puis entrez dans un contexte bar.pause()
!!
def reconcile_transactions ():
qs = Transaction . objects . filter () # django example, or in sqlalchemy: session.query(Transaction).filter()
with alive_bar ( qs . count ()) as bar :
for transaction in qs :
if faulty ( transaction ):
with bar . pause ():
yield transaction
bar ()
C'est ça! C'est aussi simple que ça ! o/
Maintenant, exécutez gen = reconcile_transactions()
pour instancier le générateur, et chaque fois que vous voulez la prochaine transaction défectueuse, appelez simplement next(gen, None)
! Je l'aime...
La barre alive-progress
commencera et s'exécutera comme d'habitude, mais dès que toute incohérence sera trouvée, la barre s'arrêtera elle-même, désactivant le fil de rafraîchissement et se souvenant de son état exact, et vous rendra la transaction directement sur l'invite! C'est presque magique! ?
In [11]: gen = reconcile_transactions()
In [12]: next(gen, None)
|█████████████████████ | 105/200 [52%] in 5s (18.8/s, eta: 4s)
Out[12]: Transaction<#123>
Vous pouvez ensuite inspecter la transaction avec le raccourci _
habituel d' ipython
(ou simplement l'attribuer directement avec t = next(gen, None)
), et vous êtes prêt pour le réparer!
Lorsque vous avez terminé, réactivez simplement la barre avec le même next
appel qu'avant !! La barre réapparaît, rallume tout et continue comme si elle ne s'était jamais arrêtée !! Ok, c'est magique?
In [21]: next(gen, None)
|█████████████████████ | ▁▃▅ 106/200 [52%] in 5s (18.8/s, eta: 4s)
Rincez et répétez jusqu'à ce que le reçu final apparaisse, et il n'y aura plus de transactions défectueuses. ?
Vous devez donc surveiller une opération fixe, sans aucune boucle, non?
Cela fonctionnera à coup sûr! Voici un exemple naïf (nous ferons mieux dans un instant):
with alive_bar ( 4 ) as bar :
corpus = read_file ( file )
bar () # file was read, tokenizing
tokens = tokenize ( corpus )
bar () # tokens generated, processing
data = process ( tokens )
bar () # process finished, sending response
resp = send ( data )
bar () # we're done! four bar calls with `total=4`
C'est naïf car il suppose que toutes les étapes prennent le même temps, mais en fait, chacun peut prendre un temps très différent. Pensez que read_file
et tokenize
peuvent être extrêmement rapides, ce qui fait monter en flèche le pourcentage à 50%, puis s'arrêtant pendant longtemps dans l'étape process
... vous obtenez le point, il peut ruiner l'expérience utilisateur et créer un ETA très trompeur.
Pour améliorer cela, vous devez distribuer les pourcentages des étapes en conséquence! Depuis que vous avez dit alive_bar
, il y avait quatre étapes, lorsque la première a été terminée, elle comprenait 1/4 ou 25% de l'ensemble du traitement était terminé ... Ainsi, vous devez mesurer la durée de vos étapes et utilisez le mode manuel pour Augmentez le pourcentage de barre de la bonne quantité à chaque étape!
Vous pouvez utiliser mon autre projet open source à peu près pour mesurer facilement ces durées! Essayez simplement de simuler avec des entrées représentatives pour obtenir de meilleurs résultats. Quelque chose comme:
from about_time import about_time
with about_time () as t_total : # this about_time will measure the whole time of the block.
with about_time () as t1 : # the other four will get the relative timings within the whole.
corpus = read_file ( file ) # `about_time` supports several calling conventions, including one-liners.
with about_time () as t2 : # see its documentation for more details.
tokens = tokenize ( corpus )
with about_time () as t3 :
data = process ( tokens )
with about_time () as t4 :
resp = send ( data )
print ( f'percentage1 = { t1 . duration / t_total . duration } ' )
print ( f'percentage2 = { t2 . duration / t_total . duration } ' )
print ( f'percentage3 = { t3 . duration / t_total . duration } ' )
print ( f'percentage4 = { t4 . duration / t_total . duration } ' )
Et voilà ! Vous connaissez maintenant les horaires relatifs de toutes les étapes et pouvez les utiliser pour améliorer votre code d'origine! Obtenez simplement les horaires cumulatifs et placez-les dans un mode manuel alive_bar
!
Par exemple, si les horaires que vous avez trouvés étaient de 10%, 30%, 20% et 40%, vous utiliseriez 0,1, 0,4, 0,6 et 1,0 (le dernier devrait toujours être 1,0):
with alive_bar ( 4 , manual = True ) as bar :
corpus = read_big_file ()
bar ( 0.1 ) # 10%
tokens = tokenize ( corpus )
bar ( 0.4 ) # 30% + 10% from previous steps
data = process ( tokens )
bar ( 0.6 ) # 20% + 40% from previous steps
resp = send ( data )
bar ( 1. ) # always 1. in the last step
C'est ça! L'expérience utilisateur et l'ETA devraient être considérablement améliorés maintenant.
Oui, vous pouvez calibrer la vitesse du spinner!
Les barres alive-progress
ont une rétroaction visuelle cool du débit actuel, vous pouvez donc voir à quelle vitesse votre traitement est, car le spinner fonctionne plus vite ou plus lent avec.
Pour que cela se produise, j'ai assemblé et mis en œuvre quelques courbes FPS pour trouver empiriquement laquelle a donné la meilleure sensation de vitesse:
(Version interactive [ici] (https://www.desmos.com/calculator/ema05elsux)))
Le graphique montre les courbes logarithmiques (rouges), paraboliques (bleues) et linéaires (vertes), ce sont ceux avec qui j'ai commencé. Ce n'était pas une tâche facile, j'ai fait des dizaines de tests, et je n'en ai jamais trouvé un qui a vraiment inspiré cette sensation de vitesse que je cherchais. Le meilleur semblait être le logarithmique, mais cela a mal réagi avec de petits nombres. Je sais que je pourrais le faire fonctionner avec quelques rebondissements pour ces petits nombres, alors j'ai beaucoup expérimenté et ajusté la courbe logarithmique (orange pointillé) jusqu'à ce que je trouve enfin le comportement que je m'attendais! C'est celui qui semblait fournir les meilleurs changements de vitesse perçus tout autour de tout le spectre de quelques à des milliards ... c'est la courbe avec laquelle je me suis installé, et c'est celui utilisé dans tous les modes et conditions. À l'avenir et si quelqu'un le trouve utile, cette courbe pourrait être configurable.
Eh bien, l'étalonnage par défaut en alive-progress
est de 1 000 000 en modes limités, c'est-à-dire qu'il faut 1 million d'itérations par seconde pour que la barre se rafraîchisse à 60 images par seconde. Dans le mode manuel illimité, il est de 1,0 (100%). Les deux permettent une vaste gamme d'exploitation et fonctionnent généralement assez bien.
Par exemple, jetez un œil à l'effet que ces étalonnages très différents ont, exécutant le même code à la même vitesse! Remarquez la sensation que le spinner passe à l'utilisateur, ce traitement va-t-il lentement ou va-t-il vite? Et rappelez-vous que ce n'est pas seulement le spinner rafraîchissant, mais toute la ligne, avec le rendu du bar et tous les widgets, donc tout devient plus fluide ou lent:
Donc, si votre traitement atteint à peine 20 articles par seconde, et que vous pensez que
alive-progress
rend lent, vous pourriez augmenter ce sens de la vitesse en le calibrant pour le dire40
, et il exécutera Waaaay plus vite ... Mieux vaut toujours laisser une hauteur d'espace pour la tête et l'étalonner à quelque chose entre 50% et 100% plus, puis le modifier à partir de là pour trouver celui que vous aimez le plus! :)
Ces animations étonnantes alive-progress
elles d'afficher?
Pycharm est génial, j'adore! Mais je ne comprendrai jamais pourquoi ils ont handicapé imiter un terminal par défaut ... si vous utilisez la console de sortie de Pycharm, veuillez l'activer sur toutes vos configurations d'exécution:
Je vous recommande même de vous rendre dans
File
>New Projects Setup
>Run Configuration Templates
, sélectionnezPython
et l'activez également, afin que tous les nouveaux que vous créez auront déjà cet ensemble.
En plus de cela, certains terminaux se considèrent comme "non interactifs", comme lorsqu'ils manquent d'un vrai terminal (PyCharm et Jupyter par exemple), dans Shell Pipelines ( cat file.txt | python program.py
), ou en arrière-plan processus (non connectés à un tty).
Lorsque alive-progress
se trouve dans un terminal non interactif, il désactive automatiquement toutes sortes d'animations, n'impression uniquement du reçu final. Ceci est fait afin d'éviter à la fois de gâcher la sortie du pipeline et de spammer votre fichier journal avec des milliers de rafraîchissements alive-progress
.
Donc, quand vous savez que c'est sûr, vous pouvez les forcer à voir alive-progress
dans toute sa gloire! Voici l'argument force_tty
:
with alive_bar ( 1000 , force_tty = True ) as bar :
for i in range ( 1000 ):
time . sleep ( .01 )
bar ()
Les valeurs acceptées sont:
force_tty=True
-> Active toujours les animations et les cahiers de jupyter-détectes automatiquement!force_tty=False
-> désactive toujours les animations, en gardant uniquement le reçu finalforce_tty=None
(par défaut) -> détection automatique, selon l'état TTY du terminal Vous pouvez également le définir à l'échelle du système à l'aide config_handler
, vous n'avez donc plus besoin de le passer manuellement.
Notez que les cahiers de la console et du jupyter de Pycharm sont fortement instrumentés et ont donc beaucoup plus de frais généraux, de sorte que le résultat peut ne pas être aussi fluide que vous le pensez. En plus de cela, les cahiers Jupyter ne prennent pas en charge les codes d'échappement ANSI, j'ai donc dû développer des solutions de contournement pour imiter des fonctions telles que "effacer la ligne" et "effacer à partir du curseur" ... pour voir les animations fluides et lisses
alive_bar
comme je le voulais , préfèrent toujours un terminal à part entière.
alive-progress
n'avait eu aucune dépendance. Maintenant, il en a deux: un est à propos (un autre projet intéressant, si je le dis moi-même), qui est utilisé pour suivre le temps nécessaire pour la compilation de spinner et pour générer ses interprétations respectueuses de l'homme. L'autre est Graphème, pour détecter les ruptures de cluster Graphème (j'ai ouvert un problème là-bas sur l'avenir et l'exactitude de celui-ci, et l'auteur garantit qu'il a l'intention de mettre à jour le projet sur chaque nouvelle version Unicode);alive-progress
n'avait pas eu de classe Python unique! Maintenant, il en a quelques minuscules pour des raisons très spécifiques (modifiez les callmables, les adaptateurs d'itérateur et certains descripteurs pour les widgets alive_bar
).alive_bar
lui-même n'est qu'une fonction! Bien que, pour être juste, c'est "juste" une fonction où je branche dynamiquement plusieurs fermetures de l'intérieur en elle-même (rappelez-vous que les fonctions Python ont un __dict__
comme les classes?). alive_bar
remarque les changements de taille du terminal, mais tronque simplement la ligne en conséquence).contrib
d'une manière ou d'une autre, pour permettre un moyen simple de partager des filateurs et des barres sympas des utilisateurs.skipped
stderr
et d'autres fichiers au lieu de stdout
monitor
, elapsed
, stats
Compléter ici.
alive_it
invalid, détecter les utilisations imbriquées de Alive_Progress et lancer un message d'erreur plus clairalive_it
skipped
, nouveau paramètre de configuration max_cols
pour Jupyter, corrigez la taille du terminal lors de l'utilisation de Stderr, prend officiellement Python 3.11sys.stderr
et d'autres fichiers au lieu de sys.stdout
, a lissé l'estimation du taux, plus de requêtes dans les widgets en cours d'exécution données, aidez le système dans les erreurs de configurationmonitor
personnalisable, elapsed
et Widgets de base stats
, New monitor_end
, elapsed_end
et stats_end
Core Widgets, une meilleure prise en charge de Ctrl + C, ce qui fait alive_bar
s'arrête prématurément prématurémentclick.echo()
Support; performance plus rapide; détection plus sûre des colonnes terminales; bar.current
agit comme une propriété; supprimer Python 3.6.check()
outils dans les filateurs et les barres; Les bars et les moteurs Spinners réorganisent; Nouveaux modes d'animation dans les filateurs séquentiels et séquentiels; Nouveaux filateurs, barres et thèmes intégrés; Dynamic Showtime avec les thèmes, la protection de défilement et les modèles de filtre; journalisation améliorée pour les fichiers; plusieurs nouvelles options de configuration pour personnaliser l'apparence; Nouvel adaptateur Iterator alive_it
; utilise time.perf_counter()
horloge haute résolution; Nécessite Python 3.6+ (et soutient officiellement Python 3.9 et 3.10)bar.current()
Méthode; Les Newlines sont imprimées sur Vanilla Python REPL; La barre est tronquée à 80 caractères sur Windowsbar.text()
Méthode, pour définir les messages situationnels à tout moment, sans position d'incrémentation (paramètre de texte de dépréciation dans bar()
); Optimisations de performancesbackground
au lieu de blank
, qui accepte les chaînes de taille arbitraire et reste fixée en arrière-plan, simulant une barre qui va "dessus"show_spinners
et show_bars
, nouvel utilitaire print_chars
, show_bars
obtenez des démonstrations avancées (réessayez!) alive_progress
essaiera toujours de suivre Python, donc à partir de la version 2.0, je vais abandonner la prise en charge de toutes les versions Python qui entrent EOL. Voir leur horaire ici.
Mais ne vous inquiétez pas si vous ne pouvez pas encore migrer: les versions alive_progress
sont vivaces, alors continuez à utiliser celle qui fonctionne pour vous et vous êtes bon.
Je recommande fortement de définir des packages plus anciens alive_progress
dans un fichier required.txt avec les formats suivants. Ceux-ci rapporteront toujours les dernières versions de build avant une version donnée, donc, si jamais je publie des corrections de bugs, vous les obtiendrez aussi.
❯ pip install -U " alive_progress<2 "
❯ pip install -U " alive_progress<2.2 "
❯ pip install -U " alive_progress<3.2 "
Ce logiciel est concédé sous licence MIT. Consultez le fichier LICENSE dans le répertoire de distribution principal pour le texte complet de la licence.
Le maintien d'un projet open source est difficile et long, et j'ai mis beaucoup de choses et d'efforts.
Si vous avez apprécié mon travail, vous pouvez me soutenir avec un don! Merci ?