├── app ├── templates │ ├── main │ │ ├── main_base.html │ │ ├── user_profile_page.html │ │ ├── home_page.html │ │ ├── admin_page.html │ │ └── user_page.html │ ├── README.md │ ├── common │ │ └── form_macros.html │ └── layout.html ├── models │ ├── __init__.py │ ├── feedeater_models.py │ └── user_models.py ├── commands │ ├── __init__.py │ └── init_db.py ├── tasks.py ├── views │ ├── __init__.py │ └── main_views.py ├── celeryapp │ ├── celery_worker.py │ └── __init__.py ├── static │ ├── css │ │ └── app.css │ └── bootstrap │ │ └── js │ │ ├── html5shiv.min.js │ │ ├── respond.min.js │ │ └── bootstrap.min.js ├── README.md ├── local_settings_example.py ├── settings.py ├── __init__.py └── services.py ├── tests ├── __init__.py ├── .coveragerc ├── README.md ├── test_page_urls.py └── conftest.py ├── migrations ├── README.md ├── script.py.mako ├── alembic.ini ├── versions │ ├── 0001c8ac1a69_initial_version.py │ └── b3a7e2b375e0_feedeater.py └── env.py ├── Pipfile ├── create_feeds.py ├── manage.py ├── README.md ├── .gitignore └── Pipfile.lock /app/templates/main/main_base.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | -------------------------------------------------------------------------------- /app/models/__init__.py: -------------------------------------------------------------------------------- 1 | # __init__.py is a special Python file that allows a directory to become 2 | # a Python package so it can be accessed using the 'import' statement. 3 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # __init__.py is a special Python file that allows a directory to become 2 | # a Python package so it can be accessed using the 'import' statement. 3 | 4 | # Intentionally left empty -------------------------------------------------------------------------------- /app/commands/__init__.py: -------------------------------------------------------------------------------- 1 | # __init__.py is a special Python file that allows a directory to become 2 | # a Python package so it can be accessed using the 'import' statement. 3 | 4 | from .init_db import InitDbCommand -------------------------------------------------------------------------------- /app/tasks.py: -------------------------------------------------------------------------------- 1 | from app import celeryapp 2 | from app.services import FeedEater 3 | 4 | celery = celeryapp.celery 5 | 6 | 7 | @celery.task() 8 | def fetch_articles(feed_id: int): 9 | feedeater = FeedEater() 10 | feedeater.fetch(feed_id) 11 | -------------------------------------------------------------------------------- /app/views/__init__.py: -------------------------------------------------------------------------------- 1 | # __init__.py is a special Python file that allows a directory to become 2 | # a Python package so it can be accessed using the 'import' statement. 3 | 4 | from .main_views import main_blueprint 5 | 6 | def register_blueprints(app): 7 | app.register_blueprint(main_blueprint) -------------------------------------------------------------------------------- /tests/.coveragerc: -------------------------------------------------------------------------------- 1 | # Configuration for the test coverage tool (py.test --cov) 2 | # 3 | # Copyright 2014 SolidBuilds.com. All rights reserved 4 | # 5 | # Authors: Ling Thio 6 | 7 | [run] 8 | omit = app/startup/reset_db.py 9 | 10 | [report] 11 | show_missing = True 12 | -------------------------------------------------------------------------------- /app/celeryapp/celery_worker.py: -------------------------------------------------------------------------------- 1 | """ 2 | Run using the command: 3 | 4 | python celery -A app.celeryapp.celery_worker.celery worker --concurrency=2 -E -l info 5 | """ 6 | from app import celeryapp, create_app 7 | 8 | app = create_app() 9 | celery = celeryapp.create_celery_app(app) 10 | celeryapp.celery = celery 11 | -------------------------------------------------------------------------------- /app/static/css/app.css: -------------------------------------------------------------------------------- 1 | /**** element styles ****/ 2 | hr { border-color: #cccccc; margin: 0px; } 3 | 4 | /**** header, main and footer divs ****/ 5 | .header-title { font-size: 30px; } 6 | 7 | /**** class-based style modifiers ****/ 8 | 9 | .no-margins { margin: 0px; } 10 | 11 | .with-margins { margin: 10px; } 12 | 13 | .col-centered { float: none; margin: 0 auto; } 14 | -------------------------------------------------------------------------------- /migrations/README.md: -------------------------------------------------------------------------------- 1 | # migrations directory 2 | 3 | This directory has been generated by Flask-Migrate and contains Alembic configuration files. 4 | Alembic is a Database Schema Migration tool. 5 | 6 | The 'versions' subdirectory contains the Database Schema Migration files. 7 | 8 | Use `python manage.py db` to see a list of Flask-Migrate commands. 9 | 10 | See also [Flask-Migrate](flask-migrate.readthedocs.org) and [Alembic](alembic.readthedocs.org). -------------------------------------------------------------------------------- /app/README.md: -------------------------------------------------------------------------------- 1 | # app directory 2 | 3 | This directory contains the Flask application code. 4 | 5 | The code has been organized into the following sub-directories: 6 | 7 | # Sub-directories 8 | commands # Commands made available to manage.py 9 | models # Database Models and their Forms 10 | static # Static asset files that will be mapped to the "/static/" URL 11 | templates # Jinja2 HTML template files 12 | views # View functions 13 | 14 | -------------------------------------------------------------------------------- /migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = ${repr(up_revision)} 11 | down_revision = ${repr(down_revision)} 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | ${imports if imports else ""} 16 | 17 | def upgrade(): 18 | ${upgrades if upgrades else "pass"} 19 | 20 | 21 | def downgrade(): 22 | ${downgrades if downgrades else "pass"} 23 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | flask = "==1.1.1" 10 | celery = "==5.2.3" 11 | sqlalchemy = "==1.3.8" 12 | pymysql = "==1.1.1" 13 | redis = "==4.5.4" 14 | Flask-Login = "==0.4.0" 15 | Flask-Migrate = "==2.0.2" 16 | Flask-Script = "==2.0.5" 17 | Flask-SQLAlchemy = "==2.1" 18 | Flask-WTF = "==1.0.0" 19 | Flask-User = {git = "https://github.com/lingthio/Flask-User.git",ref = "master"} 20 | email-validator = "==1.1.3" # needed by Flask-User 21 | pytest = "==3.0.5" 22 | pytest-cov = "==2.4.0" 23 | attrs = "==19.1.0" 24 | feedparser = "==6.0.8" 25 | requests = "==2.32.4" 26 | 27 | [requires] 28 | python_version = "3.9" 29 | -------------------------------------------------------------------------------- /app/templates/README.md: -------------------------------------------------------------------------------- 1 | # This directory contains Jinja2 template files 2 | 3 | This Flask application uses the Jinja2 templating engine to render 4 | data into HTML files. 5 | 6 | The template files are organized into the following directories: 7 | 8 | common # Common base template files and macros 9 | flask_user # Flask-User template files (register, login, etc.) 10 | pages # Template files for web pages 11 | 12 | Flask-User makes use of standard template files that reside in 13 | `PATH/TO/VIRTUALENV/lib/PYTHONVERSION/site-packages/flask_user/templates/flask_user/`. 14 | These standard templates can be overruled by placing a copy in the `app/templates/flask_user/` directory. 15 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # tests directory 2 | 3 | This directory contains all the automated tests for the test tool `py.test`. 4 | 5 | **`.coverage`**: Configuration file for the Python coverage tool `coverage`. 6 | 7 | **`conftest.py`**: Defines fixtures for py.test. 8 | 9 | **`test_*`**: py.test will load any file that starts with the name `test_` 10 | and run any function that starts with the name `test_`. 11 | 12 | 13 | ## Testing the app 14 | 15 | # Run all the automated tests in the tests/ directory 16 | ./runtests.sh # will run "py.test -s tests/" 17 | 18 | 19 | ## Generating a test coverage report 20 | 21 | # Run tests and show a test coverage report 22 | ./runcoverage.sh # will run py.test with coverage options 23 | 24 | -------------------------------------------------------------------------------- /app/templates/main/user_profile_page.html: -------------------------------------------------------------------------------- 1 | {% extends "main/main_base.html" %} {# main/main_base.html extends layout.html #} 2 | 3 | {% block content %} 4 |

User Profile

5 | 6 |

Change password

7 | 8 | {% from "common/form_macros.html" import render_field, render_submit_field %} 9 |
10 |
11 |
12 | {{ form.hidden_tag() }} 13 | 14 | {{ render_field(form.first_name, tabindex=240) }} 15 | 16 | {{ render_field(form.last_name, tabindex=250) }} 17 | 18 | {{ render_submit_field(form.submit, tabindex=280) }} 19 |
20 |
21 |
22 | 23 | {% endblock %} -------------------------------------------------------------------------------- /create_feeds.py: -------------------------------------------------------------------------------- 1 | from multiprocessing.pool import ThreadPool 2 | 3 | import requests 4 | 5 | 6 | def submit_feed(i: int): 7 | print(f'***Request {i}') 8 | 9 | args = {'title': f'Feed {i}', 'url': 'https://www.robinwieruch.de/index.xml'} 10 | 11 | url = f'http://localhost:5000/new-task?title={args["title"]}&url={args["url"]}' 12 | req = requests.get(url) 13 | 14 | print(f'R {i} Status code: {req.status_code}') 15 | print(f'R {i} Response text: {req.text}') 16 | req.raise_for_status() 17 | 18 | 19 | def create_feeds(): 20 | pool = ThreadPool(5) 21 | results = pool.map(submit_feed, range(1, 11)) 22 | pool.close() 23 | pool.join() 24 | return results 25 | 26 | 27 | if __name__ == '__main__': 28 | create_feeds() 29 | # submit_feed(1) 30 | -------------------------------------------------------------------------------- /app/templates/main/home_page.html: -------------------------------------------------------------------------------- 1 | {% extends "main/main_base.html" %} {# main/main_base.html extends layout.html #} 2 | 3 | {% block content %} 4 |

{%trans%}Home page{%endtrans%}

5 |

{%trans%}Register{%endtrans%}

6 |

{%trans%}Sign in{%endtrans%}

7 |

{%trans%}Home Page{%endtrans%} (accessible to anyone)

8 |

{%trans%}Member Page{%endtrans%} (login_required: member@example.com / Password1)

9 |

{%trans%}Admin Page{%endtrans%} (roles_required: admin@example.com / Password1)

10 |

{%trans%}Sign out{%endtrans%}

