API - display password reset link expiration delay in email
This commit is contained in:
		@@ -194,7 +194,7 @@
 | 
				
			|||||||
  <![endif]-->
 | 
					  <![endif]-->
 | 
				
			||||||
  </head>
 | 
					  </head>
 | 
				
			||||||
  <body>
 | 
					  <body>
 | 
				
			||||||
    <span class="preheader">Use this link to reset your password. The link is only valid for 24 hours.</span>
 | 
					    <span class="preheader">Use this link to reset your password. The link is only valid for {{ expiration_delay }}.</span>
 | 
				
			||||||
    <table class="email-wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation">
 | 
					    <table class="email-wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation">
 | 
				
			||||||
      <tr>
 | 
					      <tr>
 | 
				
			||||||
        <td align="center">
 | 
					        <td align="center">
 | 
				
			||||||
@@ -214,7 +214,7 @@
 | 
				
			|||||||
                      <div class="f-fallback">
 | 
					                      <div class="f-fallback">
 | 
				
			||||||
                        <h1>Hi {{username}},</h1>
 | 
					                        <h1>Hi {{username}},</h1>
 | 
				
			||||||
                        <p>You recently requested to reset your password for your account. Use the button below to reset it.
 | 
					                        <p>You recently requested to reset your password for your account. Use the button below to reset it.
 | 
				
			||||||
                          <strong>This password reset is only valid for the next 24 hours.</strong>
 | 
					                          <strong>This password reset link is only valid for {{ expiration_delay }}.</strong>
 | 
				
			||||||
                        </p>
 | 
					                        </p>
 | 
				
			||||||
                        <table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
 | 
					                        <table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
 | 
				
			||||||
                          <tr>
 | 
					                          <tr>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
Hi {{username}},
 | 
					Hi {{username}},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
You recently requested to reset your password for your FitTrackee account. Use the button below to reset it. This password reset is only valid for the next 24 hours.
 | 
					You recently requested to reset your password for your FitTrackee account. Use the button below to reset it. This password reset link is only valid for {{ expiration_delay }}.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Reset your password ( {{ password_reset_url }} )
 | 
					Reset your password ( {{ password_reset_url }} )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -194,8 +194,7 @@
 | 
				
			|||||||
  <![endif]-->
 | 
					  <![endif]-->
 | 
				
			||||||
  </head>
 | 
					  </head>
 | 
				
			||||||
  <body>
 | 
					  <body>
 | 
				
			||||||
    <span class="preheader">Use this link to reset your password. The link is only valid for 24 hours.</span>
 | 
					    <span class="preheader">Utiliser ce lien pour réinitialiser le mot de passe. Ce lien n'est valide que pendant {{ expiration_delay }}.</span>
 | 
				
			||||||
    <span class="preheader">Utiliser ce lien pour réinitialiser le mot de passe. Ce lien n'est valide que pendant 1 heure.</span>
 | 
					 | 
				
			||||||
    <table class="email-wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation">
 | 
					    <table class="email-wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation">
 | 
				
			||||||
      <tr>
 | 
					      <tr>
 | 
				
			||||||
        <td align="center">
 | 
					        <td align="center">
 | 
				
			||||||
@@ -216,7 +215,7 @@
 | 
				
			|||||||
                        <h1>Bonjour  {{username}},</h1>
 | 
					                        <h1>Bonjour  {{username}},</h1>
 | 
				
			||||||
                        <p>Vous avez récemment demander la réinitilisation du mot de passe de votre compte sur FitTrackee.
 | 
					                        <p>Vous avez récemment demander la réinitilisation du mot de passe de votre compte sur FitTrackee.
 | 
				
			||||||
                          Cliquez sur le bouton ci-dessous pour le réinitialiser.
 | 
					                          Cliquez sur le bouton ci-dessous pour le réinitialiser.
 | 
				
			||||||
                          <strong>Cette réinitialisation n'est valide que pendant 1 heure.</strong>
 | 
					                          <strong>Cette réinitialisation n'est valide que pendant {{ expiration_delay }}.</strong>
 | 
				
			||||||
                        </p>
 | 
					                        </p>
 | 
				
			||||||
                        <table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
 | 
					                        <table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
 | 
				
			||||||
                          <tr>
 | 
					                          <tr>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
