├── data ├── content │ └── .gitkeep └── img │ ├── users │ └── .gitkeep │ └── products │ └── .gitkeep ├── static ├── js │ ├── lib │ ├── app.js │ ├── vue │ ├── jquery │ ├── tweenjs │ ├── bootstrap │ ├── popper │ ├── vue-color │ ├── bootstrap-vue │ ├── socket.io │ └── fontawesome ├── css │ ├── app.css │ ├── bootstrap │ ├── bootstrap-vue │ ├── tstar-bold-webfont.woff2 │ ├── fontawesome │ ├── tstar-italic-webfont.woff2 │ ├── tstar-regular-webfont.woff2 │ └── tstar-bolditalic-webfont.woff2 ├── img │ ├── users │ ├── products │ ├── background-l.png │ ├── background.png │ └── background-glass.png └── icon │ ├── favicon.ico │ └── favicon.xcf ├── migrations ├── README ├── script.py.mako ├── alembic.ini ├── versions │ └── 4659bb930f82_.py └── env.py ├── client ├── lib │ ├── vue-balance.js │ ├── vue-user.js │ ├── mixins.js │ ├── vue-animated-number.js │ ├── vue-product.js │ ├── vue-deposit.js │ ├── vue-history.js │ └── vue-adduser.js ├── package.json ├── app.scss ├── app.js └── yarn.lock ├── pyproject.toml ├── init.py ├── sample_insert.py ├── README.md ├── model.py ├── server.py ├── templates └── index.html ├── LICENSE └── poetry.lock /data/content/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/img/users/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/img/products/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/js/lib: -------------------------------------------------------------------------------- 1 | ../../client/lib -------------------------------------------------------------------------------- /static/css/app.css: -------------------------------------------------------------------------------- 1 | ../../client/app.css -------------------------------------------------------------------------------- /static/img/users: -------------------------------------------------------------------------------- 1 | ../../data/img/users -------------------------------------------------------------------------------- /static/js/app.js: -------------------------------------------------------------------------------- 1 | ../../client/app.js -------------------------------------------------------------------------------- /static/img/products: -------------------------------------------------------------------------------- 1 | ../../data/img/products -------------------------------------------------------------------------------- /static/js/vue: -------------------------------------------------------------------------------- 1 | ../../client/node_modules/vue/dist/ -------------------------------------------------------------------------------- /migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /static/js/jquery: -------------------------------------------------------------------------------- 1 | ../../client/node_modules/jquery/dist/ -------------------------------------------------------------------------------- /static/js/tweenjs: -------------------------------------------------------------------------------- 1 | ../../client/node_modules/tween.js/src/ -------------------------------------------------------------------------------- /static/js/bootstrap: -------------------------------------------------------------------------------- 1 | ../../client/node_modules/bootstrap/dist/js -------------------------------------------------------------------------------- /static/js/popper: -------------------------------------------------------------------------------- 1 | ../../client/node_modules/popper.js/dist/umd -------------------------------------------------------------------------------- /static/js/vue-color: -------------------------------------------------------------------------------- 1 | ../../client/node_modules/vue-color/dist -------------------------------------------------------------------------------- /static/css/bootstrap: -------------------------------------------------------------------------------- 1 | ../../client/node_modules/bootstrap/dist/css -------------------------------------------------------------------------------- /static/css/bootstrap-vue: -------------------------------------------------------------------------------- 1 | ../../client/node_modules/bootstrap-vue/dist -------------------------------------------------------------------------------- /static/js/bootstrap-vue: -------------------------------------------------------------------------------- 1 | ../../client/node_modules/bootstrap-vue/dist -------------------------------------------------------------------------------- /static/js/socket.io: -------------------------------------------------------------------------------- 1 | ../../client/node_modules/socket.io-client/dist/ -------------------------------------------------------------------------------- /static/css/tstar-bold-webfont.woff2: -------------------------------------------------------------------------------- 1 | ../../proprietary/tstar-bold-webfont.woff2 -------------------------------------------------------------------------------- /static/css/fontawesome: -------------------------------------------------------------------------------- 1 | ../../client/node_modules/@fortawesome/fontawesome-free/css/ -------------------------------------------------------------------------------- /static/css/tstar-italic-webfont.woff2: -------------------------------------------------------------------------------- 1 | ../../proprietary/tstar-italic-webfont.woff2 -------------------------------------------------------------------------------- /static/css/tstar-regular-webfont.woff2: -------------------------------------------------------------------------------- 1 | ../../proprietary/tstar-regular-webfont.woff2 -------------------------------------------------------------------------------- /static/js/fontawesome: -------------------------------------------------------------------------------- 1 | ../../client/node_modules/@fortawesome/fontawesome-free/js/ -------------------------------------------------------------------------------- /static/css/tstar-bolditalic-webfont.woff2: -------------------------------------------------------------------------------- 1 | ../../proprietary/tstar-bolditalic-webfont.woff2 -------------------------------------------------------------------------------- /static/icon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FREILab/unary/HEAD/static/icon/favicon.ico -------------------------------------------------------------------------------- /static/icon/favicon.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FREILab/unary/HEAD/static/icon/favicon.xcf -------------------------------------------------------------------------------- /static/img/background-l.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FREILab/unary/HEAD/static/img/background-l.png -------------------------------------------------------------------------------- /static/img/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FREILab/unary/HEAD/static/img/background.png -------------------------------------------------------------------------------- /static/img/background-glass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FREILab/unary/HEAD/static/img/background-glass.png -------------------------------------------------------------------------------- /client/lib/vue-balance.js: -------------------------------------------------------------------------------- 1 | Vue.component('balance-text', { 2 | template: ` 3 | € 4 | 7 | 8 | 9 | 10 | `, 11 | props: { 12 | user: { 13 | required: true 14 | }, 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /client/lib/vue-user.js: -------------------------------------------------------------------------------- 1 | Vue.component('user-card', { 2 | template: ` 3 | 7 | 12 |
{{ user.username }}
13 |
14 | `, 15 | props: { 16 | user: { type: Object, required: true }, 17 | }, 18 | }); 19 | 20 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "unary" 3 | version = "0.1.0" 4 | description = "A digital tally sheet for maker spaces." 5 | authors = ["Johannes Jordan "] 6 | license = "AGPLv3" 7 | readme = "README.md" 8 | 9 | [tool.poetry.dependencies] 10 | python = "^3.10" 11 | Flask = "^2.3.2" 12 | Flask-SQLAlchemy = "^3.0.3" 13 | Flask-SocketIO = "^5.3.3" 14 | flask-migrate = "^4.0.4" 15 | pyyaml = "^6.0" 16 | eventlet = "^0.33.3" 17 | 18 | [build-system] 19 | requires = ["poetry-core"] 20 | build-backend = "poetry.core.masonry.api" 21 | -------------------------------------------------------------------------------- /client/lib/mixins.js: -------------------------------------------------------------------------------- 1 | var moneyMixin = { 2 | methods: { 3 | format_money: value => Number.parseFloat(value).toFixed(2) 4 | } 5 | }; 6 | 7 | var modalMixin = { 8 | props: { 9 | domId: { required: true } 10 | }, 11 | data: () => ({ 12 | visible: false 13 | }), 14 | methods: { 15 | onShow() { this.visible = true; }, 16 | onHide(doReset) { 17 | if (doReset !== false) 18 | this.reset(); 19 | this.visible = false; 20 | }, 21 | close() { this.$refs.modal.hide(); }, 22 | reset() { Object.assign(this.$data, this.$options.data.apply(this)); } 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unary", 3 | "authors": [ 4 | "Johannes Jordan " 5 | ], 6 | "description": "Digital tally-sheet replacement", 7 | "main": "app.js", 8 | "license": "AGPL-3.0-or-later", 9 | "homepage": "", 10 | "ignore": [ 11 | "**/.*", 12 | "node_modules", 13 | "test", 14 | "tests" 15 | ], 16 | "private": true, 17 | "dependencies": { 18 | "bootstrap-vue": "latest", 19 | "jquery": "^3.3.1", 20 | "vue": "^2.5.16", 21 | "vue-color": "latest", 22 | "tween.js": "^16.6.0", 23 | "socket.io-client": "^4.5.0", 24 | "@fortawesome/fontawesome-free": "latest", 25 | "@popperjs/core": "latest" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /init.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from flask import Flask, json 3 | from flask_socketio import SocketIO 4 | from flask_sqlalchemy import SQLAlchemy 5 | from flask_migrate import Migrate 6 | import sys 7 | 8 | app = Flask(__name__) 9 | app.config['SECRET_KEY'] = '616+819+184/+/1+/4+*14+/4+4846413m,äfaiow3' 10 | # when using Flask development server under Poetry, an absolute path is needed 11 | app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{sys.path[0]}/data/db.sqlite' 12 | app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False 13 | #app.config['SQLALCHEMY_ECHO'] = True 14 | 15 | socketio = SocketIO(app, json=json) # use same JSON en-/decoders as flask does 16 | db = SQLAlchemy(app) 17 | migrate = Migrate(app, db) 18 | 19 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /sample_insert.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import collections 3 | from init import db 4 | import model as m 5 | 6 | def insert(*objects): 7 | for o in objects: 8 | db.session.add(o) 9 | print('Added {}'.format(o)) 10 | 11 | products = [] 12 | products.append(m.Product(name='Libella', picture='libella.jpg', size='0.5 l', prize=0.70, 13 | description='Alle Sorten', isOrganic=False, enabled=True)) 14 | products.append(m.Product(name='Löschzwerg', picture='löschzwerg.jpg', size='0.33 l', prize=1.0, 15 | description='', isOrganic=False, enabled=True)) 16 | products.append(m.Product(name='Bier/Radler', picture='waldhaus.jpg', size='0.33 l', prize=1.0, 17 | description='Alle Sorten außer Hopfensturm/-zauber', 18 | isOrganic=False, enabled=True)) 19 | products.append(m.Product(name='Proviant', picture='proviant.jpg', size='0.33 l', prize=1.0, 20 | description='Alle Sorten', isOrganic=True, enabled=True)) 21 | products.append(m.Product(name='Karamalz', picture='karamalz.jpg', size='0.33 l', prize=0.9, 22 | description='', isOrganic=False, enabled=True)) 23 | 24 | insert(*products) 25 | 26 | db.session.commit() 27 | -------------------------------------------------------------------------------- /client/lib/vue-animated-number.js: -------------------------------------------------------------------------------- 1 | Vue.component('animated-number', { 2 | template: '{{ tweeningValue }}', 3 | props: { 4 | value: { 5 | type: Number, 6 | required: true 7 | }, 8 | decimals: { 9 | type: Number, 10 | default: 0 11 | }, 12 | duration: { 13 | type: Number, 14 | default: 500 15 | } 16 | }, 17 | data: () => ({ 18 | tweeningValue: 0 19 | }), 20 | watch: { 21 | value(newValue, oldValue) { 22 | this.tween(oldValue, newValue); 23 | } 24 | }, 25 | mounted() { 26 | this.tweeningValue = this.value.toFixed(this.decimals); 27 | }, 28 | methods: { 29 | dynamicDuration(distance) { 30 | return this.duration * Math.log(Math.abs(distance) + 1); 31 | }, 32 | tween(start, end) { 33 | var vm = this; 34 | function animate () { 35 | if (TWEEN.update()) { 36 | requestAnimationFrame(animate); 37 | } 38 | } 39 | 40 | new TWEEN.Tween({ tweeningValue: start }) 41 | .to({ tweeningValue: end }, this.dynamicDuration(end - start)) 42 | .onUpdate(function () { 43 | vm.tweeningValue = this.tweeningValue.toFixed(vm.decimals) 44 | }) 45 | .start(); 46 | animate(); 47 | } 48 | } 49 | }); 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unary 2 | 3 | This software implements a digital tally sheet. It is used to let users self-manage their beverage consumption at the Freilab in Freiburg. 4 | 5 | The software is composed of two parts: 6 | 7 | * A client written with Vue.js, delivered via HTTP as a single page 8 | * A HTTP+Websocket server written with Python/Flask, accessing a SQLite database 9 | 10 | ## Note 11 | 12 | Feel free to get in touch about using Unary via email: unary@lanrules.de 13 | 14 | ## Setup 15 | 16 | A short idea of how to set this up: 17 | 18 | * Run `poetry install` to setup Poetry environment 19 | * Run `yarn install` in `client/` directory to install client-side dependencies 20 | * Replace the secret key in `init.py` 21 | * Set up the database (`poetry run flask --app server.py db upgrade` using flask migrations) 22 | * Run `poetry run python sample_insert.py` to insert sample products (or using https://sqlitebrowser.org/). 23 | * Add product/user images in `data/img` 24 | * Replace the font in the SCSS with a webfont you can actually deliver 25 | 26 | ## Run 27 | 28 | To run the application: 29 | 30 | * Run `poetry run flask --app server.py run` for development 31 | * Run `poetry run ./server.py` for production 32 | * Or, alternatively, add your favorite WSGI server to poetry and run it 33 | 34 | -------------------------------------------------------------------------------- /client/lib/vue-product.js: -------------------------------------------------------------------------------- 1 | let uid = 0; // unique id for each product component 2 | 3 | Vue.component('product-card', { 4 | mixins: [moneyMixin], 5 | template: ` 6 | 12 |
{{ product.name }}
13 |

14 | {{ product.size }} 15 | 16 | 17 | 18 | 19 | / 20 | 21 |

22 | 26 | € {{ format_money(product.prize) }} 27 | 28 | 33 | 34 |
35 | `, 36 | props: { 37 | product: { 38 | required: true 39 | } 40 | }, 41 | data() { 42 | uid++; 43 | return { 44 | buttonId: 'product_button_' + uid, 45 | numPopup: 0 46 | } 47 | }, 48 | methods: { 49 | clear_popups() { this.numPopup = 0; }, 50 | add_popup() { 51 | this.numPopup++; 52 | let that = this; 53 | window.setTimeout(() => that.numPopup = Math.max(that.numPopup - 1, 0), 3000); 54 | } 55 | } 56 | }); 57 | 58 | -------------------------------------------------------------------------------- /client/lib/vue-deposit.js: -------------------------------------------------------------------------------- 1 | Vue.component('deposit-modal', { 2 | mixins: [moneyMixin, modalMixin], 3 | template: ` 4 | 10 | 15 |

Bitte entscheide dich für den Betrag, den du einzahlen möchtest, 16 | und werfe diesen in die Spendekasse auf dem Tresen.

17 |

Scheine werden klar bevorzugt! Bitte nur ganze Eurobeträge!

18 |

Bestätige dann, wieviel Geld in die Kasse gelegt hast:

19 | 20 | 21 | 22 | 25 | € {{format_money(a)}} 26 | 27 | 28 | 29 | 30 | 31 | 35 | 39 | Einzahlung bestätigen 40 | 41 | 42 |
43 | `, 44 | props: { 45 | user: { 46 | required: true 47 | } 48 | }, 49 | data: () => ({ 50 | amount: '0', 51 | triggered: false // used to avoid double taps 52 | }), 53 | methods: { 54 | form_submit() { 55 | if (this.triggered) 56 | return; 57 | this.triggered = true; 58 | this.$emit('deposit', this.amount); 59 | }, 60 | } 61 | }); 62 | -------------------------------------------------------------------------------- /migrations/versions/4659bb930f82_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 4659bb930f82 4 | Revises: 5 | Create Date: 2019-03-06 19:13:10.188988 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '4659bb930f82' 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('product', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('enabled', sa.Boolean(), nullable=False), 24 | sa.Column('name', sa.String(length=80), nullable=False), 25 | sa.Column('picture', sa.String(length=80), nullable=True), 26 | sa.Column('size', sa.String(length=20), nullable=False), 27 | sa.Column('prize', sa.Float(), nullable=False), 28 | sa.Column('description', sa.String(length=500), nullable=False), 29 | sa.Column('isOrganic', sa.Boolean(), nullable=False), 30 | sa.PrimaryKeyConstraint('id'), 31 | sa.UniqueConstraint('name') 32 | ) 33 | op.create_table('user', 34 | sa.Column('id', sa.Integer(), nullable=False), 35 | sa.Column('enabled', sa.Boolean(), nullable=False), 36 | sa.Column('username', sa.String(length=80), nullable=False), 37 | sa.Column('fullname', sa.String(length=80), nullable=False), 38 | sa.Column('email', sa.String(length=80), nullable=False), 39 | sa.Column('color', sa.String(length=20), nullable=False), 40 | sa.Column('picture', sa.String(length=80), nullable=True), 41 | sa.Column('balance', sa.Float(), nullable=False), 42 | sa.Column('created', sa.DateTime(), nullable=True), 43 | sa.PrimaryKeyConstraint('id'), 44 | sa.UniqueConstraint('email'), 45 | sa.UniqueConstraint('username') 46 | ) 47 | op.create_table('transaction', 48 | sa.Column('id', sa.Integer(), nullable=False), 49 | sa.Column('user_id', sa.Integer(), nullable=False), 50 | sa.Column('product_id', sa.Integer(), nullable=True), 51 | sa.Column('amount', sa.Float(), nullable=False), 52 | sa.Column('date', sa.DateTime(), nullable=True), 53 | sa.Column('cancelled', sa.Boolean(), nullable=False), 54 | sa.Column('fulfilled', sa.Boolean(), nullable=False), 55 | sa.ForeignKeyConstraint(['product_id'], ['product.id'], ), 56 | sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), 57 | sa.PrimaryKeyConstraint('id') 58 | ) 59 | # ### end Alembic commands ### 60 | 61 | 62 | def downgrade(): 63 | # ### commands auto generated by Alembic - please adjust! ### 64 | op.drop_table('transaction') 65 | op.drop_table('user') 66 | op.drop_table('product') 67 | # ### end Alembic commands ### 68 | -------------------------------------------------------------------------------- /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 | import logging 6 | 7 | # this is the Alembic Config object, which provides 8 | # access to the values within the .ini file in use. 9 | config = context.config 10 | 11 | # Interpret the config file for Python logging. 12 | # This line sets up loggers basically. 13 | fileConfig(config.config_file_name) 14 | logger = logging.getLogger('alembic.env') 15 | 16 | # add your model's MetaData object here 17 | # for 'autogenerate' support 18 | # from myapp import mymodel 19 | # target_metadata = mymodel.Base.metadata 20 | from flask import current_app 21 | config.set_main_option('sqlalchemy.url', 22 | current_app.config.get('SQLALCHEMY_DATABASE_URI')) 23 | target_metadata = current_app.extensions['migrate'].db.metadata 24 | 25 | # other values from the config, defined by the needs of env.py, 26 | # can be acquired: 27 | # my_important_option = config.get_main_option("my_important_option") 28 | # ... etc. 29 | 30 | 31 | def run_migrations_offline(): 32 | """Run migrations in 'offline' mode. 33 | 34 | This configures the context with just a URL 35 | and not an Engine, though an Engine is acceptable 36 | here as well. By skipping the Engine creation 37 | we don't even need a DBAPI to be available. 38 | 39 | Calls to context.execute() here emit the given string to the 40 | script output. 41 | 42 | """ 43 | url = config.get_main_option("sqlalchemy.url") 44 | context.configure(url=url) 45 | 46 | with context.begin_transaction(): 47 | context.run_migrations() 48 | 49 | 50 | def run_migrations_online(): 51 | """Run migrations in 'online' mode. 52 | 53 | In this scenario we need to create an Engine 54 | and associate a connection with the context. 55 | 56 | """ 57 | 58 | # this callback is used to prevent an auto-migration from being generated 59 | # when there are no changes to the schema 60 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html 61 | def process_revision_directives(context, revision, directives): 62 | if getattr(config.cmd_opts, 'autogenerate', False): 63 | script = directives[0] 64 | if script.upgrade_ops.is_empty(): 65 | directives[:] = [] 66 | logger.info('No changes in schema detected.') 67 | 68 | engine = engine_from_config(config.get_section(config.config_ini_section), 69 | prefix='sqlalchemy.', 70 | poolclass=pool.NullPool) 71 | 72 | connection = engine.connect() 73 | context.configure(connection=connection, 74 | target_metadata=target_metadata, 75 | process_revision_directives=process_revision_directives, 76 | **current_app.extensions['migrate'].configure_args) 77 | 78 | try: 79 | with context.begin_transaction(): 80 | context.run_migrations() 81 | except Exception as exception: 82 | logger.error(exception) 83 | raise exception 84 | finally: 85 | connection.close() 86 | 87 | if context.is_offline_mode(): 88 | run_migrations_offline() 89 | else: 90 | run_migrations_online() 91 | -------------------------------------------------------------------------------- /client/lib/vue-history.js: -------------------------------------------------------------------------------- 1 | Vue.component('history-modal', { 2 | mixins: [moneyMixin, modalMixin], 3 | template: ` 4 | 10 | 15 | 41 |

Heute noch nichts getrunken? 😾

42 |
43 | 46 | 47 |
48 | 49 | 56 |
57 |

58 | Aus Datenschutzgründen werden länger zurückliegende Transaktionen nicht angezeigt. 59 |

60 |
61 | `, 62 | props: { 63 | user: { 64 | required: true 65 | } 66 | }, 67 | data() { 68 | let f = { 69 | product: { key: 'product', label: 'Produkt', tdClass: this.style_product, formatter: this.format_product }, 70 | date: { key: 'date', label: 'Uhrzeit', formatter: this.format_date }, 71 | amount: { key: 'amount', label: 'Betrag', class: 'text-right', tdClass: this.style_money, formatter: this.format_money }, 72 | cancel: { key: 'cancel', label: '', tdClass: 'button text-right'}, 73 | } 74 | return { 75 | fieldsToday: [f.product, f.date, f.amount, f.cancel], 76 | fieldsMonth: [f.product, f.amount], 77 | initialized: false, 78 | transToday: [], 79 | transMonth: [] 80 | }; 81 | }, 82 | methods: { 83 | format_product: p => (p ? p.name : 'Einzahlung'), 84 | style_product: p => (p ? '' : 'font-italic'), 85 | style_money: value => 'text-light font-weight-bold ' + (value > 0 ? 'bg-danger' : 'bg-success'), 86 | format_date: date => new Intl.DateTimeFormat( 87 | 'de-DE', {hour: 'numeric', minute: 'numeric'}).format(Date.parse(date)), 88 | style_row: item => (item.cancelled ? 'cancelled' : ''), 89 | update(today, month) { 90 | this.transToday = today; 91 | if (month) this.transMonth = month; 92 | this.initialized = true; 93 | } 94 | } 95 | }); 96 | 97 | -------------------------------------------------------------------------------- /model.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import inspect, types 3 | from sqlalchemy import inspect as sqlinspect 4 | from init import db 5 | from hashlib import sha256 6 | 7 | class ExportableMixin(object): 8 | # list of exportable attribute names 9 | _exportable_ = None 10 | 11 | def _fill_exportable_(self): 12 | # all db columns 13 | columns = [c.key for c in sqlinspect(self).attrs] 14 | # all dynamic properties 15 | properties = [k[0] for k in inspect.getmembers(self.__class__, lambda o: isinstance(o, property))] 16 | # minus blacklist 17 | blacklist = getattr(self, 'export_blacklist', []) 18 | self._exportable_ = [i for i in columns + properties if i not in blacklist] 19 | 20 | # provide a _clean_ (ie, insensitive) collection of attributes 21 | def export(self, omit=()): 22 | if self._exportable_ is None: 23 | self._fill_exportable_() 24 | ret = {} 25 | for name in self._exportable_: 26 | if name in omit: continue 27 | attr = getattr(self, name) 28 | if isinstance(attr, ExportableMixin): 29 | attr = attr.export() 30 | ret[name] = attr 31 | return ret 32 | 33 | class User(ExportableMixin, db.Model): 34 | id = db.Column(db.Integer, primary_key=True) 35 | enabled = db.Column(db.Boolean, default=True, nullable=False) 36 | username = db.Column(db.String(80), unique=True, nullable=False) 37 | fullname = db.Column(db.String(80), nullable=False) 38 | email = db.Column(db.String(80), unique=True, nullable=False) 39 | color = db.Column(db.String(20), default='black', nullable=False) 40 | picture = db.Column(db.String(80), default='generic.png') 41 | balance = db.Column(db.Float, default='0', nullable=False) 42 | created = db.Column(db.DateTime, default=db.func.now()) 43 | 44 | transactions = db.relationship('Transaction', backref='user', 45 | order_by=lambda: Transaction.date.desc()) 46 | 47 | export_blacklist = ['fullname', 'email', 'transactions', 'created'] 48 | 49 | @property # short prefix of full name that allows a broad search (privacy) 50 | def namePrefix(self): 51 | return self.fullname[:3] 52 | 53 | @property # hash email to allow comparisons (privacy) 54 | def emailDigest(self): 55 | return sha256(self.email.lower().encode('utf-8')).hexdigest() 56 | 57 | @property 58 | def lastActivity(self): 59 | if self.transactions and len(self.transactions) > 0: 60 | return self.transactions[0].date 61 | return self.created 62 | 63 | def __repr__(self): 64 | return ''.format(self.username) 65 | 66 | class Product(ExportableMixin, db.Model): 67 | id = db.Column(db.Integer, primary_key=True) 68 | enabled = db.Column(db.Boolean, default=False, nullable=False) 69 | name = db.Column(db.String(80), unique=True, nullable=False) 70 | picture = db.Column(db.String(80), default='generic.png') 71 | size = db.Column(db.String(20), nullable=False) 72 | prize = db.Column(db.Float, nullable=False) 73 | description = db.Column(db.String(500), default='', nullable=False) 74 | isOrganic = db.Column(db.Boolean, default=False, nullable=False) 75 | 76 | transactions = db.relationship('Transaction', backref='product') 77 | 78 | export_blacklist = ['transactions'] 79 | 80 | def __repr__(self): 81 | return ''.format(self.name) 82 | 83 | class Transaction(ExportableMixin, db.Model): 84 | id = db.Column(db.Integer, primary_key=True) 85 | user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) 86 | product_id = db.Column(db.Integer, db.ForeignKey('product.id')) 87 | amount = db.Column(db.Float, nullable=False) 88 | date = db.Column(db.DateTime, default=db.func.now()) 89 | cancelled = db.Column(db.Boolean, default=False, nullable=False) 90 | fulfilled = db.Column(db.Boolean, default=False, nullable=False) 91 | 92 | def __repr__(self): 93 | if (self.product_id): 94 | return ''.format(self.product_id, self.user_id, self.amount) 95 | return ''.format(self.user_id, self.amount) 96 | 97 | -------------------------------------------------------------------------------- /client/app.scss: -------------------------------------------------------------------------------- 1 | /* custom font */ 2 | @font-face { 3 | font-family: "tstar"; 4 | font-weight: normal; 5 | src: url('tstar-regular-webfont.woff2') format('woff2'); 6 | } 7 | @font-face { 8 | font-family: "tstar"; 9 | font-weight: bold; 10 | src: url('tstar-bold-webfont.woff2') format('woff2'); 11 | } 12 | @font-face { 13 | font-family: "tstar"; 14 | font-weight: normal; 15 | font-style: italic; 16 | src: url('tstar-italic-webfont.woff2') format('woff2'); 17 | } 18 | @font-face { 19 | font-family: "tstar"; 20 | font-weight: bold; 21 | font-style: italic; 22 | src: url('tstar-bolditalic-webfont.woff2') format('woff2'); 23 | } 24 | 25 | /* Bootstrap variable overrides */ 26 | $font-family-sans-serif: tstar; 27 | 28 | /* Bootstrap and its default variables */ 29 | @import "node_modules/bootstrap/scss/bootstrap"; 30 | 31 | /* app root */ 32 | #app { 33 | &:fullscreen { 34 | // set nice bg only when in fullscreen 35 | background: url('../img/background-l.png') white repeat; 36 | // fix scrolling 37 | overflow-y: scroll; 38 | 39 | & .not-fullscreen { display: none; } 40 | } 41 | &:not(:fullscreen) .fullscreen { display: none; } 42 | // fix
not standing out 43 | & hr { 44 | border-top: 1px solid white; 45 | border-bottom: 1px solid white; 46 | padding-top: 1px; 47 | background: lightgray; 48 | } 49 | } 50 | 51 | /* component style overrides */ 52 | #app { 53 | & .vc-slider-hue-warp { 54 | height: 20px; 55 | & .vc-hue-picker { 56 | width: 25px; 57 | height: 25px; 58 | transform: translate(-14px, -4px); 59 | border-radius: 12px; 60 | box-shadow: 0px 1px 6px 0 rgba(0, 0, 0, 0.7); 61 | } 62 | } 63 | & .vc-slider-swatch-picker { height: 20px; } 64 | } 65 | // patch defunct CSS in component (https://github.com/xiaokaike/vue-color/pull/174) 66 | .vc-slider-swatch:nth-child(n) .vc-slider-swatch-picker.vc-slider-swatch-picker--active { border-radius: 3.6px/2px; } 67 | 68 | /* custom elements */ 69 | // fading backgrounds 70 | .fade-bg { 71 | transition: background-color .75s ease-out; 72 | } 73 | 74 | // common buttons in the interface 75 | button.buy, button.done { 76 | font-size: 1.5rem; 77 | } 78 | 79 | // a user card 80 | .card.user { 81 | // behave like a link as we are clickable 82 | @extend a; 83 | cursor: pointer; 84 | // have glass-effect background except for colored guest button 85 | // does not work on Android background: url('../img/background-glass.png') white repeat fixed; 86 | background: rgba(255,255,255,.65); 87 | &.guest { background: none; @extend .bg-warning; } 88 | &.favorite { @extend .border-primary; } 89 | } 90 | 91 | // a product card in grid 92 | .card.product { 93 | min-height: 100%; 94 | & .card-body { 95 | padding-top: .75rem; // reduced padding 96 | // fade background out in text area 97 | background: linear-gradient(to bottom, 98 | rgba(240, 240, 240, .85), 99 | transparent 65%); 100 | // rgba( 60, 60, 60, .95)); 101 | } 102 | & .card-text { // also make text stand out 103 | text-shadow: 2px 2px 6px white, -2px -2px 6px white; 104 | } 105 | & button:not(:hover) { background: rgba(255, 255, 255, 0.85); } 106 | } 107 | 108 | // a cancelled transaction 109 | tr.cancelled { 110 | text-decoration: line-through wavy theme-color("danger"); 111 | & td { 112 | background: none !important; 113 | color: inherit !important; 114 | } 115 | } 116 | 117 | // a table cell to contain a button 118 | td.button { 119 | // reduce padding so the button fits 120 | padding-top: 0.3rem; // like table-sm 121 | padding-bottom: 0.3rem; 122 | vertical-align: middle; 123 | } 124 | 125 | /* main view animation (user select / product select) */ 126 | .slide-leave-active, 127 | .slide-enter-active { 128 | transition: .5s ease-in-out; 129 | } 130 | .slide-enter { 131 | transform: translate(100%, 0); 132 | } 133 | .slide-leave-to { 134 | transform: translate(-100%, 0); 135 | } 136 | .slide > div { 137 | position: absolute; 138 | } 139 | 140 | /* generally usable fade in/out animation */ 141 | .slowfade { 142 | transition: opacity 1.25s ease-out; 143 | &.gone { 144 | opacity: 0; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /client/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var collator = new Intl.Collator('de', { sensitivity: 'base' }); 4 | var userCompare = (a, b) => collator.compare(a.username, b.username); 5 | var productCompare = (a, b) => collator.compare(a.name, b.name); 6 | 7 | var socket = io(); 8 | 9 | var app = new Vue({ 10 | el: '#app', 11 | mixins: [moneyMixin], 12 | data: { 13 | products: initial.products.sort(productCompare), 14 | users: initial.users.sort(userCompare), 15 | addUserTemplate: initial.newUser, 16 | currentUser: null, 17 | userFilter: '', 18 | userTimeout : null, 19 | connected: socket.connected, 20 | showErrorAlert: false, 21 | lastServerError: '' 22 | }, 23 | computed: { 24 | filteredUsers() { 25 | return this.users.filter(u => { 26 | if (u.id == 1) // TODO: see guestUser below 27 | return false; 28 | let filter = this.userFilter.toLowerCase(); 29 | // allow a broad filter on real name 30 | let pre = u.namePrefix.toLowerCase(); 31 | if (pre.startsWith(filter.slice(0, pre.length))) 32 | return true; 33 | return u.username.toLowerCase().startsWith(filter); 34 | }); 35 | }, 36 | guestUser() { 37 | // TODO: hack, maybe mark on the server or deliver separately? 38 | return this.users.find(u => u.id == 1); 39 | }, 40 | favoriteUsers() { 41 | let latest = this.users.filter(u => (u !== this.guestUser && u.lastActivity)); 42 | latest.sort((a, b) => Date.parse(b.lastActivity) - Date.parse(a.lastActivity)); 43 | return latest.slice(0, 5); 44 | } 45 | }, 46 | methods: { 47 | update_timeout() { 48 | // add/update a simple timeout that returns to user selection 49 | if (this.userTimeout) 50 | clearTimeout(this.userTimeout); 51 | this.userTimeout = setTimeout(() => this.deselect_user(), 60000) 52 | }, 53 | select_user(user) { 54 | this.currentUser = user; 55 | // start deselection timeout 56 | this.update_timeout(); 57 | }, 58 | deselect_user() { 59 | this.currentUser = null; 60 | // remove user filter (considered outdated) 61 | this.userFilter = ''; 62 | // ensure there are no leftover popups 63 | if ('products' in this.$refs) 64 | this.$refs['product'].forEach((p) => p.clear_popups()); 65 | }, 66 | fetch_transactions(parameters) { 67 | socket.emit('transactions', {uid: this.currentUser.id, ...parameters}, ret => { 68 | if (ret.success) { 69 | this.$refs.history.update(ret.today, ret.month); 70 | } else { 71 | this.lastServerError = ret.message; 72 | this.showErrorAlert = true; 73 | } 74 | }); 75 | }, 76 | transact(task, parameters) { 77 | this.update_timeout(); // honor user action 78 | return new Promise((resolve, reject) => { 79 | if (!this.connected) { 80 | reject("Verzögerte Transaktion verhindert."); 81 | return; 82 | } 83 | if (!this.currentUser) { 84 | reject("Kein Nutzer ausgewählt!"); 85 | return; 86 | } 87 | socket.emit(task, {uid: this.currentUser.id, ...parameters}, ret => { 88 | if (ret.success) { 89 | resolve(ret); 90 | } else { 91 | this.lastServerError = ret.message; 92 | this.showErrorAlert = true; 93 | reject(ret.message); 94 | } 95 | }); 96 | }).catch((msg) => console.warn('Transaktionsfehler: ' + msg)); 97 | }, 98 | buy(product) { 99 | this.transact('purchase', {pid: product.id}).then( 100 | () => this.$refs.product.find(c => c.product.id === product.id).add_popup() 101 | ); 102 | }, 103 | deposit(amount) { 104 | this.transact('purchase', {amount: -amount}).then( 105 | () => this.$refs.deposit.close() // TODO: provide explicit positive feedback 106 | ); 107 | }, 108 | revert(tid) { 109 | this.transact('revert', {tid: tid}).then( 110 | () => this.fetch_transactions({short: true}) // sync current state 111 | // note: we do not follow our philosophy of push notifications as it is a tailored list 112 | ); 113 | }, 114 | add_user(account) { 115 | socket.emit('add user', account, ret => { 116 | if (!ret.success) { 117 | this.lastServerError = ret.message; 118 | this.showErrorAlert = true; 119 | } 120 | }); 121 | } 122 | } 123 | }); 124 | 125 | socket.on('connect', () => app.connected = true); 126 | socket.on('reconnect', () => app.connected = true); 127 | socket.on('disconnect', reason => { 128 | app.connected = false; 129 | if (reason === 'io server disconnect') // no automatic reconnect 130 | socket.connect(); 131 | }); 132 | 133 | socket.on('user changed', user => { 134 | let needSort = true; 135 | let target = app.currentUser; // typically the current user receives updates 136 | if (!target || target.id !== user.id) 137 | target = app.users.find(u => u.id === user.id); 138 | 139 | if (target) { 140 | needSort = target.username != user.username; 141 | Object.assign(target, user); 142 | } else { 143 | app.users.push(user); 144 | } 145 | if (needSort) { 146 | app.users.sort(userCompare); 147 | } 148 | }); 149 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from os import chdir 3 | import sys 4 | import time 5 | from flask import render_template 6 | import yaml 7 | from datetime import datetime, timedelta 8 | from init import app, db, socketio 9 | import model as m 10 | 11 | # retrieve all css, js files to bundle 12 | def assets(): 13 | return { 14 | # note: use vue.min, socket.io.slim in production 15 | 'js': [ 16 | 'jquery/jquery.slim.min', 'popper/popper.min', 'bootstrap/bootstrap.min', 17 | 'vue/vue.min', 'bootstrap-vue/bootstrap-vue.min', 'vue-color/vue-color.min', 18 | 'tweenjs/Tween', 'lib/vue-animated-number', 'lib/vue-balance', 19 | 'lib/mixins', 20 | 'lib/vue-product', 'lib/vue-user', 'lib/vue-history', 'lib/vue-deposit', 'lib/vue-adduser', 21 | 'socket.io/socket.io.min', 22 | 'fontawesome/all.min', 23 | ], 24 | 'css': [ 25 | 'app', 'bootstrap-vue/bootstrap-vue.min', 26 | 'fontawesome/svg-with-js' 27 | ] 28 | } 29 | 30 | # retrieve initial full data needed by client 31 | def payload(): 32 | products = m.Product.query.filter_by(enabled=True) 33 | users = m.User.query.filter_by(enabled=True) 34 | # Note: this may throw OSError or YAML errors 35 | with open('data/content/new_user.yaml') as f: 36 | newUser = yaml.safe_load(f) 37 | return { 38 | 'products': [p.export() for p in products.all()], 39 | 'users': [u.export() for u in users.all()], 40 | 'newUser': newUser 41 | } 42 | 43 | # build failure json response 44 | def failure(message): 45 | return {'success': False, 'message': message} 46 | 47 | # build success json response 48 | def success(payload={}): 49 | return {'success': True, **payload} 50 | 51 | # determine when this day started from our point of view 52 | def startOfDay(): 53 | if datetime.now().hour > 5: # let's say a day starts and ends at 5am 54 | return datetime.today().replace(hour=5) 55 | else: 56 | return (datetime.today() - timedelta(days=1)).replace(hour=5) 57 | 58 | @app.route("/") 59 | def home(): 60 | return render_template('index.html', assets=assets(), payload=payload()) 61 | 62 | @socketio.on('transactions') 63 | def transactions(params): 64 | if 'uid' not in params: 65 | return failure('Nutzer ungültig!') 66 | today = startOfDay() 67 | # get today's transactions with complete data 68 | daily = m.Transaction.query.filter_by(user_id=params.get('uid')) \ 69 | .filter(m.Transaction.date >= today) \ 70 | .order_by(m.Transaction.date.desc()) 71 | 72 | ret = {'today': [t.export(omit=('user')) for t in daily.all()]} 73 | 74 | if not params.get('short', False): 75 | # get longer backlog, but this time fuzzed out (we remove date info) 76 | monthly = m.Transaction.query.filter_by(user_id=params['uid']) \ 77 | .filter(m.Transaction.date < today) \ 78 | .filter(m.Transaction.date > today - timedelta(days=28)) \ 79 | .order_by(m.Transaction.date.desc()) 80 | ret['month'] = [t.export(omit=('user', 'date')) for t in monthly.all()] 81 | 82 | return success(ret) 83 | 84 | @socketio.on('purchase') 85 | def purchase(params): 86 | user = m.User.query.get(params.get('uid', None)) 87 | if user is None: 88 | return failure('Nutzer ungültig!') 89 | if 'pid' in params: 90 | product = m.Product.query.get(params['pid']) 91 | if product is None: 92 | return failure('Produkt ungültig!') 93 | amount=product.prize 94 | else: 95 | if not 'amount' in params: 96 | return failure('Weder Produkt, noch Betrag angegeben!') 97 | product = None # for deposits, basically 98 | amount=params['amount'] 99 | 100 | # perform purchase 101 | transaction = m.Transaction(user=user, product=product, amount=amount) 102 | user.balance -= amount 103 | db.session.add(transaction) 104 | try: 105 | db.session.commit() 106 | socketio.emit('user changed', user.export()) 107 | return success() 108 | except: 109 | return failure('Datenbankeintrag gescheitert!') 110 | 111 | @socketio.on('revert') 112 | def revert(params): 113 | transaction = m.Transaction.query.get(params.get('tid', None)) 114 | if transaction is None: 115 | return failure('Transaktion ungültig!') 116 | if transaction.user_id != params.get('uid', None): 117 | return failure('Transaktion und Nutzer passen nicht zusammen!') 118 | if transaction.date < startOfDay() or transaction.fulfilled: 119 | return failure('Transaktion kann nicht mehr revidiert werden!') 120 | if transaction.cancelled: 121 | return success() # avoid error message on double taps 122 | 123 | transaction.cancelled = True 124 | transaction.user.balance += transaction.amount 125 | try: 126 | db.session.commit() 127 | socketio.emit('user changed', transaction.user.export()) 128 | return success() 129 | except: 130 | return failure('Datenbankeintrag gescheitert!') 131 | 132 | @socketio.on('add user') 133 | def adduser(userdata): 134 | # only take allowed input, and ensure it's all there 135 | entries = ('username', 'fullname', 'color', 'email') 136 | try: 137 | user = m.User(**{k : userdata[k] for k in entries}) 138 | except KeyError: 139 | return failure('Unvollständiger Account!') 140 | 141 | # validate input TODO – not mission-critical here but would be nice 142 | 143 | # check duplicates 144 | if m.User.query.filter_by(username=user.username).count(): 145 | return failure('Nutzername ist bereits belegt!') 146 | if m.User.query.filter_by(email=user.email).count(): 147 | return failure('Die Email-Adresse ist bereits bekannt!') 148 | 149 | db.session.add(user) 150 | try: 151 | db.session.commit() 152 | socketio.emit('user changed', user.export()) 153 | return success() 154 | except: 155 | return failure('Datenbankeintrag gescheitert!') 156 | 157 | @app.url_defaults 158 | def add_stamp(endpoint, values): 159 | # add a version / datetime stamp to enforce browser reloading on new data 160 | values['timestamp'] = time.time() 161 | 162 | #@app.after_request 163 | #def add_header(r): 164 | # disable browser cache, remove for production! 165 | #r.headers["Cache-Control"] = "no-cache, no-store, must-revalidate" 166 | #return r 167 | 168 | if __name__ == '__main__': 169 | # ensure we find our data, assets etc. 170 | chdir(sys.path[0]) 171 | print("Running on http://localhost:5000") 172 | socketio.run(app) 173 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Getränke 7 | 8 | 9 | 10 | {% for file in assets.css %} 11 | 12 | {% endfor %} 13 | {% for file in assets.js %} 14 | 15 | {% endfor %} 16 | 17 | 18 | 19 | 20 | 24 | {% raw %} 25 | 26 |
27 | 28 | 29 | 30 |
31 | 32 | 50 | 62 | 63 | 64 | 65 | 66 |

Was möchtest du?

67 |
68 | 69 | 73 | Fertig 74 | 75 | 76 |
77 | 81 | 88 | 89 |
90 |
91 | 92 | 93 |
94 | 95 | Zum Wohl! 96 | 97 | Getränkeabrechnung im Freilab 98 | 99 | 100 | 101 | 102 | 103 |

Wer trinkt?

104 |
105 | 106 | 110 | 111 | 112 | 115 | Ich bin neu 116 | 117 | 118 |
119 | 120 |
121 | 122 | 125 | 128 | 129 | 132 | 135 | 136 | 137 |
138 | 139 |
140 | 141 | 144 | 146 | 147 | 148 |
149 |
150 |
151 | 155 | 158 | 159 | Getränkeabrechnungssystem im Vollbild starten 160 | 161 | 162 | 163 | 166 | 167 | Verbindung zum Server unterbrochen! 168 | 169 | 172 | 173 | Ups, da ist etwas schiefgelaufen! 174 | Bitte versuch es noch einmal oder melde das Problem: 175 |

{{ lastServerError }}

176 |
177 | 178 | 179 | 181 | 182 | 184 | 185 | 187 | 188 |
189 | {% endraw %} 190 | 191 | 192 | -------------------------------------------------------------------------------- /client/lib/vue-adduser.js: -------------------------------------------------------------------------------- 1 | /* equiv. to text.lower().encode('utf-8')).hexdigest() on the server side */ 2 | let digest = async function(text) { 3 | const data = new TextEncoder().encode(text.toLowerCase()); 4 | const buffer = await window.crypto.subtle.digest('SHA-256', data); 5 | const byteArray = new Uint8Array(buffer); 6 | const hexCodes = [...byteArray].map(value => { 7 | return value.toString(16).padStart(2, '0'); 8 | }); 9 | return hexCodes.join(''); 10 | }; 11 | 12 | Vue.component('adduser-modal', { 13 | mixins: [modalMixin], 14 | components: { 15 | 'color-picker': VueColor.Slider 16 | }, 17 | template: ` 18 | 26 |
27 | 28 |
29 | 30 | 37 | 39 | 40 | 41 | 48 | 50 | 51 | 52 | 59 | 61 | 62 | 63 |
64 | 65 | Deine Farbe 66 | 67 | 68 | 72 | 73 | 74 | Deine Lieblingsfarbe zur schnellen Wiedererkennung. 75 | 76 | 77 |
78 |
79 |
80 | 81 | 93 | 94 | 102 | 103 | 123 |
124 | `, 125 | props: { 126 | template: { required: true, type: Object }, 127 | users: { required: true, type: Array }, 128 | }, 129 | data() { 130 | return { 131 | step: 0, 132 | account: { 133 | username: '', fullname: '', email: '', color: '#732626', 134 | picture: 'generic.png', 135 | emailDigest: '' // computed asynchronously 136 | }, 137 | answers: this.template.questions.map(q => -1), 138 | } 139 | }, 140 | watch: { 141 | 'account.email'(text) { // trigger digest computation for emailDigest 142 | digest(text).then(digest => this.account.emailDigest = digest); 143 | } 144 | }, 145 | computed: { 146 | lastStep() { return 2 + this.template.questions.length; }, 147 | errors() { 148 | let a = this.account; 149 | let u = "", f = "", e = ""; 150 | if (a.username.length == 0) 151 | u = "Bitte angeben"; 152 | if (a.username.length > 10) 153 | u = "Bitte wähle einen kürzeren Namen"; 154 | if (this.users.find(u => u.username.toLowerCase() === a.username.toLowerCase())) 155 | u = "Dieser Name ist schon vergeben 😼"; 156 | if (!a.fullname.match(/\w+( \w+)+/)) 157 | f = "Bitte gib Vor- und Nachnamen an"; 158 | if (!a.email.match(/^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/)) 159 | e = "Bitte gib eine gültige Email-Adresse an"; 160 | if (this.users.find(u => u.emailDigest === a.emailDigest)) 161 | e = "Diese Email-Adresse gehört bereits zu einem anderen Konto 🙀"; 162 | return {username: u, fullname: f, email: e}; 163 | }, 164 | valid() { 165 | return Object.assign(...Object.entries(this.errors).map(([k, v]) => ({[k]: !v}))); 166 | }, 167 | allValid() { 168 | return Object.entries(this.valid).reduce((sum, [k, v]) => sum && v, true) 169 | }, 170 | buttonVariant() { 171 | // determine all button outfits based on quiz state (answered, wrong answer, etc) 172 | let a = this.answers; 173 | return this.template.questions.map((q, i) => 174 | q.a.map(function (_, j) { 175 | if (j == q.correct - 1 && a[i] > -1) 176 | return 'success'; 177 | else if (j == a[i]) 178 | return 'outline-danger'; 179 | else 180 | return 'outline-primary'; 181 | }) 182 | ); 183 | } 184 | }, 185 | methods: { 186 | form_submit() { 187 | if (this.allValid) 188 | this.step += 1; 189 | }, 190 | answer(i, j) { 191 | // effectively disable buttons without visual feedback we don't like 192 | if (this.answers[i] < 0) 193 | this.$set(this.answers, i, j); 194 | }, 195 | finalize() { 196 | this.$emit('create', this.account); 197 | // TODO: modal closes, the user has to re-do everything on fail 198 | this.close(); 199 | } 200 | } 201 | }); 202 | 203 | -------------------------------------------------------------------------------- /client/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@babel/parser@^7.18.4": 6 | version "7.21.4" 7 | resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.4.tgz#94003fdfc520bbe2875d4ae557b43ddb6d880f17" 8 | integrity sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw== 9 | 10 | "@fortawesome/fontawesome-free@latest": 11 | version "6.4.0" 12 | resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-6.4.0.tgz#1ee0c174e472c84b23cb46c995154dc383e3b4fe" 13 | integrity sha512-0NyytTlPJwB/BF5LtRV8rrABDbe3TdTXqNB3PdZ+UUUZAEIrdOJdmABqKjt4AXwIoJNaRVVZEXxpNrqvE1GAYQ== 14 | 15 | "@nuxt/opencollective@^0.3.2": 16 | version "0.3.3" 17 | resolved "https://registry.yarnpkg.com/@nuxt/opencollective/-/opencollective-0.3.3.tgz#80ff0eb8f6fca1d0ed5a089b9688f41bff2dd8ab" 18 | integrity sha512-6IKCd+gP0HliixqZT/p8nW3tucD6Sv/u/eR2A9X4rxT/6hXlMzA4GZQzq4d2qnBAwSwGpmKyzkyTjNjrhaA25A== 19 | dependencies: 20 | chalk "^4.1.0" 21 | consola "^2.15.0" 22 | node-fetch "^2.6.7" 23 | 24 | "@popperjs/core@latest": 25 | version "2.11.7" 26 | resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.7.tgz#ccab5c8f7dc557a52ca3288c10075c9ccd37fff7" 27 | integrity sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw== 28 | 29 | "@socket.io/component-emitter@~3.1.0": 30 | version "3.1.0" 31 | resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553" 32 | integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg== 33 | 34 | "@vue/compiler-sfc@2.7.14": 35 | version "2.7.14" 36 | resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-2.7.14.tgz#3446fd2fbb670d709277fc3ffa88efc5e10284fd" 37 | integrity sha512-aNmNHyLPsw+sVvlQFQ2/8sjNuLtK54TC6cuKnVzAY93ks4ZBrvwQSnkkIh7bsbNhum5hJBS00wSDipQ937f5DA== 38 | dependencies: 39 | "@babel/parser" "^7.18.4" 40 | postcss "^8.4.14" 41 | source-map "^0.6.1" 42 | 43 | ansi-styles@^4.1.0: 44 | version "4.3.0" 45 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" 46 | integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== 47 | dependencies: 48 | color-convert "^2.0.1" 49 | 50 | bootstrap-vue@latest: 51 | version "2.23.1" 52 | resolved "https://registry.yarnpkg.com/bootstrap-vue/-/bootstrap-vue-2.23.1.tgz#8f866f7cda27eb0e7e13a0bea8d55d8fc7a82199" 53 | integrity sha512-SEWkG4LzmMuWjQdSYmAQk1G/oOKm37dtNfjB5kxq0YafnL2W6qUAmeDTcIZVbPiQd2OQlIkWOMPBRGySk/zGsg== 54 | dependencies: 55 | "@nuxt/opencollective" "^0.3.2" 56 | bootstrap "^4.6.1" 57 | popper.js "^1.16.1" 58 | portal-vue "^2.1.7" 59 | vue-functional-data-merge "^3.1.0" 60 | 61 | bootstrap@^4.6.1: 62 | version "4.6.2" 63 | resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.6.2.tgz#8e0cd61611728a5bf65a3a2b8d6ff6c77d5d7479" 64 | integrity sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ== 65 | 66 | chalk@^4.1.0: 67 | version "4.1.2" 68 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" 69 | integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== 70 | dependencies: 71 | ansi-styles "^4.1.0" 72 | supports-color "^7.1.0" 73 | 74 | clamp@^1.0.1: 75 | version "1.0.1" 76 | resolved "https://registry.yarnpkg.com/clamp/-/clamp-1.0.1.tgz#66a0e64011816e37196828fdc8c8c147312c8634" 77 | integrity sha512-kgMuFyE78OC6Dyu3Dy7vcx4uy97EIbVxJB/B0eJ3bUNAkwdNcxYzgKltnyADiYwsR7SEqkkUPsEUT//OVS6XMA== 78 | 79 | color-convert@^2.0.1: 80 | version "2.0.1" 81 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" 82 | integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== 83 | dependencies: 84 | color-name "~1.1.4" 85 | 86 | color-name@~1.1.4: 87 | version "1.1.4" 88 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" 89 | integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== 90 | 91 | consola@^2.15.0: 92 | version "2.15.3" 93 | resolved "https://registry.yarnpkg.com/consola/-/consola-2.15.3.tgz#2e11f98d6a4be71ff72e0bdf07bd23e12cb61550" 94 | integrity sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw== 95 | 96 | csstype@^3.1.0: 97 | version "3.1.2" 98 | resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b" 99 | integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== 100 | 101 | debug@~4.3.1, debug@~4.3.2: 102 | version "4.3.4" 103 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" 104 | integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== 105 | dependencies: 106 | ms "2.1.2" 107 | 108 | engine.io-client@~6.4.0: 109 | version "6.4.0" 110 | resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.4.0.tgz#88cd3082609ca86d7d3c12f0e746d12db4f47c91" 111 | integrity sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g== 112 | dependencies: 113 | "@socket.io/component-emitter" "~3.1.0" 114 | debug "~4.3.1" 115 | engine.io-parser "~5.0.3" 116 | ws "~8.11.0" 117 | xmlhttprequest-ssl "~2.0.0" 118 | 119 | engine.io-parser@~5.0.3: 120 | version "5.0.6" 121 | resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.6.tgz#7811244af173e157295dec9b2718dfe42a64ef45" 122 | integrity sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw== 123 | 124 | has-flag@^4.0.0: 125 | version "4.0.0" 126 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" 127 | integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== 128 | 129 | jquery@^3.3.1: 130 | version "3.6.4" 131 | resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.6.4.tgz#ba065c188142100be4833699852bf7c24dc0252f" 132 | integrity sha512-v28EW9DWDFpzcD9O5iyJXg3R3+q+mET5JhnjJzQUZMHOv67bpSIHq81GEYpPNZHG+XXHsfSme3nxp/hndKEcsQ== 133 | 134 | lodash.throttle@^4.0.0: 135 | version "4.1.1" 136 | resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" 137 | integrity sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ== 138 | 139 | material-colors@^1.0.0: 140 | version "1.2.6" 141 | resolved "https://registry.yarnpkg.com/material-colors/-/material-colors-1.2.6.tgz#6d1958871126992ceecc72f4bcc4d8f010865f46" 142 | integrity sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg== 143 | 144 | ms@2.1.2: 145 | version "2.1.2" 146 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" 147 | integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== 148 | 149 | nanoid@^3.3.6: 150 | version "3.3.6" 151 | resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" 152 | integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== 153 | 154 | node-fetch@^2.6.7: 155 | version "2.6.9" 156 | resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.9.tgz#7c7f744b5cc6eb5fd404e0c7a9fec630a55657e6" 157 | integrity sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg== 158 | dependencies: 159 | whatwg-url "^5.0.0" 160 | 161 | picocolors@^1.0.0: 162 | version "1.0.0" 163 | resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" 164 | integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== 165 | 166 | popper.js@^1.16.1: 167 | version "1.16.1" 168 | resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b" 169 | integrity sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ== 170 | 171 | portal-vue@^2.1.7: 172 | version "2.1.7" 173 | resolved "https://registry.yarnpkg.com/portal-vue/-/portal-vue-2.1.7.tgz#ea08069b25b640ca08a5b86f67c612f15f4e4ad4" 174 | integrity sha512-+yCno2oB3xA7irTt0EU5Ezw22L2J51uKAacE/6hMPMoO/mx3h4rXFkkBkT4GFsMDv/vEe8TNKC3ujJJ0PTwb6g== 175 | 176 | postcss@^8.4.14: 177 | version "8.4.23" 178 | resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.23.tgz#df0aee9ac7c5e53e1075c24a3613496f9e6552ab" 179 | integrity sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA== 180 | dependencies: 181 | nanoid "^3.3.6" 182 | picocolors "^1.0.0" 183 | source-map-js "^1.0.2" 184 | 185 | socket.io-client@^4.5.0: 186 | version "4.6.1" 187 | resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.6.1.tgz#80d97d5eb0feca448a0fb6d69a7b222d3d547eab" 188 | integrity sha512-5UswCV6hpaRsNg5kkEHVcbBIXEYoVbMQaHJBXJCyEQ+CiFPV1NIOY0XOFWG4XR4GZcB8Kn6AsRs/9cy9TbqVMQ== 189 | dependencies: 190 | "@socket.io/component-emitter" "~3.1.0" 191 | debug "~4.3.2" 192 | engine.io-client "~6.4.0" 193 | socket.io-parser "~4.2.1" 194 | 195 | socket.io-parser@~4.2.1: 196 | version "4.2.2" 197 | resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.2.tgz#1dd384019e25b7a3d374877f492ab34f2ad0d206" 198 | integrity sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw== 199 | dependencies: 200 | "@socket.io/component-emitter" "~3.1.0" 201 | debug "~4.3.1" 202 | 203 | source-map-js@^1.0.2: 204 | version "1.0.2" 205 | resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" 206 | integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== 207 | 208 | source-map@^0.6.1: 209 | version "0.6.1" 210 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" 211 | integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== 212 | 213 | supports-color@^7.1.0: 214 | version "7.2.0" 215 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" 216 | integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== 217 | dependencies: 218 | has-flag "^4.0.0" 219 | 220 | tinycolor2@^1.1.2: 221 | version "1.6.0" 222 | resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.6.0.tgz#f98007460169b0263b97072c5ae92484ce02d09e" 223 | integrity sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw== 224 | 225 | tr46@~0.0.3: 226 | version "0.0.3" 227 | resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" 228 | integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== 229 | 230 | tween.js@^16.6.0: 231 | version "16.6.0" 232 | resolved "https://registry.yarnpkg.com/tween.js/-/tween.js-16.6.0.tgz#739104c9336cc4f11ee53f9ce7cede51e6723624" 233 | integrity sha512-IIhm0VsiBxNJO+WsXUvjcq+u3sKfOwSu67Yh9T5xSpXb2eHHi9+uZDDGBc7uMXjfud0bYiLkYCEb7JXIGXi5KA== 234 | 235 | vue-color@latest: 236 | version "2.8.1" 237 | resolved "https://registry.yarnpkg.com/vue-color/-/vue-color-2.8.1.tgz#a090f3dcf8ed6f07afdb865cac84b19a73302e70" 238 | integrity sha512-BoLCEHisXi2QgwlhZBg9UepvzZZmi4176vbr+31Shen5WWZwSLVgdScEPcB+yrAtuHAz42309C0A4+WiL9lNBw== 239 | dependencies: 240 | clamp "^1.0.1" 241 | lodash.throttle "^4.0.0" 242 | material-colors "^1.0.0" 243 | tinycolor2 "^1.1.2" 244 | 245 | vue-functional-data-merge@^3.1.0: 246 | version "3.1.0" 247 | resolved "https://registry.yarnpkg.com/vue-functional-data-merge/-/vue-functional-data-merge-3.1.0.tgz#08a7797583b7f35680587f8a1d51d729aa1dc657" 248 | integrity sha512-leT4kdJVQyeZNY1kmnS1xiUlQ9z1B/kdBFCILIjYYQDqZgLqCLa0UhjSSeRX6c3mUe6U5qYeM8LrEqkHJ1B4LA== 249 | 250 | vue@^2.5.16: 251 | version "2.7.14" 252 | resolved "https://registry.yarnpkg.com/vue/-/vue-2.7.14.tgz#3743dcd248fd3a34d421ae456b864a0246bafb17" 253 | integrity sha512-b2qkFyOM0kwqWFuQmgd4o+uHGU7T+2z3T+WQp8UBjADfEv2n4FEMffzBmCKNP0IGzOEEfYjvtcC62xaSKeQDrQ== 254 | dependencies: 255 | "@vue/compiler-sfc" "2.7.14" 256 | csstype "^3.1.0" 257 | 258 | webidl-conversions@^3.0.0: 259 | version "3.0.1" 260 | resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" 261 | integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== 262 | 263 | whatwg-url@^5.0.0: 264 | version "5.0.0" 265 | resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" 266 | integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== 267 | dependencies: 268 | tr46 "~0.0.3" 269 | webidl-conversions "^3.0.0" 270 | 271 | ws@~8.11.0: 272 | version "8.11.0" 273 | resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" 274 | integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== 275 | 276 | xmlhttprequest-ssl@~2.0.0: 277 | version "2.0.0" 278 | resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67" 279 | integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A== 280 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "alembic" 5 | version = "1.10.3" 6 | description = "A database migration tool for SQLAlchemy." 7 | category = "main" 8 | optional = false 9 | python-versions = ">=3.7" 10 | files = [ 11 | {file = "alembic-1.10.3-py3-none-any.whl", hash = "sha256:b2e0a6cfd3a8ce936a1168320bcbe94aefa3f4463cd773a968a55071beb3cd37"}, 12 | {file = "alembic-1.10.3.tar.gz", hash = "sha256:32a69b13a613aeb7e8093f242da60eff9daed13c0df02fff279c1b06c32965d2"}, 13 | ] 14 | 15 | [package.dependencies] 16 | Mako = "*" 17 | SQLAlchemy = ">=1.3.0" 18 | typing-extensions = ">=4" 19 | 20 | [package.extras] 21 | tz = ["python-dateutil"] 22 | 23 | [[package]] 24 | name = "bidict" 25 | version = "0.22.1" 26 | description = "The bidirectional mapping library for Python." 27 | category = "main" 28 | optional = false 29 | python-versions = ">=3.7" 30 | files = [ 31 | {file = "bidict-0.22.1-py3-none-any.whl", hash = "sha256:6ef212238eb884b664f28da76f33f1d28b260f665fc737b413b287d5487d1e7b"}, 32 | {file = "bidict-0.22.1.tar.gz", hash = "sha256:1e0f7f74e4860e6d0943a05d4134c63a2fad86f3d4732fb265bd79e4e856d81d"}, 33 | ] 34 | 35 | [package.extras] 36 | docs = ["furo", "sphinx", "sphinx-copybutton"] 37 | lint = ["pre-commit"] 38 | test = ["hypothesis", "pytest", "pytest-benchmark[histogram]", "pytest-cov", "pytest-xdist", "sortedcollections", "sortedcontainers", "sphinx"] 39 | 40 | [[package]] 41 | name = "blinker" 42 | version = "1.6.2" 43 | description = "Fast, simple object-to-object and broadcast signaling" 44 | category = "main" 45 | optional = false 46 | python-versions = ">=3.7" 47 | files = [ 48 | {file = "blinker-1.6.2-py3-none-any.whl", hash = "sha256:c3d739772abb7bc2860abf5f2ec284223d9ad5c76da018234f6f50d6f31ab1f0"}, 49 | {file = "blinker-1.6.2.tar.gz", hash = "sha256:4afd3de66ef3a9f8067559fb7a1cbe555c17dcbe15971b05d1b625c3e7abe213"}, 50 | ] 51 | 52 | [[package]] 53 | name = "click" 54 | version = "8.1.3" 55 | description = "Composable command line interface toolkit" 56 | category = "main" 57 | optional = false 58 | python-versions = ">=3.7" 59 | files = [ 60 | {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, 61 | {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, 62 | ] 63 | 64 | [package.dependencies] 65 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 66 | 67 | [[package]] 68 | name = "colorama" 69 | version = "0.4.6" 70 | description = "Cross-platform colored terminal text." 71 | category = "main" 72 | optional = false 73 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 74 | files = [ 75 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 76 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 77 | ] 78 | 79 | [[package]] 80 | name = "dnspython" 81 | version = "2.3.0" 82 | description = "DNS toolkit" 83 | category = "main" 84 | optional = false 85 | python-versions = ">=3.7,<4.0" 86 | files = [ 87 | {file = "dnspython-2.3.0-py3-none-any.whl", hash = "sha256:89141536394f909066cabd112e3e1a37e4e654db00a25308b0f130bc3152eb46"}, 88 | {file = "dnspython-2.3.0.tar.gz", hash = "sha256:224e32b03eb46be70e12ef6d64e0be123a64e621ab4c0822ff6d450d52a540b9"}, 89 | ] 90 | 91 | [package.extras] 92 | curio = ["curio (>=1.2,<2.0)", "sniffio (>=1.1,<2.0)"] 93 | dnssec = ["cryptography (>=2.6,<40.0)"] 94 | doh = ["h2 (>=4.1.0)", "httpx (>=0.21.1)", "requests (>=2.23.0,<3.0.0)", "requests-toolbelt (>=0.9.1,<0.11.0)"] 95 | doq = ["aioquic (>=0.9.20)"] 96 | idna = ["idna (>=2.1,<4.0)"] 97 | trio = ["trio (>=0.14,<0.23)"] 98 | wmi = ["wmi (>=1.5.1,<2.0.0)"] 99 | 100 | [[package]] 101 | name = "eventlet" 102 | version = "0.33.3" 103 | description = "Highly concurrent networking library" 104 | category = "main" 105 | optional = false 106 | python-versions = "*" 107 | files = [ 108 | {file = "eventlet-0.33.3-py2.py3-none-any.whl", hash = "sha256:e43b9ae05ba4bb477a10307699c9aff7ff86121b2640f9184d29059f5a687df8"}, 109 | {file = "eventlet-0.33.3.tar.gz", hash = "sha256:722803e7eadff295347539da363d68ae155b8b26ae6a634474d0a920be73cfda"}, 110 | ] 111 | 112 | [package.dependencies] 113 | dnspython = ">=1.15.0" 114 | greenlet = ">=0.3" 115 | six = ">=1.10.0" 116 | 117 | [[package]] 118 | name = "flask" 119 | version = "2.3.2" 120 | description = "A simple framework for building complex web applications." 121 | category = "main" 122 | optional = false 123 | python-versions = ">=3.8" 124 | files = [ 125 | {file = "Flask-2.3.2-py3-none-any.whl", hash = "sha256:77fd4e1249d8c9923de34907236b747ced06e5467ecac1a7bb7115ae0e9670b0"}, 126 | {file = "Flask-2.3.2.tar.gz", hash = "sha256:8c2f9abd47a9e8df7f0c3f091ce9497d011dc3b31effcf4c85a6e2b50f4114ef"}, 127 | ] 128 | 129 | [package.dependencies] 130 | blinker = ">=1.6.2" 131 | click = ">=8.1.3" 132 | itsdangerous = ">=2.1.2" 133 | Jinja2 = ">=3.1.2" 134 | Werkzeug = ">=2.3.3" 135 | 136 | [package.extras] 137 | async = ["asgiref (>=3.2)"] 138 | dotenv = ["python-dotenv"] 139 | 140 | [[package]] 141 | name = "flask-migrate" 142 | version = "4.0.4" 143 | description = "SQLAlchemy database migrations for Flask applications using Alembic." 144 | category = "main" 145 | optional = false 146 | python-versions = ">=3.6" 147 | files = [ 148 | {file = "Flask-Migrate-4.0.4.tar.gz", hash = "sha256:73293d40b10ac17736e715b377e7b7bde474cb8105165d77474df4c3619b10b3"}, 149 | {file = "Flask_Migrate-4.0.4-py3-none-any.whl", hash = "sha256:77580f27ab39bc68be4906a43c56d7674b45075bc4f883b1d0b985db5164d58f"}, 150 | ] 151 | 152 | [package.dependencies] 153 | alembic = ">=1.9.0" 154 | Flask = ">=0.9" 155 | Flask-SQLAlchemy = ">=1.0" 156 | 157 | [[package]] 158 | name = "flask-socketio" 159 | version = "5.3.3" 160 | description = "Socket.IO integration for Flask applications" 161 | category = "main" 162 | optional = false 163 | python-versions = ">=3.6" 164 | files = [ 165 | {file = "Flask-SocketIO-5.3.3.tar.gz", hash = "sha256:8f47762dd1b76916cbc01f4f8661dd4670dbeb418ca0e1aaedab909b85efee5d"}, 166 | {file = "Flask_SocketIO-5.3.3-py3-none-any.whl", hash = "sha256:1f6a5afd68a4bf01140ef891c24f04ad0c52487d4829281a42eca0a35a204acf"}, 167 | ] 168 | 169 | [package.dependencies] 170 | Flask = ">=0.9" 171 | python-socketio = ">=5.0.2" 172 | 173 | [[package]] 174 | name = "flask-sqlalchemy" 175 | version = "3.0.3" 176 | description = "Add SQLAlchemy support to your Flask application." 177 | category = "main" 178 | optional = false 179 | python-versions = ">=3.7" 180 | files = [ 181 | {file = "Flask-SQLAlchemy-3.0.3.tar.gz", hash = "sha256:2764335f3c9d7ebdc9ed6044afaf98aae9fa50d7a074cef55dde307ec95903ec"}, 182 | {file = "Flask_SQLAlchemy-3.0.3-py3-none-any.whl", hash = "sha256:add5750b2f9cd10512995261ee2aa23fab85bd5626061aa3c564b33bb4aa780a"}, 183 | ] 184 | 185 | [package.dependencies] 186 | Flask = ">=2.2" 187 | SQLAlchemy = ">=1.4.18" 188 | 189 | [[package]] 190 | name = "greenlet" 191 | version = "2.0.2" 192 | description = "Lightweight in-process concurrent programming" 193 | category = "main" 194 | optional = false 195 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" 196 | files = [ 197 | {file = "greenlet-2.0.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:bdfea8c661e80d3c1c99ad7c3ff74e6e87184895bbaca6ee8cc61209f8b9b85d"}, 198 | {file = "greenlet-2.0.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9d14b83fab60d5e8abe587d51c75b252bcc21683f24699ada8fb275d7712f5a9"}, 199 | {file = "greenlet-2.0.2-cp27-cp27m-win32.whl", hash = "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74"}, 200 | {file = "greenlet-2.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343"}, 201 | {file = "greenlet-2.0.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae"}, 202 | {file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df"}, 203 | {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088"}, 204 | {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb"}, 205 | {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d75209eed723105f9596807495d58d10b3470fa6732dd6756595e89925ce2470"}, 206 | {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a51c9751078733d88e013587b108f1b7a1fb106d402fb390740f002b6f6551a"}, 207 | {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91"}, 208 | {file = "greenlet-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645"}, 209 | {file = "greenlet-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c"}, 210 | {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca"}, 211 | {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0"}, 212 | {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2"}, 213 | {file = "greenlet-2.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:eff4eb9b7eb3e4d0cae3d28c283dc16d9bed6b193c2e1ace3ed86ce48ea8df19"}, 214 | {file = "greenlet-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5454276c07d27a740c5892f4907c86327b632127dd9abec42ee62e12427ff7e3"}, 215 | {file = "greenlet-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:7cafd1208fdbe93b67c7086876f061f660cfddc44f404279c1585bbf3cdc64c5"}, 216 | {file = "greenlet-2.0.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:910841381caba4f744a44bf81bfd573c94e10b3045ee00de0cbf436fe50673a6"}, 217 | {file = "greenlet-2.0.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:18a7f18b82b52ee85322d7a7874e676f34ab319b9f8cce5de06067384aa8ff43"}, 218 | {file = "greenlet-2.0.2-cp35-cp35m-win32.whl", hash = "sha256:03a8f4f3430c3b3ff8d10a2a86028c660355ab637cee9333d63d66b56f09d52a"}, 219 | {file = "greenlet-2.0.2-cp35-cp35m-win_amd64.whl", hash = "sha256:4b58adb399c4d61d912c4c331984d60eb66565175cdf4a34792cd9600f21b394"}, 220 | {file = "greenlet-2.0.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:703f18f3fda276b9a916f0934d2fb6d989bf0b4fb5a64825260eb9bfd52d78f0"}, 221 | {file = "greenlet-2.0.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:32e5b64b148966d9cccc2c8d35a671409e45f195864560829f395a54226408d3"}, 222 | {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dd11f291565a81d71dab10b7033395b7a3a5456e637cf997a6f33ebdf06f8db"}, 223 | {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0f72c9ddb8cd28532185f54cc1453f2c16fb417a08b53a855c4e6a418edd099"}, 224 | {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd021c754b162c0fb55ad5d6b9d960db667faad0fa2ff25bb6e1301b0b6e6a75"}, 225 | {file = "greenlet-2.0.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:3c9b12575734155d0c09d6c3e10dbd81665d5c18e1a7c6597df72fd05990c8cf"}, 226 | {file = "greenlet-2.0.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b9ec052b06a0524f0e35bd8790686a1da006bd911dd1ef7d50b77bfbad74e292"}, 227 | {file = "greenlet-2.0.2-cp36-cp36m-win32.whl", hash = "sha256:dbfcfc0218093a19c252ca8eb9aee3d29cfdcb586df21049b9d777fd32c14fd9"}, 228 | {file = "greenlet-2.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:9f35ec95538f50292f6d8f2c9c9f8a3c6540bbfec21c9e5b4b751e0a7c20864f"}, 229 | {file = "greenlet-2.0.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:d5508f0b173e6aa47273bdc0a0b5ba055b59662ba7c7ee5119528f466585526b"}, 230 | {file = "greenlet-2.0.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:f82d4d717d8ef19188687aa32b8363e96062911e63ba22a0cff7802a8e58e5f1"}, 231 | {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9c59a2120b55788e800d82dfa99b9e156ff8f2227f07c5e3012a45a399620b7"}, 232 | {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2780572ec463d44c1d3ae850239508dbeb9fed38e294c68d19a24d925d9223ca"}, 233 | {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:937e9020b514ceedb9c830c55d5c9872abc90f4b5862f89c0887033ae33c6f73"}, 234 | {file = "greenlet-2.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:36abbf031e1c0f79dd5d596bfaf8e921c41df2bdf54ee1eed921ce1f52999a86"}, 235 | {file = "greenlet-2.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:18e98fb3de7dba1c0a852731c3070cf022d14f0d68b4c87a19cc1016f3bb8b33"}, 236 | {file = "greenlet-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7"}, 237 | {file = "greenlet-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3"}, 238 | {file = "greenlet-2.0.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30"}, 239 | {file = "greenlet-2.0.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b"}, 240 | {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526"}, 241 | {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b"}, 242 | {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acd2162a36d3de67ee896c43effcd5ee3de247eb00354db411feb025aa319857"}, 243 | {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0bf60faf0bc2468089bdc5edd10555bab6e85152191df713e2ab1fcc86382b5a"}, 244 | {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a"}, 245 | {file = "greenlet-2.0.2-cp38-cp38-win32.whl", hash = "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249"}, 246 | {file = "greenlet-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40"}, 247 | {file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8"}, 248 | {file = "greenlet-2.0.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6"}, 249 | {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df"}, 250 | {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be4ed120b52ae4d974aa40215fcdfde9194d63541c7ded40ee12eb4dda57b76b"}, 251 | {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94c817e84245513926588caf1152e3b559ff794d505555211ca041f032abbb6b"}, 252 | {file = "greenlet-2.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1a819eef4b0e0b96bb0d98d797bef17dc1b4a10e8d7446be32d1da33e095dbb8"}, 253 | {file = "greenlet-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7efde645ca1cc441d6dc4b48c0f7101e8d86b54c8530141b09fd31cef5149ec9"}, 254 | {file = "greenlet-2.0.2-cp39-cp39-win32.whl", hash = "sha256:ea9872c80c132f4663822dd2a08d404073a5a9b5ba6155bea72fb2a79d1093b5"}, 255 | {file = "greenlet-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:db1a39669102a1d8d12b57de2bb7e2ec9066a6f2b3da35ae511ff93b01b5d564"}, 256 | {file = "greenlet-2.0.2.tar.gz", hash = "sha256:e7c8dc13af7db097bed64a051d2dd49e9f0af495c26995c00a9ee842690d34c0"}, 257 | ] 258 | 259 | [package.extras] 260 | docs = ["Sphinx", "docutils (<0.18)"] 261 | test = ["objgraph", "psutil"] 262 | 263 | [[package]] 264 | name = "itsdangerous" 265 | version = "2.1.2" 266 | description = "Safely pass data to untrusted environments and back." 267 | category = "main" 268 | optional = false 269 | python-versions = ">=3.7" 270 | files = [ 271 | {file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"}, 272 | {file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"}, 273 | ] 274 | 275 | [[package]] 276 | name = "jinja2" 277 | version = "3.1.2" 278 | description = "A very fast and expressive template engine." 279 | category = "main" 280 | optional = false 281 | python-versions = ">=3.7" 282 | files = [ 283 | {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, 284 | {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, 285 | ] 286 | 287 | [package.dependencies] 288 | MarkupSafe = ">=2.0" 289 | 290 | [package.extras] 291 | i18n = ["Babel (>=2.7)"] 292 | 293 | [[package]] 294 | name = "mako" 295 | version = "1.2.4" 296 | description = "A super-fast templating language that borrows the best ideas from the existing templating languages." 297 | category = "main" 298 | optional = false 299 | python-versions = ">=3.7" 300 | files = [ 301 | {file = "Mako-1.2.4-py3-none-any.whl", hash = "sha256:c97c79c018b9165ac9922ae4f32da095ffd3c4e6872b45eded42926deea46818"}, 302 | {file = "Mako-1.2.4.tar.gz", hash = "sha256:d60a3903dc3bb01a18ad6a89cdbe2e4eadc69c0bc8ef1e3773ba53d44c3f7a34"}, 303 | ] 304 | 305 | [package.dependencies] 306 | MarkupSafe = ">=0.9.2" 307 | 308 | [package.extras] 309 | babel = ["Babel"] 310 | lingua = ["lingua"] 311 | testing = ["pytest"] 312 | 313 | [[package]] 314 | name = "markupsafe" 315 | version = "2.1.2" 316 | description = "Safely add untrusted strings to HTML/XML markup." 317 | category = "main" 318 | optional = false 319 | python-versions = ">=3.7" 320 | files = [ 321 | {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"}, 322 | {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"}, 323 | {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"}, 324 | {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"}, 325 | {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"}, 326 | {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"}, 327 | {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"}, 328 | {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"}, 329 | {file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"}, 330 | {file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"}, 331 | {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"}, 332 | {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"}, 333 | {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"}, 334 | {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"}, 335 | {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"}, 336 | {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"}, 337 | {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"}, 338 | {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"}, 339 | {file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"}, 340 | {file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"}, 341 | {file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"}, 342 | {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"}, 343 | {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"}, 344 | {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"}, 345 | {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"}, 346 | {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"}, 347 | {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"}, 348 | {file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"}, 349 | {file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"}, 350 | {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"}, 351 | {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"}, 352 | {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"}, 353 | {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"}, 354 | {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"}, 355 | {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"}, 356 | {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"}, 357 | {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"}, 358 | {file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"}, 359 | {file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"}, 360 | {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"}, 361 | {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"}, 362 | {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"}, 363 | {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"}, 364 | {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"}, 365 | {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"}, 366 | {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"}, 367 | {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"}, 368 | {file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"}, 369 | {file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"}, 370 | {file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"}, 371 | ] 372 | 373 | [[package]] 374 | name = "python-engineio" 375 | version = "4.4.0" 376 | description = "Engine.IO server and client for Python" 377 | category = "main" 378 | optional = false 379 | python-versions = ">=3.6" 380 | files = [ 381 | {file = "python-engineio-4.4.0.tar.gz", hash = "sha256:bcc035c70ecc30acc3cfd49ef19aca6c51fa6caaadd0fa58c2d7480f50d04cf2"}, 382 | {file = "python_engineio-4.4.0-py3-none-any.whl", hash = "sha256:11f9c35b775fe70e0a25f67b16d5b69fbfafc368cdd87eeb6f4135a475c88e50"}, 383 | ] 384 | 385 | [package.extras] 386 | asyncio-client = ["aiohttp (>=3.4)"] 387 | client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"] 388 | 389 | [[package]] 390 | name = "python-socketio" 391 | version = "5.8.0" 392 | description = "Socket.IO server and client for Python" 393 | category = "main" 394 | optional = false 395 | python-versions = ">=3.6" 396 | files = [ 397 | {file = "python-socketio-5.8.0.tar.gz", hash = "sha256:e714f4dddfaaa0cb0e37a1e2deef2bb60590a5b9fea9c343dd8ca5e688416fd9"}, 398 | {file = "python_socketio-5.8.0-py3-none-any.whl", hash = "sha256:7adb8867aac1c2929b9c1429f1c02e12ca4c36b67c807967393e367dfbb01441"}, 399 | ] 400 | 401 | [package.dependencies] 402 | bidict = ">=0.21.0" 403 | python-engineio = ">=4.3.0" 404 | 405 | [package.extras] 406 | asyncio-client = ["aiohttp (>=3.4)"] 407 | client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"] 408 | 409 | [[package]] 410 | name = "pyyaml" 411 | version = "6.0" 412 | description = "YAML parser and emitter for Python" 413 | category = "main" 414 | optional = false 415 | python-versions = ">=3.6" 416 | files = [ 417 | {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, 418 | {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, 419 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, 420 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, 421 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, 422 | {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, 423 | {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, 424 | {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, 425 | {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, 426 | {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, 427 | {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, 428 | {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, 429 | {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, 430 | {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, 431 | {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, 432 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, 433 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, 434 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, 435 | {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, 436 | {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, 437 | {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, 438 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, 439 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, 440 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, 441 | {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, 442 | {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, 443 | {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, 444 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, 445 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, 446 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, 447 | {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, 448 | {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, 449 | {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, 450 | {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, 451 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, 452 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, 453 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, 454 | {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, 455 | {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, 456 | {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, 457 | ] 458 | 459 | [[package]] 460 | name = "six" 461 | version = "1.16.0" 462 | description = "Python 2 and 3 compatibility utilities" 463 | category = "main" 464 | optional = false 465 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 466 | files = [ 467 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 468 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 469 | ] 470 | 471 | [[package]] 472 | name = "sqlalchemy" 473 | version = "2.0.9" 474 | description = "Database Abstraction Library" 475 | category = "main" 476 | optional = false 477 | python-versions = ">=3.7" 478 | files = [ 479 | {file = "SQLAlchemy-2.0.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:734805708632e3965c2c40081f9a59263c29ffa27cba9b02d4d92dfd57ba869f"}, 480 | {file = "SQLAlchemy-2.0.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8d3ece5960b3e821e43a4927cc851b6e84a431976d3ffe02aadb96519044807e"}, 481 | {file = "SQLAlchemy-2.0.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d118e233f416d713aac715e2c1101e17f91e696ff315fc9efbc75b70d11e740"}, 482 | {file = "SQLAlchemy-2.0.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f005245e1cb9b8ca53df73ee85e029ac43155e062405015e49ec6187a2e3fb44"}, 483 | {file = "SQLAlchemy-2.0.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:34eb96c1de91d8f31e988302243357bef3f7785e1b728c7d4b98bd0c117dafeb"}, 484 | {file = "SQLAlchemy-2.0.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7e472e9627882f2d75b87ff91c5a2bc45b31a226efc7cc0a054a94fffef85862"}, 485 | {file = "SQLAlchemy-2.0.9-cp310-cp310-win32.whl", hash = "sha256:0a865b5ec4ba24f57c33b633b728e43fde77b968911a6046443f581b25d29dd9"}, 486 | {file = "SQLAlchemy-2.0.9-cp310-cp310-win_amd64.whl", hash = "sha256:6e84ab63d25d8564d7a8c05dc080659931a459ee27f6ed1cf4c91f292d184038"}, 487 | {file = "SQLAlchemy-2.0.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:db4bd1c4792da753f914ff0b688086b9a8fd78bb9bc5ae8b6d2e65f176b81eb9"}, 488 | {file = "SQLAlchemy-2.0.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ad5363a1c65fde7b7466769d4261126d07d872fc2e816487ae6cec93da604b6b"}, 489 | {file = "SQLAlchemy-2.0.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebc4eeb1737a5a9bdb0c24f4c982319fa6edd23cdee27180978c29cbb026f2bd"}, 490 | {file = "SQLAlchemy-2.0.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbda1da8d541904ba262825a833c9f619e93cb3fd1156be0a5e43cd54d588dcd"}, 491 | {file = "SQLAlchemy-2.0.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d5327f54a9c39e7871fc532639616f3777304364a0bb9b89d6033ad34ef6c5f8"}, 492 | {file = "SQLAlchemy-2.0.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ac6a0311fb21a99855953f84c43fcff4bdca27a2ffcc4f4d806b26b54b5cddc9"}, 493 | {file = "SQLAlchemy-2.0.9-cp311-cp311-win32.whl", hash = "sha256:d209594e68bec103ad5243ecac1b40bf5770c9ebf482df7abf175748a34f4853"}, 494 | {file = "SQLAlchemy-2.0.9-cp311-cp311-win_amd64.whl", hash = "sha256:865392a50a721445156809c1a6d6ab6437be70c1c2599f591a8849ed95d3c693"}, 495 | {file = "SQLAlchemy-2.0.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0b49f1f71d7a44329a43d3edd38cc5ee4c058dfef4487498393d16172007954b"}, 496 | {file = "SQLAlchemy-2.0.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4a019f723b6c1e6b3781be00fb9e0844bc6156f9951c836ff60787cc3938d76"}, 497 | {file = "SQLAlchemy-2.0.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9838bd247ee42eb74193d865e48dd62eb50e45e3fdceb0fdef3351133ee53dcf"}, 498 | {file = "SQLAlchemy-2.0.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:78612edf4ba50d407d0eb3a64e9ec76e6efc2b5d9a5c63415d53e540266a230a"}, 499 | {file = "SQLAlchemy-2.0.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:f61ab84956dc628c8dfe9d105b6aec38afb96adae3e5e7da6085b583ff6ea789"}, 500 | {file = "SQLAlchemy-2.0.9-cp37-cp37m-win32.whl", hash = "sha256:07950fc82f844a2de67ddb4e535f29b65652b4d95e8b847823ce66a6d540a41d"}, 501 | {file = "SQLAlchemy-2.0.9-cp37-cp37m-win_amd64.whl", hash = "sha256:e62c4e762d6fd2901692a093f208a6a6575b930e9458ad58c2a7f080dd6132da"}, 502 | {file = "SQLAlchemy-2.0.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b3e5864eba71a3718236a120547e52c8da2ccb57cc96cecd0480106a0c799c92"}, 503 | {file = "SQLAlchemy-2.0.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1d06e119cf79a3d80ab069f064a07152eb9ba541d084bdaee728d8a6f03fd03d"}, 504 | {file = "SQLAlchemy-2.0.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee2946042cc7851842d7a086a92b9b7b494cbe8c3e7e4627e27bc912d3a7655e"}, 505 | {file = "SQLAlchemy-2.0.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13f984a190d249769a050634b248aef8991acc035e849d02b634ea006c028fa8"}, 506 | {file = "SQLAlchemy-2.0.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e4780be0f19e5894c17f75fc8de2fe1ae233ab37827125239ceb593c6f6bd1e2"}, 507 | {file = "SQLAlchemy-2.0.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:68ed381bc340b4a3d373dbfec1a8b971f6350139590c4ca3cb722fdb50035777"}, 508 | {file = "SQLAlchemy-2.0.9-cp38-cp38-win32.whl", hash = "sha256:aa5c270ece17c0c0e0a38f2530c16b20ea05d8b794e46c79171a86b93b758891"}, 509 | {file = "SQLAlchemy-2.0.9-cp38-cp38-win_amd64.whl", hash = "sha256:1b69666e25cc03c602d9d3d460e1281810109e6546739187044fc256c67941ef"}, 510 | {file = "SQLAlchemy-2.0.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c6e27189ff9aebfb2c02fd252c629ea58657e7a5ff1a321b7fc9c2bf6dc0b5f3"}, 511 | {file = "SQLAlchemy-2.0.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8239ce63a90007bce479adf5460d48c1adae4b933d8e39a4eafecfc084e503c"}, 512 | {file = "SQLAlchemy-2.0.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f759eccb66e6d495fb622eb7f4ac146ae674d829942ec18b7f5a35ddf029597"}, 513 | {file = "SQLAlchemy-2.0.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246712af9fc761d6c13f4f065470982e175d902e77aa4218c9cb9fc9ff565a0c"}, 514 | {file = "SQLAlchemy-2.0.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6b72dccc5864ea95c93e0a9c4e397708917fb450f96737b4a8395d009f90b868"}, 515 | {file = "SQLAlchemy-2.0.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:93c78d42c14aa9a9e0866eacd5b48df40a50d0e2790ee377af7910d224afddcf"}, 516 | {file = "SQLAlchemy-2.0.9-cp39-cp39-win32.whl", hash = "sha256:f49c5d3c070a72ecb96df703966c9678dda0d4cb2e2736f88d15f5e1203b4159"}, 517 | {file = "SQLAlchemy-2.0.9-cp39-cp39-win_amd64.whl", hash = "sha256:4c3020afb144572c7bfcba9d7cce57ad42bff6e6115dffcfe2d4ae6d444a214f"}, 518 | {file = "SQLAlchemy-2.0.9-py3-none-any.whl", hash = "sha256:e730603cae5747bc6d6dece98b45a57d647ed553c8d5ecef602697b1c1501cf2"}, 519 | {file = "SQLAlchemy-2.0.9.tar.gz", hash = "sha256:95719215e3ec7337b9f57c3c2eda0e6a7619be194a5166c07c1e599f6afc20fa"}, 520 | ] 521 | 522 | [package.dependencies] 523 | greenlet = {version = "!=0.4.17", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""} 524 | typing-extensions = ">=4.2.0" 525 | 526 | [package.extras] 527 | aiomysql = ["aiomysql", "greenlet (!=0.4.17)"] 528 | aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing-extensions (!=3.10.0.1)"] 529 | asyncio = ["greenlet (!=0.4.17)"] 530 | asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] 531 | mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"] 532 | mssql = ["pyodbc"] 533 | mssql-pymssql = ["pymssql"] 534 | mssql-pyodbc = ["pyodbc"] 535 | mypy = ["mypy (>=0.910)"] 536 | mysql = ["mysqlclient (>=1.4.0)"] 537 | mysql-connector = ["mysql-connector-python"] 538 | oracle = ["cx-oracle (>=7)"] 539 | oracle-oracledb = ["oracledb (>=1.0.1)"] 540 | postgresql = ["psycopg2 (>=2.7)"] 541 | postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] 542 | postgresql-pg8000 = ["pg8000 (>=1.29.1)"] 543 | postgresql-psycopg = ["psycopg (>=3.0.7)"] 544 | postgresql-psycopg2binary = ["psycopg2-binary"] 545 | postgresql-psycopg2cffi = ["psycopg2cffi"] 546 | pymysql = ["pymysql"] 547 | sqlcipher = ["sqlcipher3-binary"] 548 | 549 | [[package]] 550 | name = "typing-extensions" 551 | version = "4.5.0" 552 | description = "Backported and Experimental Type Hints for Python 3.7+" 553 | category = "main" 554 | optional = false 555 | python-versions = ">=3.7" 556 | files = [ 557 | {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, 558 | {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, 559 | ] 560 | 561 | [[package]] 562 | name = "werkzeug" 563 | version = "2.3.3" 564 | description = "The comprehensive WSGI web application library." 565 | category = "main" 566 | optional = false 567 | python-versions = ">=3.8" 568 | files = [ 569 | {file = "Werkzeug-2.3.3-py3-none-any.whl", hash = "sha256:4866679a0722de00796a74086238bb3b98d90f423f05de039abb09315487254a"}, 570 | {file = "Werkzeug-2.3.3.tar.gz", hash = "sha256:a987caf1092edc7523edb139edb20c70571c4a8d5eed02e0b547b4739174d091"}, 571 | ] 572 | 573 | [package.dependencies] 574 | MarkupSafe = ">=2.1.1" 575 | 576 | [package.extras] 577 | watchdog = ["watchdog (>=2.3)"] 578 | 579 | [metadata] 580 | lock-version = "2.0" 581 | python-versions = "^3.10" 582 | content-hash = "2987d447ba8ea868630c996669c6d9154970c3eb0d87b0251879ed4d40507442" 583 | --------------------------------------------------------------------------------