From 10170c493b9d7306651caed9067ac21651c79373 Mon Sep 17 00:00:00 2001 From: Alex Warren Date: Tue, 12 Nov 2019 12:04:54 -0800 Subject: [PATCH 01/14] Add schemas for Sequences and SequenceResponces --- api/models.py | 68 ++++++++++++++++++++++++++++++ api/resources/__init__.py | 1 + api/resources/response.py | 1 - api/resources/sequence.py | 39 +++++++++++++++++ api/resources/sequence_response.py | 39 +++++++++++++++++ 5 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 api/resources/sequence.py create mode 100644 api/resources/sequence_response.py diff --git a/api/models.py b/api/models.py index f0b7b6f..ba4f99d 100644 --- a/api/models.py +++ b/api/models.py @@ -296,6 +296,14 @@ class Response(BaseModel): UUID(as_uuid=True), db.ForeignKey("challenges.id"), primary_key=True ) + sequence_response = db.relationship("SequenceResponse", backref="responses") + sequence_response_id = db.Column( + UUID(as_uuid=True), + db.ForeignKey("sequence_responses.id", name="responses_sequence_responses_id_fkey"), + primary_key=True, + nullable=True, + ) + user = db.relationship("User", backref="responses") user_id = db.Column(UUID(as_uuid=True), db.ForeignKey("users.id"), primary_key=True) @@ -311,3 +319,63 @@ class Response(BaseModel): onupdate=datetime.utcnow, nullable=False, ) + + +class Sequence(BaseModel): + __tablename__ = "sequences" + + id = db.Column( + UUID(as_uuid=True), + server_default=sqlalchemy.text("gen_random_uuid()"), + primary_key=True, + ) + + title = db.Column(db.Unicode(length=255), nullable=False) + items_json = db.Column(db.UnicodeText, nullable=False) + items_length = db.Column(db.Integer, nullable=False) + + created_at = db.Column( + db.DateTime(timezone=True), server_default=func.now(), nullable=False + ) + updated_at = db.Column( + db.DateTime(timezone=True), + server_default=func.now(), + onupdate=datetime.utcnow, + nullable=False, + ) + + +class SequenceResponse(BaseModel): + __tablename__ = "sequence_responses" + + id = db.Column( + UUID(as_uuid=True), + server_default=sqlalchemy.text("gen_random_uuid()"), + primary_key=True, + unique=True, # Needed to add this so that responses+ this fkey didn't error. + ) + + user = db.relationship("User", backref="sequence_responses") + user_id = db.Column(UUID(as_uuid=True), db.ForeignKey("users.id"), primary_key=True) + + sequence = db.relationship("Sequence", backref="sequence_responses") + sequence_id = db.Column( + UUID(as_uuid=True), db.ForeignKey("sequences.id"), primary_key=True + ) + + hidden_from_respondent = db.Column(db.Boolean, default=False, nullable=False) + + started_at = db.Column(db.DateTime(timezone=True), nullable=True) + + items_finished = db.Column(db.Integer, default=0, nullable=False) + + created_at = db.Column( + db.DateTime(timezone=True), server_default=func.now(), nullable=False + ) + + updated_at = db.Column( + db.DateTime(timezone=True), + server_default=func.now(), + onupdate=datetime.utcnow, + nullable=False, + ) diff --git a/api/resources/__init__.py b/api/resources/__init__.py index 5bc85bd..885923c 100644 --- a/api/resources/__init__.py +++ b/api/resources/__init__.py @@ -2,3 +2,4 @@ from .video import * from .challenge import * from .response import * +from .sequence import * diff --git a/api/resources/response.py b/api/resources/response.py index 738d3e3..5081f99 100644 --- a/api/resources/response.py +++ b/api/resources/response.py @@ -1,7 +1,6 @@ from flask import request, abort from flask_restful import Resource from flask_jwt_extended import jwt_required, get_jwt_identity -from sqlalchemy.orm import joinedload import api.models as m import api.schemas as s diff --git a/api/resources/sequence.py b/api/resources/sequence.py new file mode 100644 index 0000000..84e36de --- /dev/null +++ b/api/resources/sequence.py @@ -0,0 +1,39 @@ +from flask import request, abort +from flask_restful import Resource +from flask_jwt_extended import jwt_required + +import api.models as m +import api.schemas as s + + +class Sequence(Resource): + @jwt_required + def get(self, sequence_id): + sequence = m.Sequence.query.get_or_404(sequence_id) + + # TODO: Get all the videos and challenges that are required + + return s.SequenceSchema().jsonify(sequence).json, 200 + + +class SequenceList(Resource): + @jwt_required + def get(self): + sequences = m.Sequence.all() + + return s.SequenceSchema(many=True).jsonify(sequences).json, 200 + + +class CreateSequence(Resource): + @jwt_required + def post(self): + try: + title = request.form["title"] + items_json = request.form["itemsJson"] + except (KeyError, AttributeError) as e: + print("Request missing values") + abort(400) + + sequence = m.Sequence.create(title=title, items_json=user) + + return s.SequenceSchema().jsonify(sequence).json, 201 diff --git a/api/resources/sequence_response.py b/api/resources/sequence_response.py new file mode 100644 index 0000000..57d61ae --- /dev/null +++ b/api/resources/sequence_response.py @@ -0,0 +1,39 @@ +from flask import request, abort +from flask_restful import Resource +from flask_jwt_extended import jwt_required, get_jwt_identity + +import api.models as m +import api.schemas as s + + +class SequenceResponse(Resource): + @jwt_required + def get(self, sequence_response_id): + sequence_response = m.SequenceResponse.query.get_or_404(sequence_response_id) + + # TODO: Get all the responses that are required + + return s.SequenceResponseSchema().jsonify(sequence_response).json, 200 + + +class SequenceResponseList(Resource): + @jwt_required + def get(self): + sequence_responses = m.SequenceResponse.all() + + return s.SequenceResponseSchema(many=True).jsonify(sequence_responses).json, 200 + + +class CreateSequenceResponse(Resource): + @jwt_required + def post(self): + user = m.User.query.get(get_jwt_identity()) + try: + sequence_id = request.form["sequenceId"] + except (KeyError, AttributeError) as e: + print("Request sequence_id") + abort(400) + + sequence_response = m.SequenceResponse.create(sequence_id=sequence_id) + + return s.SequenceResponseSchema().jsonify(sequence_response).json, 201 From d5ca3cfec619a7c0d0d01b3876751b5dfbc15542 Mon Sep 17 00:00:00 2001 From: Alex Warren Date: Tue, 12 Nov 2019 23:24:10 -0800 Subject: [PATCH 02/14] Finish endpoints for sequence response and sequence --- api/auth.py | 38 ++---- api/models.py | 56 ++++++-- api/resources/__init__.py | 1 + api/resources/response.py | 12 +- api/resources/sequence.py | 43 +++++- api/resources/sequence_response.py | 96 +++++++++++-- api/routes.py | 14 ++ api/schemas.py | 64 +++++++-- ...38c0cb_add_sequences_sequence_responses.py | 127 ++++++++++++++++++ 9 files changed, 384 insertions(+), 67 deletions(-) create mode 100644 migrations/versions/a1a16038c0cb_add_sequences_sequence_responses.py diff --git a/api/auth.py b/api/auth.py index 2692eba..3c48b74 100644 --- a/api/auth.py +++ b/api/auth.py @@ -1,5 +1,5 @@ from functools import wraps -from flask import request, jsonify +from flask import request, jsonify, abort from flask_jwt_extended import verify_jwt_in_request, get_jwt_claims @@ -16,6 +16,12 @@ def is_admin(): return get_role() == "admin" or is_super_admin() +def abortUnauthorized(): + abort( + 403, "You are not authorized to access this resource", error="UnauthorizedError" + ) + + def super_admin_required(fn): @wraps(fn) def wrapper(*args, **kwargs): @@ -24,15 +30,7 @@ def wrapper(*args, **kwargs): if is_super_admin(): return fn(*args, **kwargs) else: - return ( - jsonify( - { - "error": "UnauthorizedError", - "message": "You are not authorized to access this resource", - } - ), - 403, - ) + abortUnauthorized() return wrapper @@ -45,15 +43,7 @@ def wrapper(*args, **kwargs): if get_role() in set(["super_admin", "admin"]): return fn(*args, **kwargs) else: - return ( - jsonify( - { - "error": "UnauthorizedError", - "message": "You are not authorized to access this resource", - } - ), - 403, - ) + abortUnauthorized() return wrapper @@ -66,14 +56,6 @@ def wrapper(*args, **kwargs): if get_role() in set(["super_admin", "admin", "user"]): return fn(*args, **kwargs) else: - return ( - jsonify( - { - "error": "UnauthorizedError", - "message": "You are not authorized to access this resource", - } - ), - 403, - ) + abortUnauthorized() return wrapper diff --git a/api/models.py b/api/models.py index ba4f99d..f83e13f 100644 --- a/api/models.py +++ b/api/models.py @@ -13,7 +13,7 @@ import sqlalchemy from sqlalchemy.exc import DatabaseError from sqlalchemy.ext.hybrid import hybrid_property -from sqlalchemy.orm import backref +from sqlalchemy.orm import backref, relationship from sqlalchemy.sql import func from sqlalchemy.dialects.postgresql import UUID @@ -282,6 +282,16 @@ class BlacklistedToken(BaseModel): exp = db.Column(db.DateTime(timezone=True), nullable=False) +sequence_response_response_association_table = db.Table( + "sequence_response_response_association", + BaseModel.metadata, + db.Column( + "sequence_responses_id", UUID(as_uuid=True), db.ForeignKey("sequence_responses.id") + ), + db.Column("response_id", UUID(as_uuid=True), db.ForeignKey("responses.id")), +) + + class Response(BaseModel): __tablename__ = "responses" @@ -289,6 +299,7 @@ class Response(BaseModel): UUID(as_uuid=True), server_default=sqlalchemy.text("gen_random_uuid()"), primary_key=True, + unique=True, ) challenge = db.relationship("Challenge", backref="responses") @@ -296,20 +307,18 @@ class Response(BaseModel): UUID(as_uuid=True), db.ForeignKey("challenges.id"), primary_key=True ) - sequence_response = db.relationship("SequenceResponse", backref="responses") - sequence_response_id = db.Column( - UUID(as_uuid=True), - db.ForeignKey("sequence_responses.id", name="responses_sequence_responses_id_fkey"), - primary_key=True, - nullable=True, - ) - user = db.relationship("User", backref="responses") user_id = db.Column(UUID(as_uuid=True), db.ForeignKey("users.id"), primary_key=True) video = db.relationship("Video", backref=backref("response", uselist=False)) video_id = db.Column(UUID(as_uuid=True), db.ForeignKey("videos.id"), nullable=True) + hidden = db.Column(db.Boolean, default=False, nullable=False) + + sequence_responses = relationship( + "SequenceResponse", secondary=sequence_response_response_association_table + ) + created_at = db.Column( db.DateTime(timezone=True), server_default=func.now(), nullable=False ) @@ -321,6 +330,21 @@ class Response(BaseModel): ) +sequence_challenges_association_table = db.Table( + "sequence_challenges_association", + BaseModel.metadata, + db.Column("sequence_id", UUID(as_uuid=True), db.ForeignKey("sequences.id")), + db.Column("challenge_id", UUID(as_uuid=True), db.ForeignKey("challenges.id")), +) + +sequence_videos_association_table = db.Table( + "sequence_videos_association", + BaseModel.metadata, + db.Column("sequence_id", UUID(as_uuid=True), db.ForeignKey("sequences.id")), + db.Column("video_id", UUID(as_uuid=True), db.ForeignKey("videos.id")), +) + + class Sequence(BaseModel): __tablename__ = "sequences" @@ -334,6 +358,12 @@ class Sequence(BaseModel): items_json = db.Column(db.UnicodeText, nullable=False) items_length = db.Column(db.Integer, nullable=False) + challenges = relationship( + "Challenge", secondary=sequence_challenges_association_table + ) + + videos = relationship("Video", secondary=sequence_videos_association_table) + created_at = db.Column( db.DateTime(timezone=True), server_default=func.now(), nullable=False ) @@ -363,11 +393,15 @@ class SequenceResponse(BaseModel): UUID(as_uuid=True), db.ForeignKey("sequences.id"), primary_key=True ) - hidden_from_respondent = db.Column(db.Boolean, default=False, nullable=False) + responses = relationship( + "Response", secondary=sequence_response_response_association_table + ) + + hide_responses = db.Column(db.Boolean, default=False, nullable=False) started_at = db.Column(db.DateTime(timezone=True), nullable=True) - items_finished = db.Column(db.Integer, default=0, nullable=False) + finished = db.Column(db.Boolean, default=False, nullable=False) created_at = db.Column( db.DateTime(timezone=True), server_default=func.now(), nullable=False diff --git a/api/resources/__init__.py b/api/resources/__init__.py index 885923c..6813de7 100644 --- a/api/resources/__init__.py +++ b/api/resources/__init__.py @@ -3,3 +3,4 @@ from .challenge import * from .response import * from .sequence import * +from .sequence_response import * diff --git a/api/resources/response.py b/api/resources/response.py index 5081f99..95bc3fe 100644 --- a/api/resources/response.py +++ b/api/resources/response.py @@ -4,7 +4,7 @@ import api.models as m import api.schemas as s -from api.auth import is_admin +from api.auth import is_admin, abortUnauthorized class Response(Resource): @@ -12,6 +12,13 @@ class Response(Resource): def get(self, response_id): response = m.Response.query.get_or_404(response_id) + if response.sequence_response is not None: + if response.sequence_response.hidden_from_respondent and not is_admin(): + abortUnauthorized() + + if not is_admin() and response.user_id is not get_jwt_identity(): + abortUnauthorized() + return s.ResponseSchema().jsonify(response).json, 200 @@ -34,8 +41,7 @@ def post(self): challenge_id = request.form["challengeId"] video_blob = request.files["videoBlob"] except (KeyError, AttributeError) as e: - print("Request missing values") - abort(400) + abort(400, "Request missing values") video = m.Video().create_and_upload(video_blob) challenge = m.Challenge.query.get_or_404(challenge_id) diff --git a/api/resources/sequence.py b/api/resources/sequence.py index 84e36de..bc2e6cc 100644 --- a/api/resources/sequence.py +++ b/api/resources/sequence.py @@ -1,9 +1,12 @@ from flask import request, abort from flask_restful import Resource from flask_jwt_extended import jwt_required +from sqlalchemy.exc import DataError +import json import api.models as m import api.schemas as s +from api.auth import is_admin class Sequence(Resource): @@ -11,9 +14,10 @@ class Sequence(Resource): def get(self, sequence_id): sequence = m.Sequence.query.get_or_404(sequence_id) - # TODO: Get all the videos and challenges that are required - - return s.SequenceSchema().jsonify(sequence).json, 200 + if is_admin(): + return s.SequenceSchema().jsonify(sequence).json, 200 + else: + return s.SequenceHiddenResponsesSchema().jsonify(sequence).json, 200 class SequenceList(Resource): @@ -21,7 +25,10 @@ class SequenceList(Resource): def get(self): sequences = m.Sequence.all() - return s.SequenceSchema(many=True).jsonify(sequences).json, 200 + if is_admin(): + return s.SequenceSchema(many=True).jsonify(sequences).json, 200 + else: + return s.SequenceHiddenResponsesSchema(many=True).jsonify(sequences).json, 200 class CreateSequence(Resource): @@ -31,9 +38,31 @@ def post(self): title = request.form["title"] items_json = request.form["itemsJson"] except (KeyError, AttributeError) as e: - print("Request missing values") - abort(400) + print(e) + abort(402, "Request missing values") + + try: + items = json.loads(items_json) + except json.JSONDecodeError as e: + abort(400, "invalid json") + + try: + videos = [] + challenges = [] + for item in items: + if item.get("type") == "video": + videos.append(m.Video.query.get(item["video_id"])) + elif item.get("type") == "challenge": + challenges.append(m.Challenge.query.get(item["challenge_id"])) + except DataError: + abort(400, "Invalid challenge or video id") - sequence = m.Sequence.create(title=title, items_json=user) + sequence = m.Sequence.create( + title=title, + items_json=items_json, + items_length=len(items), + videos=videos, + challenges=challenges, + ) return s.SequenceSchema().jsonify(sequence).json, 201 diff --git a/api/resources/sequence_response.py b/api/resources/sequence_response.py index 57d61ae..bd58af6 100644 --- a/api/resources/sequence_response.py +++ b/api/resources/sequence_response.py @@ -4,6 +4,7 @@ import api.models as m import api.schemas as s +from api.auth import is_admin class SequenceResponse(Resource): @@ -11,29 +12,104 @@ class SequenceResponse(Resource): def get(self, sequence_response_id): sequence_response = m.SequenceResponse.query.get_or_404(sequence_response_id) - # TODO: Get all the responses that are required - - return s.SequenceResponseSchema().jsonify(sequence_response).json, 200 + if s.sequence_response.hide_responses and not is_admin(): + return ( + s.SequenceResponseHiddenResponsesSchema().jsonify(sequence_response).json, + 200, + ) + else: + return s.SequenceResponseSchema().jsonify(sequence_response).json, 200 class SequenceResponseList(Resource): @jwt_required def get(self): - sequence_responses = m.SequenceResponse.all() + if is_admin(): + sequence_responses = m.SequenceResponse.all() + return s.SequenceResponseSchema(many=True).jsonify(sequence_responses).json, 200 + else: + sequence_responses = m.SequenceResponse.where(user_id=get_jwt_identity()).all() + return ( + s.SequenceResponseHiddenResponsesSchema(many=True) + .jsonify(sequence_responses) + .json, + 200, + ) + + +class CreateSequenceResponseInvite(Resource): + @jwt_required + def post(self): + try: + email = request.form["email"] + full_name = request.form["fullName"] + sequence_id = request.form["sequenceId"] + hide_responses = request.form["hideResponses"] + except (KeyError, AttributeError) as e: + abort(400, "Missing expected form values") + + user = m.User.query.filter_by(email=email).one_or_none() + + if user is None: + # TODO create a good fake password + user = m.User.create(full_name=full_name, email=email, password="temp_password") + + sequence_response = m.SequenceResponse.create( + sequence_id=sequence_id, hide_responses=hide_responses, user=user + ) + + # TODO: Send an email invite? - return s.SequenceResponseSchema(many=True).jsonify(sequence_responses).json, 200 + return ( + s.SequenceResponseHiddenResponsesSchema().jsonify(sequence_response).json, + 201, + ) -class CreateSequenceResponse(Resource): +class StartSequenceResponse(Resource): @jwt_required def post(self): user = m.User.query.get(get_jwt_identity()) + try: sequence_id = request.form["sequenceId"] except (KeyError, AttributeError) as e: - print("Request sequence_id") - abort(400) + abort(400, "Missing expected form values") + + sequence_response = m.SequenceResponse.create( + sequence_id=sequence_id, hide_responses=False, user=user + ) + + return ( + s.SequenceResponseHiddenResponsesSchema().jsonify(sequence_response).json, + 201, + ) + + +class RespondToSequenceResponse(Resource): + @jwt_required + def post(self): + user = m.User.query.get(get_jwt_identity()) + try: + challenge_id = request.form["challengeId"] + items_finished = request.form["itemsFinished"] + sequence_response_id = request.form["sequenceResponseId"] + video_blob = request.files["videoBlob"] + except (KeyError, AttributeError) as e: + abort(400, "Request missing values") + + video = m.Video().create_and_upload(video_blob) + challenge = m.Challenge.query.get_or_404(challenge_id) + sequence_response = m.Challenge.query.get_or_404(sequence_response_id) + + response = m.Response.create( + challenge=challenge, + user=user, + video=video, + sequence_responses=[sequence_response], + ) - sequence_response = m.SequenceResponse.create(sequence_id=sequence_id) + finished = items_finished >= sequence_response.items_finished - 1 + sequence_response.update(items_finished=items_finished, finished=finished) - return s.SequenceResponseSchema().jsonify(sequence_response).json, 201 + return "Ok", 201 diff --git a/api/routes.py b/api/routes.py index 42f654e..5417386 100644 --- a/api/routes.py +++ b/api/routes.py @@ -16,6 +16,20 @@ api.add_resource(resources.CreateResponse, "/api/responses/create") api.add_resource(resources.Response, "/api/responses/") +api.add_resource(resources.SequenceList, "/api/sequences") +api.add_resource(resources.CreateSequence, "/api/sequences/create") +api.add_resource(resources.Sequence, "/api/sequences/") + +api.add_resource(resources.SequenceResponseList, "/api/sequenceresponses") +api.add_resource( + resources.CreateSequenceResponseInvite, "/api/sequenceresponses/invite" +) +api.add_resource(resources.StartSequenceResponse, "/api/sequenceresponses/start") +api.add_resource(resources.RespondToSequenceResponse, "/api/sequenceresponses/respond") +api.add_resource( + resources.SequenceResponse, "/api/sequenceresponses/" +) + api.add_resource(resources.UserRegister, "/api/auth/register") api.add_resource(resources.UserLogin, "/api/auth/login") api.add_resource(resources.UserLogout, "/api/auth/logout") diff --git a/api/schemas.py b/api/schemas.py index 6384b79..a723945 100644 --- a/api/schemas.py +++ b/api/schemas.py @@ -24,7 +24,7 @@ class Meta: model = models.Response exclude = ["challenges"] # Prevent circular serialization sqla_session = db.session - + video = fields.Nested(VideoSchema) user = fields.Nested(UserSchema) @@ -40,17 +40,65 @@ class Meta: responses = fields.Nested(ResponseWithoutChallenges, many=True) +class ChallengeSchemaWithoutResponses(ChallengeSchema): + class Meta: + model = models.Challenge + exclude = ["responses"] # Prevent circular serialization + sqla_session = db.session + + class ResponseSchema(ModelSchema): class Meta: model = models.Response sqla_session = db.session - class ChallengeWithoutResponses(ChallengeSchema): - class Meta: - model = models.Challenge - exclude = ["responses"] # Prevent circular serialization - sqla_session = db.session - - challenge = fields.Nested(ChallengeWithoutResponses) + challenge = fields.Nested(ChallengeSchemaWithoutResponses) video = fields.Nested(VideoSchema) user = fields.Nested(UserSchema) + + +class SequenceResponseWithoutSequenceSchema(ModelSchema): + class Meta: + model = models.SequenceResponse + exclude = ["sequence"] + sqla_session = db.session + + responses = fields.List(fields.Nested(ResponseSchema)) + + +class SequenceSchema(ModelSchema): + class Meta: + model = models.Sequence + sqla_session = db.session + + challenges = fields.List(fields.Nested(ChallengeSchemaWithoutResponses)) + videos = fields.List(fields.Nested(VideoSchema)) + responses = fields.List(fields.Nested(SequenceResponseWithoutSequenceSchema)) + + +class SequenceHiddenResponsesSchema(ModelSchema): + class Meta: + model = models.Sequence + exclude = ["sequence_responses"] + sqla_session = db.session + + challenges = fields.List(fields.Nested(ChallengeSchemaWithoutResponses)) + videos = fields.List(fields.Nested(VideoSchema)) + + +class SequenceResponseSchema(ModelSchema): + class Meta: + model = models.SequenceResponse + sqla_session = db.session + + sequence = fields.Nested(SequenceSchema) + responses = fields.List(fields.Nested(ResponseSchema)) + + +class SequenceResponseHiddenResponsesSchema(ModelSchema): + class Meta: + model = models.SequenceResponse + exclude = ["responses"] + sqla_session = db.session + + sequence = fields.Nested(SequenceHiddenResponsesSchema) diff --git a/migrations/versions/a1a16038c0cb_add_sequences_sequence_responses.py b/migrations/versions/a1a16038c0cb_add_sequences_sequence_responses.py new file mode 100644 index 0000000..3d28ff1 --- /dev/null +++ b/migrations/versions/a1a16038c0cb_add_sequences_sequence_responses.py @@ -0,0 +1,127 @@ +"""add sequences sequence_responses + +Revision ID: a1a16038c0cb +Revises: 27b8286ec905 +Create Date: 2019-11-13 06:14:56.589100 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "a1a16038c0cb" +down_revision = "27b8286ec905" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_unique_constraint(None, "responses", ["id"]) + + op.create_table( + "sequences", + sa.Column( + "id", + postgresql.UUID(as_uuid=True), + server_default=sa.text("gen_random_uuid()"), + nullable=False, + ), + sa.Column("title", sa.Unicode(length=255), nullable=False), + sa.Column("items_json", sa.UnicodeText(), nullable=False), + sa.Column("items_length", sa.Integer(), nullable=False), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "sequence_videos_association", + sa.Column("sequence_id", postgresql.UUID(as_uuid=True), nullable=True), + sa.Column("video_id", postgresql.UUID(as_uuid=True), nullable=True), + sa.ForeignKeyConstraint(["sequence_id"], ["sequences.id"]), + sa.ForeignKeyConstraint(["video_id"], ["videos.id"]), + ) + op.create_table( + "sequence_responses", + sa.Column( + "id", + postgresql.UUID(as_uuid=True), + server_default=sa.text("gen_random_uuid()"), + nullable=False, + ), + sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=False), + sa.Column("sequence_id", postgresql.UUID(as_uuid=True), nullable=False), + sa.Column("hide_responses", sa.Boolean(), nullable=False), + sa.Column("started_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("items_finished", sa.Integer(), nullable=False), + sa.Column("finished", sa.Boolean(), nullable=False), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ), + sa.ForeignKeyConstraint(["sequence_id"], ["sequences.id"]), + sa.ForeignKeyConstraint(["user_id"], ["users.id"]), + sa.PrimaryKeyConstraint("id", "user_id", "sequence_id"), + sa.UniqueConstraint("id"), + ) + op.create_table( + "sequence_challenges_association", + sa.Column("sequence_id", postgresql.UUID(as_uuid=True), nullable=True), + sa.Column("challenge_id", postgresql.UUID(as_uuid=True), nullable=True), + sa.ForeignKeyConstraint(["challenge_id"], ["challenges.id"]), + sa.ForeignKeyConstraint(["sequence_id"], ["sequences.id"]), + ) + op.create_table( + "sequence_response_response_association", + sa.Column("sequence_responses_id", postgresql.UUID(as_uuid=True), nullable=True), + sa.Column("response_id", postgresql.UUID(as_uuid=True), nullable=True), + sa.ForeignKeyConstraint(["response_id"], ["responses.id"]), + sa.ForeignKeyConstraint(["sequence_responses_id"], ["sequence_responses.id"]), + ) + op.add_column("responses", sa.Column("hidden", sa.Boolean(), nullable=True)) + + # Added these to allow non-nullable + op.execute("UPDATE responses SET hidden = false") + op.alter_column("responses", "hidden", nullable=False) + + op.drop_column("responses", "sequence_response_id") + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "responses", + sa.Column( + "sequence_response_id", postgresql.UUID(), autoincrement=False, nullable=True + ), + ) + op.drop_column("responses", "hidden") + op.drop_table("sequence_response_response_association") + op.drop_table("sequence_challenges_association") + op.drop_table("sequence_responses") + op.drop_table("sequence_videos_association") + op.drop_table("sequences") + # This drop may not work... + op.drop_constraint("responses_id_key", "responses", type_="unique") + # ### end Alembic commands ### From 844c395c0d27c4c56e4e41cce15cbcc5302a0f12 Mon Sep 17 00:00:00 2001 From: Alex Warren Date: Sat, 1 Feb 2020 12:05:56 -0800 Subject: [PATCH 03/14] in the middle of getting tests for sequences --- Makefile | 2 +- README.md | 10 ++++++++ api/auth.py | 18 +++++++++++--- api/tests/factories.py | 55 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 81 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 2305da3..50895a9 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ test: docker-compose exec api bash -c "FLASK_ENV=testing ENV_FOR_DYNACONF=testing pipenv run pytest" .PHONY: test-dev -## Runs the tests +## Runs the tests continuously on file changes test-dev: docker-compose exec api bash -c "FLASK_ENV=testing ENV_FOR_DYNACONF=testing pipenv run ptw -- --testmon" diff --git a/README.md b/README.md index dd4f92c..d0122c9 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,8 @@ $ docker-compose up -d $ make migrate ``` +You may want to rebuild the image with `docker-compose build` or `docker-compose up --build`. + You should see output that looks like this: ``` @@ -70,6 +72,14 @@ We use [pipenv](https://github.com/pypa/pipenv) to manage our python dependencie # pipenv install ``` +### Changing the DB + +```bash +$ dc exec api pipenv run flask db migrate -m "message" +$ dc exec api pipenv run flask db upgrade +``` + + ### Recreate the db and blow away old versions rm migrations/versions/* diff --git a/api/auth.py b/api/auth.py index 3c48b74..776e0f3 100644 --- a/api/auth.py +++ b/api/auth.py @@ -22,6 +22,18 @@ def abortUnauthorized(): ) +def unauthorized(): + return ( + jsonify( + { + "error": "UnauthorizedError", + "message": "You are not authorized to access this resource", + } + ), + 403, + ) + + def super_admin_required(fn): @wraps(fn) def wrapper(*args, **kwargs): @@ -30,7 +42,7 @@ def wrapper(*args, **kwargs): if is_super_admin(): return fn(*args, **kwargs) else: - abortUnauthorized() + return unauthorized() return wrapper @@ -43,7 +55,7 @@ def wrapper(*args, **kwargs): if get_role() in set(["super_admin", "admin"]): return fn(*args, **kwargs) else: - abortUnauthorized() + return unauthorized() return wrapper @@ -56,6 +68,6 @@ def wrapper(*args, **kwargs): if get_role() in set(["super_admin", "admin", "user"]): return fn(*args, **kwargs) else: - abortUnauthorized() + return unauthorized() return wrapper diff --git a/api/tests/factories.py b/api/tests/factories.py index 9a40a52..aa39c5e 100644 --- a/api/tests/factories.py +++ b/api/tests/factories.py @@ -53,3 +53,58 @@ class Meta: class ChallengeWithResponseFactory(ChallengeFactory): membership = factory.RelatedFactory(ResponseFactory, "challenge") + + +def create_sequence_json(challenges, videos): + result = [] + i = 100 + for challenge in challenges: + result.append({"id": i, "type": "challenge", "challenge_id": challenge.id}) + i += 1 + for video in videos: + result.append({"id": i, "type": "video", "challenge_id": video.id}) + i += 1 + + +class SequenceFactory(factory.alchemy.SQLAlchemyModelFactory): + class Meta: + model = m.Sequence + sqlalchemy_session = m.db.session + + title = factory.Faker("name") + items_json = "[]" + items_length = 0 + + # @factory.post_generation + # def items_json(self, create, extracted, **kwargs): + # if not create: + # return # Simple build, do nothing. + + # print(kwargs) + # self.items_json = create_sequence_json(kwargs["challenges"], kwargs["videos"]) + # self.items_length = len(kwargs["challenges"]) + len(kwargs["videos"]) + + @factory.post_generation + def challenges(self, create, extracted): + if not create: + return # Simple build, do nothing. + + if extracted: + for challenge in extracted: + self.challeges.add(challenge) + + print("post_generation challenges") + + @factory.post_generation + def videos(self, create, extracted): + if not create: + return # Simple build, do nothing. + + if extracted: + for video in extracted: + self.videos.add(video) + + print("post_generation video") + self.items_json = create_sequence_json(self.challenges, self.videos) + self.items_length = len(self.challenges) + len(self.videos) + print("created items_json") From 3f60027424ec2f312808666a5ad577b10621c7c5 Mon Sep 17 00:00:00 2001 From: Alex Warren Date: Sat, 1 Feb 2020 12:06:34 -0800 Subject: [PATCH 04/14] add file for last commit --- api/tests/test_sequence.py | 64 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 api/tests/test_sequence.py diff --git a/api/tests/test_sequence.py b/api/tests/test_sequence.py new file mode 100644 index 0000000..97f2fad --- /dev/null +++ b/api/tests/test_sequence.py @@ -0,0 +1,64 @@ +import io +import uuid + +from flask import url_for +import api.tests.factories as f +import api.models as m + + +def test_create_sequence(client, access_token): + resp = client.post( + url_for("createsequence"), + data=dict(title="empty sequence", itemsJson="[]"), + headers={"Authorization": f"Bearer {access_token}"}, + ) + + print(resp) + assert resp.status_code == 201 + + +def test_create_sequence_invalid_json(client, access_token): + resp = client.post( + url_for("createresponse"), + data=dict(title="broken json sequence", itemsJson="["), + headers={"Authorization": f"Bearer {access_token}"}, + ) + + assert resp.status_code == 400 + + +# test +# with different numbers of videos and challenges +# loop through +# challenge,video > [(2,0), (2,1), (1,2), (2,0), (0,0)] +# generate_simple_json + + +def test_list_responses(client, user, session, access_token, admin_access_token): + original_count = m.Sequence.query.count() + + variations = [(2, 0), (2, 1), (1, 2), (2, 0), (0, 0)] + + challenges = f.ChallengeFactory.create_batch(size=2) + videos = f.VideoFactory.create_batch(size=2) + + sequences = [] + + for challenge_count, video_count in variations: + sequences.append( + f.SequenceFactory.create( + challenges=challenges[:challenge_count], videos=videos[:video_count] + ) + ) + + resp = client.get( + url_for("sequencelist"), headers={"Authorization": f"Bearer {admin_access_token}"} + ) + + assert resp.status_code == 200 + assert len(resp.json) - original_count == len(variations) + + # check that the responses have the all the fields we expect + first = resp.json[0] + assert first["challenges"] != None + assert first["videos"] == None From b58ba17104171a96e033efd4f470deb263c7b97e Mon Sep 17 00:00:00 2001 From: Alex Warren Date: Fri, 3 Apr 2020 13:52:26 -0700 Subject: [PATCH 05/14] Add VS code black and lint settings --- .vscode/settings.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ab05a32 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "editor.formatOnSave": true, + "python.formatting.provider": "black", + "python.linting.pylintEnabled": false, + "python.linting.flake8Enabled": true, + "python.linting.enabled": true +} \ No newline at end of file From cbe5d999c0c2dadd8d2f754648cf5cf96f2fe1a0 Mon Sep 17 00:00:00 2001 From: Alex Warren Date: Fri, 3 Apr 2020 13:53:03 -0700 Subject: [PATCH 06/14] disable docker_layer_caching for free circleCI --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 8f5b81a..69540c0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -18,7 +18,7 @@ jobs: chmod +x ~/docker-compose mv ~/docker-compose /usr/local/bin/docker-compose - setup_remote_docker: - docker_layer_caching: true + docker_layer_caching: false # This feature needs a paid plan - run: name: Start container and verify it is working From 12f18e235b695b7b8f09ff404b9c527ce96b6e9b Mon Sep 17 00:00:00 2001 From: Alex Warren Date: Fri, 3 Apr 2020 14:29:02 -0700 Subject: [PATCH 07/14] Finish switch to static ffmpeg and fix version --- Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6687db9..28fe5d8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -37,7 +37,7 @@ USER human ENV PATH="/home/human/.local/bin:${PATH}" COPY --from=build-env /tmp/eradman-entr-33816756113b/entr /usr/local/bin/ -COPY --from=build-env /tmp/ffmpeg-4.2.1-amd64-static/* /usr/local/bin/ +COPY --from=build-env /tmp/ffmpeg-4.*-amd64-static/* /usr/local/bin/ COPY ./Pipfile* /app/ ENV LC_ALL C.UTF-8 @@ -54,7 +54,7 @@ COPY . /app FROM debian:10.0 AS prod-env RUN apt-get update -y && \ - apt-get install -y python3-dev python3-pip postgresql-client libpq-dev ffmpeg && \ + apt-get install -y python3-dev python3-pip postgresql-client libpq-dev && \ update-alternatives --install /usr/local/bin/python python /usr/bin/python3.7 1 && \ update-alternatives --install /usr/local/bin/pip pip /usr/bin/pip3 1 && \ rm -rf /var/lib/apt/lists/* @@ -66,7 +66,7 @@ USER human ENV PATH="/home/human/.local/bin:${PATH}" -COPY --from=build-env /tmp/ffmpeg-4.2.1-amd64-static/* /usr/local/bin/ +COPY --from=build-env /tmp/ffmpeg-4.*-amd64-static/* /usr/local/bin/ COPY ./Pipfile* /app/ ENV LC_ALL C.UTF-8 From 62c88a2d9a5a59b27aef98b0b3f5be6b43cd1733 Mon Sep 17 00:00:00 2001 From: Alex Warren Date: Fri, 3 Apr 2020 15:14:27 -0700 Subject: [PATCH 08/14] chanbe pipfile pytest to be any, rather than 4.6.3 --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index b43eacd..9e74085 100644 --- a/Pipfile +++ b/Pipfile @@ -4,7 +4,7 @@ url = "https://pypi.org/simple" verify_ssl = true [dev-packages] -pytest = "==4.6.3" +pytest = "*" pytest-flask = "*" pytest-factoryboy = "*" pytest-watch = "*" From d147a47cc01ca3c8e6203b62b83d48e0e08855b6 Mon Sep 17 00:00:00 2001 From: Alex Warren Date: Sat, 4 Apr 2020 17:01:21 -0700 Subject: [PATCH 09/14] Re-enable docker-layer-caching --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 69540c0..0c54fcf 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -18,7 +18,7 @@ jobs: chmod +x ~/docker-compose mv ~/docker-compose /usr/local/bin/docker-compose - setup_remote_docker: - docker_layer_caching: false # This feature needs a paid plan + docker_layer_caching: true - run: name: Start container and verify it is working From 93db84188ea2e7660fbad57001584239426bb385 Mon Sep 17 00:00:00 2001 From: Alex Warren Date: Sun, 5 Apr 2020 08:34:17 -0700 Subject: [PATCH 10/14] Fix Werkzeug FileStorage import error while pytest --- Pipfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Pipfile b/Pipfile index 9e74085..6da6a23 100644 --- a/Pipfile +++ b/Pipfile @@ -40,6 +40,7 @@ pillow = "*" jsonpatch = "*" ffmpeg-python = "*" celery = {extras = ["redis"],version = "*"} +Werkzeug = ">=0.16.1" # Needed because of a bug in .16 see: https://github.com/OpenAPITools/openapi-generator/issues/5235 [requires] python_version = "3.7" From 76c8b7c92170f5fd0101c9218d2379d958f095da Mon Sep 17 00:00:00 2001 From: Alex Warren Date: Sun, 5 Apr 2020 09:56:59 -0700 Subject: [PATCH 11/14] Remove broken unused import and pipenv update --- Pipfile.lock | 870 +++++++++++++++++++----------------------- api/tests/conftest.py | 94 +++-- 2 files changed, 448 insertions(+), 516 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index e58b4df..ba03a40 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "9117e59ab043a86e0c598051d4c5e91b6e9f7f1803974b1687c38911dc71b0e6" + "sha256": "2af715f5b293ac402e61c22f4028e71b07b2728b79d14c597c21e1862a9625aa" }, "pipfile-spec": 6, "requires": { @@ -18,16 +18,16 @@ "default": { "alembic": { "hashes": [ - "sha256:9f907d7e8b286a1cfb22db9084f9ce4fde7ad7956bb496dc7c952e10ac90e36a" + "sha256:035ab00497217628bf5d0be82d664d8713ab13d37b630084da8e1f98facf4dbf" ], - "version": "==1.2.1" + "version": "==1.4.2" }, "amqp": { "hashes": [ - "sha256:19a917e260178b8d410122712bac69cb3e6db010d68f6101e7307508aded5e68", - "sha256:19d851b879a471fcfdcf01df9936cff924f422baa77653289f7095dedd5fb26a" + "sha256:6e649ca13a7df3faacdc8bbb280aa9a6602d22fd9d545336077e573a1f4ff3b8", + "sha256:77f1aef9410698d20eaeac5b73a87817365f457a507d82edf292e12cbb83b08d" ], - "version": "==2.5.1" + "version": "==2.5.2" }, "aniso8601": { "hashes": [ @@ -36,13 +36,6 @@ ], "version": "==8.0.0" }, - "arrow": { - "hashes": [ - "sha256:10257c5daba1a88db34afa284823382f4963feca7733b9107956bed041aff24f", - "sha256:c2325911fcd79972cf493cfd957072f9644af8ad25456201ae1ede3316576eb4" - ], - "version": "==0.15.2" - }, "backcall": { "hashes": [ "sha256:38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4", @@ -56,6 +49,7 @@ "sha256:0b0069c752ec14172c5f78208f1863d7ad6755a6fae6fe76ec2c80d13be41e42", "sha256:19a4b72a6ae5bb467fea018b825f0a7d917789bcfe893e53f15c92805d187294", "sha256:5432dd7b34107ae8ed6c10a71b4397f1c853bd39a4d6ffa7e35f40584cffd161", + "sha256:6305557019906466fc42dbc53b46da004e72fd7a551c044a827e572c82191752", "sha256:69361315039878c0680be456640f8705d76cb4a3a3fe1e057e0f261b74be4b31", "sha256:6fe49a60b25b584e2f4ef175b29d3a83ba63b3a4df1b4c0605b826668d1b6be5", "sha256:74a015102e877d0ccd02cdeaa18b32aa7273746914a6c5d0456dd442cb65b99c", @@ -66,6 +60,7 @@ "sha256:a595c12c618119255c90deb4b046e1ca3bcfad64667c43d1166f2b04bc72db09", "sha256:c9457fa5c121e94a58d6505cadca8bed1c64444b83b3204928a866ca2e599105", "sha256:cb93f6b2ab0f6853550b74e051d297c27a638719753eb9ff66d1e4072be67133", + "sha256:ce4e4f0deb51d38b1611a27f330426154f2980e66582dc5f438aad38b5f24fc1", "sha256:d7bdc26475679dd073ba0ed2766445bb5b20ca4793ca0db32b399dccc6bc84b7", "sha256:ff032765bb8716d9387fd5376d987a937254b0619eff0972779515b5c98820bc" ], @@ -73,10 +68,10 @@ }, "billiard": { "hashes": [ - "sha256:01afcb4e7c4fd6480940cfbd4d9edc19d7a7509d6ada533984d0d0f49901ec82", - "sha256:b8809c74f648dfe69b973c8e660bcec00603758c9db8ba89d7719f88d5f01f26" + "sha256:bff575450859a6e0fbc2f9877d9b715b0bbc07c3565bb7ed2280526a0cdf5ede", + "sha256:d91725ce6425f33a97dfa72fb6bfef0e47d4652acd98a032bd1a7fbf06d5fa6a" ], - "version": "==3.6.1.0" + "version": "==3.6.3.0" }, "blinker": { "hashes": [ @@ -86,61 +81,61 @@ }, "cachetools": { "hashes": [ - "sha256:428266a1c0d36dc5aca63a2d7c5942e88c2c898d72139fca0e97fdd2380517ae", - "sha256:8ea2d3ce97850f31e4a08b0e2b5e6c34997d7216a9d2c98e0f3978630d4da69a" + "sha256:9a52dd97a85f257f4e4127f15818e71a0c7899f121b34591fcc1173ea79a0198", + "sha256:b304586d357c43221856be51d73387f93e2a961598a9b6b6670664746f3b6c6c" ], - "version": "==3.1.1" + "version": "==4.0.0" }, "celery": { "extras": [ "redis" ], "hashes": [ - "sha256:4c4532aa683f170f40bd76f928b70bc06ff171a959e06e71bf35f2f9d6031ef9", - "sha256:528e56767ae7e43a16cfef24ee1062491f5754368d38fcfffa861cdb9ef219be" + "sha256:108a0bf9018a871620936c33a3ee9f6336a89f8ef0a0f567a9001f4aa361415f", + "sha256:5b4b37e276033fe47575107a2775469f0b721646a08c96ec2c61531e4fe45f2a" ], "index": "pypi", - "version": "==4.3.0" + "version": "==4.4.2" }, "certifi": { "hashes": [ - "sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50", - "sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef" + "sha256:b0e07438175de96ab74de9ab5dc40985ef8b44a41e9636a2000099dc3b670ddd", + "sha256:e68768546aa055623812ada64aec5e1f02ca20a9e7f3d3432dd8b0f35a6e7951" ], - "version": "==2019.9.11" + "version": "==2020.4.5" }, "cffi": { "hashes": [ - "sha256:041c81822e9f84b1d9c401182e174996f0bae9991f33725d059b771744290774", - "sha256:046ef9a22f5d3eed06334d01b1e836977eeef500d9b78e9ef693f9380ad0b83d", - "sha256:066bc4c7895c91812eff46f4b1c285220947d4aa46fa0a2651ff85f2afae9c90", - "sha256:066c7ff148ae33040c01058662d6752fd73fbc8e64787229ea8498c7d7f4041b", - "sha256:2444d0c61f03dcd26dbf7600cf64354376ee579acad77aef459e34efcb438c63", - "sha256:300832850b8f7967e278870c5d51e3819b9aad8f0a2c8dbe39ab11f119237f45", - "sha256:34c77afe85b6b9e967bd8154e3855e847b70ca42043db6ad17f26899a3df1b25", - "sha256:46de5fa00f7ac09f020729148ff632819649b3e05a007d286242c4882f7b1dc3", - "sha256:4aa8ee7ba27c472d429b980c51e714a24f47ca296d53f4d7868075b175866f4b", - "sha256:4d0004eb4351e35ed950c14c11e734182591465a33e960a4ab5e8d4f04d72647", - "sha256:4e3d3f31a1e202b0f5a35ba3bc4eb41e2fc2b11c1eff38b362de710bcffb5016", - "sha256:50bec6d35e6b1aaeb17f7c4e2b9374ebf95a8975d57863546fa83e8d31bdb8c4", - "sha256:55cad9a6df1e2a1d62063f79d0881a414a906a6962bc160ac968cc03ed3efcfb", - "sha256:5662ad4e4e84f1eaa8efce5da695c5d2e229c563f9d5ce5b0113f71321bcf753", - "sha256:59b4dc008f98fc6ee2bb4fd7fc786a8d70000d058c2bbe2698275bc53a8d3fa7", - "sha256:73e1ffefe05e4ccd7bcea61af76f36077b914f92b76f95ccf00b0c1b9186f3f9", - "sha256:a1f0fd46eba2d71ce1589f7e50a9e2ffaeb739fb2c11e8192aa2b45d5f6cc41f", - "sha256:a2e85dc204556657661051ff4bab75a84e968669765c8a2cd425918699c3d0e8", - "sha256:a5457d47dfff24882a21492e5815f891c0ca35fefae8aa742c6c263dac16ef1f", - "sha256:a8dccd61d52a8dae4a825cdbb7735da530179fea472903eb871a5513b5abbfdc", - "sha256:ae61af521ed676cf16ae94f30fe202781a38d7178b6b4ab622e4eec8cefaff42", - "sha256:b012a5edb48288f77a63dba0840c92d0504aa215612da4541b7b42d849bc83a3", - "sha256:d2c5cfa536227f57f97c92ac30c8109688ace8fa4ac086d19d0af47d134e2909", - "sha256:d42b5796e20aacc9d15e66befb7a345454eef794fdb0737d1af593447c6c8f45", - "sha256:dee54f5d30d775f525894d67b1495625dd9322945e7fee00731952e0368ff42d", - "sha256:e070535507bd6aa07124258171be2ee8dfc19119c28ca94c9dfb7efd23564512", - "sha256:e1ff2748c84d97b065cc95429814cdba39bcbd77c9c85c89344b317dc0d9cbff", - "sha256:ed851c75d1e0e043cbf5ca9a8e1b13c4c90f3fbd863dacb01c0808e2b5204201" - ], - "version": "==1.12.3" + "sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff", + "sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b", + "sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac", + "sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0", + "sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384", + "sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26", + "sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6", + "sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b", + "sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e", + "sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd", + "sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2", + "sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66", + "sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc", + "sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8", + "sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55", + "sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4", + "sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5", + "sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d", + "sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78", + "sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa", + "sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793", + "sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f", + "sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a", + "sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f", + "sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30", + "sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f", + "sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3", + "sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c" + ], + "version": "==1.14.0" }, "chardet": { "hashes": [ @@ -151,32 +146,25 @@ }, "click": { "hashes": [ - "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", - "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" + "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc", + "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a" ], - "version": "==7.0" - }, - "croniter": { - "hashes": [ - "sha256:0d905dbe6f131a910fd3dde792f0129788cd2cb3a8048c5f7aaa212670b0cef2", - "sha256:538adeb3a7f7816c3cdec6db974c441620d764c25ff4ed0146ee7296b8a50590" - ], - "version": "==0.3.30" + "version": "==7.1.1" }, "decorator": { "hashes": [ - "sha256:86156361c50488b84a3f148056ea716ca587df2f0de1d34750d35c21312725de", - "sha256:f069f3a01830ca754ba5258fde2278454a0b5b79e0d7f5c13b3b97e57d4acff6" + "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760", + "sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7" ], - "version": "==4.4.0" + "version": "==4.4.2" }, "dynaconf": { "hashes": [ - "sha256:117b7e52698af82a535bcb71012f3221e1ab9c869bb8163ebf156569f32ff07d", - "sha256:abeb44db4249c443083584cdd4d9c5c10cd773f11067e270660d15e6eef668d7" + "sha256:26b84f2b234a203f6005463d954c9f007181c09345eaaab3fc38503acbdadc7d", + "sha256:e803cdab2d7addd539c4ee8d121f15ab0b63a83a5b723150e1746aa7e8063adb" ], "index": "pypi", - "version": "==2.2.0" + "version": "==2.2.3" }, "factory-boy": { "hashes": [ @@ -188,10 +176,10 @@ }, "faker": { "hashes": [ - "sha256:45cc9cca3de8beba5a2da3bd82a6e5544f53da1a702645c8485f682366c15026", - "sha256:a6459ff518d1fc6ee2238a7209e6c899517872c7e1115510279033ffe6fe8ef3" + "sha256:2d3f866ef25e1a5af80e7b0ceeacc3c92dec5d0fdbad3e2cb6adf6e60b22188f", + "sha256:b89aa33837498498e15c709eb40c31386408a901a53c7a5e12a425737a767976" ], - "version": "==2.0.2" + "version": "==4.0.2" }, "ffmpeg-python": { "hashes": [ @@ -203,11 +191,11 @@ }, "flask": { "hashes": [ - "sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52", - "sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6" + "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060", + "sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557" ], "index": "pypi", - "version": "==1.1.1" + "version": "==1.1.2" }, "flask-bcrypt": { "hashes": [ @@ -226,42 +214,34 @@ }, "flask-jwt-extended": { "hashes": [ - "sha256:7e89db96a5d589460853ad4e9cf51ff4ff5848029d65262149375b80ce95d8fe" + "sha256:0aa8ee6fa7eb3be9314e39dd199ac8e19389a95371f9d54e155c7aa635e319dd" ], "index": "pypi", - "version": "==3.24.0" + "version": "==3.24.1" }, "flask-marshmallow": { "hashes": [ - "sha256:4f507f883838b397638a3a36c7d36ee146b255a49db952f5d9de3f6f4522e8a8", - "sha256:69e99e3a123393894884a032ae2d11e6bdf4519a505819b66cec7eda32057741" + "sha256:01520ef1851ccb64d4ffb33196cddff895cc1302ae1585bff1abf58684a8111a", + "sha256:28b969193958d9602ab5d6add6d280e0e360c8e373d3492c2f73b024ecd36374" ], "index": "pypi", - "version": "==0.10.1" + "version": "==0.11.0" }, "flask-migrate": { "hashes": [ - "sha256:6fb038be63d4c60727d5dfa5f581a6189af5b4e2925bc378697b4f0a40cfb4e1", - "sha256:a96ff1875a49a40bd3e8ac04fce73fdb0870b9211e6168608cbafa4eb839d502" + "sha256:4dc4a5cce8cbbb06b8dc963fd86cf8136bd7d875aabe2d840302ea739b243732", + "sha256:a69d508c2e09d289f6e55a417b3b8c7bfe70e640f53d2d9deb0d056a384f37ee" ], "index": "pypi", - "version": "==2.5.2" + "version": "==2.5.3" }, "flask-restful": { "hashes": [ - "sha256:ecd620c5cc29f663627f99e04f17d1f16d095c83dc1d618426e2ad68b03092f8", - "sha256:f8240ec12349afe8df1db168ea7c336c4e5b0271a36982bff7394f93275f2ca9" - ], - "index": "pypi", - "version": "==0.3.7" - }, - "flask-rq2": { - "hashes": [ - "sha256:3ef6395065255447f8e1516ccca24858ba87da1d71a6975e0e3b55256bf04967", - "sha256:abe1e52d3b98abe37e85830a614ba6af864516f1b6cf2229f352f8500eafc5fd" + "sha256:5ea9a5991abf2cb69b4aac19793faac6c032300505b325687d7c305ffaa76915", + "sha256:d891118b951921f1cec80cabb4db98ea6058a35e6404788f9e70d5b243813ec2" ], "index": "pypi", - "version": "==18.3" + "version": "==0.3.8" }, "flask-shell-ipython": { "hashes": [ @@ -281,80 +261,81 @@ }, "future": { "hashes": [ - "sha256:6142ef79e2416e432931d527452a1cab3aa4a754a0a53d25b2589f79e1106f34" + "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d" ], - "version": "==0.18.0" + "version": "==0.18.2" }, "google-api-core": { "hashes": [ - "sha256:b95895a9398026bc0500cf9b4a3f82c3f72c3f9150b26ff53af40c74e91c264a", - "sha256:df8adc4b97f5ab4328a0e745bee77877cf4a7d4601cb1cd5959d2bbf8fba57aa" + "sha256:859f7392676761f2b160c6ee030c3422135ada4458f0948c5690a6a7c8d86294", + "sha256:92e962a087f1c4b8d1c5c88ade1c1dfd550047dcffb320c57ef6a534a20403e2" ], - "version": "==1.14.3" + "version": "==1.16.0" }, "google-auth": { "hashes": [ - "sha256:0f7c6a64927d34c1a474da92cfc59e552a5d3b940d3266606c6a28b72888b9e4", - "sha256:20705f6803fd2c4d1cc2dcb0df09d4dfcb9a7d51fd59e94a3a28231fd93119ed" + "sha256:a5ee4c40fef77ea756cf2f1c0adcf475ecb53af6700cf9c133354cdc9b267148", + "sha256:cab6c707e6ee20e567e348168a5c69dc6480384f777a9e5159f4299ad177dcc0" ], - "version": "==1.6.3" + "version": "==1.13.1" }, "google-cloud-core": { "hashes": [ - "sha256:0ee17abc74ff02176bee221d4896a00a3c202f3fb07125a7d814ccabd20d7eb5", - "sha256:10750207c1a9ad6f6e082d91dbff3920443bdaf1c344a782730489a9efa802f1" + "sha256:6ae5c62931e8345692241ac1939b85a10d6c38dc9e2854bdbacb7e5ac3033229", + "sha256:878f9ad080a40cdcec85b92242c4b5819eeb8f120ebc5c9f640935e24fc129d8" ], - "version": "==1.0.3" + "version": "==1.3.0" }, "google-cloud-storage": { "hashes": [ - "sha256:13a6a820311662eb91a99810568c2bca5ddc7e44e2163fed4cb3f4d47da132cf", - "sha256:2e7e2435978bda1c209b70a9a00b8cbc53c3b00d6f09eb2c991ebba857babf24" + "sha256:3af167094142a61b1bda3489da4a724e55f2703b236431b27f71c9936d94f8d8", + "sha256:62d5efa529fb39ae01504698b7053f2a009877d0d4b3c8f297e3e68c8c38a117" ], "index": "pypi", - "version": "==1.20.0" + "version": "==1.27.0" }, "google-resumable-media": { "hashes": [ - "sha256:5fd2e641f477e50be925a55bcfdf0b0cb97c2b92aacd7b15c1d339f70d55c1c7", - "sha256:cdeb8fbb3551a665db921023603af2f0d6ac59ad8b48259cb510b8799505775f" + "sha256:2a8fd188afe1cbfd5998bf20602f76b0336aa892de88fe842a806b9a3ed78d2a", + "sha256:b86140d5a0b6d290084b11bde90ee9aecad357ba0e0d67388d016b8340320927" ], - "version": "==0.4.1" + "version": "==0.5.0" }, "googleapis-common-protos": { "hashes": [ - "sha256:e61b8ed5e36b976b487c6e7b15f31bb10c7a0ca7bd5c0e837f4afab64b53a0c6" + "sha256:013c91704279119150e44ef770086fdbba158c1f978a6402167d47d5409e226e" ], - "version": "==1.6.0" + "version": "==1.51.0" }, "gunicorn": { "hashes": [ - "sha256:aa8e0b40b4157b36a5df5e599f45c9c76d6af43845ba3b3b0efe2c70473c2471", - "sha256:fa2662097c66f920f53f70621c6c58ca4a3c4d3434205e608e121b5b3b71f4f3" + "sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626", + "sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c" ], "index": "pypi", - "version": "==19.9.0" + "version": "==20.0.4" }, "idna": { "hashes": [ - "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", - "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", + "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" ], - "version": "==2.8" + "version": "==2.9" }, "importlib-metadata": { "hashes": [ - "sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26", - "sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af" + "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f", + "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e" ], - "version": "==0.23" + "markers": "python_version < '3.8'", + "version": "==1.6.0" }, "ipython": { "hashes": [ - "sha256:c4ab005921641e40a68e405e286e7a1fcc464497e14d81b6914b4fd95e5dee9b", - "sha256:dd76831f065f17bddd7eaa5c781f5ea32de5ef217592cf019e34043b56895aa1" + "sha256:ca478e52ae1f88da0102360e57e528b92f3ae4316aabac80a2cd7f7ab2efb48a", + "sha256:eb8d075de37f678424527b5ef6ea23f7b80240ca031c2dd6de5879d687a65333" ], - "version": "==7.8.0" + "version": "==7.13.0" }, "ipython-genutils": { "hashes": [ @@ -372,25 +353,25 @@ }, "jedi": { "hashes": [ - "sha256:786b6c3d80e2f06fd77162a07fed81b8baa22dde5d62896a790a331d6ac21a27", - "sha256:ba859c74fa3c966a22f2aeebe1b74ee27e2a462f56d3f5f7ca4a59af61bfe42e" + "sha256:b4f4052551025c6b0b0b193b29a6ff7bdb74c52450631206c262aef9f7159ad2", + "sha256:d5c871cb9360b414f981e7072c52c33258d598305280fef91c6cae34739d65d5" ], - "version": "==0.15.1" + "version": "==0.16.0" }, "jinja2": { "hashes": [ - "sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f", - "sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de" + "sha256:93187ffbc7808079673ef52771baa950426fd664d3aad1d0fa3e95644360e250", + "sha256:b0eaf100007721b5c16c1fc1eecb87409464edc10469ddc9a22a27a99123be49" ], - "version": "==2.10.3" + "version": "==2.11.1" }, "jsonpatch": { "hashes": [ - "sha256:83f29a2978c13da29bfdf89da9d65542d62576479caf215df19632d7dc04c6e6", - "sha256:cbb72f8bf35260628aea6b508a107245f757d1ec839a19c34349985e2c05645a" + "sha256:cc3a7241010a1fd3f50145a3b33be2c03c1e679faa19934b628bb07d0f64819e", + "sha256:ddc0f7628b8bfdd62e3cbfbc24ca6671b0b6265b50d186c2cf3659dc0f78fd6a" ], "index": "pypi", - "version": "==1.24" + "version": "==1.25" }, "jsonpointer": { "hashes": [ @@ -401,16 +382,17 @@ }, "kombu": { "hashes": [ - "sha256:31edb84947996fdda065b6560c128d5673bb913ff34aa19e7b84755217a24deb", - "sha256:c9078124ce2616b29cf6607f0ac3db894c59154252dee6392cdbbe15e5c4b566" + "sha256:2d1cda774126a044d91a7ff5fa6d09edf99f46924ab332a810760fe6740e9b76", + "sha256:598e7e749d6ab54f646b74b2d2df67755dee13894f73ab02a2a9feb8870c7cb2" ], - "version": "==4.6.5" + "version": "==4.6.8" }, "mako": { "hashes": [ - "sha256:a36919599a9b7dc5d86a7a8988f23a9a3a3d083070023bab23d64f7f1d1e0a4b" + "sha256:3139c5d64aa5d175dbafb95027057128b5fbd05a40c53999f3905ceb53366d9d", + "sha256:8e8b53c71c7e59f3de716b6832c4e401d903af574f6962edbbbf6ecc2a5fe6c9" ], - "version": "==1.1.0" + "version": "==1.1.2" }, "markupsafe": { "hashes": [ @@ -418,13 +400,16 @@ "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", + "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", + "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", + "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", @@ -441,7 +426,9 @@ "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", - "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7" + "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", + "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", + "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be" ], "version": "==1.1.1" }, @@ -455,33 +442,26 @@ }, "marshmallow-sqlalchemy": { "hashes": [ - "sha256:b53ae45f6f113ae5433211786129ecb6eaf3646a3a333e769eeb22593b6dbe9c", - "sha256:c4dd561ff42f39e44619e6558a28da7154fea62ffada6815403f1381762c87db" + "sha256:9301c6fd197bd97337820ea1417aa1233d0ee3e22748ebd5821799bc841a57e8", + "sha256:dde9e20bcb710e9e59f765a38e3d6d17f1b2d6b4320cbdc2cea0f6b57f70d08c" ], "index": "pypi", - "version": "==0.19.0" - }, - "more-itertools": { - "hashes": [ - "sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", - "sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4" - ], - "version": "==7.2.0" + "version": "==0.22.3" }, "parso": { "hashes": [ - "sha256:63854233e1fadb5da97f2744b6b24346d2750b85965e7e399bec1620232797dc", - "sha256:666b0ee4a7a1220f65d367617f2cd3ffddff3e205f3f16a0284df30e774c2a9c" + "sha256:0c5659e0c6eba20636f99a04f469798dca8da279645ce5c387315b2c23912157", + "sha256:8515fc12cfca6ee3aa59138741fc5624d62340c97e401c74875769948d4f2995" ], - "version": "==0.5.1" + "version": "==0.6.2" }, "pexpect": { "hashes": [ - "sha256:2094eefdfcf37a1fdbfb9aa090862c1a4878e5c7e0e7e7088bdb511c558e5cd1", - "sha256:9e2c1fd0e6ee3a49b28f95d4b33bc389c89b20af6a1255906e90ff1262ce62eb" + "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937", + "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c" ], "markers": "sys_platform != 'win32'", - "version": "==4.7.0" + "version": "==4.8.0" }, "pickleshare": { "hashes": [ @@ -492,98 +472,99 @@ }, "pillow": { "hashes": [ - "sha256:00fdeb23820f30e43bba78eb9abb00b7a937a655de7760b2e09101d63708b64e", - "sha256:01f948e8220c85eae1aa1a7f8edddcec193918f933fb07aaebe0bfbbcffefbf1", - "sha256:08abf39948d4b5017a137be58f1a52b7101700431f0777bec3d897c3949f74e6", - "sha256:099a61618b145ecb50c6f279666bbc398e189b8bc97544ae32b8fcb49ad6b830", - "sha256:2c1c61546e73de62747e65807d2cc4980c395d4c5600ecb1f47a650c6fa78c79", - "sha256:2ed9c4f694861642401f27dc3cb99772be67cd190e84845c749dae0a06c3bfae", - "sha256:338581b30b908e111be578f0297255f6b57a51358cd16fa0e6f664c9a1f88bff", - "sha256:38c7d48a21cd06fdeee93987147b9b1c55b73b4cfcbf83240568bfbd5adee447", - "sha256:43fd026f613c8e48a25eba1a92f4d2ad7f3903c95d8c33a11611a7717d2ab654", - "sha256:4548236844327a718ce3bb182ab32a16fa2050c61e334e959f554cac052fb0df", - "sha256:5090857876c58885cfa388dc649e5db30aae98a068c26f3fd0ac9d7d9a4d9572", - "sha256:5bbba34f97a26a93f5e8dec469ca4ddd712451418add43da946dbaed7f7a98d2", - "sha256:65a28969a025a0eb4594637b6103201dc4ed2a9508bdab56ac33e43e3081c404", - "sha256:892bb52b70bd5ea9dbbc3ac44f38e84f5a04e9d8b1bff48159d96cb795b81159", - "sha256:8a9becd5cbd5062f973bcd2e7bc79483af310222de112b6541f8af1f93a3cc42", - "sha256:972a7aaeb7c4a2795b52eef52ee991ef040b31009f36deca6207a986607b55f3", - "sha256:97b119c436bfa96a92ac2ca525f7025836d4d4e64b1c9f9eff8dbaf3ff1d86f3", - "sha256:9ba37698e242223f8053cc158f130aee046a96feacbeab65893dbe94f5530118", - "sha256:b1b0e1f626a0f079c0d3696db70132fb1f29aa87c66aecb6501a9b8be64ce9f7", - "sha256:c14c1224fd1a5be2733530d648a316974dbbb3c946913562c6005a76f21ca042", - "sha256:c79a8546c48ae6465189e54e3245a97ddf21161e33ff7eaa42787353417bb2b6", - "sha256:ceb76935ac4ebdf6d7bc845482a4450b284c6ccfb281e34da51d510658ab34d8", - "sha256:e22bffaad04b4d16e1c091baed7f2733fc1ebb91e0c602abf1b6834d17158b1f", - "sha256:ec883b8e44d877bda6f94a36313a1c6063f8b1997aa091628ae2f34c7f97c8d5", - "sha256:f1baa54d50ec031d1a9beb89974108f8f2c0706f49798f4777df879df0e1adb6", - "sha256:f53a5385932cda1e2c862d89460992911a89768c65d176ff8c50cddca4d29bed" + "sha256:04a10558320eba9137d6a78ca6fc8f4a5801f1b971152938851dc4629d903579", + "sha256:0f89ddc77cf421b8cd34ae852309501458942bf370831b4a9b406156b599a14e", + "sha256:251e5618125ec12ac800265d7048f5857a8f8f1979db9ea3e11382e159d17f68", + "sha256:291bad7097b06d648222b769bbfcd61e40d0abdfe10df686d20ede36eb8162b6", + "sha256:2f0b52a08d175f10c8ea36685115681a484c55d24d0933f9fd911e4111c04144", + "sha256:3713386d1e9e79cea1c5e6aaac042841d7eef838cc577a3ca153c8bedf570287", + "sha256:433bbc2469a2351bea53666d97bb1eb30f0d56461735be02ea6b27654569f80f", + "sha256:4510c6b33277970b1af83c987277f9a08ec2b02cc20ac0f9234e4026136bb137", + "sha256:50a10b048f4dd81c092adad99fa5f7ba941edaf2f9590510109ac2a15e706695", + "sha256:670e58d3643971f4afd79191abd21623761c2ebe61db1c2cb4797d817c4ba1a7", + "sha256:6c1924ed7dbc6ad0636907693bbbdd3fdae1d73072963e71f5644b864bb10b4d", + "sha256:721c04d3c77c38086f1f95d1cd8df87f2f9a505a780acf8575912b3206479da1", + "sha256:8d5799243050c2833c2662b824dfb16aa98e408d2092805edea4300a408490e7", + "sha256:90cd441a1638ae176eab4d8b6b94ab4ec24b212ed4c3fbee2a6e74672481d4f8", + "sha256:a5dc9f28c0239ec2742d4273bd85b2aa84655be2564db7ad1eb8f64b1efcdc4c", + "sha256:b2f3e8cc52ecd259b94ca880fea0d15f4ebc6da2cd3db515389bb878d800270f", + "sha256:b7453750cf911785009423789d2e4e5393aae9cbb8b3f471dab854b85a26cb89", + "sha256:b99b2607b6cd58396f363b448cbe71d3c35e28f03e442ab00806463439629c2c", + "sha256:cd47793f7bc9285a88c2b5551d3f16a2ddd005789614a34c5f4a598c2a162383", + "sha256:d6bf085f6f9ec6a1724c187083b37b58a8048f86036d42d21802ed5d1fae4853", + "sha256:da737ab273f4d60ae552f82ad83f7cbd0e173ca30ca20b160f708c92742ee212", + "sha256:eb84e7e5b07ff3725ab05977ac56d5eeb0c510795aeb48e8b691491be3c5745b" ], "index": "pypi", - "version": "==6.2.0" + "version": "==7.1.1" }, "prompt-toolkit": { "hashes": [ - "sha256:46642344ce457641f28fc9d1c9ca939b63dadf8df128b86f1b9860e59c73a5e4", - "sha256:e7f8af9e3d70f514373bf41aa51bc33af12a6db3f71461ea47fea985defb2c31", - "sha256:f15af68f66e664eaa559d4ac8a928111eebd5feda0c11738b5998045224829db" + "sha256:563d1a4140b63ff9dd587bda9557cffb2fe73650205ab6f4383092fb882e7dc8", + "sha256:df7e9e63aea609b1da3a65641ceaf5bc7d05e0a04de5bd45d05dbeffbabf9e04" ], - "version": "==2.0.10" + "version": "==3.0.5" }, "protobuf": { "hashes": [ - "sha256:125713564d8cfed7610e52444c9769b8dcb0b55e25cc7841f2290ee7bc86636f", - "sha256:1accdb7a47e51503be64d9a57543964ba674edac103215576399d2d0e34eac77", - "sha256:27003d12d4f68e3cbea9eb67427cab3bfddd47ff90670cb367fcd7a3a89b9657", - "sha256:3264f3c431a631b0b31e9db2ae8c927b79fc1a7b1b06b31e8e5bcf2af91fe896", - "sha256:3c5ab0f5c71ca5af27143e60613729e3488bb45f6d3f143dc918a20af8bab0bf", - "sha256:45dcf8758873e3f69feab075e5f3177270739f146255225474ee0b90429adef6", - "sha256:56a77d61a91186cc5676d8e11b36a5feb513873e4ae88d2ee5cf530d52bbcd3b", - "sha256:5984e4947bbcef5bd849d6244aec507d31786f2dd3344139adc1489fb403b300", - "sha256:6b0441da73796dd00821763bb4119674eaf252776beb50ae3883bed179a60b2a", - "sha256:6f6677c5ade94d4fe75a912926d6796d5c71a2a90c2aeefe0d6f211d75c74789", - "sha256:84a825a9418d7196e2acc48f8746cf1ee75877ed2f30433ab92a133f3eaf8fbe", - "sha256:b842c34fe043ccf78b4a6cf1019d7b80113707d68c88842d061fa2b8fb6ddedc", - "sha256:ca33d2f09dae149a1dcf942d2d825ebb06343b77b437198c9e2ef115cf5d5bc1", - "sha256:db83b5c12c0cd30150bb568e6feb2435c49ce4e68fe2d7b903113f0e221e58fe", - "sha256:f50f3b1c5c1c1334ca7ce9cad5992f098f460ffd6388a3cabad10b66c2006b09", - "sha256:f99f127909731cafb841c52f9216e447d3e4afb99b17bebfad327a75aee206de" - ], - "version": "==3.10.0" + "sha256:0bae429443cc4748be2aadfdaf9633297cfaeb24a9a02d0ab15849175ce90fab", + "sha256:24e3b6ad259544d717902777b33966a1a069208c885576254c112663e6a5bb0f", + "sha256:310a7aca6e7f257510d0c750364774034272538d51796ca31d42c3925d12a52a", + "sha256:52e586072612c1eec18e1174f8e3bb19d08f075fc2e3f91d3b16c919078469d0", + "sha256:73152776dc75f335c476d11d52ec6f0f6925774802cd48d6189f4d5d7fe753f4", + "sha256:7774bbbaac81d3ba86de646c39f154afc8156717972bf0450c9dbfa1dc8dbea2", + "sha256:82d7ac987715d8d1eb4068bf997f3053468e0ce0287e2729c30601feb6602fee", + "sha256:8eb9c93798b904f141d9de36a0ba9f9b73cc382869e67c9e642c0aba53b0fc07", + "sha256:adf0e4d57b33881d0c63bb11e7f9038f98ee0c3e334c221f0858f826e8fb0151", + "sha256:c40973a0aee65422d8cb4e7d7cbded95dfeee0199caab54d5ab25b63bce8135a", + "sha256:c77c974d1dadf246d789f6dad1c24426137c9091e930dbf50e0a29c1fcf00b1f", + "sha256:dd9aa4401c36785ea1b6fff0552c674bdd1b641319cb07ed1fe2392388e9b0d7", + "sha256:e11df1ac6905e81b815ab6fd518e79be0a58b5dc427a2cf7208980f30694b956", + "sha256:e2f8a75261c26b2f5f3442b0525d50fd79a71aeca04b5ec270fc123536188306", + "sha256:e512b7f3a4dd780f59f1bf22c302740e27b10b5c97e858a6061772668cd6f961", + "sha256:ef2c2e56aaf9ee914d3dccc3408d42661aaf7d9bb78eaa8f17b2e6282f214481", + "sha256:fac513a9dc2a74b99abd2e17109b53945e364649ca03d9f7a0b96aa8d1807d0a", + "sha256:fdfb6ad138dbbf92b5dbea3576d7c8ba7463173f7d2cb0ca1bd336ec88ddbd80" + ], + "version": "==3.11.3" }, "psycopg2-binary": { "hashes": [ - "sha256:080c72714784989474f97be9ab0ddf7b2ad2984527e77f2909fcd04d4df53809", - "sha256:110457be80b63ff4915febb06faa7be002b93a76e5ba19bf3f27636a2ef58598", - "sha256:171352a03b22fc099f15103959b52ee77d9a27e028895d7e5fde127aa8e3bac5", - "sha256:19d013e7b0817087517a4b3cab39c084d78898369e5c46258aab7be4f233d6a1", - "sha256:249b6b21ae4eb0f7b8423b330aa80fab5f821b9ffc3f7561a5e2fd6bb142cf5d", - "sha256:2ac0731d2d84b05c7bb39e85b7e123c3a0acd4cda631d8d542802c88deb9e87e", - "sha256:2b6d561193f0dc3f50acfb22dd52ea8c8dfbc64bcafe3938b5f209cc17cb6f00", - "sha256:2bd23e242e954214944481124755cbefe7c2cf563b1a54cd8d196d502f2578bf", - "sha256:3e1239242ca60b3725e65ab2f13765fc199b03af9eaf1b5572f0e97bdcee5b43", - "sha256:3eb70bb697abbe86b1d2b1316370c02ba320bfd1e9e35cf3b9566a855ea8e4e5", - "sha256:51a2fc7e94b98bd1bb5d4570936f24fc2b0541b63eccadf8fdea266db8ad2f70", - "sha256:52f1bdafdc764b7447e393ed39bb263eccb12bfda25a4ac06d82e3a9056251f6", - "sha256:5b3581319a3951f1e866f4f6c5e42023db0fae0284273b82e97dfd32c51985cd", - "sha256:63c1b66e3b2a3a336288e4bcec499e0dc310cd1dceaed1c46fa7419764c68877", - "sha256:8123a99f24ecee469e5c1339427bcdb2a33920a18bb5c0d58b7c13f3b0298ba3", - "sha256:85e699fcabe7f817c0f0a412d4e7c6627e00c412b418da7666ff353f38e30f67", - "sha256:8dbff4557bbef963697583366400822387cccf794ccb001f1f2307ed21854c68", - "sha256:908d21d08d6b81f1b7e056bbf40b2f77f8c499ab29e64ec5113052819ef1c89b", - "sha256:af39d0237b17d0a5a5f638e9dffb34013ce2b1d41441fd30283e42b22d16858a", - "sha256:af51bb9f055a3f4af0187149a8f60c9d516cf7d5565b3dac53358796a8fb2a5b", - "sha256:b2ecac57eb49e461e86c092761e6b8e1fd9654dbaaddf71a076dcc869f7014e2", - "sha256:cd37cc170678a4609becb26b53a2bc1edea65177be70c48dd7b39a1149cabd6e", - "sha256:d17e3054b17e1a6cb8c1140f76310f6ede811e75b7a9d461922d2c72973f583e", - "sha256:d305313c5a9695f40c46294d4315ed3a07c7d2b55e48a9010dad7db7a66c8b7f", - "sha256:dd0ef0eb1f7dd18a3f4187226e226a7284bda6af5671937a221766e6ef1ee88f", - "sha256:e1adff53b56db9905db48a972fb89370ad5736e0450b96f91bcf99cadd96cfd7", - "sha256:f0d43828003c82dbc9269de87aa449e9896077a71954fbbb10a614c017e65737", - "sha256:f78e8b487de4d92640105c1389e5b90be3496b1d75c90a666edd8737cc2dbab7" + "sha256:040234f8a4a8dfd692662a8308d78f63f31a97e1c42d2480e5e6810c48966a29", + "sha256:086f7e89ec85a6704db51f68f0dcae432eff9300809723a6e8782c41c2f48e03", + "sha256:18ca813fdb17bc1db73fe61b196b05dd1ca2165b884dd5ec5568877cabf9b039", + "sha256:19dc39616850342a2a6db70559af55b22955f86667b5f652f40c0e99253d9881", + "sha256:2166e770cb98f02ed5ee2b0b569d40db26788e0bf2ec3ae1a0d864ea6f1d8309", + "sha256:3a2522b1d9178575acee4adf8fd9f979f9c0449b00b4164bb63c3475ea6528ed", + "sha256:3aa773580f85a28ffdf6f862e59cb5a3cc7ef6885121f2de3fca8d6ada4dbf3b", + "sha256:3b5deaa3ee7180585a296af33e14c9b18c218d148e735c7accf78130765a47e3", + "sha256:407af6d7e46593415f216c7f56ba087a9a42bd6dc2ecb86028760aa45b802bd7", + "sha256:4c3c09fb674401f630626310bcaf6cd6285daf0d5e4c26d6e55ca26a2734e39b", + "sha256:4c6717962247445b4f9e21c962ea61d2e884fc17df5ddf5e35863b016f8a1f03", + "sha256:50446fae5681fc99f87e505d4e77c9407e683ab60c555ec302f9ac9bffa61103", + "sha256:5057669b6a66aa9ca118a2a860159f0ee3acf837eda937bdd2a64f3431361a2d", + "sha256:5dd90c5438b4f935c9d01fcbad3620253da89d19c1f5fca9158646407ed7df35", + "sha256:659c815b5b8e2a55193ede2795c1e2349b8011497310bb936da7d4745652823b", + "sha256:69b13fdf12878b10dc6003acc8d0abf3ad93e79813fd5f3812497c1c9fb9be49", + "sha256:7a1cb80e35e1ccea3e11a48afe65d38744a0e0bde88795cc56a4d05b6e4f9d70", + "sha256:7e6e3c52e6732c219c07bd97fff6c088f8df4dae3b79752ee3a817e6f32e177e", + "sha256:7f42a8490c4fe854325504ce7a6e4796b207960dabb2cbafe3c3959cb00d1d7e", + "sha256:84156313f258eafff716b2961644a4483a9be44a5d43551d554844d15d4d224e", + "sha256:8578d6b8192e4c805e85f187bc530d0f52ba86c39172e61cd51f68fddd648103", + "sha256:890167d5091279a27e2505ff0e1fb273f8c48c41d35c5b92adbf4af80e6b2ed6", + "sha256:98e10634792ac0e9e7a92a76b4991b44c2325d3e7798270a808407355e7bb0a1", + "sha256:9aadff9032e967865f9778485571e93908d27dab21d0fdfdec0ca779bb6f8ad9", + "sha256:9f24f383a298a0c0f9b3113b982e21751a8ecde6615494a3f1470eb4a9d70e9e", + "sha256:a73021b44813b5c84eda4a3af5826dd72356a900bac9bd9dd1f0f81ee1c22c2f", + "sha256:afd96845e12638d2c44d213d4810a08f4dc4a563f9a98204b7428e567014b1cd", + "sha256:b73ddf033d8cd4cc9dfed6324b1ad2a89ba52c410ef6877998422fcb9c23e3a8", + "sha256:b8f490f5fad1767a1331df1259763b3bad7d7af12a75b950c2843ba319b2415f", + "sha256:dbc5cd56fff1a6152ca59445178652756f4e509f672e49ccdf3d79c1043113a4", + "sha256:eac8a3499754790187bb00574ab980df13e754777d346f85e0ff6df929bcd964", + "sha256:eaed1c65f461a959284649e37b5051224f4db6ebdc84e40b5e65f2986f101a08" ], "index": "pypi", - "version": "==2.8.3" + "version": "==2.8.4" }, "ptyprocess": { "hashes": [ @@ -594,30 +575,31 @@ }, "pyasn1": { "hashes": [ - "sha256:62cdade8b5530f0b185e09855dd422bc05c0bbff6b72ff61381c09dac7befd8c", - "sha256:a9495356ca1d66ed197a0f72b41eb1823cf7ea8b5bd07191673e8147aecf8604" + "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", + "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba" ], - "version": "==0.4.7" + "version": "==0.4.8" }, "pyasn1-modules": { "hashes": [ - "sha256:0c35a52e00b672f832e5846826f1fb7507907f7d52fba6faa9e3c4cbe874fe4b", - "sha256:b6ada4f840fe51abf5a6bd545b45bf537bea62221fa0dde2e8a553ed9f06a4e3" + "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e", + "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74" ], - "version": "==0.2.7" + "version": "==0.2.8" }, "pycparser": { "hashes": [ - "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3" + "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", + "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" ], - "version": "==2.19" + "version": "==2.20" }, "pygments": { "hashes": [ - "sha256:71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127", - "sha256:881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297" + "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44", + "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324" ], - "version": "==2.4.2" + "version": "==2.6.1" }, "pyjwt": { "hashes": [ @@ -628,24 +610,24 @@ }, "python-box": { "hashes": [ - "sha256:7d091f833680a02b6f1fc6c062ea8f6e5ece28b30b13675b2cdb8d9eb07db918", - "sha256:ff2ccbbc06b9b8cfb4d00e3113079bfcd4a050365ad9aa774cb3003da705ad06" + "sha256:694a7555e3ff9fbbce734bbaef3aad92b8e4ed0659d3ed04d56b6a0a0eff26a9", + "sha256:a71d3dc9dbaa34c8597d3517c89a8041bd62fa875f23c0f3dad55e1958e3ce10" ], - "version": "==3.4.5" + "version": "==3.4.6" }, "python-dateutil": { "hashes": [ - "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", - "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" + "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", + "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" ], - "version": "==2.8.0" + "version": "==2.8.1" }, "python-dotenv": { "hashes": [ - "sha256:debd928b49dbc2bf68040566f55cdb3252458036464806f4094487244e2a4093", - "sha256:f157d71d5fec9d4bd5f51c82746b6344dffa680ee85217c123f4a0c8117c4544" + "sha256:81822227f771e0cab235a2939f0f265954ac4763cafd806d845801c863bf372f", + "sha256:92b3123fb2d58a284f76cc92bfe4ee6c502c32ded73e8b051c4f6afc8b6751ed" ], - "version": "==0.10.3" + "version": "==0.12.0" }, "python-editor": { "hashes": [ @@ -664,38 +646,17 @@ }, "redis": { "hashes": [ - "sha256:1cfb8c2e5991699a186c3b32e54755deb27e9769f3ae8ad4303ac28bc2bbf7d4", - "sha256:e4ee2c1e1c4b2bbde47bbfc02c7fbd5d3819bb858c2338746fc72f08cc4f93fa" + "sha256:0dcfb335921b88a850d461dc255ff4708294943322bd55de6cfd68972490ca1f", + "sha256:b205cffd05ebfd0a468db74f0eedbff8df1a7bfc47521516ade4692991bb0833" ], - "version": "==3.3.9" + "version": "==3.4.1" }, "requests": { "hashes": [ - "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", - "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" + "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee", + "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6" ], - "version": "==2.22.0" - }, - "rq": { - "hashes": [ - "sha256:2798d26a7b850e759f23f69695a389d676a9c08f2c14f96f0d34d9648c9d5616", - "sha256:4f27c6a690d1bd02b9157e615d8819555b9b359c0c4ec8ff0013d160c31b40bb" - ], - "version": "==1.1.0" - }, - "rq-dashboard": { - "hashes": [ - "sha256:f08bd2431ddc337e51b8887cb7198f8dbc06cabcbb2dd9420d9727f249dbad2c" - ], - "index": "pypi", - "version": "==0.5.3" - }, - "rq-scheduler": { - "hashes": [ - "sha256:06038a42d33d653f89d534ba3bb95694b9d82b39fdd17c6ac6d9bf77d1acdefb", - "sha256:9f9f68d0a4749c83f023d903e148b81da2191229e25ac644a9ff9d6eac31bff4" - ], - "version": "==0.9.1" + "version": "==2.23.0" }, "rsa": { "hashes": [ @@ -709,39 +670,40 @@ "flask" ], "hashes": [ - "sha256:15e51e74b924180c98bcd636cb4634945b0a99a124d50b433c3a9dc6a582e8db", - "sha256:1d6a2ee908ec6d8f96c27d78bc39e203df4d586d287c233140af7d8d1aca108a" + "sha256:23808d571d2461a4ce3784ec12bbee5bdb8c026c143fe79d36cef8a6d653e71f", + "sha256:bb90a4e19c7233a580715fc986cc44be2c48fc10b31e71580a2037e1c94b6950" ], "index": "pypi", - "version": "==0.12.3" + "version": "==0.14.3" }, "six": { "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", + "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c" ], - "version": "==1.12.0" + "version": "==1.14.0" }, "sqlalchemy": { "hashes": [ - "sha256:0f0768b5db594517e1f5e1572c73d14cf295140756431270d89496dc13d5e46c" + "sha256:c4cca4aed606297afbe90d4306b49ad3a4cd36feb3f87e4bfd655c57fd9ef445" ], - "version": "==1.3.10" + "version": "==1.3.15" }, "sqlalchemy-mixins": { "hashes": [ - "sha256:e2f5c6f552925084668841deb0d7c633b7523d5ae072b1f1ddf1a285da73cb14" + "sha256:784a4dad4779c1acf33c4897001c1acbf35f5c37ffd537b596fbde138ca1693e", + "sha256:97bc52ef5e5b5f226130c8d3d667d600c083cc1627e2a1417462c7b10f8f3cd5" ], "index": "pypi", - "version": "==1.1" + "version": "==1.2.1" }, "structlog": { "hashes": [ - "sha256:5feae03167620824d3ae3e8915ea8589fc28d1ad6f3edf3cc90ed7c7cb33fab5", - "sha256:db441b81c65b0f104a7ce5d86c5432be099956b98b8a2c8be0b3fb3a7a0b1536" + "sha256:7a48375db6274ed1d0ae6123c486472aa1d0890b08d314d2b016f3aa7f35990b", + "sha256:8a672be150547a93d90a7d74229a29e765be05bd156a35cdcc527ebf68e9af92" ], "index": "pypi", - "version": "==19.1.0" + "version": "==20.1.0" }, "text-unidecode": { "hashes": [ @@ -764,20 +726,12 @@ ], "version": "==4.3.3" }, - "typing": { - "hashes": [ - "sha256:91dfe6f3f706ee8cc32d38edbbf304e9b7583fb37108fef38229617f8b3eba23", - "sha256:c8cabb5ab8945cd2f54917be357d134db9cc1eb039e59d1606dc1e60cb1d9d36", - "sha256:f38d83c5a7a7086543a0f649564d661859c5146a85775ab90c0d2f93ffaa9714" - ], - "version": "==3.7.4.1" - }, "urllib3": { "hashes": [ - "sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398", - "sha256:9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86" + "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc", + "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc" ], - "version": "==1.25.6" + "version": "==1.25.8" }, "vine": { "hashes": [ @@ -788,98 +742,83 @@ }, "wcwidth": { "hashes": [ - "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", - "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c" + "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1", + "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1" ], - "version": "==0.1.7" + "version": "==0.1.9" }, "werkzeug": { "hashes": [ - "sha256:7280924747b5733b246fe23972186c6b348f9ae29724135a6dfc1e53cea433e7", - "sha256:e5f4a1f98b52b18a93da705a7458e55afb26f32bff83ff5d19189f92462d65c4" + "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43", + "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c" ], - "version": "==0.16.0" + "version": "==1.0.1" }, "zipp": { "hashes": [ - "sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", - "sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335" + "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b", + "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96" ], - "version": "==0.6.0" + "version": "==3.1.0" } }, "develop": { - "argh": { - "hashes": [ - "sha256:a9b3aaa1904eeb78e32394cd46c6f37ac0fb4af6dc488daa58971bdc7d7fcaf3", - "sha256:e9535b8c84dc9571a48999094fda7f33e63c3f1b74f3e5f3ac0105a58405bb65" - ], - "version": "==0.26.2" - }, - "atomicwrites": { - "hashes": [ - "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", - "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6" - ], - "version": "==1.3.0" - }, "attrs": { "hashes": [ - "sha256:ec20e7a4825331c1b5ebf261d111e16fa9612c1f7a5e1f884f12bd53a664dfd2", - "sha256:f913492e1663d3c36f502e5e9ba6cd13cf19d7fab50aa13239e420fef95e1396" + "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", + "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" ], - "version": "==19.2.0" + "version": "==19.3.0" }, "click": { "hashes": [ - "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", - "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" + "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc", + "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a" ], - "version": "==7.0" + "version": "==7.1.1" }, "colorama": { "hashes": [ - "sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", - "sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48" + "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", + "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1" ], - "version": "==0.4.1" + "version": "==0.4.3" }, "coverage": { "hashes": [ - "sha256:08907593569fe59baca0bf152c43f3863201efb6113ecb38ce7e97ce339805a6", - "sha256:0be0f1ed45fc0c185cfd4ecc19a1d6532d72f86a2bac9de7e24541febad72650", - "sha256:141f08ed3c4b1847015e2cd62ec06d35e67a3ac185c26f7635f4406b90afa9c5", - "sha256:19e4df788a0581238e9390c85a7a09af39c7b539b29f25c89209e6c3e371270d", - "sha256:23cc09ed395b03424d1ae30dcc292615c1372bfba7141eb85e11e50efaa6b351", - "sha256:245388cda02af78276b479f299bbf3783ef0a6a6273037d7c60dc73b8d8d7755", - "sha256:331cb5115673a20fb131dadd22f5bcaf7677ef758741312bee4937d71a14b2ef", - "sha256:386e2e4090f0bc5df274e720105c342263423e77ee8826002dcffe0c9533dbca", - "sha256:3a794ce50daee01c74a494919d5ebdc23d58873747fa0e288318728533a3e1ca", - "sha256:60851187677b24c6085248f0a0b9b98d49cba7ecc7ec60ba6b9d2e5574ac1ee9", - "sha256:63a9a5fc43b58735f65ed63d2cf43508f462dc49857da70b8980ad78d41d52fc", - "sha256:6b62544bb68106e3f00b21c8930e83e584fdca005d4fffd29bb39fb3ffa03cb5", - "sha256:6ba744056423ef8d450cf627289166da65903885272055fb4b5e113137cfa14f", - "sha256:7494b0b0274c5072bddbfd5b4a6c6f18fbbe1ab1d22a41e99cd2d00c8f96ecfe", - "sha256:826f32b9547c8091679ff292a82aca9c7b9650f9fda3e2ca6bf2ac905b7ce888", - "sha256:93715dffbcd0678057f947f496484e906bf9509f5c1c38fc9ba3922893cda5f5", - "sha256:9a334d6c83dfeadae576b4d633a71620d40d1c379129d587faa42ee3e2a85cce", - "sha256:af7ed8a8aa6957aac47b4268631fa1df984643f07ef00acd374e456364b373f5", - "sha256:bf0a7aed7f5521c7ca67febd57db473af4762b9622254291fbcbb8cd0ba5e33e", - "sha256:bf1ef9eb901113a9805287e090452c05547578eaab1b62e4ad456fcc049a9b7e", - "sha256:c0afd27bc0e307a1ffc04ca5ec010a290e49e3afbe841c5cafc5c5a80ecd81c9", - "sha256:dd579709a87092c6dbee09d1b7cfa81831040705ffa12a1b248935274aee0437", - "sha256:df6712284b2e44a065097846488f66840445eb987eb81b3cc6e4149e7b6982e1", - "sha256:e07d9f1a23e9e93ab5c62902833bf3e4b1f65502927379148b6622686223125c", - "sha256:e2ede7c1d45e65e209d6093b762e98e8318ddeff95317d07a27a2140b80cfd24", - "sha256:e4ef9c164eb55123c62411f5936b5c2e521b12356037b6e1c2617cef45523d47", - "sha256:eca2b7343524e7ba246cab8ff00cab47a2d6d54ada3b02772e908a45675722e2", - "sha256:eee64c616adeff7db37cc37da4180a3a5b6177f5c46b187894e633f088fb5b28", - "sha256:ef824cad1f980d27f26166f86856efe11eff9912c4fed97d3804820d43fa550c", - "sha256:efc89291bd5a08855829a3c522df16d856455297cf35ae827a37edac45f466a7", - "sha256:fa964bae817babece5aa2e8c1af841bebb6d0b9add8e637548809d040443fee0", - "sha256:ff37757e068ae606659c28c3bd0d923f9d29a85de79bf25b2b34b148473b5025" - ], - "version": "==4.5.4" + "sha256:03f630aba2b9b0d69871c2e8d23a69b7fe94a1e2f5f10df5049c0df99db639a0", + "sha256:046a1a742e66d065d16fb564a26c2a15867f17695e7f3d358d7b1ad8a61bca30", + "sha256:0a907199566269e1cfa304325cc3b45c72ae341fbb3253ddde19fa820ded7a8b", + "sha256:165a48268bfb5a77e2d9dbb80de7ea917332a79c7adb747bd005b3a07ff8caf0", + "sha256:1b60a95fc995649464e0cd48cecc8288bac5f4198f21d04b8229dc4097d76823", + "sha256:1f66cf263ec77af5b8fe14ef14c5e46e2eb4a795ac495ad7c03adc72ae43fafe", + "sha256:2e08c32cbede4a29e2a701822291ae2bc9b5220a971bba9d1e7615312efd3037", + "sha256:3844c3dab800ca8536f75ae89f3cf566848a3eb2af4d9f7b1103b4f4f7a5dad6", + "sha256:408ce64078398b2ee2ec08199ea3fcf382828d2f8a19c5a5ba2946fe5ddc6c31", + "sha256:443be7602c790960b9514567917af538cac7807a7c0c0727c4d2bbd4014920fd", + "sha256:4482f69e0701139d0f2c44f3c395d1d1d37abd81bfafbf9b6efbe2542679d892", + "sha256:4a8a259bf990044351baf69d3b23e575699dd60b18460c71e81dc565f5819ac1", + "sha256:513e6526e0082c59a984448f4104c9bf346c2da9961779ede1fc458e8e8a1f78", + "sha256:5f587dfd83cb669933186661a351ad6fc7166273bc3e3a1531ec5c783d997aac", + "sha256:62061e87071497951155cbccee487980524d7abea647a1b2a6eb6b9647df9006", + "sha256:641e329e7f2c01531c45c687efcec8aeca2a78a4ff26d49184dce3d53fc35014", + "sha256:65a7e00c00472cd0f59ae09d2fb8a8aaae7f4a0cf54b2b74f3138d9f9ceb9cb2", + "sha256:6ad6ca45e9e92c05295f638e78cd42bfaaf8ee07878c9ed73e93190b26c125f7", + "sha256:73aa6e86034dad9f00f4bbf5a666a889d17d79db73bc5af04abd6c20a014d9c8", + "sha256:7c9762f80a25d8d0e4ab3cb1af5d9dffbddb3ee5d21c43e3474c84bf5ff941f7", + "sha256:85596aa5d9aac1bf39fe39d9fa1051b0f00823982a1de5766e35d495b4a36ca9", + "sha256:86a0ea78fd851b313b2e712266f663e13b6bc78c2fb260b079e8b67d970474b1", + "sha256:8a620767b8209f3446197c0e29ba895d75a1e272a36af0786ec70fe7834e4307", + "sha256:922fb9ef2c67c3ab20e22948dcfd783397e4c043a5c5fa5ff5e9df5529074b0a", + "sha256:9fad78c13e71546a76c2f8789623eec8e499f8d2d799f4b4547162ce0a4df435", + "sha256:a37c6233b28e5bc340054cf6170e7090a4e85069513320275a4dc929144dccf0", + "sha256:c3fc325ce4cbf902d05a80daa47b645d07e796a80682c1c5800d6ac5045193e5", + "sha256:cda33311cb9fb9323958a69499a667bd728a39a7aa4718d7622597a44c4f1441", + "sha256:db1d4e38c9b15be1521722e946ee24f6db95b189d1447fa9ff18dd16ba89f732", + "sha256:eda55e6e9ea258f5e4add23bcf33dc53b2c319e70806e180aecbff8d90ea24de", + "sha256:f372cdbb240e09ee855735b9d85e7f50730dcfb6296b74b95a3e5dea0615c4c1" + ], + "version": "==5.0.4" }, "docopt": { "hashes": [ @@ -897,39 +836,41 @@ }, "faker": { "hashes": [ - "sha256:45cc9cca3de8beba5a2da3bd82a6e5544f53da1a702645c8485f682366c15026", - "sha256:a6459ff518d1fc6ee2238a7209e6c899517872c7e1115510279033ffe6fe8ef3" + "sha256:2d3f866ef25e1a5af80e7b0ceeacc3c92dec5d0fdbad3e2cb6adf6e60b22188f", + "sha256:b89aa33837498498e15c709eb40c31386408a901a53c7a5e12a425737a767976" ], - "version": "==2.0.2" + "version": "==4.0.2" }, "fakeredis": { "hashes": [ - "sha256:1993b88bd629b1d651312757aa091a93612ae8772777e1a441bae81e7b013e25", - "sha256:3e1bfb9de5a5ab5796b6101fbe7927fe1456fa8e72cbcd3625c9437e278bf581" + "sha256:bcb2faeabb1bd7ff2fecaff9b2a47ebfaf31700ee260a2ef66c6cf041d7a78df", + "sha256:d7e8b198ce5cce85374c6bca69bf372cc090d33938f0453c4262df7b281c7c88" ], "index": "pypi", - "version": "==1.0.5" + "version": "==1.4.0" }, "fancycompleter": { "hashes": [ - "sha256:d2522f1f3512371f295379c4c0d1962de06762eb586c199620a2a5d423539b12" + "sha256:09e0feb8ae242abdfd7ef2ba55069a46f011814a80fe5476be48f51b00247272", + "sha256:dd076bca7d9d524cc7f25ec8f35ef95388ffef9ef46def4d3d25e9b044ad7080" ], - "version": "==0.8" + "version": "==0.9.1" }, "flask": { "hashes": [ - "sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52", - "sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6" + "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060", + "sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557" ], "index": "pypi", - "version": "==1.1.1" + "version": "==1.1.2" }, "importlib-metadata": { "hashes": [ - "sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26", - "sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af" + "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f", + "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e" ], - "version": "==0.23" + "markers": "python_version < '3.8'", + "version": "==1.6.0" }, "inflection": { "hashes": [ @@ -946,10 +887,10 @@ }, "jinja2": { "hashes": [ - "sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f", - "sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de" + "sha256:93187ffbc7808079673ef52771baa950426fd664d3aad1d0fa3e95644360e250", + "sha256:b0eaf100007721b5c16c1fc1eecb87409464edc10469ddc9a22a27a99123be49" ], - "version": "==2.10.3" + "version": "==2.11.1" }, "markupsafe": { "hashes": [ @@ -957,13 +898,16 @@ "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", + "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", + "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", + "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", @@ -980,31 +924,33 @@ "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", - "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7" + "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", + "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", + "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be" ], "version": "==1.1.1" }, "mock": { "hashes": [ - "sha256:83657d894c90d5681d62155c82bda9c1187827525880eda8ff5df4ec813437c3", - "sha256:d157e52d4e5b938c550f39eb2fd15610db062441a9c2747d3dbfa9298211d0f8" + "sha256:3f9b2c0196c60d21838f307f5825a7b86b678cedc58ab9e50a8988187b4d81e0", + "sha256:dd33eb70232b6118298d516bbcecd26704689c386594f0f3c4f13867b2c56f72" ], "index": "pypi", - "version": "==3.0.5" + "version": "==4.0.2" }, "more-itertools": { "hashes": [ - "sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", - "sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4" + "sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c", + "sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507" ], - "version": "==7.2.0" + "version": "==8.2.0" }, "packaging": { "hashes": [ - "sha256:28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47", - "sha256:d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108" + "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3", + "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752" ], - "version": "==19.2" + "version": "==20.3" }, "pathtools": { "hashes": [ @@ -1021,39 +967,45 @@ }, "pluggy": { "hashes": [ - "sha256:0db4b7601aae1d35b4a033282da476845aa19185c1e6964b25cf324b5e4ec3e6", - "sha256:fa5fa1622fa6dd5c030e9cad086fa19ef6a0cf6d7a2d12318e10cb49d6d68f34" + "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", + "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" ], - "version": "==0.13.0" + "version": "==0.13.1" }, "py": { "hashes": [ - "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", - "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53" + "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa", + "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0" ], - "version": "==1.8.0" + "version": "==1.8.1" }, "pygments": { "hashes": [ - "sha256:71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127", - "sha256:881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297" + "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44", + "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324" ], - "version": "==2.4.2" + "version": "==2.6.1" }, "pyparsing": { "hashes": [ - "sha256:6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80", - "sha256:d9338df12903bbf5d65a0e4e87c2161968b10d2e489652bb47001d82a9b028b4" + "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f", + "sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec" + ], + "version": "==2.4.6" + }, + "pyrepl": { + "hashes": [ + "sha256:292570f34b5502e871bbb966d639474f2b57fbfcd3373c2d6a2f3d56e681a775" ], - "version": "==2.4.2" + "version": "==0.9.0" }, "pytest": { "hashes": [ - "sha256:4a784f1d4f2ef198fe9b7aef793e9fa1a3b2f84e822d9b3a64a181293a572d45", - "sha256:926855726d8ae8371803f7b2e6ec0a69953d9c6311fa7c3b6c1b929ff92d27da" + "sha256:0e5b30f5cb04e887b91b1ee519fa3d89049595f428c1db76e73bd7f17b09b172", + "sha256:84dde37075b8805f3d1f392cc47e38a0e59518fb46a431cfdaf7cf1ce805f970" ], "index": "pypi", - "version": "==4.6.3" + "version": "==5.4.1" }, "pytest-factoryboy": { "hashes": [ @@ -1064,35 +1016,35 @@ }, "pytest-flask": { "hashes": [ - "sha256:283730b469604ecb94caac28df99a40b7c785b828dd8d3323596718b51dfaeb2", - "sha256:d874781b622210d8c5d8061cdb091cb059fcb12203125110bd8e6f9256ccbf49" + "sha256:44948d3feab48c69e89b087129cc4db66bad9cb5aa472c08dfc798c69f4eac67", + "sha256:4d5678a045c07317618d80223ea124e21e8acc89dae109542dd1fdf6783d96c2" ], "index": "pypi", - "version": "==0.15.0" + "version": "==1.0.0" }, "pytest-mock": { "hashes": [ - "sha256:34520283d459cdf1d0dbb58a132df804697f1b966ecedf808bbf3d255af8f659", - "sha256:f1ab8aefe795204efe7a015900296d1719e7bf0f4a0558d71e8599da1d1309d0" + "sha256:98e02534f170e4f37d7e1abdfc5973fd4207aa609582291717f643764e71c925", + "sha256:a4494016753a30231f8519bfd160242a0f3c8fb82ca36e7b6f82a7fb602ac6b8" ], "index": "pypi", - "version": "==1.11.1" + "version": "==3.0.0" }, "pytest-structlog": { "hashes": [ - "sha256:41154e912f4210e42a8b944eef2b83397a0315417563861257118e53051f1947", - "sha256:978f2e7d98d14d5addf4e4caaf9216ceac75bbe9e527d46eed0441a1116c3300" + "sha256:2b76b39aa53d6bcfd8cd5082fa82e759a0be4b58f4dbfad6bceeeee77ee63813", + "sha256:82b387afbdbe343c31e5454deb99a55fd9b876d0c552392bbe0670b26e385ef1" ], "index": "pypi", - "version": "==0.1" + "version": "==0.2" }, "pytest-testmon": { "hashes": [ - "sha256:f542d168103d14748be7afd45071df2f1b1dda218e33dedd300a87885bffc2f4", - "sha256:f622fd9d0f5a0df253f0e6773713c3df61306b64abdfb202d39a85dcba1d1f59" + "sha256:e79852203894bbd5a6adb7e0541316c0a3a84322e9766f746ed6b8b62f9897d9", + "sha256:fdb016d953036051d1ef0e36569b7168cefa4914014789a65a4ffefc87f85ac5" ], "index": "pypi", - "version": "==0.9.19" + "version": "==1.0.2" }, "pytest-watch": { "hashes": [ @@ -1103,42 +1055,24 @@ }, "python-dateutil": { "hashes": [ - "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", - "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" - ], - "version": "==2.8.0" - }, - "pyyaml": { - "hashes": [ - "sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", - "sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", - "sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", - "sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", - "sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", - "sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", - "sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", - "sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", - "sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", - "sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", - "sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", - "sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", - "sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8" + "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", + "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" ], - "version": "==5.1.2" + "version": "==2.8.1" }, "redis": { "hashes": [ - "sha256:1cfb8c2e5991699a186c3b32e54755deb27e9769f3ae8ad4303ac28bc2bbf7d4", - "sha256:e4ee2c1e1c4b2bbde47bbfc02c7fbd5d3819bb858c2338746fc72f08cc4f93fa" + "sha256:0dcfb335921b88a850d461dc255ff4708294943322bd55de6cfd68972490ca1f", + "sha256:b205cffd05ebfd0a468db74f0eedbff8df1a7bfc47521516ade4692991bb0833" ], - "version": "==3.3.9" + "version": "==3.4.1" }, "six": { "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", + "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c" ], - "version": "==1.12.0" + "version": "==1.14.0" }, "sortedcontainers": { "hashes": [ @@ -1149,11 +1083,11 @@ }, "structlog": { "hashes": [ - "sha256:5feae03167620824d3ae3e8915ea8589fc28d1ad6f3edf3cc90ed7c7cb33fab5", - "sha256:db441b81c65b0f104a7ce5d86c5432be099956b98b8a2c8be0b3fb3a7a0b1536" + "sha256:7a48375db6274ed1d0ae6123c486472aa1d0890b08d314d2b016f3aa7f35990b", + "sha256:8a672be150547a93d90a7d74229a29e765be05bd156a35cdcc527ebf68e9af92" ], "index": "pypi", - "version": "==19.1.0" + "version": "==20.1.0" }, "text-unidecode": { "hashes": [ @@ -1164,23 +1098,23 @@ }, "watchdog": { "hashes": [ - "sha256:965f658d0732de3188211932aeb0bb457587f04f63ab4c1e33eab878e9de961d" + "sha256:c560efb643faed5ef28784b2245cf8874f939569717a4a12826a173ac644456b" ], - "version": "==0.9.0" + "version": "==0.10.2" }, "wcwidth": { "hashes": [ - "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", - "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c" + "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1", + "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1" ], - "version": "==0.1.7" + "version": "==0.1.9" }, "werkzeug": { "hashes": [ - "sha256:7280924747b5733b246fe23972186c6b348f9ae29724135a6dfc1e53cea433e7", - "sha256:e5f4a1f98b52b18a93da705a7458e55afb26f32bff83ff5d19189f92462d65c4" + "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43", + "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c" ], - "version": "==0.16.0" + "version": "==1.0.1" }, "wmctrl": { "hashes": [ @@ -1190,10 +1124,10 @@ }, "zipp": { "hashes": [ - "sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", - "sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335" + "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b", + "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96" ], - "version": "==0.6.0" + "version": "==3.1.0" } } } diff --git a/api/tests/conftest.py b/api/tests/conftest.py index 98602e4..78d27a0 100644 --- a/api/tests/conftest.py +++ b/api/tests/conftest.py @@ -5,124 +5,122 @@ from unittest.mock import PropertyMock from api.app import create_app, db -from api.models import BaseModel import api.tests.factories as f -from werkzeug import FileStorage TEST_WEBM_PATH = "./api/tests/data/test.webm" @pytest.fixture(scope="session") def app(request): - app = create_app(__name__) - ctx = app.app_context() - ctx.push() + app = create_app(__name__) + ctx = app.app_context() + ctx.push() - def teardown(): - ctx.pop() + def teardown(): + ctx.pop() - request.addfinalizer(teardown) - return app + request.addfinalizer(teardown) + return app @pytest.fixture(autouse=True, scope="function") def session(app, request): - # No committing during tests - db.session.commit = db.session.flush + # No committing during tests + db.session.commit = db.session.flush - def teardown(): - db.session.rollback() + def teardown(): + db.session.rollback() - request.addfinalizer(teardown) - return db.session + request.addfinalizer(teardown) + return db.session @pytest.fixture(scope="function") def super_admin(): - return f.UserFactory( - email="super_admin@example.com", password="hunter2", role="super_admin" - ).save() + return f.UserFactory( + email="super_admin@example.com", password="hunter2", role="super_admin" + ).save() @pytest.fixture(scope="function") def admin(): - return f.UserFactory( - email="admin@example.com", password="hunter2", role="admin" - ).save() + return f.UserFactory( + email="admin@example.com", password="hunter2", role="admin" + ).save() @pytest.fixture(scope="function") def user(): - return f.UserFactory( - email="test-user@example.com", password="hunter2", role="user" - ).save() + return f.UserFactory( + email="test-user@example.com", password="hunter2", role="user" + ).save() @pytest.fixture(scope="function") def end_user(): - return f.UserFactory( - email="test-end-user@example.com", password="hunter2", role="end_user" - ).save() + return f.UserFactory( + email="test-end-user@example.com", password="hunter2", role="end_user" + ).save() @pytest.fixture(scope="function") def super_admin_access_token(super_admin): - return super_admin.create_access_token_with_claims() + return super_admin.create_access_token_with_claims() @pytest.fixture(scope="function") def admin_access_token(admin): - return admin.create_access_token_with_claims() + return admin.create_access_token_with_claims() @pytest.fixture(scope="function") def access_token(user): - return user.create_access_token_with_claims() + return user.create_access_token_with_claims() @pytest.fixture(scope="function") def refresh_token(user): - return user.create_refresh_token() + return user.create_refresh_token() @pytest.fixture(scope="function") def video(): - return f.VideoFactory.create().save() + return f.VideoFactory.create().save() @pytest.fixture(scope="function") def challenge(): - return f.ChallengeFactory.create().save() + return f.ChallengeFactory.create().save() @pytest.fixture(scope="function") def response(): - return f.ResponseFactory.create().save() + return f.ResponseFactory.create().save() @pytest.fixture def video_blob(): - return open(TEST_WEBM_PATH, "rb") + return open(TEST_WEBM_PATH, "rb") def save_video_into(file_path): - shutil.copy(TEST_WEBM_PATH, file_path) + shutil.copy(TEST_WEBM_PATH, file_path) def check_file_exists(file_path, **kwargs): - if os.path.getsize(file_path) == 0: - raise ValueError("file is empty, {}".format(file_path)) + if os.path.getsize(file_path) == 0: + raise ValueError("file is empty, {}".format(file_path)) @pytest.fixture(autouse=True) def mock_storage(mocker): - _storage = mocker.patch("api.models.storage") - type( - _storage.Client.return_value.get_bucket.return_value.blob.return_value - ).public_url = PropertyMock(return_value=str(uuid.uuid4())) - _storage.Client().get_bucket().blob().download_to_filename.side_effect = ( - save_video_into - ) - _storage.Client().get_bucket().blob().upload_from_filename.side_effect = ( - check_file_exists - ) + _storage = mocker.patch("api.models.storage") + type( + _storage.Client.return_value.get_bucket.return_value.blob.return_value + ).public_url = PropertyMock(return_value=str(uuid.uuid4())) + _storage.Client().get_bucket().blob().download_to_filename.side_effect = ( + save_video_into + ) + _storage.Client().get_bucket().blob().upload_from_filename.side_effect = ( + check_file_exists + ) From 05470f79ae0e1f3512173996853a5ab1f744dc4d Mon Sep 17 00:00:00 2001 From: Alex Warren Date: Sun, 5 Apr 2020 09:59:47 -0700 Subject: [PATCH 12/14] Revert "Fix Werkzeug FileStorage import error while pytest" This reverts commit f887b6ef8c5b7de2434ef0e110643a2a5dd7804c. --- Pipfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Pipfile b/Pipfile index 6da6a23..9e74085 100644 --- a/Pipfile +++ b/Pipfile @@ -40,7 +40,6 @@ pillow = "*" jsonpatch = "*" ffmpeg-python = "*" celery = {extras = ["redis"],version = "*"} -Werkzeug = ">=0.16.1" # Needed because of a bug in .16 see: https://github.com/OpenAPITools/openapi-generator/issues/5235 [requires] python_version = "3.7" From df8774a7a6dabd59b437b357568f0c67c9d24582 Mon Sep 17 00:00:00 2001 From: Alex Warren Date: Sun, 5 Apr 2020 14:53:05 -0700 Subject: [PATCH 13/14] Fix FLASK environment vars, migrate helm 2>3 --- api/app.py | 253 +++++++++++++++++++-------------------- k8s/human-api/Chart.yaml | 2 +- k8s/tiller.yaml | 21 +--- 3 files changed, 130 insertions(+), 146 deletions(-) diff --git a/api/app.py b/api/app.py index 7554050..fc038cc 100644 --- a/api/app.py +++ b/api/app.py @@ -1,30 +1,29 @@ -import os import sys import uuid -import json import logging import structlog from dynaconf import FlaskDynaconf -from flask import Flask, request +from flask import Flask from flask_bcrypt import Bcrypt from flask_cors import CORS from flask_marshmallow import Marshmallow from flask_migrate import Migrate from flask_sqlalchemy import SQLAlchemy as _BaseSQLAlchemy -from flask_marshmallow import Marshmallow from sqlalchemy.exc import DatabaseError from sqlalchemy_mixins import AllFeaturesMixin import sentry_sdk from sentry_sdk.integrations.flask import FlaskIntegration -from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration + +# TODO: Hook up sentry with sqlalchemy +# from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration class SQLAlchemy(_BaseSQLAlchemy): - def apply_pool_defaults(self, app, options): - super(SQLAlchemy, self).apply_pool_defaults(app, options) - options["pool_pre_ping"] = True + def apply_pool_defaults(self, app, options): + super(SQLAlchemy, self).apply_pool_defaults(app, options) + options["pool_pre_ping"] = True db = SQLAlchemy() @@ -33,144 +32,144 @@ def apply_pool_defaults(self, app, options): class BaseModel(db.Model, AllFeaturesMixin): - __abstract__ = True - pass + __abstract__ = True + pass def create_app(name=__name__): - import api.routes as routes - import api.resources as resources - from api.jobs import celery - - app = Flask(name) - FlaskDynaconf(app, ENVVAR_PREFIX_FOR_DYNACONF="FLASK_") # Initialize config - config_logging(app) - config_sentry(app) - config_db(app) - config_redis(app) - - cors = CORS(app, resources={r"/api/*": {"origins": app.config["ALLOWED_ORIGINS"]}}) - - db.init_app(app) # This needs to come before Marshmallow - BaseModel.set_session(db.session) - migrate = Migrate(app, db) - ma = Marshmallow(app) - routes.api.init_app(app) - resources.jwt.init_app(app) - bcrypt.init_app(app) - celery.conf.update(app.config) - - @app.before_request - def set_request_id(): - logger.new(request_id=str(uuid.uuid4())) - - @app.route("/healthcheck") - def healthcheck(): - return "ok" - - @app.route("/version") - def version(): - return app.config["GIT_COMMIT_SHA"] - - @app.route("/encode_a") - def encode_a(): - from api.jobs import ingest_local_video2 - - ingest_local_video2.queue("async") - return "ok", 201 - - @app.route("/encode_c") - def encode_c(): - from api.jobs import ingest_local_video2 - - ingest_local_video2.delay("celery") - return "ok", 201 - - @app.route("/encode") - def encode(): - from api.jobs import ingest_local_video2 - - ingest_local_video2("sync") - return "ok", 201 - - @app.shell_context_processor - def make_shell_context(): - """ + import api.routes as routes + import api.resources as resources + from api.jobs import celery + + app = Flask(name) + FlaskDynaconf(app, ENVVAR_PREFIX_FOR_DYNACONF="FLASK") # Initialize config + config_logging(app) + config_sentry(app) + config_db(app) + config_redis(app) + + cors = CORS(app, resources={r"/api/*": {"origins": app.config["ALLOWED_ORIGINS"]}}) + + db.init_app(app) # This needs to come before Marshmallow + BaseModel.set_session(db.session) + migrate = Migrate(app, db) + ma = Marshmallow(app) + routes.api.init_app(app) + resources.jwt.init_app(app) + bcrypt.init_app(app) + celery.conf.update(app.config) + + @app.before_request + def set_request_id(): + logger.new(request_id=str(uuid.uuid4())) + + @app.route("/healthcheck") + def healthcheck(): + return "ok" + + @app.route("/version") + def version(): + return app.config["GIT_COMMIT_SHA"] + + @app.route("/encode_a") + def encode_a(): + from api.jobs import ingest_local_video2 + + ingest_local_video2.queue("async") + return "ok", 201 + + @app.route("/encode_c") + def encode_c(): + from api.jobs import ingest_local_video2 + + ingest_local_video2.delay("celery") + return "ok", 201 + + @app.route("/encode") + def encode(): + from api.jobs import ingest_local_video2 + + ingest_local_video2("sync") + return "ok", 201 + + @app.shell_context_processor + def make_shell_context(): + """ Adds these to the global scope of the shell for more convenient prototyping/debugging in the shell """ - from api.utils import module_classes_as_dict - - return { - "db": db, - **module_classes_as_dict("api.models"), - **module_classes_as_dict("api.schemas"), - **module_classes_as_dict("api.tests.factories"), - } - - @app.after_request - def session_commit(res): - res.headers["X-HF-git-commit-sha"] = app.config["GIT_COMMIT_SHA"] - if res.status_code >= 400: - return res - try: - db.session.commit() - return res - except DatabaseError: - db.session.rollback() - raise - - return app + from api.utils import module_classes_as_dict + + return { + "db": db, + **module_classes_as_dict("api.models"), + **module_classes_as_dict("api.schemas"), + **module_classes_as_dict("api.tests.factories"), + } + + @app.after_request + def session_commit(res): + res.headers["X-HF-git-commit-sha"] = app.config["GIT_COMMIT_SHA"] + if res.status_code >= 400: + return res + try: + db.session.commit() + return res + except DatabaseError: + db.session.rollback() + raise + + return app def config_logging(app): - logging.basicConfig( - format="%(message)s", stream=sys.stdout, level=logging.INFO - ) # TODO make level configurable + logging.basicConfig( + format="%(message)s", stream=sys.stdout, level=logging.INFO + ) # TODO make level configurable - processors = [ - structlog.processors.KeyValueRenderer(key_order=["event", "request_id"]) - ] + processors = [ + structlog.processors.KeyValueRenderer(key_order=["event", "request_id"]) + ] - structlog.configure( - processors=processors, - context_class=structlog.threadlocal.wrap_dict(dict), - logger_factory=structlog.stdlib.LoggerFactory(), - ) + structlog.configure( + processors=processors, + context_class=structlog.threadlocal.wrap_dict(dict), + logger_factory=structlog.stdlib.LoggerFactory(), + ) def config_db(app): - app.config["SQLALCHEMY_DATABASE_URI"] = "postgresql://{}:{}@{}/{}".format( - app.config["DB_USER"], - app.config["DB_PASSWORD"], - app.config["DB_HOST"], - app.config["DB_NAME"], - ) + app.config["SQLALCHEMY_DATABASE_URI"] = "postgresql://{}:{}@{}/{}".format( + app.config["DB_USER"], + app.config["DB_PASSWORD"], + app.config["DB_HOST"], + app.config["DB_NAME"], + ) - app.logger.info( - "App configured to talk to DB: %s", - "postgresql://{}:*REDACTED*@{}/{}".format( - app.config["DB_USER"], app.config["DB_HOST"], app.config["DB_NAME"] - ), - ) + app.logger.info( + "App configured to talk to DB: %s", + "postgresql://{}:*REDACTED*@{}/{}".format( + app.config["DB_USER"], app.config["DB_HOST"], app.config["DB_NAME"] + ), + ) def config_redis(app): - from api.utils import get_redis_url + from api.utils import get_redis_url - redis_url = get_redis_url() + redis_url = get_redis_url() - app.logger.info( - "App configured to talk to Redis: %s", - "redis://*REDACTED*@{}:{}/{}".format( - app.config["REDIS_HOST"], app.config["REDIS_PORT"], app.config["REDIS_DB"] - ), - ) + app.logger.info( + "App configured to talk to Redis: %s", + "redis://*REDACTED*@{}:{}/{}".format( + app.config["REDIS_HOST"], app.config["REDIS_PORT"], app.config["REDIS_DB"] + ), + ) def config_sentry(app): - sentry_sdk.init( - app.config["SENTRY_DSN"], - integrations=[FlaskIntegration(transaction_style="url")], - environment=app.config["ENV"], - release=f"human-factor-api@{app.config['GIT_COMMIT_SHA']}", - ) + sentry_sdk.init( + app.config["SENTRY_DSN"], + integrations=[FlaskIntegration(transaction_style="url")], + environment=app.config["ENV"], + release=f"human-factor-api@{app.config['GIT_COMMIT_SHA']}", + ) diff --git a/k8s/human-api/Chart.yaml b/k8s/human-api/Chart.yaml index 7fd351a..6c9189c 100644 --- a/k8s/human-api/Chart.yaml +++ b/k8s/human-api/Chart.yaml @@ -2,4 +2,4 @@ apiVersion: v1 appVersion: "1.1" description: A Helm chart to deploy human-api name: human-api -version: 0.1.0 +version: 0.1.1 diff --git a/k8s/tiller.yaml b/k8s/tiller.yaml index 1fcf47d..06236e0 100644 --- a/k8s/tiller.yaml +++ b/k8s/tiller.yaml @@ -1,18 +1,3 @@ -apiVersion: v1 -kind: ServiceAccount -metadata: - name: tiller - namespace: kube-system ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: tiller -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: cluster-admin -subjects: - - kind: ServiceAccount - name: tiller - namespace: kube-system +# Alex migrated from helm2 to helm3 +# https://github.com/helm/helm-2to3 +# All the releases have been migrated and tiller is uninstalled from K8 \ No newline at end of file From 1d87fdfb1589c8222fb192458ff28a1563ce3e0d Mon Sep 17 00:00:00 2001 From: Alex Warren Date: Sun, 5 Apr 2020 18:13:29 -0700 Subject: [PATCH 14/14] Add more logging and fix flake8 and indenting to 2 --- api/admin/resources/video.py | 26 ++-- api/app.py | 244 +++++++++++++++++------------------ api/jobs.py | 8 +- api/models.py | 12 +- 4 files changed, 145 insertions(+), 145 deletions(-) diff --git a/api/admin/resources/video.py b/api/admin/resources/video.py index 0b622a1..772c618 100644 --- a/api/admin/resources/video.py +++ b/api/admin/resources/video.py @@ -1,8 +1,6 @@ import structlog -from flask import request from flask_restful import Resource -from flask_jwt_extended import jwt_required from api.models import Video from api.jobs import ingest_video @@ -12,20 +10,20 @@ class VideoEncode(Resource): - @super_admin_required - def post(self, video_id): - video = Video.query.get_or_404(video_id) - log.info("Enqueueing for encoding", video_id=video.id) - ingest_video.delay(video.id) + @super_admin_required + def post(self, video_id): + video = Video.query.get_or_404(video_id) + log.info("Enqueueing for encoding", video_id=video.id) + ingest_video.delay(video.id) - return "ok", 201 + return "ok", 201 class VideoEncodeAll(Resource): - @super_admin_required - def post(self): - for video in Video.where(encoded_at=None): - log.info("Enqueueing for encoding", video_id=video.id) - ingest_video.delay(video.id) + @super_admin_required + def post(self): + for video in Video.where(encoded_at=None): + log.info("Enqueueing for encoding", video_id=video.id) + ingest_video.delay(video.id) - return "ok", 201 + return "ok", 201 diff --git a/api/app.py b/api/app.py index fc038cc..d64b1cd 100644 --- a/api/app.py +++ b/api/app.py @@ -21,9 +21,9 @@ class SQLAlchemy(_BaseSQLAlchemy): - def apply_pool_defaults(self, app, options): - super(SQLAlchemy, self).apply_pool_defaults(app, options) - options["pool_pre_ping"] = True + def apply_pool_defaults(self, app, options): + super(SQLAlchemy, self).apply_pool_defaults(app, options) + options["pool_pre_ping"] = True db = SQLAlchemy() @@ -32,144 +32,144 @@ def apply_pool_defaults(self, app, options): class BaseModel(db.Model, AllFeaturesMixin): - __abstract__ = True - pass + __abstract__ = True + pass def create_app(name=__name__): - import api.routes as routes - import api.resources as resources - from api.jobs import celery - - app = Flask(name) - FlaskDynaconf(app, ENVVAR_PREFIX_FOR_DYNACONF="FLASK") # Initialize config - config_logging(app) - config_sentry(app) - config_db(app) - config_redis(app) - - cors = CORS(app, resources={r"/api/*": {"origins": app.config["ALLOWED_ORIGINS"]}}) - - db.init_app(app) # This needs to come before Marshmallow - BaseModel.set_session(db.session) - migrate = Migrate(app, db) - ma = Marshmallow(app) - routes.api.init_app(app) - resources.jwt.init_app(app) - bcrypt.init_app(app) - celery.conf.update(app.config) - - @app.before_request - def set_request_id(): - logger.new(request_id=str(uuid.uuid4())) - - @app.route("/healthcheck") - def healthcheck(): - return "ok" - - @app.route("/version") - def version(): - return app.config["GIT_COMMIT_SHA"] - - @app.route("/encode_a") - def encode_a(): - from api.jobs import ingest_local_video2 - - ingest_local_video2.queue("async") - return "ok", 201 - - @app.route("/encode_c") - def encode_c(): - from api.jobs import ingest_local_video2 - - ingest_local_video2.delay("celery") - return "ok", 201 - - @app.route("/encode") - def encode(): - from api.jobs import ingest_local_video2 - - ingest_local_video2("sync") - return "ok", 201 - - @app.shell_context_processor - def make_shell_context(): - """ + import api.routes as routes + import api.resources as resources + from api.jobs import celery + + app = Flask(name) + FlaskDynaconf(app, ENVVAR_PREFIX_FOR_DYNACONF="FLASK") # Initialize config + config_logging(app) + config_sentry(app) + config_db(app) + config_redis(app) + + cors = CORS(app, resources={r"/api/*": {"origins": app.config["ALLOWED_ORIGINS"]}}) + + db.init_app(app) # This needs to come before Marshmallow + BaseModel.set_session(db.session) + migrate = Migrate(app, db) + ma = Marshmallow(app) + routes.api.init_app(app) + resources.jwt.init_app(app) + bcrypt.init_app(app) + celery.conf.update(app.config) + + @app.before_request + def set_request_id(): + logger.new(request_id=str(uuid.uuid4())) + + @app.route("/healthcheck") + def healthcheck(): + return "ok" + + @app.route("/version") + def version(): + return app.config["GIT_COMMIT_SHA"] + + @app.route("/encode_a") + def encode_a(): + from api.jobs import ingest_local_video2 + + ingest_local_video2.queue("async") + return "ok", 201 + + @app.route("/encode_c") + def encode_c(): + from api.jobs import ingest_local_video2 + + ingest_local_video2.delay("celery") + return "ok", 201 + + @app.route("/encode") + def encode(): + from api.jobs import ingest_local_video2 + + ingest_local_video2("sync") + return "ok", 201 + + @app.shell_context_processor + def make_shell_context(): + """ Adds these to the global scope of the shell for more convenient prototyping/debugging in the shell """ - from api.utils import module_classes_as_dict - - return { - "db": db, - **module_classes_as_dict("api.models"), - **module_classes_as_dict("api.schemas"), - **module_classes_as_dict("api.tests.factories"), - } - - @app.after_request - def session_commit(res): - res.headers["X-HF-git-commit-sha"] = app.config["GIT_COMMIT_SHA"] - if res.status_code >= 400: - return res - try: - db.session.commit() - return res - except DatabaseError: - db.session.rollback() - raise - - return app + from api.utils import module_classes_as_dict + + return { + "db": db, + **module_classes_as_dict("api.models"), + **module_classes_as_dict("api.schemas"), + **module_classes_as_dict("api.tests.factories"), + } + + @app.after_request + def session_commit(res): + res.headers["X-HF-git-commit-sha"] = app.config["GIT_COMMIT_SHA"] + if res.status_code >= 400: + return res + try: + db.session.commit() + return res + except DatabaseError: + db.session.rollback() + raise + + return app def config_logging(app): - logging.basicConfig( - format="%(message)s", stream=sys.stdout, level=logging.INFO - ) # TODO make level configurable + logging.basicConfig( + format="%(message)s", stream=sys.stdout, level=logging.INFO + ) # TODO make level configurable - processors = [ - structlog.processors.KeyValueRenderer(key_order=["event", "request_id"]) - ] + processors = [ + structlog.processors.KeyValueRenderer(key_order=["event", "request_id"]) + ] - structlog.configure( - processors=processors, - context_class=structlog.threadlocal.wrap_dict(dict), - logger_factory=structlog.stdlib.LoggerFactory(), - ) + structlog.configure( + processors=processors, + context_class=structlog.threadlocal.wrap_dict(dict), + logger_factory=structlog.stdlib.LoggerFactory(), + ) def config_db(app): - app.config["SQLALCHEMY_DATABASE_URI"] = "postgresql://{}:{}@{}/{}".format( - app.config["DB_USER"], - app.config["DB_PASSWORD"], - app.config["DB_HOST"], - app.config["DB_NAME"], - ) + app.config["SQLALCHEMY_DATABASE_URI"] = "postgresql://{}:{}@{}/{}".format( + app.config["DB_USER"], + app.config["DB_PASSWORD"], + app.config["DB_HOST"], + app.config["DB_NAME"], + ) - app.logger.info( - "App configured to talk to DB: %s", - "postgresql://{}:*REDACTED*@{}/{}".format( - app.config["DB_USER"], app.config["DB_HOST"], app.config["DB_NAME"] - ), - ) + app.logger.info( + "App configured to talk to DB: %s", + "postgresql://{}:*REDACTED*@{}/{}".format( + app.config["DB_USER"], app.config["DB_HOST"], app.config["DB_NAME"] + ), + ) def config_redis(app): - from api.utils import get_redis_url + from api.utils import get_redis_url - redis_url = get_redis_url() + redis_url = get_redis_url() - app.logger.info( - "App configured to talk to Redis: %s", - "redis://*REDACTED*@{}:{}/{}".format( - app.config["REDIS_HOST"], app.config["REDIS_PORT"], app.config["REDIS_DB"] - ), - ) + app.logger.info( + "App configured to talk to Redis: %s", + "redis://*REDACTED*@{}:{}/{}".format( + app.config["REDIS_HOST"], app.config["REDIS_PORT"], app.config["REDIS_DB"] + ), + ) def config_sentry(app): - sentry_sdk.init( - app.config["SENTRY_DSN"], - integrations=[FlaskIntegration(transaction_style="url")], - environment=app.config["ENV"], - release=f"human-factor-api@{app.config['GIT_COMMIT_SHA']}", - ) + sentry_sdk.init( + app.config["SENTRY_DSN"], + integrations=[FlaskIntegration(transaction_style="url")], + environment=app.config["ENV"], + release=f"human-factor-api@{app.config['GIT_COMMIT_SHA']}", + ) diff --git a/api/jobs.py b/api/jobs.py index 39f4b43..076ea6c 100644 --- a/api/jobs.py +++ b/api/jobs.py @@ -1,13 +1,9 @@ -import os import uuid -import ffmpeg -import subprocess + import structlog -from dynaconf import settings -from celery import Celery +from celery import Celery -from api.ffmpeg import encode_mp4 from api.utils import get_redis_url redis_url = get_redis_url() diff --git a/api/models.py b/api/models.py index f83e13f..1998f38 100644 --- a/api/models.py +++ b/api/models.py @@ -1,17 +1,15 @@ import os +import structlog from datetime import datetime, timedelta from dynaconf import settings from flask import current_app -from flask_sqlalchemy import SQLAlchemy, Model -from flask_sqlalchemy import SQLAlchemy, Model from flask_jwt_extended import create_access_token, create_refresh_token from google.cloud import storage from tempfile import TemporaryDirectory import sqlalchemy -from sqlalchemy.exc import DatabaseError from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.orm import backref, relationship from sqlalchemy.sql import func @@ -22,6 +20,8 @@ from api.utils import get_extension_from_content_type from api.jobs import ingest_video +log = structlog.get_logger() + BUFFER_SIZE = 2 ** 14 # 16KiB @@ -111,6 +111,7 @@ def ingest_local_source(self, temp_dir, source_video_name): still_path = os.path.join(temp_dir, still_name) thumbnail_path = os.path.join(temp_dir, output_thumbnail_name) + log.info("enc: ncoding video", source_path) ffmpeg.encode_mp4( source_path, reencoded_path, @@ -119,6 +120,7 @@ def ingest_local_source(self, temp_dir, source_video_name): ) video_stats = ffmpeg.info(reencoded_path) + log.info("enc: encoding video", reencoded_path) ffmpeg.capture_still( reencoded_path, still_path, at_time=video_stats["duration"] / 1.8 @@ -132,6 +134,8 @@ def ingest_local_source(self, temp_dir, source_video_name): still_blob = bucket.blob(config["BUCKET_STILL_PREFIX"] + still_name) thumbnail_blob = bucket.blob(config["BUCKET_THUMB_PREFIX"] + still_name) + log.info("enc: uploading blobs", reencoded_blob, still_blob, thumbnail_blob) + reencoded_blob.upload_from_filename( reencoded_path, content_type="video/mp4", predefined_acl="publicRead" ) @@ -144,6 +148,8 @@ def ingest_local_source(self, temp_dir, source_video_name): thumbnail_path, content_type="image/jpeg", predefined_acl="publicRead" ) + log.info("enc: updating postgres") + self.update( url=reencoded_blob.public_url, still_url=still_blob.public_url,