Bonjour {{username}},
 | 
					Bonjour {{username}},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Vous avez récemment demander la réinitilisation du mot de passe de votre compte sur FitTrackee.
 | 
					Vous avez récemment demander la réinitilisation du mot de passe de votre compte sur FitTrackee.
 | 
				
			||||||
Cliquez sur le lien ci-dessous pour le réinitialiser. Ce lien n'est valide que pendant 1 heure.
 | 
					Cliquez sur le lien ci-dessous pour le réinitialiser. Ce lien n'est valide que pendant {{ expiration_delay }}.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Réinitialiser le mot de passe: ( {{ password_reset_url }} )
 | 
					Réinitialiser le mot de passe: ( {{ password_reset_url }} )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
expected_en_text_body = """Hi test,
 | 
					expected_en_text_body = """Hi test,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
You recently requested to reset your password for your FitTrackee account. Use the button below to reset it. This password reset is only valid for the next 24 hours.
 | 
					You recently requested to reset your password for your FitTrackee account. Use the button below to reset it. This password reset link is only valid for 3 seconds.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Reset your password ( http://localhost/password-reset?token=xxx )
 | 
					Reset your password ( http://localhost/password-reset?token=xxx )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -14,7 +14,7 @@ The FitTrackee Team"""
 | 
				
			|||||||
expected_fr_text_body = """Bonjour test,
 | 
					expected_fr_text_body = """Bonjour test,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Vous avez récemment demander la réinitilisation du mot de passe de votre compte sur FitTrackee.
 | 
					Vous avez récemment demander la réinitilisation du mot de passe de votre compte sur FitTrackee.
 | 
				
			||||||
Cliquez sur le lien ci-dessous pour le réinitialiser. Ce lien n'est valide que pendant 1 heure.
 | 
					Cliquez sur le lien ci-dessous pour le réinitialiser. Ce lien n'est valide que pendant 3 secondes.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Réinitialiser le mot de passe: ( http://localhost/password-reset?token=xxx )
 | 
					Réinitialiser le mot de passe: ( http://localhost/password-reset?token=xxx )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -25,7 +25,7 @@ Merci,
 | 
				
			|||||||
L'équipe FitTrackee"""
 | 
					L'équipe FitTrackee"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
expected_en_html_body = """  <body>
 | 
					expected_en_html_body = """  <body>
 | 
				
			||||||
    <span class="preheader">Use this link to reset your password. The link is only valid for 24 hours.</span>
 | 
					    <span class="preheader">Use this link to reset your password. The link is only valid for 3 seconds.</span>
 | 
				
			||||||
    <table class="email-wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation">
 | 
					    <table class="email-wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation">
 | 
				
			||||||
      <tr>
 | 
					      <tr>
 | 
				
			||||||
        <td align="center">
 | 
					        <td align="center">
 | 
				
			||||||
@@ -45,7 +45,7 @@ expected_en_html_body = """  <body>
 | 
				
			|||||||
                      <div class="f-fallback">
 | 
					                      <div class="f-fallback">
 | 
				
			||||||
                        <h1>Hi test,</h1>
 | 
					                        <h1>Hi test,</h1>
 | 
				
			||||||
                        <p>You recently requested to reset your password for your account. Use the button below to reset it.
 | 
					                        <p>You recently requested to reset your password for your account. Use the button below to reset it.
 | 
				
			||||||
                          <strong>This password reset is only valid for the next 24 hours.</strong>
 | 
					                          <strong>This password reset link is only valid for 3 seconds.</strong>
 | 
				
			||||||
                        </p>
 | 
					                        </p>
 | 
				
			||||||
                        <table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
 | 
					                        <table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
 | 
				
			||||||
                          <tr>
 | 
					                          <tr>
 | 
				
			||||||
@@ -99,8 +99,7 @@ expected_en_html_body = """  <body>
 | 
				
			|||||||
</html>"""
 | 
					</html>"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
expected_fr_html_body = """  <body>
 | 
					expected_fr_html_body = """  <body>
 | 
				
			||||||
    <span class="preheader">Use this link to reset your password. The link is only valid for 24 hours.</span>
 | 
					    <span class="preheader">Utiliser ce lien pour réinitialiser le mot de passe. Ce lien n'est valide que pendant 3 secondes.</span>
 | 
				
			||||||
    <span class="preheader">Utiliser ce lien pour réinitialiser le mot de passe. Ce lien n'est valide que pendant 1 heure.</span>
 | 
					 | 
				
			||||||
    <table class="email-wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation">
 | 
					    <table class="email-wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation">
 | 
				
			||||||
      <tr>
 | 
					      <tr>
 | 
				
			||||||
        <td align="center">
 | 
					        <td align="center">
 | 
				
			||||||
@@ -121,7 +120,7 @@ expected_fr_html_body = """  <body>
 | 
				
			|||||||
                        <h1>Bonjour  test,</h1>
 | 
					                        <h1>Bonjour  test,</h1>
 | 
				
			||||||
                        <p>Vous avez récemment demander la réinitilisation du mot de passe de votre compte sur FitTrackee.
 | 
					                        <p>Vous avez récemment demander la réinitilisation du mot de passe de votre compte sur FitTrackee.
 | 
				
			||||||
                          Cliquez sur le bouton ci-dessous pour le réinitialiser.
 | 
					                          Cliquez sur le bouton ci-dessous pour le réinitialiser.
 | 
				
			||||||
                          <strong>Cette réinitialisation n'est valide que pendant 1 heure.</strong>
 | 
					                          <strong>Cette réinitialisation n'est valide que pendant 3 secondes.</strong>
 | 
				
			||||||
                        </p>
 | 
					                        </p>
 | 
				
			||||||
                        <table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
 | 
					                        <table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
 | 
				
			||||||
                          <tr>
 | 
					                          <tr>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -32,6 +32,7 @@ class TestEmailMessage:
 | 
				
			|||||||
class TestEmailSending:
 | 
					class TestEmailSending:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    email_data = {
 | 
					    email_data = {
 | 
				
			||||||
 | 
					        'expiration_delay': '3 seconds',
 | 
				
			||||||
        'username': 'test',
 | 
					        'username': 'test',
 | 
				
			||||||
        'password_reset_url': f'http://localhost/password-reset?token=xxx',
 | 
					        'password_reset_url': f'http://localhost/password-reset?token=xxx',
 | 
				
			||||||
        'operating_system': 'Linux',
 | 
					        'operating_system': 'Linux',
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -33,6 +33,7 @@ class TestEmailTemplateForPasswordRequest:
 | 
				
			|||||||
    def test_it_gets_text_body(self, app, lang, expected_text_body):
 | 
					    def test_it_gets_text_body(self, app, lang, expected_text_body):
 | 
				
			||||||
        email_template = EmailTemplate(app.config.get('TEMPLATES_FOLDER'))
 | 
					        email_template = EmailTemplate(app.config.get('TEMPLATES_FOLDER'))
 | 
				
			||||||
        email_data = {
 | 
					        email_data = {
 | 
				
			||||||
 | 
					            'expiration_delay': '3 seconds' if lang == 'en' else '3 secondes',
 | 
				
			||||||
            'username': 'test',
 | 
					            'username': 'test',
 | 
				
			||||||
            'password_reset_url': f'http://localhost/password-reset?token=xxx',
 | 
					            'password_reset_url': f'http://localhost/password-reset?token=xxx',
 | 
				
			||||||
            'operating_system': 'Linux',
 | 
					            'operating_system': 'Linux',
 | 
				
			||||||
@@ -48,6 +49,7 @@ class TestEmailTemplateForPasswordRequest:
 | 
				
			|||||||
    def test_it_gets_en_html_body(self, app):
 | 
					    def test_it_gets_en_html_body(self, app):
 | 
				
			||||||
        email_template = EmailTemplate(app.config.get('TEMPLATES_FOLDER'))
 | 
					        email_template = EmailTemplate(app.config.get('TEMPLATES_FOLDER'))
 | 
				
			||||||
        email_data = {
 | 
					        email_data = {
 | 
				
			||||||
 | 
					            'expiration_delay': '3 seconds',
 | 
				
			||||||
            'username': 'test',
 | 
					            'username': 'test',
 | 
				
			||||||
            'password_reset_url': f'http://localhost/password-reset?token=xxx',
 | 
					            'password_reset_url': f'http://localhost/password-reset?token=xxx',
 | 
				
			||||||
            'operating_system': 'Linux',
 | 
					            'operating_system': 'Linux',
 | 
				
			||||||
@@ -63,6 +65,7 @@ class TestEmailTemplateForPasswordRequest:
 | 
				
			|||||||
    def test_it_gets_fr_html_body(self, app):
 | 
					    def test_it_gets_fr_html_body(self, app):
 | 
				
			||||||
        email_template = EmailTemplate(app.config.get('TEMPLATES_FOLDER'))
 | 
					        email_template = EmailTemplate(app.config.get('TEMPLATES_FOLDER'))
 | 
				
			||||||
        email_data = {
 | 
					        email_data = {
 | 
				
			||||||
 | 
					            'expiration_delay': '3 secondes',
 | 
				
			||||||
            'username': 'test',
 | 
					            'username': 'test',
 | 
				
			||||||
            'password_reset_url': f'http://localhost/password-reset?token=xxx',
 | 
					            'password_reset_url': f'http://localhost/password-reset?token=xxx',
 | 
				
			||||||
            'operating_system': 'Linux',
 | 
					            'operating_system': 'Linux',
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,6 +14,7 @@ from .utils import (
 | 
				
			|||||||
    authenticate,
 | 
					    authenticate,
 | 
				
			||||||
    check_passwords,
 | 
					    check_passwords,
 | 
				
			||||||
    display_readable_file_size,
 | 
					    display_readable_file_size,
 | 
				
			||||||
 | 
					    get_readable_duration,
 | 
				
			||||||
    register_controls,
 | 
					    register_controls,
 | 
				
			||||||
    verify_extension_and_size,
 | 
					    verify_extension_and_size,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@@ -700,6 +701,10 @@ def request_password_reset():
 | 
				
			|||||||
        password_reset_token = user.encode_password_reset_token(user.id)
 | 
					        password_reset_token = user.encode_password_reset_token(user.id)
 | 
				
			||||||
        ui_url = current_app.config['UI_URL']
 | 
					        ui_url = current_app.config['UI_URL']
 | 
				
			||||||
        email_data = {
 | 
					        email_data = {
 | 
				
			||||||
 | 
					            'expiration_delay': get_readable_duration(
 | 
				
			||||||
 | 
					                current_app.config.get('PASSWORD_TOKEN_EXPIRATION_SECONDS'),
 | 
				
			||||||
 | 
					                'en' if user.language is None else user.language,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
            'username': user.username,
 | 
					            'username': user.username,
 | 
				
			||||||
            'password_reset_url': (
 | 
					            'password_reset_url': (
 | 
				
			||||||
                f'{ui_url}/password-reset?token={password_reset_token.decode()}'  # noqa
 | 
					                f'{ui_url}/password-reset?token={password_reset_token.decode()}'  # noqa
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,8 @@
 | 
				
			|||||||
import re
 | 
					import re
 | 
				
			||||||
 | 
					from datetime import timedelta
 | 
				
			||||||
from functools import wraps
 | 
					from functools import wraps
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import humanize
 | 
				
			||||||
from flask import current_app, jsonify, request
 | 
					from flask import current_app, jsonify, request
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .models import User
 | 
					from .models import User
 | 
				
			||||||
@@ -148,3 +150,12 @@ def display_readable_file_size(size_in_bytes):
 | 
				
			|||||||
            return f"{size_in_bytes:3.1f}{unit}"
 | 
					            return f"{size_in_bytes:3.1f}{unit}"
 | 
				
			||||||
        size_in_bytes /= 1024.0
 | 
					        size_in_bytes /= 1024.0
 | 
				
			||||||
    return f"{size_in_bytes} bytes"
 | 
					    return f"{size_in_bytes} bytes"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_readable_duration(duration, locale='en'):
 | 
				
			||||||
 | 
					    if locale != 'en':
 | 
				
			||||||
 | 
					        _t = humanize.i18n.activate(locale)  # noqa
 | 
				
			||||||
 | 
					    readable_duration = humanize.naturaldelta(timedelta(seconds=duration))
 | 
				
			||||||
 | 
					    if locale != 'en':
 | 
				
			||||||
 | 
					        humanize.i18n.deactivate()
 | 
				
			||||||
 | 
					    return readable_duration
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										17
									
								
								fittrackee_api/poetry.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										17
									
								
								fittrackee_api/poetry.lock
									
									
									
										generated
									
									
									
								
							@@ -322,6 +322,17 @@ gevent = ["gevent (>=0.13)"]
 | 
				
			|||||||
setproctitle = ["setproctitle"]
 | 
					setproctitle = ["setproctitle"]
 | 
				
			||||||
tornado = ["tornado (>=0.2)"]
 | 
					tornado = ["tornado (>=0.2)"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					category = "main"
 | 
				
			||||||
 | 
					description = "Python humanize utilities"
 | 
				
			||||||
 | 
					name = "humanize"
 | 
				
			||||||
 | 
					optional = false
 | 
				
			||||||
 | 
					python-versions = ">=3.5"
 | 
				
			||||||
 | 
					version = "2.5.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[package.extras]
 | 
				
			||||||
 | 
					tests = ["freezegun", "pytest", "pytest-cov"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
category = "main"
 | 
					category = "main"
 | 
				
			||||||
description = "Internationalized Domain Names in Applications (IDNA)"
 | 
					description = "Internationalized Domain Names in Applications (IDNA)"
 | 
				
			||||||
@@ -995,7 +1006,7 @@ docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
 | 
				
			|||||||
testing = ["jaraco.itertools", "func-timeout"]
 | 
					testing = ["jaraco.itertools", "func-timeout"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[metadata]
 | 
					[metadata]
 | 
				
			||||||
content-hash = "4199f7c3f0fe738bf7d846017b57e9903aff0b4697a8570be54a9ed63bd20306"
 | 
					content-hash = "344f1311eea15fb17b78dea1fe646a994498814a5ff76374d78f37d0643de282"
 | 
				
			||||||
python-versions = "^3.7"
 | 
					python-versions = "^3.7"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[metadata.files]
 | 
					[metadata.files]
 | 
				
			||||||
@@ -1192,6 +1203,10 @@ gunicorn = [
 | 
				
			|||||||
    {file = "gunicorn-20.0.4-py2.py3-none-any.whl", hash = "sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c"},
 | 
					    {file = "gunicorn-20.0.4-py2.py3-none-any.whl", hash = "sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c"},
 | 
				
			||||||
    {file = "gunicorn-20.0.4.tar.gz", hash = "sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626"},
 | 
					    {file = "gunicorn-20.0.4.tar.gz", hash = "sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626"},
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					humanize = [
 | 
				
			||||||
 | 
					    {file = "humanize-2.5.0-py3-none-any.whl", hash = "sha256:89062c6db8601693b7d223443d0d7529aa9577df43a1387ddd4b9c273abb4a51"},
 | 
				
			||||||
 | 
					    {file = "humanize-2.5.0.tar.gz", hash = "sha256:8a68bd9bccb899fd9bfb1e6d96c1e84e4475551cc9a5b5bdbd69b9b1cfd19c80"},
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
idna = [
 | 
					idna = [
 | 
				
			||||||
    {file = "idna-2.9-py2.py3-none-any.whl", hash = "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"},
 | 
					    {file = "idna-2.9-py2.py3-none-any.whl", hash = "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"},
 | 
				
			||||||
    {file = "idna-2.9.tar.gz", hash = "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"},
 | 
					    {file = "idna-2.9.tar.gz", hash = "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"},
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,6 +18,7 @@ pytz = "^2020.1"
 | 
				
			|||||||
python-forecastio = "^1.4"
 | 
					python-forecastio = "^1.4"
 | 
				
			||||||
gunicorn = "^20.0"
 | 
					gunicorn = "^20.0"
 | 
				
			||||||
tqdm = "^4.42"
 | 
					tqdm = "^4.42"
 | 
				
			||||||
 | 
					humanize = "^2.5.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[tool.poetry.dev-dependencies]
 | 
					[tool.poetry.dev-dependencies]
 | 
				
			||||||
pytest = "^5.3"
 | 
					pytest = "^5.3"
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user