11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /app/templates/main/admin_page.html: -------------------------------------------------------------------------------- 1 | {% extends "main/main_base.html" %} {# main/main_base.html extends layout.html #} 2 | 3 | {% block content %} 4 |

{%trans%}Admin Page{%endtrans%}

5 |

{%trans%}Register{%endtrans%}

6 |

{%trans%}Sign in{%endtrans%}

7 |

{%trans%}Home Page{%endtrans%} (accessible to anyone)

8 |

{%trans%}Member Page{%endtrans%} (login_required: member@example.com / Password1)

9 |

{%trans%}Admin Page{%endtrans%} (roles_required: admin@example.com / Password1)

10 |

{%trans%}Sign out{%endtrans%}

11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /app/templates/main/user_page.html: -------------------------------------------------------------------------------- 1 | {% extends "main/main_base.html" %} {# main/main_base.html extends layout.html #} 2 | 3 | {% block content %} 4 |

{%trans%}Members page{%endtrans%}

5 |

{%trans%}Register{%endtrans%}

6 |

{%trans%}Sign in{%endtrans%}

7 |

{%trans%}Home Page{%endtrans%} (accessible to anyone)

8 |

{%trans%}Member Page{%endtrans%} (login_required: member@example.com / Password1)

9 |

{%trans%}Admin Page{%endtrans%} (roles_required: admin@example.com / Password1)

10 |

{%trans%}Sign out{%endtrans%}

11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | """This file sets up a command line manager. 2 | 3 | Use "python manage.py" for a list of available commands. 4 | Use "python manage.py runserver" to start the development web server on localhost:5000. 5 | Use "python manage.py runserver --help" for a list of runserver options. 6 | """ 7 | 8 | from flask_migrate import MigrateCommand 9 | from flask_script import Manager 10 | 11 | from app import create_app 12 | from app.commands import InitDbCommand 13 | 14 | # Setup Flask-Script with command line commands 15 | manager = Manager(create_app) 16 | manager.add_command('db', MigrateCommand) 17 | manager.add_command('init_db', InitDbCommand) 18 | 19 | if __name__ == "__main__": 20 | # python manage.py # shows available commands 21 | # python manage.py runserver --help # shows available runserver options 22 | manager.run() 23 | -------------------------------------------------------------------------------- /migrations/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | # file_template = %%(rev)s_%%(slug)s 6 | 7 | # set to 'true' to run the environment during 8 | # the 'revision' command, regardless of autogenerate 9 | # revision_environment = false 10 | 11 | 12 | # Logging configuration 13 | [loggers] 14 | keys = root,sqlalchemy,alembic 15 | 16 | [handlers] 17 | keys = console 18 | 19 | [formatters] 20 | keys = generic 21 | 22 | [logger_root] 23 | level = WARN 24 | handlers = console 25 | qualname = 26 | 27 | [logger_sqlalchemy] 28 | level = WARN 29 | handlers = 30 | qualname = sqlalchemy.engine 31 | 32 | [logger_alembic] 33 | level = INFO 34 | handlers = 35 | qualname = alembic 36 | 37 | [handler_console] 38 | class = StreamHandler 39 | args = (sys.stderr,) 40 | level = NOTSET 41 | formatter = generic 42 | 43 | [formatter_generic] 44 | format = %(levelname)-5.5s [%(name)s] %(message)s 45 | datefmt = %H:%M:%S 46 | -------------------------------------------------------------------------------- /app/local_settings_example.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # ***************************** 4 | # Environment specific settings 5 | # ***************************** 6 | 7 | # DO NOT use "DEBUG = True" in production environments 8 | DEBUG = True 9 | 10 | # DO NOT use Unsecure Secrets in production environments 11 | # Generate a safe one with: 12 | # python -c "import os; print repr(os.urandom(24));" 13 | SECRET_KEY = 'This is an UNSECURE Secret. CHANGE THIS for production environments.' 14 | 15 | # SQLAlchemy settings 16 | SQLALCHEMY_DATABASE_URI = 'sqlite:///../app.sqlite' 17 | SQLALCHEMY_TRACK_MODIFICATIONS = False # Avoids a SQLAlchemy Warning 18 | 19 | # Flask-Mail settings 20 | # For smtp.gmail.com to work, you MUST set "Allow less secure apps" to ON in Google Accounts. 21 | # Change it in https://myaccount.google.com/security#connectedapps (near the bottom). 22 | MAIL_SERVER = 'smtp.gmail.com' 23 | MAIL_PORT = 587 24 | MAIL_USE_SSL = False 25 | MAIL_USE_TLS = True 26 | MAIL_USERNAME = 'yourname@gmail.com' 27 | MAIL_PASSWORD = 'password' 28 | 29 | # Sendgrid settings 30 | SENDGRID_API_KEY='place-your-sendgrid-api-key-here' 31 | 32 | # Flask-User settings 33 | USER_APP_NAME = 'Flask-User starter app' 34 | USER_EMAIL_SENDER_NAME = 'Your name' 35 | USER_EMAIL_SENDER_EMAIL = 'yourname@gmail.com' 36 | 37 | ADMINS = [ 38 | '"Admin One" ', 39 | ] 40 | -------------------------------------------------------------------------------- /app/settings.py: -------------------------------------------------------------------------------- 1 | # Settings common to all environments (development|staging|production) 2 | # Place environment specific settings in env_settings.py 3 | # An example file (env_settings_example.py) can be used as a starting point 4 | 5 | import os 6 | 7 | # Application settings 8 | APP_NAME = "Flask-Celery-SQLAlchemy" 9 | APP_SYSTEM_ERROR_SUBJECT_LINE = APP_NAME + " system error" 10 | 11 | # Flask settings 12 | CSRF_ENABLED = True 13 | 14 | # Flask-SQLAlchemy settings 15 | SQLALCHEMY_TRACK_MODIFICATIONS = False 16 | 17 | # Flask-User settings 18 | USER_APP_NAME = APP_NAME 19 | USER_ENABLE_CHANGE_PASSWORD = True # Allow users to change their password 20 | USER_ENABLE_CHANGE_USERNAME = False # Allow users to change their username 21 | USER_ENABLE_CONFIRM_EMAIL = True # Force users to confirm their email 22 | USER_ENABLE_FORGOT_PASSWORD = True # Allow users to reset their passwords 23 | USER_ENABLE_EMAIL = True # Register with Email 24 | USER_ENABLE_REGISTRATION = True # Allow new users to register 25 | USER_REQUIRE_RETYPE_PASSWORD = True # Prompt for `retype password` in: 26 | USER_ENABLE_USERNAME = False # Register and Login with username 27 | USER_AFTER_LOGIN_ENDPOINT = 'main.member_page' 28 | USER_AFTER_LOGOUT_ENDPOINT = 'main.home_page' 29 | 30 | TESTING = False 31 | 32 | # Celery 33 | CELERY_REDIS_USE_SSL = False 34 | CELERY_ACCEPT_CONTENT = ['json'] 35 | CELERY_TASK_SERIALIZER = 'json' 36 | CELERY_RESULT_SERIALIZER = 'json' 37 | CELERY_REDIS_MAX_CONNECTIONS = 5 38 | -------------------------------------------------------------------------------- /app/templates/common/form_macros.html: -------------------------------------------------------------------------------- 1 | {% macro render_field(field, label=None, label_visible=true, right_url=None, right_label=None) -%} 2 |
3 | {% if field.type != 'HiddenField' and label_visible %} 4 | {% if not label %}{% set label=field.label.text %}{% endif %} 5 | 6 | {% endif %} 7 | {{ field(class_='form-control', **kwargs) }} 8 | {% if field.errors %} 9 | {% for e in field.errors %} 10 |

{{ e }}

11 | {% endfor %} 12 | {% endif %} 13 |
14 | {%- endmacro %} 15 | 16 | {% macro render_checkbox_field(field, label=None) -%} 17 | {% if not label %}{% set label=field.label.text %}{% endif %} 18 |
19 | 22 |
23 | {%- endmacro %} 24 | 25 | {% macro render_radio_field(field) -%} 26 | {% for value, label, checked in field.iter_choices() %} 27 |
28 | 32 |
33 | {% endfor %} 34 | {%- endmacro %} 35 | 36 | {% macro render_submit_field(field, label=None, tabindex=None) -%} 37 | {% if not label %}{% set label=field.label.text %}{% endif %} 38 | {##} 39 | 42 | {%- endmacro %} 43 | -------------------------------------------------------------------------------- /tests/test_page_urls.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014 SolidBuilds.com. All rights reserved 2 | # 3 | # Authors: Ling Thio 4 | 5 | from __future__ import print_function # Use print() instead of print 6 | from flask import url_for 7 | 8 | 9 | def test_page_urls(client): 10 | # Visit home page 11 | response = client.get(url_for('main.home_page'), follow_redirects=True) 12 | assert response.status_code==200 13 | 14 | # Login as user and visit User page 15 | response = client.post(url_for('user.login'), follow_redirects=True, 16 | data=dict(email='user@example.com', password='Password1')) 17 | assert response.status_code==200 18 | response = client.get(url_for('main.member_page'), follow_redirects=True) 19 | assert response.status_code==200 20 | 21 | # Edit User Profile page 22 | response = client.get(url_for('main.user_profile_page'), follow_redirects=True) 23 | assert response.status_code==200 24 | response = client.post(url_for('main.user_profile_page'), follow_redirects=True, 25 | data=dict(first_name='User', last_name='User')) 26 | response = client.get(url_for('main.member_page'), follow_redirects=True) 27 | assert response.status_code==200 28 | 29 | # Logout 30 | response = client.get(url_for('user.logout'), follow_redirects=True) 31 | assert response.status_code==200 32 | 33 | # Login as admin and visit Admin page 34 | response = client.post(url_for('user.login'), follow_redirects=True, 35 | data=dict(email='admin@example.com', password='Password1')) 36 | assert response.status_code==200 37 | response = client.get(url_for('main.admin_page'), follow_redirects=True) 38 | assert response.status_code==200 39 | 40 | # Logout 41 | response = client.get(url_for('user.logout'), follow_redirects=True) 42 | assert response.status_code==200 43 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # This file contains pytest 'fixtures'. 2 | # If a test functions specifies the name of a fixture function as a parameter, 3 | # the fixture function is called and its result is passed to the test function. 4 | # 5 | # Copyright 2014 SolidBuilds.com. All rights reserved 6 | # 7 | # Authors: Ling Thio 8 | 9 | import pytest 10 | from app import create_app, db as the_db 11 | 12 | # Initialize the Flask-App with test-specific settings 13 | the_app = create_app(dict( 14 | TESTING=True, # Propagate exceptions 15 | LOGIN_DISABLED=False, # Enable @register_required 16 | MAIL_SUPPRESS_SEND=True, # Disable Flask-Mail send 17 | SERVER_NAME='localhost', # Enable url_for() without request context 18 | SQLALCHEMY_DATABASE_URI='sqlite:///:memory:', # In-memory SQLite DB 19 | WTF_CSRF_ENABLED=False, # Disable CSRF form validation 20 | )) 21 | 22 | # Setup an application context (since the tests run outside of the webserver context) 23 | the_app.app_context().push() 24 | 25 | # Create and populate roles and users tables 26 | from app.commands.init_db import init_db 27 | init_db() 28 | 29 | 30 | @pytest.fixture(scope='session') 31 | def app(): 32 | """ Makes the 'app' parameter available to test functions. """ 33 | return the_app 34 | 35 | 36 | @pytest.fixture(scope='session') 37 | def db(): 38 | """ Makes the 'db' parameter available to test functions. """ 39 | return the_db 40 | 41 | @pytest.fixture(scope='function') 42 | def session(db, request): 43 | """Creates a new database session for a test.""" 44 | connection = db.engine.connect() 45 | transaction = connection.begin() 46 | 47 | options = dict(bind=connection, binds={}) 48 | session = db.create_scoped_session(options=options) 49 | 50 | db.session = session 51 | 52 | def teardown(): 53 | transaction.rollback() 54 | connection.close() 55 | session.remove() 56 | 57 | request.addfinalizer(teardown) 58 | return session 59 | 60 | @pytest.fixture(scope='session') 61 | def client(app): 62 | return app.test_client() 63 | 64 | -------------------------------------------------------------------------------- /migrations/versions/0001c8ac1a69_initial_version.py: -------------------------------------------------------------------------------- 1 | """Initial version 2 | 3 | Revision ID: 0001c8ac1a69 4 | Revises: None 5 | Create Date: 2015-07-14 17:46:32.620018 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '0001c8ac1a69' 11 | down_revision = None 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | ### commands auto generated by Alembic - please adjust! ### 19 | op.create_table('role', 20 | sa.Column('id', sa.Integer(), nullable=False), 21 | sa.Column('name', sa.String(length=50), nullable=True), 22 | sa.Column('description', sa.String(length=255), server_default='', nullable=True), 23 | sa.PrimaryKeyConstraint('id'), 24 | sa.UniqueConstraint('name') 25 | ) 26 | op.create_table('user', 27 | sa.Column('id', sa.Integer(), nullable=False), 28 | sa.Column('username', sa.String(length=50), nullable=False), 29 | sa.Column('password', sa.String(length=255), server_default='', nullable=False), 30 | sa.Column('reset_password_token', sa.String(length=100), server_default='', nullable=False), 31 | sa.Column('email', sa.String(length=255), nullable=False), 32 | sa.Column('confirmed_at', sa.DateTime(), nullable=True), 33 | sa.Column('is_active', sa.Boolean(), server_default='0', nullable=False), 34 | sa.Column('first_name', sa.String(length=50), server_default='', nullable=False), 35 | sa.Column('last_name', sa.String(length=50), server_default='', nullable=False), 36 | sa.PrimaryKeyConstraint('id'), 37 | sa.UniqueConstraint('email'), 38 | sa.UniqueConstraint('username') 39 | ) 40 | op.create_table('user_roles', 41 | sa.Column('id', sa.Integer(), nullable=False), 42 | sa.Column('user_id', sa.Integer(), nullable=True), 43 | sa.Column('role_id', sa.Integer(), nullable=True), 44 | sa.ForeignKeyConstraint(['role_id'], ['role.id'], ondelete='CASCADE'), 45 | sa.ForeignKeyConstraint(['user_id'], ['user.id'], ondelete='CASCADE'), 46 | sa.PrimaryKeyConstraint('id') 47 | ) 48 | ### end Alembic commands ### 49 | 50 | 51 | def downgrade(): 52 | ### commands auto generated by Alembic - please adjust! ### 53 | op.drop_table('user_roles') 54 | op.drop_table('user') 55 | op.drop_table('role') 56 | ### end Alembic commands ### 57 | -------------------------------------------------------------------------------- /app/models/feedeater_models.py: -------------------------------------------------------------------------------- 1 | from app import db 2 | 3 | 4 | class Feed(db.Model): 5 | id: int = db.Column(db.Integer, primary_key=True) 6 | 7 | ACTIVE = 1 8 | INACTIVE = 2 9 | DELETED = 0 10 | STATUSES = ( 11 | (ACTIVE, 'Active'), 12 | (DELETED, 'Deleted'), 13 | (INACTIVE, 'Inactive (fetch failed)'), 14 | ) 15 | 16 | status: int = db.Column(db.Integer, nullable=False, default='1', server_default='1') 17 | 18 | url: str = db.Column(db.String(200), nullable=False) 19 | title: str = db.Column(db.String(100), nullable=False) 20 | type: str = db.Column(db.String(10), nullable=False) 21 | htmlUrl: str = db.Column(db.String(200), nullable=True) 22 | 23 | etag: str = db.Column(db.String(200), nullable=True) 24 | updated: str = db.Column(db.DateTime(), nullable=True) 25 | 26 | created: str = db.Column(db.DateTime(), nullable=False) 27 | fetched: str = db.Column(db.DateTime(), nullable=True) 28 | 29 | # articles: List[Article] = attrib(default=None) 30 | 31 | # tags: List[str] = attrib(default=None) 32 | 33 | def __str__(self): 34 | return f'' 35 | 36 | 37 | class Article(db.Model): 38 | id = db.Column(db.Integer, primary_key=True) 39 | 40 | article_id: str = db.Column(db.String(200), nullable=False) 41 | title: str = db.Column(db.String(500), nullable=True) 42 | published: str = db.Column(db.DateTime(), nullable=True) 43 | summary: str = db.Column(db.Text(), nullable=True) 44 | link: str = db.Column(db.String(300), nullable=True) 45 | fetched: str = db.Column(db.DateTime(), nullable=False) 46 | 47 | feed_id = db.Column(db.Integer(), db.ForeignKey('feed.id', ondelete='CASCADE'), nullable=False) 48 | feed = db.relationship('Feed', backref=db.backref('articles', lazy='dynamic')) 49 | 50 | 51 | class FeedResult(db.Model): 52 | id = db.Column(db.Integer, primary_key=True) 53 | 54 | status_code = db.Column(db.Integer, nullable=True) 55 | completed_date = db.Column(db.DateTime(), nullable=False) 56 | log = db.Column(db.JSON, nullable=True) 57 | had_exception = db.Column(db.Boolean, default=False, nullable=False) 58 | 59 | feed_id = db.Column(db.Integer(), db.ForeignKey('feed.id', ondelete='CASCADE'), nullable=False) 60 | feed = db.relationship('Feed', backref=db.backref('results', lazy='dynamic')) 61 | -------------------------------------------------------------------------------- /migrations/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | from alembic import context 3 | from sqlalchemy import engine_from_config, pool 4 | from logging.config import fileConfig 5 | 6 | # this is the Alembic Config object, which provides 7 | # access to the values within the .ini file in use. 8 | config = context.config 9 | 10 | # Interpret the config file for Python logging. 11 | # This line sets up loggers basically. 12 | fileConfig(config.config_file_name) 13 | 14 | # add your model's MetaData object here 15 | # for 'autogenerate' support 16 | # from myapp import mymodel 17 | # target_metadata = mymodel.Base.metadata 18 | from flask import current_app 19 | config.set_main_option('sqlalchemy.url', current_app.config.get('SQLALCHEMY_DATABASE_URI')) 20 | target_metadata = current_app.extensions['migrate'].db.metadata 21 | 22 | # other values from the config, defined by the needs of env.py, 23 | # can be acquired: 24 | # my_important_option = config.get_main_option("my_important_option") 25 | # ... etc. 26 | 27 | 28 | def run_migrations_offline(): 29 | """Run migrations in 'offline' mode. 30 | 31 | This configures the context with just a URL 32 | and not an Engine, though an Engine is acceptable 33 | here as well. By skipping the Engine creation 34 | we don't even need a DBAPI to be available. 35 | 36 | Calls to context.execute() here emit the given string to the 37 | script output. 38 | 39 | """ 40 | url = config.get_main_option("sqlalchemy.url") 41 | context.configure(url=url) 42 | 43 | with context.begin_transaction(): 44 | context.run_migrations() 45 | 46 | 47 | def run_migrations_online(): 48 | """Run migrations in 'online' mode. 49 | 50 | In this scenario we need to create an Engine 51 | and associate a connection with the context. 52 | 53 | """ 54 | engine = engine_from_config(config.get_section(config.config_ini_section), 55 | prefix='sqlalchemy.', 56 | poolclass=pool.NullPool) 57 | 58 | connection = engine.connect() 59 | context.configure(connection=connection, 60 | target_metadata=target_metadata, 61 | **current_app.extensions['migrate'].configure_args) 62 | 63 | try: 64 | with context.begin_transaction(): 65 | context.run_migrations() 66 | finally: 67 | connection.close() 68 | 69 | if context.is_offline_mode(): 70 | run_migrations_offline() 71 | else: 72 | run_migrations_online() 73 | 74 | -------------------------------------------------------------------------------- /migrations/versions/b3a7e2b375e0_feedeater.py: -------------------------------------------------------------------------------- 1 | """ 2 | Create feedeater tables (feed, article, feed_result) 3 | 4 | Revision ID: b3a7e2b375e0 5 | Revises: 0001c8ac1a69 6 | Create Date: 2018-12-23 15:04:57.578448 7 | 8 | """ 9 | 10 | # revision identifiers, used by Alembic. 11 | revision = 'b3a7e2b375e0' 12 | down_revision = '0001c8ac1a69' 13 | 14 | from alembic import op 15 | import sqlalchemy as sa 16 | from sqlalchemy.dialects import mysql 17 | 18 | 19 | def upgrade(): 20 | op.create_table('feed', 21 | sa.Column('id', sa.Integer(), nullable=False), 22 | sa.Column('status', sa.Integer(), server_default='1', nullable=False), 23 | sa.Column('url', sa.String(length=200), nullable=False), 24 | sa.Column('title', sa.String(length=100), nullable=False), 25 | sa.Column('type', sa.String(length=10), nullable=False), 26 | sa.Column('htmlUrl', sa.String(length=200), nullable=True), 27 | sa.Column('etag', sa.String(length=200), nullable=True), 28 | sa.Column('updated', sa.DateTime(), nullable=True), 29 | sa.Column('created', sa.DateTime(), nullable=False), 30 | sa.Column('fetched', sa.DateTime(), nullable=True), 31 | sa.PrimaryKeyConstraint('id') 32 | ) 33 | op.create_table('article', 34 | sa.Column('id', sa.Integer(), nullable=False), 35 | sa.Column('article_id', sa.String(length=200), nullable=False), 36 | sa.Column('title', sa.String(length=500), nullable=True), 37 | sa.Column('published', sa.DateTime(), nullable=True), 38 | sa.Column('summary', sa.Text(), nullable=True), 39 | sa.Column('link', sa.String(length=300), nullable=True), 40 | sa.Column('fetched', sa.DateTime(), nullable=False), 41 | sa.Column('feed_id', sa.Integer(), nullable=False), 42 | sa.ForeignKeyConstraint(['feed_id'], ['feed.id'], ondelete='CASCADE'), 43 | sa.PrimaryKeyConstraint('id') 44 | ) 45 | op.create_table('feed_result', 46 | sa.Column('id', sa.Integer(), nullable=False), 47 | sa.Column('status_code', sa.Integer(), nullable=True), 48 | sa.Column('completed_date', sa.DateTime(), nullable=False), 49 | sa.Column('log', sa.JSON(), nullable=True), 50 | sa.Column('had_exception', sa.Boolean(), nullable=False), 51 | sa.Column('feed_id', sa.Integer(), nullable=False), 52 | sa.ForeignKeyConstraint(['feed_id'], ['feed.id'], ondelete='CASCADE'), 53 | sa.PrimaryKeyConstraint('id') 54 | ) 55 | 56 | 57 | def downgrade(): 58 | op.drop_table('feed_result') 59 | op.drop_table('article') 60 | op.drop_table('feed') 61 | -------------------------------------------------------------------------------- /app/views/main_views.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from flask import Blueprint, redirect, render_template, flash, jsonify 4 | from flask import request, url_for 5 | from flask_user import current_user, login_required, roles_required 6 | 7 | from app import db, tasks 8 | from app.models.feedeater_models import Feed 9 | from app.models.user_models import UserProfileForm 10 | 11 | main_blueprint = Blueprint('main', __name__, template_folder='templates') 12 | 13 | 14 | # The Home page is accessible to anyone 15 | @main_blueprint.route('/') 16 | def home_page(): 17 | return render_template('main/home_page.html') 18 | 19 | 20 | # The User page is accessible to authenticated users (users that have logged in) 21 | @main_blueprint.route('/member') 22 | @login_required # Limits access to authenticated users 23 | def member_page(): 24 | return render_template('main/user_page.html') 25 | 26 | 27 | # The Admin page is accessible to users with the 'admin' role 28 | @main_blueprint.route('/admin') 29 | @roles_required('admin') # Limits access to users with the 'admin' role 30 | def admin_page(): 31 | return render_template('main/admin_page.html') 32 | 33 | 34 | @main_blueprint.route('/main/profile', methods=['GET', 'POST']) 35 | @login_required 36 | def user_profile_page(): 37 | # Initialize form 38 | form = UserProfileForm(request.form, obj=current_user) 39 | 40 | # Process valid POST 41 | if request.method == 'POST' and form.validate(): 42 | # Copy form fields to user_profile fields 43 | form.populate_obj(current_user) 44 | 45 | # Save user_profile 46 | db.session.commit() 47 | 48 | # Redirect to home page 49 | return redirect(url_for('main.home_page')) 50 | 51 | # Process GET or invalid POST 52 | return render_template('main/user_profile_page.html', 53 | form=form) 54 | 55 | 56 | @main_blueprint.route('/task') 57 | def task(): 58 | feeds = Feed.query.filter_by(status=1) 59 | 60 | for feed in feeds: 61 | tasks.fetch_articles.delay(feed.id) 62 | 63 | flash('Fetch articles task kicked off') 64 | return render_template('main/home_page.html') 65 | 66 | 67 | @main_blueprint.route('/new-task', methods=['GET', 'POST']) 68 | def new_task(): 69 | title = request.args.get('title') 70 | url = request.args.get('url') 71 | 72 | feed = Feed( 73 | title=title, status=1, url=url, type='rss', 74 | created=datetime.datetime.utcnow(), updated=datetime.datetime.utcnow()) 75 | db.session.add(feed) 76 | 77 | db.session.commit() 78 | 79 | tasks.fetch_articles.delay(feed.id) 80 | 81 | return jsonify({'message': 'Feed created.', 'feed_id': feed.id}) 82 | -------------------------------------------------------------------------------- /app/static/bootstrap/js/html5shiv.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @preserve HTML5 Shiv 3.7.3 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed 3 | */ 4 | !function(a,b){function c(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function d(){var a=t.elements;return"string"==typeof a?a.split(" "):a}function e(a,b){var c=t.elements;"string"!=typeof c&&(c=c.join(" ")),"string"!=typeof a&&(a=a.join(" ")),t.elements=c+" "+a,j(b)}function f(a){var b=s[a[q]];return b||(b={},r++,a[q]=r,s[r]=b),b}function g(a,c,d){if(c||(c=b),l)return c.createElement(a);d||(d=f(c));var e;return e=d.cache[a]?d.cache[a].cloneNode():p.test(a)?(d.cache[a]=d.createElem(a)).cloneNode():d.createElem(a),!e.canHaveChildren||o.test(a)||e.tagUrn?e:d.frag.appendChild(e)}function h(a,c){if(a||(a=b),l)return a.createDocumentFragment();c=c||f(a);for(var e=c.frag.cloneNode(),g=0,h=d(),i=h.length;i>g;g++)e.createElement(h[g]);return e}function i(a,b){b.cache||(b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag()),a.createElement=function(c){return t.shivMethods?g(c,a,b):b.createElem(c)},a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+d().join().replace(/[\w\-:]+/g,function(a){return b.createElem(a),b.frag.createElement(a),'c("'+a+'")'})+");return n}")(t,b.frag)}function j(a){a||(a=b);var d=f(a);return!t.shivCSS||k||d.hasCSS||(d.hasCSS=!!c(a,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),l||i(a,d),a}var k,l,m="3.7.3",n=a.html5||{},o=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,p=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,q="_html5shiv",r=0,s={};!function(){try{var a=b.createElement("a");a.innerHTML="",k="hidden"in a,l=1==a.childNodes.length||function(){b.createElement("a");var a=b.createDocumentFragment();return"undefined"==typeof a.cloneNode||"undefined"==typeof a.createDocumentFragment||"undefined"==typeof a.createElement}()}catch(c){k=!0,l=!0}}();var t={elements:n.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:m,shivCSS:n.shivCSS!==!1,supportsUnknownElements:l,shivMethods:n.shivMethods!==!1,type:"default",shivDocument:j,createElement:g,createDocumentFragment:h,addElements:e};a.html5=t,j(b),"object"==typeof module&&module.exports&&(module.exports=t)}("undefined"!=typeof window?window:this,document); -------------------------------------------------------------------------------- /app/models/user_models.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014 SolidBuilds.com. All rights reserved 2 | # 3 | # Authors: Ling Thio 4 | 5 | from flask_user import UserMixin 6 | # from flask_user.forms import RegisterForm 7 | from flask_wtf import FlaskForm 8 | from wtforms import StringField, SubmitField, validators 9 | from app import db 10 | 11 | 12 | # Define the User data model. Make sure to add the flask_user.UserMixin !! 13 | class User(db.Model, UserMixin): 14 | __tablename__ = 'users' 15 | id = db.Column(db.Integer, primary_key=True) 16 | 17 | # User authentication information (required for Flask-User) 18 | email = db.Column(db.Unicode(255), nullable=False, server_default=u'', unique=True) 19 | email_confirmed_at = db.Column(db.DateTime()) 20 | password = db.Column(db.String(255), nullable=False, server_default='') 21 | # reset_password_token = db.Column(db.String(100), nullable=False, server_default='') 22 | active = db.Column(db.Boolean(), nullable=False, server_default='0') 23 | 24 | # User information 25 | active = db.Column('is_active', db.Boolean(), nullable=False, server_default='0') 26 | first_name = db.Column(db.Unicode(50), nullable=False, server_default=u'') 27 | last_name = db.Column(db.Unicode(50), nullable=False, server_default=u'') 28 | 29 | # Relationships 30 | roles = db.relationship('Role', secondary='users_roles', 31 | backref=db.backref('users', lazy='dynamic')) 32 | 33 | 34 | # Define the Role data model 35 | class Role(db.Model): 36 | __tablename__ = 'roles' 37 | id = db.Column(db.Integer(), primary_key=True) 38 | name = db.Column(db.String(50), nullable=False, server_default=u'', unique=True) # for @roles_accepted() 39 | label = db.Column(db.Unicode(255), server_default=u'') # for display purposes 40 | 41 | 42 | # Define the UserRoles association model 43 | class UsersRoles(db.Model): 44 | __tablename__ = 'users_roles' 45 | id = db.Column(db.Integer(), primary_key=True) 46 | user_id = db.Column(db.Integer(), db.ForeignKey('users.id', ondelete='CASCADE')) 47 | role_id = db.Column(db.Integer(), db.ForeignKey('roles.id', ondelete='CASCADE')) 48 | 49 | 50 | # # Define the User registration form 51 | # # It augments the Flask-User RegisterForm with additional fields 52 | # class MyRegisterForm(RegisterForm): 53 | # first_name = StringField('First name', validators=[ 54 | # validators.DataRequired('First name is required')]) 55 | # last_name = StringField('Last name', validators=[ 56 | # validators.DataRequired('Last name is required')]) 57 | 58 | 59 | # Define the User profile form 60 | class UserProfileForm(FlaskForm): 61 | first_name = StringField('First name', validators=[ 62 | validators.DataRequired('First name is required')]) 63 | last_name = StringField('Last name', validators=[ 64 | validators.DataRequired('Last name is required')]) 65 | submit = SubmitField('Save') 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flask + Celery + SQLAlchemy Example App 2 | 3 | This example app demonstrates how to write Celery tasks that work with Flask and 4 | SQLAlchemy. I had a hard time finding a complete example that worked correctly. 5 | 6 | Based on the [the Flask-User-Starter-App](https://github.com/lingthio/Flask-User-starter-app). 7 | 8 | ## Code characteristics 9 | 10 | * Tested on Python 3.9 11 | * Well organized directories with lots of comments 12 | * app 13 | * commands 14 | * models 15 | * static 16 | * templates 17 | * views 18 | * tests 19 | * Includes test framework (`py.test`) 20 | * Includes database migration framework (`alembic`) 21 | * Sends error emails to admins for unhandled exceptions 22 | 23 | 24 | ## Setting up a development environment 25 | 26 | We assume that you have `git` and `virtualenv` and `virtualenvwrapper` installed. 27 | 28 | # Clone the code repository into ~/dev/my_app 29 | mkdir -p ~/dev 30 | cd ~/dev 31 | git clone https://github.com/lingthio/Flask-User-starter-app.git my_app 32 | 33 | # Create the 'my_app' virtual environment 34 | mkvirtualenv -p PATH/TO/PYTHON my_app 35 | 36 | # Install required Python packages 37 | cd ~/dev/my_app 38 | workon my_app 39 | pipenv install 40 | 41 | 42 | # Configuring SMTP 43 | 44 | Copy the `local_settings_example.py` file to `local_settings.py`. 45 | 46 | cp app/local_settings_example.py app/local_settings.py 47 | 48 | Edit the `local_settings.py` file. 49 | 50 | Specifically set all the MAIL_... settings to match your SMTP settings 51 | 52 | Note that Google's SMTP server requires the configuration of "less secure apps". 53 | See https://support.google.com/accounts/answer/6010255?hl=en 54 | 55 | Note that Yahoo's SMTP server requires the configuration of "Allow apps that use less secure sign in". 56 | See https://help.yahoo.com/kb/SLN27791.html 57 | 58 | 59 | ## Initializing the Database 60 | 61 | # Create DB tables and populate the roles and users tables 62 | python manage.py init_db 63 | 64 | # Or if you have Fabric installed: 65 | fab init_db 66 | 67 | 68 | ## Running the app 69 | 70 | # Start the Flask development web server 71 | python manage.py runserver 72 | 73 | # Or if you have Fabric installed: 74 | fab runserver 75 | 76 | Point your web browser to http://localhost:5000/ 77 | 78 | You can make use of the following users: 79 | - email `user@example.com` with password `Password1`. 80 | - email `admin@example.com` with password `Password1`. 81 | 82 | 83 | ## Running the automated tests 84 | 85 | # Start the Flask development web server 86 | py.test tests/ 87 | 88 | # Or if you have Fabric installed: 89 | fab test 90 | 91 | 92 | ## Trouble shooting 93 | 94 | If you make changes in the Models and run into DB schema issues, delete the sqlite DB file `app.sqlite`. 95 | 96 | 97 | ## Acknowledgements 98 | 99 | With thanks to the following Flask extensions: 100 | 101 | * [Alembic](http://alembic.zzzcomputing.com/) 102 | * [Flask](http://flask.pocoo.org/) 103 | * [Flask-Login](https://flask-login.readthedocs.io/) 104 | * [Flask-Migrate](https://flask-migrate.readthedocs.io/) 105 | * [Flask-Script](https://flask-script.readthedocs.io/) 106 | * [Flask-User](http://flask-user.readthedocs.io/en/v0.6/) 107 | 108 | 109 | [Flask-User-starter-app](https://github.com/lingthio/Flask-User-starter-app) was used as a starting point for this code repository. 110 | 111 | 112 | ## Authors 113 | 114 | - Kurt Wiersma (kwiersma at gmail.com) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### macOS template 3 | # General 4 | .DS_Store 5 | .AppleDouble 6 | .LSOverride 7 | 8 | # Icon must end with two \r 9 | Icon 10 | 11 | # Thumbnails 12 | ._* 13 | 14 | # Files that might appear in the root of a volume 15 | .DocumentRevisions-V100 16 | .fseventsd 17 | .Spotlight-V100 18 | .TemporaryItems 19 | .Trashes 20 | .VolumeIcon.icns 21 | .com.apple.timemachine.donotpresent 22 | 23 | # Directories potentially created on remote AFP share 24 | .AppleDB 25 | .AppleDesktop 26 | Network Trash Folder 27 | Temporary Items 28 | .apdisk 29 | ### JetBrains template 30 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 31 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 32 | 33 | # User-specific stuff 34 | .idea/**/workspace.xml 35 | .idea/**/tasks.xml 36 | .idea/**/usage.statistics.xml 37 | .idea/**/dictionaries 38 | .idea/**/shelf 39 | 40 | # Sensitive or high-churn files 41 | .idea/**/dataSources/ 42 | .idea/**/dataSources.ids 43 | .idea/**/dataSources.local.xml 44 | .idea/**/sqlDataSources.xml 45 | .idea/**/dynamic.xml 46 | .idea/**/uiDesigner.xml 47 | .idea/**/dbnavigator.xml 48 | 49 | # Gradle 50 | .idea/**/gradle.xml 51 | .idea/**/libraries 52 | 53 | # Gradle and Maven with auto-import 54 | # When using Gradle or Maven with auto-import, you should exclude module files, 55 | # since they will be recreated, and may cause churn. Uncomment if using 56 | # auto-import. 57 | # .idea/modules.xml 58 | # .idea/*.iml 59 | # .idea/modules 60 | 61 | # CMake 62 | cmake-build-*/ 63 | 64 | # Mongo Explorer plugin 65 | .idea/**/mongoSettings.xml 66 | 67 | # File-based project format 68 | *.iws 69 | 70 | # IntelliJ 71 | out/ 72 | 73 | # mpeltonen/sbt-idea plugin 74 | .idea_modules/ 75 | 76 | # JIRA plugin 77 | atlassian-ide-plugin.xml 78 | 79 | # Cursive Clojure plugin 80 | .idea/replstate.xml 81 | 82 | # Crashlytics plugin (for Android Studio and IntelliJ) 83 | com_crashlytics_export_strings.xml 84 | crashlytics.properties 85 | crashlytics-build.properties 86 | fabric.properties 87 | 88 | # Editor-based Rest Client 89 | .idea/httpRequests 90 | ### Python template 91 | # Byte-compiled / optimized / DLL files 92 | __pycache__/ 93 | *.py[cod] 94 | *$py.class 95 | 96 | # C extensions 97 | *.so 98 | 99 | # Distribution / packaging 100 | .Python 101 | build/ 102 | develop-eggs/ 103 | dist/ 104 | downloads/ 105 | eggs/ 106 | .eggs/ 107 | lib/ 108 | lib64/ 109 | parts/ 110 | sdist/ 111 | var/ 112 | wheels/ 113 | *.egg-info/ 114 | .installed.cfg 115 | *.egg 116 | MANIFEST 117 | 118 | # PyInstaller 119 | # Usually these files are written by a python script from a template 120 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 121 | *.manifest 122 | *.spec 123 | 124 | # Installer logs 125 | pip-log.txt 126 | pip-delete-this-directory.txt 127 | 128 | # Unit test / coverage reports 129 | htmlcov/ 130 | .tox/ 131 | .coverage 132 | .coverage.* 133 | .cache 134 | nosetests.xml 135 | coverage.xml 136 | *.cover 137 | .hypothesis/ 138 | .pytest_cache/ 139 | 140 | # Translations 141 | *.mo 142 | *.pot 143 | 144 | # Django stuff: 145 | *.log 146 | local_settings.py 147 | db.sqlite3 148 | 149 | # Flask stuff: 150 | instance/ 151 | .webassets-cache 152 | 153 | # Scrapy stuff: 154 | .scrapy 155 | 156 | # Sphinx documentation 157 | docs/_build/ 158 | 159 | # PyBuilder 160 | target/ 161 | 162 | # Jupyter Notebook 163 | .ipynb_checkpoints 164 | 165 | # pyenv 166 | .python-version 167 | 168 | # celery beat schedule file 169 | celerybeat-schedule 170 | 171 | # SageMath parsed files 172 | *.sage.py 173 | 174 | # Environments 175 | .env 176 | .venv 177 | env/ 178 | venv/ 179 | ENV/ 180 | env.bak/ 181 | venv.bak/ 182 | 183 | # Spyder project settings 184 | .spyderproject 185 | .spyproject 186 | 187 | # Rope project settings 188 | .ropeproject 189 | 190 | # mkdocs documentation 191 | /site 192 | 193 | # mypy 194 | .mypy_cache/ 195 | -------------------------------------------------------------------------------- /app/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Flask-User Starter App 11 | 12 | 13 | 14 | 15 | 16 | 20 | 21 | 22 | 23 | 24 | 25 | {% block body %} 26 | 27 |
28 | 29 |
30 | {% if current_user.is_authenticated %} 31 | {{ current_user.first_name or current_user.username or current_user.email }} 32 |   |   33 | Sign out 34 | {% else %} 35 | Sign in 36 | {% endif %} 37 |
38 |
39 |
40 | 41 |
42 | {% block pre_content %}{% endblock %} 43 | 44 | {# One-time system messages called Flash messages #} 45 | {% block flash_messages %} 46 | {%- with messages = get_flashed_messages(with_categories=true) -%} 47 | {% if messages %} 48 | {% for category, message in messages %} 49 | {% if category=='error' %} 50 | {% set category='danger' %} 51 | {% endif %} 52 |
{{ message|safe }}
53 | {% endfor %} 54 | {% endif %} 55 | {%- endwith %} 56 | {% endblock %} 57 | 58 | {% block content %}{% endblock %} 59 | 60 | {% block post_content %}{% endblock %} 61 |
62 | 63 |
64 |
65 | 69 | 70 | 71 | 72 | 73 | 74 | 84 | {% endblock %} 85 | 86 | 87 | -------------------------------------------------------------------------------- /app/commands/init_db.py: -------------------------------------------------------------------------------- 1 | # This file defines command line commands for manage.py 2 | 3 | import datetime 4 | 5 | from flask import current_app 6 | from flask_script import Command 7 | 8 | from app import db 9 | from app.models.feedeater_models import Feed 10 | from app.models.user_models import User, Role 11 | 12 | 13 | class InitDbCommand(Command): 14 | """ Initialize the database.""" 15 | 16 | def run(self): 17 | init_db() 18 | print('Database has been initialized.') 19 | 20 | 21 | def init_db(): 22 | """ Initialize the database.""" 23 | db.drop_all() 24 | db.create_all() 25 | create_users() 26 | create_feeds() 27 | 28 | 29 | def create_feeds(): 30 | feed = Feed( 31 | title='Real Python', status=1, url='https://realpython.com/atom.xml', type='rss', 32 | created=datetime.datetime.utcnow(), updated=datetime.datetime.utcnow()) 33 | db.session.add(feed) 34 | feed2 = Feed( 35 | title='Planet Python', status=1, url='http://planetpython.org/rss20.xml', type='rss', 36 | created=datetime.datetime.utcnow(), updated=datetime.datetime.utcnow()) 37 | db.session.add(feed2) 38 | feed3 = Feed( 39 | title="Simon Willison's Weblog", url='https://simonwillison.net/atom/everything/', type='rss', 40 | created=datetime.datetime.utcnow(), updated=datetime.datetime.utcnow() 41 | ) 42 | db.session.add(feed3) 43 | feed4 = Feed( 44 | title="Django community", url='https://www.djangoproject.com/rss/community/blogs/', type='rss', 45 | created=datetime.datetime.utcnow(), updated=datetime.datetime.utcnow()) 46 | db.session.add(feed4) 47 | feed5 = Feed( 48 | title="PyCharm Blog", url='http://feeds.feedburner.com/Pycharm', type='rss', 49 | created=datetime.datetime.utcnow(), updated=datetime.datetime.utcnow()) 50 | db.session.add(feed5) 51 | feed6 = Feed( 52 | title="The Django weblog", url='https://www.djangoproject.com/rss/weblog/', type='rss', 53 | created=datetime.datetime.utcnow(), updated=datetime.datetime.utcnow()) 54 | db.session.add(feed6) 55 | feed7 = Feed( 56 | title="SQLAlchemy", url='https://www.sqlalchemy.org/blog/feed/atom/index.xml', type='rss', 57 | created=datetime.datetime.utcnow(), updated=datetime.datetime.utcnow()) 58 | db.session.add(feed7) 59 | feed8 = Feed( 60 | title="Blog — Pallets Project", url='http://www.palletsprojects.com/blog/feed.xml', type='rss', 61 | created=datetime.datetime.utcnow(), updated=datetime.datetime.utcnow()) 62 | db.session.add(feed8) 63 | 64 | db.session.commit() 65 | 66 | 67 | def create_users(): 68 | """ Create users """ 69 | 70 | # Create all tables 71 | db.create_all() 72 | 73 | # Adding roles 74 | admin_role = find_or_create_role('admin', u'Admin') 75 | 76 | # Add users 77 | user = find_or_create_user(u'Admin', u'Example', u'admin@example.com', 'Password1', admin_role) 78 | user = find_or_create_user(u'Member', u'Example', u'member@example.com', 'Password1') 79 | 80 | # Save to DB 81 | db.session.commit() 82 | 83 | 84 | def find_or_create_role(name, label): 85 | """ Find existing role or create new role """ 86 | role = Role.query.filter(Role.name == name).first() 87 | if not role: 88 | role = Role(name=name, label=label) 89 | db.session.add(role) 90 | return role 91 | 92 | 93 | def find_or_create_user(first_name, last_name, email, password, role=None): 94 | """ Find existing user or create new user """ 95 | user = User.query.filter(User.email == email).first() 96 | if not user: 97 | user = User(email=email, 98 | first_name=first_name, 99 | last_name=last_name, 100 | password=current_app.user_manager.password_manager.hash_password(password), 101 | active=True, 102 | email_confirmed_at=datetime.datetime.utcnow()) 103 | if role: 104 | user.roles.append(role) 105 | db.session.add(user) 106 | return user 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /app/static/bootstrap/js/respond.min.js: -------------------------------------------------------------------------------- 1 | /*! Respond.js v1.4.2: min/max-width media query polyfill * Copyright 2013 Scott Jehl 2 | * Licensed under https://github.com/scottjehl/Respond/blob/master/LICENSE-MIT 3 | * */ 4 | 5 | !function(a){"use strict";a.matchMedia=a.matchMedia||function(a){var b,c=a.documentElement,d=c.firstElementChild||c.firstChild,e=a.createElement("body"),f=a.createElement("div");return f.id="mq-test-1",f.style.cssText="position:absolute;top:-100em",e.style.background="none",e.appendChild(f),function(a){return f.innerHTML='­',c.insertBefore(e,d),b=42===f.offsetWidth,c.removeChild(e),{matches:b,media:a}}}(a.document)}(this),function(a){"use strict";function b(){u(!0)}var c={};a.respond=c,c.update=function(){};var d=[],e=function(){var b=!1;try{b=new a.XMLHttpRequest}catch(c){b=new a.ActiveXObject("Microsoft.XMLHTTP")}return function(){return b}}(),f=function(a,b){var c=e();c&&(c.open("GET",a,!0),c.onreadystatechange=function(){4!==c.readyState||200!==c.status&&304!==c.status||b(c.responseText)},4!==c.readyState&&c.send(null))};if(c.ajax=f,c.queue=d,c.regex={media:/@media[^\{]+\{([^\{\}]*\{[^\}\{]*\})+/gi,keyframes:/@(?:\-(?:o|moz|webkit)\-)?keyframes[^\{]+\{(?:[^\{\}]*\{[^\}\{]*\})+[^\}]*\}/gi,urls:/(url\()['"]?([^\/\)'"][^:\)'"]+)['"]?(\))/g,findStyles:/@media *([^\{]+)\{([\S\s]+?)$/,only:/(only\s+)?([a-zA-Z]+)\s?/,minw:/\([\s]*min\-width\s*:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/,maxw:/\([\s]*max\-width\s*:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/},c.mediaQueriesSupported=a.matchMedia&&null!==a.matchMedia("only all")&&a.matchMedia("only all").matches,!c.mediaQueriesSupported){var g,h,i,j=a.document,k=j.documentElement,l=[],m=[],n=[],o={},p=30,q=j.getElementsByTagName("head")[0]||k,r=j.getElementsByTagName("base")[0],s=q.getElementsByTagName("link"),t=function(){var a,b=j.createElement("div"),c=j.body,d=k.style.fontSize,e=c&&c.style.fontSize,f=!1;return b.style.cssText="position:absolute;font-size:1em;width:1em",c||(c=f=j.createElement("body"),c.style.background="none"),k.style.fontSize="100%",c.style.fontSize="100%",c.appendChild(b),f&&k.insertBefore(c,k.firstChild),a=b.offsetWidth,f?k.removeChild(c):c.removeChild(b),k.style.fontSize=d,e&&(c.style.fontSize=e),a=i=parseFloat(a)},u=function(b){var c="clientWidth",d=k[c],e="CSS1Compat"===j.compatMode&&d||j.body[c]||d,f={},o=s[s.length-1],r=(new Date).getTime();if(b&&g&&p>r-g)return a.clearTimeout(h),h=a.setTimeout(u,p),void 0;g=r;for(var v in l)if(l.hasOwnProperty(v)){var w=l[v],x=w.minw,y=w.maxw,z=null===x,A=null===y,B="em";x&&(x=parseFloat(x)*(x.indexOf(B)>-1?i||t():1)),y&&(y=parseFloat(y)*(y.indexOf(B)>-1?i||t():1)),w.hasquery&&(z&&A||!(z||e>=x)||!(A||y>=e))||(f[w.media]||(f[w.media]=[]),f[w.media].push(m[w.rules]))}for(var C in n)n.hasOwnProperty(C)&&n[C]&&n[C].parentNode===q&&q.removeChild(n[C]);n.length=0;for(var D in f)if(f.hasOwnProperty(D)){var E=j.createElement("style"),F=f[D].join("\n");E.type="text/css",E.media=D,q.insertBefore(E,o.nextSibling),E.styleSheet?E.styleSheet.cssText=F:E.appendChild(j.createTextNode(F)),n.push(E)}},v=function(a,b,d){var e=a.replace(c.regex.keyframes,"").match(c.regex.media),f=e&&e.length||0;b=b.substring(0,b.lastIndexOf("/"));var g=function(a){return a.replace(c.regex.urls,"$1"+b+"$2$3")},h=!f&&d;b.length&&(b+="/"),h&&(f=1);for(var i=0;f>i;i++){var j,k,n,o;h?(j=d,m.push(g(a))):(j=e[i].match(c.regex.findStyles)&&RegExp.$1,m.push(RegExp.$2&&g(RegExp.$2))),n=j.split(","),o=n.length;for(var p=0;o>p;p++)k=n[p],l.push({media:k.split("(")[0].match(c.regex.only)&&RegExp.$2||"all",rules:m.length-1,hasquery:k.indexOf("(")>-1,minw:k.match(c.regex.minw)&&parseFloat(RegExp.$1)+(RegExp.$2||""),maxw:k.match(c.regex.maxw)&&parseFloat(RegExp.$1)+(RegExp.$2||"")})}u()},w=function(){if(d.length){var b=d.shift();f(b.href,function(c){v(c,b.href,b.media),o[b.href]=!0,a.setTimeout(function(){w()},0)})}},x=function(){for(var b=0;b models.Feed: 47 | db_sess = db.session 48 | feed = models.Feed.query.get(feed_id) 49 | feed_url = feed.url 50 | 51 | print(f'Fetching feed: {feed_url}') 52 | result = self._process(feed_url, feed.etag) 53 | # print('====================== Results ======================') 54 | # print(result) 55 | 56 | feed_result = models.FeedResult() 57 | feed_result.feed = feed 58 | feed_result.status_code = result.status 59 | feed_result.completed_date = datetime.utcnow() 60 | db_sess.add(feed_result) 61 | 62 | if result.status == "302": 63 | # Feed has not changed (via etag value) 64 | db_sess.commit() 65 | return feed 66 | 67 | feed.etag = result.etag 68 | if result.updated: 69 | feed.updated = datetime.fromtimestamp(mktime(result.updated)) 70 | feed.fetched = datetime.utcnow() 71 | 72 | created_count = 0 73 | for article in result.articles: 74 | newarticle, created = self._get_or_create( 75 | db_sess, 76 | models.Article, 77 | article_id=article.id, 78 | feed_id=feed.id) 79 | if not newarticle.published: 80 | newarticle.published = datetime.utcnow() 81 | if not newarticle.fetched: 82 | newarticle.fetched = datetime.utcnow() 83 | newarticle.title = article.title 84 | newarticle.article_id = article.id 85 | newarticle.summary = article.summary 86 | newarticle.link = article.link 87 | newarticle.fetched = datetime.utcnow() 88 | if article.published: 89 | newarticle.published = datetime.fromtimestamp(mktime(article.published)) 90 | if created: 91 | created_count += 1 92 | newarticle.feed = feed 93 | db_sess.add(newarticle) 94 | 95 | log = Log(articles=len(result.articles), new_articles=created_count) 96 | if result.exception: 97 | feed_result.had_exception = True 98 | log.exception = str(result.exception) 99 | feed.status = models.Feed.INACTIVE # Inactive 100 | feed_result.log = log.to_dict() 101 | 102 | db_sess.commit() 103 | 104 | return feed 105 | 106 | def _get_or_create(self, session, model, **kwargs): 107 | instance = session.query(model).filter_by(**kwargs).first() 108 | if instance: 109 | return instance, False 110 | else: 111 | instance = model(**kwargs) 112 | session.add(instance) 113 | return instance, True 114 | 115 | def _process(self, feedUrl: str, etag: str) -> FeedResult: 116 | feed = feedparser.parse(feedUrl, etag=etag) 117 | print('Feed fetched, parsing now...') 118 | 119 | result = FeedResult() 120 | if feed.get('status'): 121 | result.status = feed.status 122 | if feed.get('bozo') and feed.get('bozo') == 1 and not feed.get('entries'): 123 | result.exception = feed.get('bozo_exception') 124 | print(f"Feed {feedUrl} returned an exception {feed.get('bozo_exception')}") 125 | return result 126 | if (feed.status and feed.status == 302) and feed.get('etag'): 127 | # print(feed) 128 | print('** Feed has not changed and returned 302.') 129 | result.status = "302" 130 | return result 131 | 132 | result.title = feed['feed'].get('title') 133 | result.etag = feed.get('etag') 134 | result.updated = feed.get('updated_parsed') 135 | 136 | articles = [] 137 | for item in feed['entries']: 138 | if item.get('id'): 139 | article = Article() 140 | article.id = item.get('id') 141 | article.title = item.get('title') 142 | article.published = item.get('published_parsed') 143 | article.summary = item.get('summary') 144 | article.link = item.get('link') 145 | articles.append(article) 146 | 147 | result.articles = articles 148 | 149 | return result 150 | -------------------------------------------------------------------------------- /app/static/bootstrap/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.7 (http://getbootstrap.com) 3 | * Copyright 2011-2016 Twitter, Inc. 4 | * Licensed under the MIT license 5 | */ 6 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1||b[0]>3)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher, but lower than version 4")}(jQuery),+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){if(a(b.target).is(this))return b.handleObj.handler.apply(this,arguments)}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.7",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a("#"===f?[]:f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.7",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c).prop(c,!0)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c).prop(c,!1))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")?(c.prop("checked")&&(a=!1),b.find(".active").removeClass("active"),this.$element.addClass("active")):"checkbox"==c.prop("type")&&(c.prop("checked")!==this.$element.hasClass("active")&&(a=!1),this.$element.toggleClass("active")),c.prop("checked",this.$element.hasClass("active")),a&&c.trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active")),this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target).closest(".btn");b.call(d,"toggle"),a(c.target).is('input[type="radio"], input[type="checkbox"]')||(c.preventDefault(),d.is("input,button")?d.trigger("focus"):d.find("input:visible,button:visible").first().trigger("focus"))}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(b.type))})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=null,this.sliding=null,this.interval=null,this.$active=null,this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",a.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.3.7",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){if(!/input|textarea/i.test(a.target.tagName)){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()}},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.getItemForDirection=function(a,b){var c=this.getItemIndex(b),d="prev"==a&&0===c||"next"==a&&c==this.$items.length-1;if(d&&!this.options.wrap)return b;var e="prev"==a?-1:1,f=(c+e)%this.$items.length;return this.$items.eq(f)},c.prototype.to=function(a){var b=this,c=this.getItemIndex(this.$active=this.$element.find(".item.active"));if(!(a>this.$items.length-1||a<0))return this.sliding?this.$element.one("slid.bs.carousel",function(){b.to(a)}):c==a?this.pause().cycle():this.slide(a>c?"next":"prev",this.$items.eq(a))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){if(!this.sliding)return this.slide("next")},c.prototype.prev=function(){if(!this.sliding)return this.slide("prev")},c.prototype.slide=function(b,d){var e=this.$element.find(".item.active"),f=d||this.getItemForDirection(b,e),g=this.interval,h="next"==b?"left":"right",i=this;if(f.hasClass("active"))return this.sliding=!1;var j=f[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:h});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,g&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(f)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:h});return a.support.transition&&this.$element.hasClass("slide")?(f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),e.one("bsTransitionEnd",function(){f.removeClass([b,h].join(" ")).addClass("active"),e.removeClass(["active",h].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(c.TRANSITION_DURATION)):(e.removeClass("active"),f.addClass("active"),this.sliding=!1,this.$element.trigger(m)),g&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this};var e=function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}};a(document).on("click.bs.carousel.data-api","[data-slide]",e).on("click.bs.carousel.data-api","[data-slide-to]",e),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){var c,d=b.attr("data-target")||(c=b.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"");return a(d)}function c(b){return this.each(function(){var c=a(this),e=c.data("bs.collapse"),f=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b);!e&&f.toggle&&/show|hide/.test(b)&&(f.toggle=!1),e||c.data("bs.collapse",e=new d(this,f)),"string"==typeof b&&e[b]()})}var d=function(b,c){this.$element=a(b),this.options=a.extend({},d.DEFAULTS,c),this.$trigger=a('[data-toggle="collapse"][href="#'+b.id+'"],[data-toggle="collapse"][data-target="#'+b.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};d.VERSION="3.3.7",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0},d.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},d.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b,e=this.$parent&&this.$parent.children(".panel").children(".in, .collapsing");if(!(e&&e.length&&(b=e.data("bs.collapse"),b&&b.transitioning))){var f=a.Event("show.bs.collapse");if(this.$element.trigger(f),!f.isDefaultPrevented()){e&&e.length&&(c.call(e,"hide"),b||e.data("bs.collapse",null));var g=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[g](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var h=function(){this.$element.removeClass("collapsing").addClass("collapse in")[g](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return h.call(this);var i=a.camelCase(["scroll",g].join("-"));this.$element.one("bsTransitionEnd",a.proxy(h,this)).emulateTransitionEnd(d.TRANSITION_DURATION)[g](this.$element[0][i])}}}},d.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var e=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(e,this)).emulateTransitionEnd(d.TRANSITION_DURATION):e.call(this)}}},d.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},d.prototype.getParent=function(){return a(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(a.proxy(function(c,d){var e=a(d);this.addAriaAndCollapsedClass(b(e),e)},this)).end()},d.prototype.addAriaAndCollapsedClass=function(a,b){var c=a.hasClass("in");a.attr("aria-expanded",c),b.toggleClass("collapsed",!c).attr("aria-expanded",c)};var e=a.fn.collapse;a.fn.collapse=c,a.fn.collapse.Constructor=d,a.fn.collapse.noConflict=function(){return a.fn.collapse=e,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(d){var e=a(this);e.attr("data-target")||d.preventDefault();var f=b(e),g=f.data("bs.collapse"),h=g?"toggle":e.data();c.call(f,h)})}(jQuery),+function(a){"use strict";function b(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function c(c){c&&3===c.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=b(d),f={relatedTarget:this};e.hasClass("open")&&(c&&"click"==c.type&&/input|textarea/i.test(c.target.tagName)&&a.contains(e[0],c.target)||(e.trigger(c=a.Event("hide.bs.dropdown",f)),c.isDefaultPrevented()||(d.attr("aria-expanded","false"),e.removeClass("open").trigger(a.Event("hidden.bs.dropdown",f)))))}))}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.3.7",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=b(e),g=f.hasClass("open");if(c(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a(document.createElement("div")).addClass("dropdown-backdrop").insertAfter(a(this)).on("click",c);var h={relatedTarget:this};if(f.trigger(d=a.Event("show.bs.dropdown",h)),d.isDefaultPrevented())return;e.trigger("focus").attr("aria-expanded","true"),f.toggleClass("open").trigger(a.Event("shown.bs.dropdown",h))}return!1}},g.prototype.keydown=function(c){if(/(38|40|27|32)/.test(c.which)&&!/input|textarea/i.test(c.target.tagName)){var d=a(this);if(c.preventDefault(),c.stopPropagation(),!d.is(".disabled, :disabled")){var e=b(d),g=e.hasClass("open");if(!g&&27!=c.which||g&&27==c.which)return 27==c.which&&e.find(f).trigger("focus"),d.trigger("click");var h=" li:not(.disabled):visible a",i=e.find(".dropdown-menu"+h);if(i.length){var j=i.index(c.target);38==c.which&&j>0&&j--,40==c.which&&jdocument.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&a?this.scrollbarWidth:"",paddingRight:this.bodyIsOverflowing&&!a?this.scrollbarWidth:""})},c.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:"",paddingRight:""})},c.prototype.checkScrollbar=function(){var a=window.innerWidth;if(!a){var b=document.documentElement.getBoundingClientRect();a=b.right-Math.abs(b.left)}this.bodyIsOverflowing=document.body.clientWidth
',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},c.prototype.init=function(b,c,d){if(this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.$viewport=this.options.viewport&&a(a.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},c.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},c.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusin"==b.type?"focus":"hover"]=!0),c.tip().hasClass("in")||"in"==c.hoverState?void(c.hoverState="in"):(clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show())},c.prototype.isInStateTrue=function(){for(var a in this.inState)if(this.inState[a])return!0;return!1},c.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);if(c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusout"==b.type?"focus":"hover"]=!1),!c.isInStateTrue())return clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide()},c.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(b);var d=a.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(b.isDefaultPrevented()||!d)return;var e=this,f=this.tip(),g=this.getUID(this.type);this.setContent(),f.attr("id",g),this.$element.attr("aria-describedby",g),this.options.animation&&f.addClass("fade");var h="function"==typeof this.options.placement?this.options.placement.call(this,f[0],this.$element[0]):this.options.placement,i=/\s?auto?\s?/i,j=i.test(h);j&&(h=h.replace(i,"")||"top"),f.detach().css({top:0,left:0,display:"block"}).addClass(h).data("bs."+this.type,this),this.options.container?f.appendTo(this.options.container):f.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var k=this.getPosition(),l=f[0].offsetWidth,m=f[0].offsetHeight;if(j){var n=h,o=this.getPosition(this.$viewport);h="bottom"==h&&k.bottom+m>o.bottom?"top":"top"==h&&k.top-mo.width?"left":"left"==h&&k.left-lg.top+g.height&&(e.top=g.top+g.height-i)}else{var j=b.left-f,k=b.left+f+c;jg.right&&(e.left=g.left+g.width-k)}return e},c.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},c.prototype.getUID=function(a){do a+=~~(1e6*Math.random());while(document.getElementById(a));return a},c.prototype.tip=function(){if(!this.$tip&&(this.$tip=a(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},c.prototype.enable=function(){this.enabled=!0},c.prototype.disable=function(){this.enabled=!1},c.prototype.toggleEnabled=function(){this.enabled=!this.enabled},c.prototype.toggle=function(b){var c=this;b&&(c=a(b.currentTarget).data("bs."+this.type),c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c))),b?(c.inState.click=!c.inState.click,c.isInStateTrue()?c.enter(c):c.leave(c)):c.tip().hasClass("in")?c.leave(c):c.enter(c)},c.prototype.destroy=function(){var a=this;clearTimeout(this.timeout),this.hide(function(){a.$element.off("."+a.type).removeData("bs."+a.type),a.$tip&&a.$tip.detach(),a.$tip=null,a.$arrow=null,a.$viewport=null,a.$element=null})};var d=a.fn.tooltip;a.fn.tooltip=b,a.fn.tooltip.Constructor=c,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=d,this}}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof b&&b;!e&&/destroy|hide/.test(b)||(e||d.data("bs.popover",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");c.VERSION="3.3.7",c.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),c.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),c.prototype.constructor=c,c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},c.prototype.hasContent=function(){return this.getTitle()||this.getContent()},c.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var d=a.fn.popover;a.fn.popover=b,a.fn.popover.Constructor=c,a.fn.popover.noConflict=function(){return a.fn.popover=d,this}}(jQuery),+function(a){"use strict";function b(c,d){this.$body=a(document.body),this.$scrollElement=a(a(c).is(document.body)?window:c),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",a.proxy(this.process,this)),this.refresh(),this.process()}function c(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})}b.VERSION="3.3.7",b.DEFAULTS={offset:10},b.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},b.prototype.refresh=function(){var b=this,c="offset",d=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),a.isWindow(this.$scrollElement[0])||(c="position",d=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var b=a(this),e=b.data("target")||b.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[c]().top+d,e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){b.offsets.push(this[0]),b.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.getScrollHeight(),d=this.options.offset+c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(this.scrollHeight!=c&&this.refresh(),b>=d)return g!=(a=f[f.length-1])&&this.activate(a);if(g&&b=e[a]&&(void 0===e[a+1]||b .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),b.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),h?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu").length&&b.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),e&&e()}var g=d.find("> .active"),h=e&&a.support.transition&&(g.length&&g.hasClass("fade")||!!d.find("> .fade").length);g.length&&h?g.one("bsTransitionEnd",f).emulateTransitionEnd(c.TRANSITION_DURATION):f(),g.removeClass("in")};var d=a.fn.tab;a.fn.tab=b,a.fn.tab.Constructor=c,a.fn.tab.noConflict=function(){return a.fn.tab=d,this};var e=function(c){c.preventDefault(),b.call(a(this),"show")};a(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',e).on("click.bs.tab.data-api",'[data-toggle="pill"]',e)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof b&&b;e||d.data("bs.affix",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.options=a.extend({},c.DEFAULTS,d),this.$target=a(this.options.target).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(b),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};c.VERSION="3.3.7",c.RESET="affix affix-top affix-bottom",c.DEFAULTS={offset:0,target:window},c.prototype.getState=function(a,b,c,d){var e=this.$target.scrollTop(),f=this.$element.offset(),g=this.$target.height();if(null!=c&&"top"==this.affixed)return e=a-d&&"bottom"},c.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(c.RESET).addClass("affix");var a=this.$target.scrollTop(),b=this.$element.offset();return this.pinnedOffset=b.top-a},c.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},c.prototype.checkPosition=function(){if(this.$element.is(":visible")){var b=this.$element.height(),d=this.options.offset,e=d.top,f=d.bottom,g=Math.max(a(document).height(),a(document.body).height());"object"!=typeof d&&(f=e=d),"function"==typeof e&&(e=d.top(this.$element)),"function"==typeof f&&(f=d.bottom(this.$element));var h=this.getState(g,b,e,f);if(this.affixed!=h){null!=this.unpin&&this.$element.css("top","");var i="affix"+(h?"-"+h:""),j=a.Event(i+".bs.affix");if(this.$element.trigger(j),j.isDefaultPrevented())return;this.affixed=h,this.unpin="bottom"==h?this.getPinnedOffset():null,this.$element.removeClass(c.RESET).addClass(i).trigger(i.replace("affix","affixed")+".bs.affix")}"bottom"==h&&this.$element.offset({top:g-b-f})}};var d=a.fn.affix;a.fn.affix=b,a.fn.affix.Constructor=c,a.fn.affix.noConflict=function(){return a.fn.affix=d,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var c=a(this),d=c.data();d.offset=d.offset||{},null!=d.offsetBottom&&(d.offset.bottom=d.offsetBottom),null!=d.offsetTop&&(d.offset.top=d.offsetTop),b.call(c,d)})})}(jQuery); -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "d4c19dc8cec974b23411dfe69c6553ea945722e23f71b79ac583c957268ff40d" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.9" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "alembic": { 20 | "hashes": [ 21 | "sha256:03226222f1cf943deee6c85d9464261a6c710cd19b4fe867a3ad1f25afda610f", 22 | "sha256:8e7645c32e4f200675e69f0745415335eb59a3663f5feb487abfa0b30c45888b" 23 | ], 24 | "markers": "python_version >= '3.7'", 25 | "version": "==1.12.0" 26 | }, 27 | "amqp": { 28 | "hashes": [ 29 | "sha256:2c1b13fecc0893e946c65cbd5f36427861cffa4ea2201d8f6fca22e2a373b5e2", 30 | "sha256:6f0956d2c23d8fa6e7691934d8c3930eadb44972cbbd1a7ae3a520f735d43359" 31 | ], 32 | "markers": "python_version >= '3.6'", 33 | "version": "==5.1.1" 34 | }, 35 | "async-timeout": { 36 | "hashes": [ 37 | "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f", 38 | "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028" 39 | ], 40 | "markers": "python_full_version <= '3.11.2'", 41 | "version": "==4.0.3" 42 | }, 43 | "attrs": { 44 | "hashes": [ 45 | "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", 46 | "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" 47 | ], 48 | "index": "pypi", 49 | "version": "==19.1.0" 50 | }, 51 | "bcrypt": { 52 | "hashes": [ 53 | "sha256:089098effa1bc35dc055366740a067a2fc76987e8ec75349eb9484061c54f535", 54 | "sha256:08d2947c490093a11416df18043c27abe3921558d2c03e2076ccb28a116cb6d0", 55 | "sha256:0eaa47d4661c326bfc9d08d16debbc4edf78778e6aaba29c1bc7ce67214d4410", 56 | "sha256:27d375903ac8261cfe4047f6709d16f7d18d39b1ec92aaf72af989552a650ebd", 57 | "sha256:2b3ac11cf45161628f1f3733263e63194f22664bf4d0c0f3ab34099c02134665", 58 | "sha256:2caffdae059e06ac23fce178d31b4a702f2a3264c20bfb5ff541b338194d8fab", 59 | "sha256:3100851841186c25f127731b9fa11909ab7b1df6fc4b9f8353f4f1fd952fbf71", 60 | "sha256:5ad4d32a28b80c5fa6671ccfb43676e8c1cc232887759d1cd7b6f56ea4355215", 61 | "sha256:67a97e1c405b24f19d08890e7ae0c4f7ce1e56a712a016746c8b2d7732d65d4b", 62 | "sha256:705b2cea8a9ed3d55b4491887ceadb0106acf7c6387699fca771af56b1cdeeda", 63 | "sha256:8a68f4341daf7522fe8d73874de8906f3a339048ba406be6ddc1b3ccb16fc0d9", 64 | "sha256:a522427293d77e1c29e303fc282e2d71864579527a04ddcfda6d4f8396c6c36a", 65 | "sha256:ae88eca3024bb34bb3430f964beab71226e761f51b912de5133470b649d82344", 66 | "sha256:b1023030aec778185a6c16cf70f359cbb6e0c289fd564a7cfa29e727a1c38f8f", 67 | "sha256:b3b85202d95dd568efcb35b53936c5e3b3600c7cdcc6115ba461df3a8e89f38d", 68 | "sha256:b57adba8a1444faf784394de3436233728a1ecaeb6e07e8c22c8848f179b893c", 69 | "sha256:bf4fa8b2ca74381bb5442c089350f09a3f17797829d958fad058d6e44d9eb83c", 70 | "sha256:ca3204d00d3cb2dfed07f2d74a25f12fc12f73e606fcaa6975d1f7ae69cacbb2", 71 | "sha256:cbb03eec97496166b704ed663a53680ab57c5084b2fc98ef23291987b525cb7d", 72 | "sha256:e9a51bbfe7e9802b5f3508687758b564069ba937748ad7b9e890086290d2f79e", 73 | "sha256:fbdaec13c5105f0c4e5c52614d04f0bca5f5af007910daa8b6b12095edaa67b3" 74 | ], 75 | "markers": "python_version >= '3.6'", 76 | "version": "==4.0.1" 77 | }, 78 | "billiard": { 79 | "hashes": [ 80 | "sha256:299de5a8da28a783d51b197d496bef4f1595dd023a93a4f59dde1886ae905547", 81 | "sha256:87103ea78fa6ab4d5c751c4909bcff74617d985de7fa8b672cf8618afd5a875b" 82 | ], 83 | "version": "==3.6.4.0" 84 | }, 85 | "blinker": { 86 | "hashes": [ 87 | "sha256:4afd3de66ef3a9f8067559fb7a1cbe555c17dcbe15971b05d1b625c3e7abe213", 88 | "sha256:c3d739772abb7bc2860abf5f2ec284223d9ad5c76da018234f6f50d6f31ab1f0" 89 | ], 90 | "markers": "python_version >= '3.7'", 91 | "version": "==1.6.2" 92 | }, 93 | "celery": { 94 | "hashes": [ 95 | "sha256:8aacd02fc23a02760686d63dde1eb0daa9f594e735e73ea8fb15c2ff15cb608c", 96 | "sha256:e2cd41667ad97d4f6a2f4672d1c6a6ebada194c619253058b5f23704aaadaa82" 97 | ], 98 | "index": "pypi", 99 | "version": "==5.2.3" 100 | }, 101 | "certifi": { 102 | "hashes": [ 103 | "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", 104 | "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3" 105 | ], 106 | "markers": "python_version >= '3.6'", 107 | "version": "==2025.4.26" 108 | }, 109 | "cffi": { 110 | "hashes": [ 111 | "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", 112 | "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", 113 | "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1", 114 | "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", 115 | "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", 116 | "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", 117 | "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", 118 | "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", 119 | "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", 120 | "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", 121 | "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc", 122 | "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", 123 | "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", 124 | "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", 125 | "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", 126 | "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", 127 | "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", 128 | "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", 129 | "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", 130 | "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b", 131 | "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", 132 | "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", 133 | "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c", 134 | "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", 135 | "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", 136 | "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", 137 | "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8", 138 | "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1", 139 | "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", 140 | "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", 141 | "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", 142 | "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", 143 | "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", 144 | "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", 145 | "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", 146 | "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", 147 | "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", 148 | "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", 149 | "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", 150 | "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", 151 | "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", 152 | "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", 153 | "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", 154 | "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964", 155 | "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", 156 | "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", 157 | "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", 158 | "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", 159 | "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", 160 | "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", 161 | "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", 162 | "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", 163 | "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", 164 | "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", 165 | "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", 166 | "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", 167 | "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", 168 | "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9", 169 | "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", 170 | "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", 171 | "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", 172 | "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", 173 | "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", 174 | "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", 175 | "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", 176 | "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", 177 | "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b" 178 | ], 179 | "markers": "platform_python_implementation != 'PyPy'", 180 | "version": "==1.17.1" 181 | }, 182 | "charset-normalizer": { 183 | "hashes": [ 184 | "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4", 185 | "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45", 186 | "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", 187 | "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", 188 | "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", 189 | "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", 190 | "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d", 191 | "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", 192 | "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184", 193 | "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", 194 | "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b", 195 | "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64", 196 | "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", 197 | "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", 198 | "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", 199 | "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344", 200 | "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58", 201 | "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", 202 | "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471", 203 | "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", 204 | "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", 205 | "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836", 206 | "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", 207 | "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", 208 | "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", 209 | "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1", 210 | "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01", 211 | "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", 212 | "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58", 213 | "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", 214 | "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", 215 | "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2", 216 | "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a", 217 | "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597", 218 | "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", 219 | "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5", 220 | "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb", 221 | "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f", 222 | "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", 223 | "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", 224 | "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", 225 | "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", 226 | "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7", 227 | "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7", 228 | "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455", 229 | "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", 230 | "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4", 231 | "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", 232 | "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3", 233 | "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", 234 | "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", 235 | "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", 236 | "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", 237 | "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", 238 | "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", 239 | "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", 240 | "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12", 241 | "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa", 242 | "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", 243 | "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", 244 | "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f", 245 | "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", 246 | "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", 247 | "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5", 248 | "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02", 249 | "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", 250 | "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", 251 | "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e", 252 | "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", 253 | "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", 254 | "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", 255 | "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", 256 | "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681", 257 | "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba", 258 | "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", 259 | "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a", 260 | "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", 261 | "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", 262 | "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", 263 | "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", 264 | "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027", 265 | "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7", 266 | "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518", 267 | "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", 268 | "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", 269 | "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", 270 | "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", 271 | "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da", 272 | "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", 273 | "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f", 274 | "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", 275 | "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f" 276 | ], 277 | "markers": "python_version >= '3.7'", 278 | "version": "==3.4.2" 279 | }, 280 | "click": { 281 | "hashes": [ 282 | "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", 283 | "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" 284 | ], 285 | "markers": "python_version >= '3.7'", 286 | "version": "==8.1.7" 287 | }, 288 | "click-didyoumean": { 289 | "hashes": [ 290 | "sha256:a0713dc7a1de3f06bc0df5a9567ad19ead2d3d5689b434768a6145bff77c0667", 291 | "sha256:f184f0d851d96b6d29297354ed981b7dd71df7ff500d82fa6d11f0856bee8035" 292 | ], 293 | "markers": "python_version < '4.0' and python_full_version >= '3.6.2'", 294 | "version": "==0.3.0" 295 | }, 296 | "click-plugins": { 297 | "hashes": [ 298 | "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b", 299 | "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8" 300 | ], 301 | "version": "==1.1.1" 302 | }, 303 | "click-repl": { 304 | "hashes": [ 305 | "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9", 306 | "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812" 307 | ], 308 | "markers": "python_version >= '3.6'", 309 | "version": "==0.3.0" 310 | }, 311 | "coverage": { 312 | "hashes": [ 313 | "sha256:025ded371f1ca280c035d91b43252adbb04d2aea4c7105252d3cbc227f03b375", 314 | "sha256:04312b036580ec505f2b77cbbdfb15137d5efdfade09156961f5277149f5e344", 315 | "sha256:0575c37e207bb9b98b6cf72fdaaa18ac909fb3d153083400c2d48e2e6d28bd8e", 316 | "sha256:07d156269718670d00a3b06db2288b48527fc5f36859425ff7cec07c6b367745", 317 | "sha256:1f111a7d85658ea52ffad7084088277135ec5f368457275fc57f11cebb15607f", 318 | "sha256:220eb51f5fb38dfdb7e5d54284ca4d0cd70ddac047d750111a68ab1798945194", 319 | "sha256:229c0dd2ccf956bf5aeede7e3131ca48b65beacde2029f0361b54bf93d36f45a", 320 | "sha256:245c5a99254e83875c7fed8b8b2536f040997a9b76ac4c1da5bff398c06e860f", 321 | "sha256:2829c65c8faaf55b868ed7af3c7477b76b1c6ebeee99a28f59a2cb5907a45760", 322 | "sha256:4aba512a15a3e1e4fdbfed2f5392ec221434a614cc68100ca99dcad7af29f3f8", 323 | "sha256:4c96dd7798d83b960afc6c1feb9e5af537fc4908852ef025600374ff1a017392", 324 | "sha256:50dd1e2dd13dbbd856ffef69196781edff26c800a74f070d3b3e3389cab2600d", 325 | "sha256:5289490dd1c3bb86de4730a92261ae66ea8d44b79ed3cc26464f4c2cde581fbc", 326 | "sha256:53669b79f3d599da95a0afbef039ac0fadbb236532feb042c534fbb81b1a4e40", 327 | "sha256:553d7094cb27db58ea91332e8b5681bac107e7242c23f7629ab1316ee73c4981", 328 | "sha256:586649ada7cf139445da386ab6f8ef00e6172f11a939fc3b2b7e7c9082052fa0", 329 | "sha256:5ae4c6da8b3d123500f9525b50bf0168023313963e0e2e814badf9000dd6ef92", 330 | "sha256:5b4ee7080878077af0afa7238df1b967f00dc10763f6e1b66f5cced4abebb0a3", 331 | "sha256:5d991e13ad2ed3aced177f524e4d670f304c8233edad3210e02c465351f785a0", 332 | "sha256:614f1f98b84eb256e4f35e726bfe5ca82349f8dfa576faabf8a49ca09e630086", 333 | "sha256:636a8ac0b044cfeccae76a36f3b18264edcc810a76a49884b96dd744613ec0b7", 334 | "sha256:6407424621f40205bbe6325686417e5e552f6b2dba3535dd1f90afc88a61d465", 335 | "sha256:6bc6f3f4692d806831c136c5acad5ccedd0262aa44c087c46b7101c77e139140", 336 | "sha256:6cb7fe1581deb67b782c153136541e20901aa312ceedaf1467dcb35255787952", 337 | "sha256:74bb470399dc1989b535cb41f5ca7ab2af561e40def22d7e188e0a445e7639e3", 338 | "sha256:75c8f0df9dfd8ff745bccff75867d63ef336e57cc22b2908ee725cc552689ec8", 339 | "sha256:770f143980cc16eb601ccfd571846e89a5fe4c03b4193f2e485268f224ab602f", 340 | "sha256:7eb0b188f30e41ddd659a529e385470aa6782f3b412f860ce22b2491c89b8593", 341 | "sha256:7eb3cd48d54b9bd0e73026dedce44773214064be93611deab0b6a43158c3d5a0", 342 | "sha256:87d38444efffd5b056fcc026c1e8d862191881143c3aa80bb11fcf9dca9ae204", 343 | "sha256:8a07b692129b8a14ad7a37941a3029c291254feb7a4237f245cfae2de78de037", 344 | "sha256:966f10df9b2b2115da87f50f6a248e313c72a668248be1b9060ce935c871f276", 345 | "sha256:a6191b3a6ad3e09b6cfd75b45c6aeeffe7e3b0ad46b268345d159b8df8d835f9", 346 | "sha256:aab8e9464c00da5cb9c536150b7fbcd8850d376d1151741dd0d16dfe1ba4fd26", 347 | "sha256:ac3c5b7e75acac31e490b7851595212ed951889918d398b7afa12736c85e13ce", 348 | "sha256:ac9ad38204887349853d7c313f53a7b1c210ce138c73859e925bc4e5d8fc18e7", 349 | "sha256:b9c0c19f70d30219113b18fe07e372b244fb2a773d4afde29d5a2f7930765136", 350 | "sha256:c397c70cd20f6df7d2a52283857af622d5f23300c4ca8e5bd8c7a543825baa5a", 351 | "sha256:c6601a60318f9c3945be6ea0f2a80571f4299b6801716f8a6e4846892737ebe4", 352 | "sha256:c6f55d38818ca9596dc9019eae19a47410d5322408140d9a0076001a3dcb938c", 353 | "sha256:ca70466ca3a17460e8fc9cea7123c8cbef5ada4be3140a1ef8f7b63f2f37108f", 354 | "sha256:ca833941ec701fda15414be400c3259479bfde7ae6d806b69e63b3dc423b1832", 355 | "sha256:cd0f7429ecfd1ff597389907045ff209c8fdb5b013d38cfa7c60728cb484b6e3", 356 | "sha256:cd694e19c031733e446c8024dedd12a00cda87e1c10bd7b8539a87963685e969", 357 | "sha256:cdd088c00c39a27cfa5329349cc763a48761fdc785879220d54eb785c8a38520", 358 | "sha256:de30c1aa80f30af0f6b2058a91505ea6e36d6535d437520067f525f7df123887", 359 | "sha256:defbbb51121189722420a208957e26e49809feafca6afeef325df66c39c4fdb3", 360 | "sha256:f09195dda68d94a53123883de75bb97b0e35f5f6f9f3aa5bf6e496da718f0cb6", 361 | "sha256:f12d8b11a54f32688b165fd1a788c408f927b0960984b899be7e4c190ae758f1", 362 | "sha256:f1a317fdf5c122ad642db8a97964733ab7c3cf6009e1a8ae8821089993f175ff", 363 | "sha256:f2781fd3cabc28278dc982a352f50c81c09a1a500cc2086dc4249853ea96b981", 364 | "sha256:f4f456590eefb6e1b3c9ea6328c1e9fa0f1006e7481179d749b3376fc793478e" 365 | ], 366 | "markers": "python_version >= '3.8'", 367 | "version": "==7.3.1" 368 | }, 369 | "cryptography": { 370 | "hashes": [ 371 | "sha256:00918d859aa4e57db8299607086f793fa7813ae2ff5a4637e318a25ef82730f7", 372 | "sha256:1e8d181e90a777b63f3f0caa836844a1182f1f265687fac2115fcf245f5fbec3", 373 | "sha256:1f9a92144fa0c877117e9748c74501bea842f93d21ee00b0cf922846d9d0b183", 374 | "sha256:21377472ca4ada2906bc313168c9dc7b1d7ca417b63c1c3011d0c74b7de9ae69", 375 | "sha256:24979e9f2040c953a94bf3c6782e67795a4c260734e5264dceea65c8f4bae64a", 376 | "sha256:2a46a89ad3e6176223b632056f321bc7de36b9f9b93b2cc1cccf935a3849dc62", 377 | "sha256:322eb03ecc62784536bc173f1483e76747aafeb69c8728df48537eb431cd1911", 378 | "sha256:436df4f203482f41aad60ed1813811ac4ab102765ecae7a2bbb1dbb66dcff5a7", 379 | "sha256:4f422e8c6a28cf8b7f883eb790695d6d45b0c385a2583073f3cec434cc705e1a", 380 | "sha256:53f23339864b617a3dfc2b0ac8d5c432625c80014c25caac9082314e9de56f41", 381 | "sha256:5fed5cd6102bb4eb843e3315d2bf25fede494509bddadb81e03a859c1bc17b83", 382 | "sha256:610a83540765a8d8ce0f351ce42e26e53e1f774a6efb71eb1b41eb01d01c3d12", 383 | "sha256:6c8acf6f3d1f47acb2248ec3ea261171a671f3d9428e34ad0357148d492c7864", 384 | "sha256:6f76fdd6fd048576a04c5210d53aa04ca34d2ed63336d4abd306d0cbe298fddf", 385 | "sha256:72198e2b5925155497a5a3e8c216c7fb3e64c16ccee11f0e7da272fa93b35c4c", 386 | "sha256:887143b9ff6bad2b7570da75a7fe8bbf5f65276365ac259a5d2d5147a73775f2", 387 | "sha256:888fcc3fce0c888785a4876ca55f9f43787f4c5c1cc1e2e0da71ad481ff82c5b", 388 | "sha256:8e6a85a93d0642bd774460a86513c5d9d80b5c002ca9693e63f6e540f1815ed0", 389 | "sha256:94f99f2b943b354a5b6307d7e8d19f5c423a794462bde2bf310c770ba052b1c4", 390 | "sha256:9b336599e2cb77b1008cb2ac264b290803ec5e8e89d618a5e978ff5eb6f715d9", 391 | "sha256:a2d8a7045e1ab9b9f803f0d9531ead85f90c5f2859e653b61497228b18452008", 392 | "sha256:b8272f257cf1cbd3f2e120f14c68bff2b6bdfcc157fafdee84a1b795efd72862", 393 | "sha256:bf688f615c29bfe9dfc44312ca470989279f0e94bb9f631f85e3459af8efc009", 394 | "sha256:d9c5b9f698a83c8bd71e0f4d3f9f839ef244798e5ffe96febfa9714717db7af7", 395 | "sha256:dd7c7e2d71d908dc0f8d2027e1604102140d84b155e658c20e8ad1304317691f", 396 | "sha256:df978682c1504fc93b3209de21aeabf2375cb1571d4e61907b3e7a2540e83026", 397 | "sha256:e403f7f766ded778ecdb790da786b418a9f2394f36e8cc8b796cc056ab05f44f", 398 | "sha256:eb3889330f2a4a148abead555399ec9a32b13b7c8ba969b72d8e500eb7ef84cd", 399 | "sha256:f4daefc971c2d1f82f03097dc6f216744a6cd2ac0f04c68fb935ea2ba2a0d420", 400 | "sha256:f51f5705ab27898afda1aaa430f34ad90dc117421057782022edf0600bec5f14", 401 | "sha256:fd0ee90072861e276b0ff08bd627abec29e32a53b2be44e41dbcdf87cbee2b00" 402 | ], 403 | "index": "pypi", 404 | "markers": "python_version >= '3.7' and python_full_version not in '3.9.0, 3.9.1'", 405 | "version": "==44.0.1" 406 | }, 407 | "dnspython": { 408 | "hashes": [ 409 | "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50", 410 | "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc" 411 | ], 412 | "index": "pypi", 413 | "markers": "python_version >= '3.8'", 414 | "version": "==2.6.1" 415 | }, 416 | "email-validator": { 417 | "hashes": [ 418 | "sha256:5675c8ceb7106a37e40e2698a57c056756bf3f272cfa8682a4f87ebd95d8440b", 419 | "sha256:aa237a65f6f4da067119b7df3f13e89c25c051327b2b5b66dc075f33d62480d7" 420 | ], 421 | "index": "pypi", 422 | "version": "==1.1.3" 423 | }, 424 | "feedparser": { 425 | "hashes": [ 426 | "sha256:1b7f57841d9cf85074deb316ed2c795091a238adb79846bc46dccdaf80f9c59a", 427 | "sha256:5ce0410a05ab248c8c7cfca3a0ea2203968ee9ff4486067379af4827a59f9661" 428 | ], 429 | "index": "pypi", 430 | "version": "==6.0.8" 431 | }, 432 | "flask": { 433 | "hashes": [ 434 | "sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52", 435 | "sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6" 436 | ], 437 | "index": "pypi", 438 | "version": "==1.1.1" 439 | }, 440 | "flask-login": { 441 | "hashes": [ 442 | "sha256:d25e356b14a59f52da0ab30c31c2ad285fa23a840f0f6971df7ed247c77082a7", 443 | "sha256:f9149b63ec6b32aec44acb061ad851eb4eb065e742341147d116d69f8e35ae2b" 444 | ], 445 | "index": "pypi", 446 | "version": "==0.4.0" 447 | }, 448 | "flask-mail": { 449 | "hashes": [ 450 | "sha256:22e5eb9a940bf407bcf30410ecc3708f3c56cc44b29c34e1726fe85006935f41" 451 | ], 452 | "version": "==0.9.1" 453 | }, 454 | "flask-migrate": { 455 | "hashes": [ 456 | "sha256:c77272b936ec94209d5c709f9ec43947f4a25513c1b12cc25241586abdfa84b1" 457 | ], 458 | "index": "pypi", 459 | "version": "==2.0.2" 460 | }, 461 | "flask-script": { 462 | "hashes": [ 463 | "sha256:cef76eac751396355429a14c38967bb14d4973c53e07dec94af5cc8fb017107f" 464 | ], 465 | "index": "pypi", 466 | "version": "==2.0.5" 467 | }, 468 | "flask-sqlalchemy": { 469 | "hashes": [ 470 | "sha256:c5244de44cc85d2267115624d83faef3f9e8f088756788694f305a5d5ad137c5" 471 | ], 472 | "index": "pypi", 473 | "version": "==2.1" 474 | }, 475 | "flask-user": { 476 | "git": "https://github.com/lingthio/Flask-User.git", 477 | "ref": "5c652e6479036c3d33aa1626524e4e65bd3b961e" 478 | }, 479 | "flask-wtf": { 480 | "hashes": [ 481 | "sha256:01feccfc395405cea48a3f36c23f0d766e2cc6fd2a5a065ad50ad3e5827ec797", 482 | "sha256:872fbb17b5888bfc734edbdcf45bc08fb365ca39f69d25dc752465a455517b28" 483 | ], 484 | "index": "pypi", 485 | "version": "==1.0.0" 486 | }, 487 | "idna": { 488 | "hashes": [ 489 | "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", 490 | "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" 491 | ], 492 | "markers": "python_version >= '3.6'", 493 | "version": "==3.10" 494 | }, 495 | "itsdangerous": { 496 | "hashes": [ 497 | "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44", 498 | "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a" 499 | ], 500 | "markers": "python_version >= '3.7'", 501 | "version": "==2.1.2" 502 | }, 503 | "jinja2": { 504 | "hashes": [ 505 | "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", 506 | "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67" 507 | ], 508 | "index": "pypi", 509 | "markers": "python_version >= '3.7'", 510 | "version": "==3.1.6" 511 | }, 512 | "kombu": { 513 | "hashes": [ 514 | "sha256:0ba213f630a2cb2772728aef56ac6883dc3a2f13435e10048f6e97d48506dbbd", 515 | "sha256:b753c9cfc9b1e976e637a7cbc1a65d446a22e45546cd996ea28f932082b7dc9e" 516 | ], 517 | "markers": "python_version >= '3.8'", 518 | "version": "==5.3.2" 519 | }, 520 | "mako": { 521 | "hashes": [ 522 | "sha256:c97c79c018b9165ac9922ae4f32da095ffd3c4e6872b45eded42926deea46818", 523 | "sha256:d60a3903dc3bb01a18ad6a89cdbe2e4eadc69c0bc8ef1e3773ba53d44c3f7a34" 524 | ], 525 | "markers": "python_version >= '3.7'", 526 | "version": "==1.2.4" 527 | }, 528 | "markupsafe": { 529 | "hashes": [ 530 | "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", 531 | "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", 532 | "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", 533 | "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", 534 | "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", 535 | "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", 536 | "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", 537 | "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", 538 | "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", 539 | "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", 540 | "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", 541 | "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", 542 | "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", 543 | "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", 544 | "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", 545 | "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", 546 | "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", 547 | "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", 548 | "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", 549 | "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", 550 | "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", 551 | "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", 552 | "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", 553 | "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", 554 | "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", 555 | "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", 556 | "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", 557 | "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", 558 | "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", 559 | "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", 560 | "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", 561 | "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", 562 | "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", 563 | "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", 564 | "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", 565 | "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", 566 | "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", 567 | "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", 568 | "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", 569 | "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", 570 | "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", 571 | "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", 572 | "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", 573 | "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", 574 | "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", 575 | "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", 576 | "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", 577 | "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", 578 | "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", 579 | "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", 580 | "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", 581 | "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", 582 | "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", 583 | "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", 584 | "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", 585 | "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", 586 | "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", 587 | "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", 588 | "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", 589 | "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", 590 | "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50" 591 | ], 592 | "markers": "python_version >= '3.9'", 593 | "version": "==3.0.2" 594 | }, 595 | "passlib": { 596 | "hashes": [ 597 | "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", 598 | "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04" 599 | ], 600 | "version": "==1.7.4" 601 | }, 602 | "prompt-toolkit": { 603 | "hashes": [ 604 | "sha256:04505ade687dc26dc4284b1ad19a83be2f2afe83e7a828ace0c72f3a1df72aac", 605 | "sha256:9dffbe1d8acf91e3de75f3b544e4842382fc06c6babe903ac9acb74dc6e08d88" 606 | ], 607 | "markers": "python_version >= '3.7'", 608 | "version": "==3.0.39" 609 | }, 610 | "py": { 611 | "hashes": [ 612 | "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", 613 | "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" 614 | ], 615 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 616 | "version": "==1.11.0" 617 | }, 618 | "pycparser": { 619 | "hashes": [ 620 | "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", 621 | "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc" 622 | ], 623 | "markers": "python_version >= '3.8'", 624 | "version": "==2.22" 625 | }, 626 | "pymysql": { 627 | "hashes": [ 628 | "sha256:4de15da4c61dc132f4fb9ab763063e693d521a80fd0e87943b9a453dd4c19d6c", 629 | "sha256:e127611aaf2b417403c60bf4dc570124aeb4a57f5f37b8e95ae399a42f904cd0" 630 | ], 631 | "index": "pypi", 632 | "markers": "python_version >= '3.7'", 633 | "version": "==1.1.1" 634 | }, 635 | "pytest": { 636 | "hashes": [ 637 | "sha256:4a003aa956f023ce91aa6e166b555e6f02a4b0aeb459ac61e14f64c0d39037fd", 638 | "sha256:c97bdefdca852d48c3144b8a534a78527534793bac959c10211ed3e037925020" 639 | ], 640 | "index": "pypi", 641 | "version": "==3.0.5" 642 | }, 643 | "pytest-cov": { 644 | "hashes": [ 645 | "sha256:10e37e876f49ddec80d6c83a54b657157f1387ebc0f7755285f8c156130014a1", 646 | "sha256:53d4179086e1eec1c688705977387432c01031b0a7bd91b8ff6c912c08c3820d" 647 | ], 648 | "index": "pypi", 649 | "version": "==2.4.0" 650 | }, 651 | "pytz": { 652 | "hashes": [ 653 | "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b", 654 | "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7" 655 | ], 656 | "version": "==2023.3.post1" 657 | }, 658 | "redis": { 659 | "hashes": [ 660 | "sha256:2c19e6767c474f2e85167909061d525ed65bea9301c0770bb151e041b7ac89a2", 661 | "sha256:73ec35da4da267d6847e47f68730fdd5f62e2ca69e3ef5885c6a78a9374c3893" 662 | ], 663 | "index": "pypi", 664 | "version": "==4.5.4" 665 | }, 666 | "requests": { 667 | "hashes": [ 668 | "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", 669 | "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422" 670 | ], 671 | "index": "pypi", 672 | "markers": "python_version >= '3.8'", 673 | "version": "==2.32.4" 674 | }, 675 | "setuptools": { 676 | "hashes": [ 677 | "sha256:22c7348c6d2976a52632c67f7ab0cdf40147db7789f9aed18734643fe9cf3373", 678 | "sha256:4ce92f1e1f8f01233ee9952c04f6b81d1e02939d6e1b488428154974a4d0783e" 679 | ], 680 | "markers": "python_version >= '3.6'", 681 | "version": "==59.6.0" 682 | }, 683 | "sgmllib3k": { 684 | "hashes": [ 685 | "sha256:7868fb1c8bfa764c1ac563d3cf369c381d1325d36124933a726f29fcdaa812e9" 686 | ], 687 | "version": "==1.0.0" 688 | }, 689 | "sqlalchemy": { 690 | "hashes": [ 691 | "sha256:2f8ff566a4d3a92246d367f2e9cd6ed3edeef670dcd6dda6dfdc9efed88bcd80" 692 | ], 693 | "index": "pypi", 694 | "version": "==1.3.8" 695 | }, 696 | "typing-extensions": { 697 | "hashes": [ 698 | "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0", 699 | "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef" 700 | ], 701 | "markers": "python_version >= '3.8'", 702 | "version": "==4.8.0" 703 | }, 704 | "urllib3": { 705 | "hashes": [ 706 | "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", 707 | "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc" 708 | ], 709 | "index": "pypi", 710 | "markers": "python_version >= '3.9'", 711 | "version": "==2.5.0" 712 | }, 713 | "vine": { 714 | "hashes": [ 715 | "sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30", 716 | "sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e" 717 | ], 718 | "markers": "python_version >= '3.6'", 719 | "version": "==5.0.0" 720 | }, 721 | "wcwidth": { 722 | "hashes": [ 723 | "sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e", 724 | "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0" 725 | ], 726 | "version": "==0.2.6" 727 | }, 728 | "werkzeug": { 729 | "hashes": [ 730 | "sha256:554b257c74bbeb7a0d254160a4f8ffe185243f52a52035060b761ca62d977f03", 731 | "sha256:bba1f19f8ec89d4d607a3bd62f1904bd2e609472d93cd85e9d4e178f472c3748" 732 | ], 733 | "index": "pypi", 734 | "markers": "python_version >= '3.8'", 735 | "version": "==2.3.8" 736 | }, 737 | "wtforms": { 738 | "hashes": [ 739 | "sha256:6b351bbb12dd58af57ffef05bc78425d08d1914e0fd68ee14143b7ade023c5bc", 740 | "sha256:837f2f0e0ca79481b92884962b914eba4e72b7a2daaf1f939c890ed0124b834b" 741 | ], 742 | "markers": "python_version >= '3.7'", 743 | "version": "==3.0.1" 744 | } 745 | }, 746 | "develop": {} 747 | } 748 | --------------------------------------------------------------------------------