Compare commits

...

6 Commits

22 changed files with 828 additions and 392 deletions

View File

@ -2,6 +2,8 @@
Article query interface Article query interface
""" """
from typing import Optional
from sqlalchemy import select from sqlalchemy import select
from amanuensis.db import * from amanuensis.db import *
@ -12,17 +14,18 @@ def create(
db: DbContext, db: DbContext,
lexicon_id: int, lexicon_id: int,
user_id: int, user_id: int,
character_id: int) -> Article: character_id: Optional[int],
) -> Article:
""" """
Create a new article in a lexicon. Create a new article in a lexicon.
""" """
# Verify argument types are correct # Verify argument types are correct
if not isinstance(lexicon_id, int): if not isinstance(lexicon_id, int):
raise ArgumentError('lexicon_id') raise ArgumentError("lexicon_id")
if not isinstance(user_id, int): if not isinstance(user_id, int):
raise ArgumentError('user_id') raise ArgumentError("user_id")
if character_id is not None and not isinstance(character_id, int): if character_id is not None and not isinstance(character_id, int):
raise ArgumentError('character_id') raise ArgumentError("character_id")
# Check that the user is a member of this lexicon # Check that the user is a member of this lexicon
mem: Membership = db( mem: Membership = db(
@ -31,31 +34,30 @@ def create(
.where(Membership.lexicon_id == lexicon_id) .where(Membership.lexicon_id == lexicon_id)
).scalar_one_or_none() ).scalar_one_or_none()
if not mem: if not mem:
raise ArgumentError('User is not a member of lexicon') raise ArgumentError("User is not a member of lexicon")
# If the character id is provided, check that the user owns the character # If the character id is provided, check that the user owns the character
# and the character belongs to the lexicon # and the character belongs to the lexicon
if character_id is not None: if character_id is not None:
character: Character = db( character: Character = db(
select(Character) select(Character).where(Character.id == character_id)
.where(Character.id == character_id)
).scalar_one_or_none() ).scalar_one_or_none()
if not character: if not character:
raise ArgumentError('Character does not exist') raise ArgumentError("Character does not exist")
if character.user.id != user_id: if character.user.id != user_id:
raise ArgumentError('Character is owned by the wrong player') raise ArgumentError("Character is owned by the wrong player")
if character.lexicon.id != lexicon_id: if character.lexicon.id != lexicon_id:
raise ArgumentError('Character belongs to the wrong lexicon') raise ArgumentError("Character belongs to the wrong lexicon")
signature = character.signature signature = character.signature
else: else:
signature = '~Ersatz Scrivener' signature = "~Ersatz Scrivener"
new_article = Article( new_article = Article(
lexicon_id=lexicon_id, lexicon_id=lexicon_id,
user_id=user_id, user_id=user_id,
character_id=character_id, character_id=character_id,
title='Article title', title="Article title",
body=f'\n\n{signature}', body=f"\n\n{signature}",
) )
db.session.add(new_article) db.session.add(new_article)
db.session.commit() db.session.commit()

View File

@ -2,6 +2,8 @@
Character query interface Character query interface
""" """
from typing import Optional
from sqlalchemy import select, func from sqlalchemy import select, func
from amanuensis.db import * from amanuensis.db import *
@ -13,27 +15,28 @@ def create(
lexicon_id: int, lexicon_id: int,
user_id: int, user_id: int,
name: str, name: str,
signature: str) -> Character: signature: Optional[str],
) -> Character:
""" """
Create a new character for a user. Create a new character for a user.
""" """
# Verify argument types are correct # Verify argument types are correct
if not isinstance(lexicon_id, int): if not isinstance(lexicon_id, int):
raise ArgumentError('lexicon_id') raise ArgumentError("lexicon_id")
if not isinstance(user_id, int): if not isinstance(user_id, int):
raise ArgumentError('user_id') raise ArgumentError("user_id")
if not isinstance(name, str): if not isinstance(name, str):
raise ArgumentError('name') raise ArgumentError("name")
if signature is not None and not isinstance(signature, str): if signature is not None and not isinstance(signature, str):
raise ArgumentError('signature') raise ArgumentError("signature")
# Verify character name is valid # Verify character name is valid
if not name.strip(): if not name.strip():
raise ArgumentError('Character name cannot be blank') raise ArgumentError("Character name cannot be blank")
# If no signature is provided, use a default signature # If no signature is provided, use a default signature
if not signature or not signature.strip(): if not signature or not signature.strip():
signature = f'~{name}' signature = f"~{name}"
# Check that the user is a member of this lexicon # Check that the user is a member of this lexicon
mem: Membership = db( mem: Membership = db(
@ -42,7 +45,7 @@ def create(
.where(Membership.lexicon_id == lexicon_id) .where(Membership.lexicon_id == lexicon_id)
).scalar_one_or_none() ).scalar_one_or_none()
if not mem: if not mem:
raise ArgumentError('User is not a member of lexicon') raise ArgumentError("User is not a member of lexicon")
# Check that this user is below the limit for creating characters # Check that this user is below the limit for creating characters
num_user_chars = db( num_user_chars = db(
@ -50,8 +53,11 @@ def create(
.where(Character.lexicon_id == lexicon_id) .where(Character.lexicon_id == lexicon_id)
.where(Character.user_id == user_id) .where(Character.user_id == user_id)
).scalar() ).scalar()
if mem.lexicon.character_limit is not None and num_user_chars >= mem.lexicon.character_limit: if (
raise ArgumentError('User is at character limit') mem.lexicon.character_limit is not None
and num_user_chars >= mem.lexicon.character_limit
):
raise ArgumentError("User is at character limit")
new_character = Character( new_character = Character(
lexicon_id=lexicon_id, lexicon_id=lexicon_id,

View File

@ -0,0 +1,74 @@
"""
Index query interface
"""
import re
from typing import Optional
from amanuensis.db import DbContext, ArticleIndex, IndexType
from amanuensis.errors import ArgumentError
def create(
db: DbContext,
lexicon_id: int,
index_type: IndexType,
pattern: str,
logical_order: int,
display_order: int,
capacity: Optional[int],
) -> ArticleIndex:
"""
Create a new index in a lexicon.
"""
# Verify argument types are correct
if not isinstance(lexicon_id, int):
raise ArgumentError("lexicon_id")
if not isinstance(index_type, IndexType):
raise ArgumentError("index_type")
if not isinstance(pattern, str):
raise ArgumentError("pattern")
if not isinstance(logical_order, int):
raise ArgumentError("logical_order")
if not isinstance(display_order, int):
raise ArgumentError("display_order")
if capacity is not None and not isinstance(capacity, int):
raise ArgumentError("capacity")
# Verify the pattern is valid for the index type:
if index_type == IndexType.CHAR:
if len(pattern) < 1:
raise ArgumentError(
f"Pattern '{pattern}' too short for index type {index_type}"
)
elif index_type == IndexType.RANGE:
range_def = re.match(r"^(.)-(.)$", pattern)
if not range_def:
raise ArgumentError(f"Pattern '{pattern}' is not a valid range format")
start_char, end_char = range_def.group(1), range_def.group(2)
if start_char >= end_char:
raise ArgumentError(
f"Range start '{start_char}' is not before range end '{end_char}'"
)
elif index_type == IndexType.PREFIX:
if len(pattern) < 1:
raise ArgumentError(
f"Pattern '{pattern}' too short for index type {index_type}"
)
elif index_type == IndexType.ETC:
if len(pattern) < 1:
raise ArgumentError(
f"Pattern '{pattern}' too short for index type {index_type}"
)
new_index = ArticleIndex(
lexicon_id=lexicon_id,
index_type=index_type,
pattern=pattern,
logical_order=logical_order,
display_order=display_order,
capacity=capacity,
)
db.session.add(new_index)
db.session.commit()
return new_index

View File

@ -10,39 +10,39 @@ from amanuensis.db import DbContext, Lexicon
from amanuensis.errors import ArgumentError from amanuensis.errors import ArgumentError
RE_ALPHANUM_DASH_UNDER = re.compile(r'^[A-Za-z0-9-_]*$') RE_ALPHANUM_DASH_UNDER = re.compile(r"^[A-Za-z0-9-_]*$")
def create( def create(
db: DbContext, db: DbContext,
name: str, name: str,
title: str, title: str,
prompt: str) -> Lexicon: prompt: str,
) -> Lexicon:
""" """
Create a new lexicon. Create a new lexicon.
""" """
# Verify name # Verify name
if not isinstance(name, str): if not isinstance(name, str):
raise ArgumentError('Lexicon name must be a string') raise ArgumentError("Lexicon name must be a string")
if not name.strip(): if not name.strip():
raise ArgumentError('Lexicon name must not be blank') raise ArgumentError("Lexicon name must not be blank")
if not RE_ALPHANUM_DASH_UNDER.match(name): if not RE_ALPHANUM_DASH_UNDER.match(name):
raise ArgumentError('Lexicon name may only contain alphanumerics, dash, and underscore') raise ArgumentError(
"Lexicon name may only contain alphanumerics, dash, and underscore"
)
# Verify title # Verify title
if title is not None and not isinstance(name, str): if title is not None and not isinstance(name, str):
raise ArgumentError('Lexicon name must be a string') raise ArgumentError("Lexicon name must be a string")
# Verify prompt # Verify prompt
if not isinstance(prompt, str): if not isinstance(prompt, str):
raise ArgumentError('Lexicon prompt must be a string') raise ArgumentError("Lexicon prompt must be a string")
# Query the db to make sure the lexicon name isn't taken # Query the db to make sure the lexicon name isn't taken
if db( if db(select(func.count(Lexicon.id)).where(Lexicon.name == name)).scalar() > 0:
select(func.count(Lexicon.id)) raise ArgumentError("Lexicon name is already taken")
.where(Lexicon.name == name)
).scalar() > 0:
raise ArgumentError('Lexicon name is already taken')
new_lexicon = Lexicon( new_lexicon = Lexicon(
name=name, name=name,

View File

@ -12,25 +12,29 @@ def create(
db: DbContext, db: DbContext,
user_id: int, user_id: int,
lexicon_id: int, lexicon_id: int,
is_editor: bool) -> Membership: is_editor: bool,
) -> Membership:
""" """
Create a new user membership in a lexicon. Create a new user membership in a lexicon.
""" """
# Verify argument types are correct # Verify argument types are correct
if not isinstance(user_id, int): if not isinstance(user_id, int):
raise ArgumentError('user_id') raise ArgumentError("user_id")
if not isinstance(lexicon_id, int): if not isinstance(lexicon_id, int):
raise ArgumentError('lexicon_id') raise ArgumentError("lexicon_id")
if not isinstance(is_editor, bool): if not isinstance(is_editor, bool):
raise ArgumentError('is_editor') raise ArgumentError("is_editor")
# Verify user has not already joined lexicon # Verify user has not already joined lexicon
if db( if (
select(func.count(Membership.id)) db(
.where(Membership.user_id == user_id) select(func.count(Membership.id))
.where(Membership.lexicon_id == lexicon_id) .where(Membership.user_id == user_id)
).scalar() > 0: .where(Membership.lexicon_id == lexicon_id)
raise ArgumentError('User is already a member of lexicon') ).scalar()
> 0
):
raise ArgumentError("User is already a member of lexicon")
new_membership = Membership( new_membership = Membership(
user_id=user_id, user_id=user_id,

View File

@ -9,34 +9,32 @@ from sqlalchemy import select, func
from amanuensis.db import DbContext, Post from amanuensis.db import DbContext, Post
from amanuensis.errors import ArgumentError from amanuensis.errors import ArgumentError
def create( def create(
db: DbContext, db: DbContext,
lexicon_id: int, lexicon_id: int,
user_id: int, user_id: int,
body: str) -> Post: body: str,
) -> Post:
""" """
Create a new post Create a new post
""" """
# Verify lexicon id # Verify lexicon id
if not isinstance(lexicon_id, int): if not isinstance(lexicon_id, int):
raise ArgumentError('Lexicon id must be an integer.') raise ArgumentError("Lexicon id must be an integer.")
# Verify user_id # Verify user_id
if not (isinstance(user_id, int) or user_id is None): if not (isinstance(user_id, int) or user_id is None):
raise ArgumentError('User id must be an integer.') raise ArgumentError("User id must be an integer.")
# Verify body # Verify body
if not isinstance(body, str): if not isinstance(body, str):
raise ArgumentError('Post body must be a string.') raise ArgumentError("Post body must be a string.")
if not body.strip(): if not body.strip():
raise ArgumentError('Post body cannot be empty.') raise ArgumentError("Post body cannot be empty.")
new_post = Post( new_post = Post(lexicon_id=lexicon_id, user_id=user_id, body=body)
lexicon_id=lexicon_id,
user_id=user_id,
body=body
)
db.session.add(new_post) db.session.add(new_post)
db.session.commit() db.session.commit()
return new_post return new_post

View File

@ -11,8 +11,8 @@ from amanuensis.db import DbContext, User
from amanuensis.errors import ArgumentError from amanuensis.errors import ArgumentError
RE_NO_LETTERS = re.compile(r'^[0-9-_]*$') RE_NO_LETTERS = re.compile(r"^[0-9-_]*$")
RE_ALPHANUM_DASH_UNDER = re.compile(r'^[A-Za-z0-9-_]*$') RE_ALPHANUM_DASH_UNDER = re.compile(r"^[A-Za-z0-9-_]*$")
def create( def create(
@ -21,41 +21,41 @@ def create(
password: str, password: str,
display_name: str, display_name: str,
email: str, email: str,
is_site_admin: bool) -> User: is_site_admin: bool,
) -> User:
""" """
Create a new user. Create a new user.
""" """
# Verify username # Verify username
if not isinstance(username, str): if not isinstance(username, str):
raise ArgumentError('Username must be a string') raise ArgumentError("Username must be a string")
if len(username) < 3 or len(username) > 32: if len(username) < 3 or len(username) > 32:
raise ArgumentError('Username must be between 3 and 32 characters') raise ArgumentError("Username must be between 3 and 32 characters")
if RE_NO_LETTERS.match(username): if RE_NO_LETTERS.match(username):
raise ArgumentError('Username must contain a letter') raise ArgumentError("Username must contain a letter")
if not RE_ALPHANUM_DASH_UNDER.match(username): if not RE_ALPHANUM_DASH_UNDER.match(username):
raise ArgumentError('Username may only contain alphanumerics, dash, and underscore') raise ArgumentError(
"Username may only contain alphanumerics, dash, and underscore"
)
# Verify password # Verify password
if not isinstance(password, str): if not isinstance(password, str):
raise ArgumentError('Password must be a string') raise ArgumentError("Password must be a string")
# Verify display name # Verify display name
if display_name is not None and not isinstance(display_name, str): if display_name is not None and not isinstance(display_name, str):
raise ArgumentError('Display name must be a string') raise ArgumentError("Display name must be a string")
# If display name is not provided, use the username # If display name is not provided, use the username
if not display_name or not display_name.strip(): if not display_name or not display_name.strip():
display_name = username display_name = username
# Verify email # Verify email
if not isinstance(email, str): if not isinstance(email, str):
raise ArgumentError('Email must be a string') raise ArgumentError("Email must be a string")
# Query the db to make sure the username isn't taken # Query the db to make sure the username isn't taken
if db( if db(select(func.count(User.id)).where(User.username == username)).scalar() > 0:
select(func.count(User.id)) raise ArgumentError("Username is already taken")
.where(User.username == username)
).scalar() > 0:
raise ArgumentError('Username is already taken')
new_user = User( new_user = User(
username=username, username=username,

View File

@ -15,17 +15,17 @@ from .models import (
) )
__all__ = [ __all__ = [
'DbContext', "DbContext",
'User', "User",
'Lexicon', "Lexicon",
'Membership', "Membership",
'Character', "Character",
'ArticleState', "ArticleState",
'Article', "Article",
'IndexType', "IndexType",
'ArticleIndex', "ArticleIndex",
'ArticleIndexRule', "ArticleIndexRule",
'ArticleContentRuleType', "ArticleContentRuleType",
'ArticleContentRule', "ArticleContentRule",
'Post', "Post",
] ]

View File

@ -8,22 +8,25 @@ from sqlalchemy.orm import sessionmaker
# Define naming conventions for generated constraints # Define naming conventions for generated constraints
metadata = MetaData(naming_convention={ metadata = MetaData(
"ix": "ix_%(column_0_label)s", naming_convention={
"uq": "uq_%(table_name)s_%(column_0_name)s", "ix": "ix_%(column_0_label)s",
"ck": "ck_%(table_name)s_%(constraint_name)s", "uq": "uq_%(table_name)s_%(column_0_name)s",
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", "ck": "ck_%(table_name)s_%(constraint_name)s",
"pk": "pk_%(table_name)s" "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
}) "pk": "pk_%(table_name)s",
}
)
# Base class for ORM models # Base class for ORM models
ModelBase = declarative_base(metadata=metadata) ModelBase = declarative_base(metadata=metadata)
class DbContext(): class DbContext:
def __init__(self, db_uri, debug=False): def __init__(self, db_uri, debug=False):
# Create an engine and enable foreign key constraints in sqlite # Create an engine and enable foreign key constraints in sqlite
self.engine = create_engine(db_uri, echo=debug) self.engine = create_engine(db_uri, echo=debug)
@event.listens_for(self.engine, "connect") @event.listens_for(self.engine, "connect")
def set_sqlite_pragma(dbapi_connection, connection_record): def set_sqlite_pragma(dbapi_connection, connection_record):
cursor = dbapi_connection.cursor() cursor = dbapi_connection.cursor()

View File

@ -28,15 +28,16 @@ class Uuid(TypeDecorator):
""" """
A uuid backed by a char(32) field in sqlite. A uuid backed by a char(32) field in sqlite.
""" """
impl = CHAR(32) impl = CHAR(32)
def process_bind_param(self, value, dialect): def process_bind_param(self, value, dialect):
if value is None: if value is None:
return value return value
elif not isinstance(value, uuid.UUID): elif not isinstance(value, uuid.UUID):
return f'{uuid.UUID(value).int:32x}' return f"{uuid.UUID(value).int:32x}"
else: else:
return f'{value.int:32x}' return f"{value.int:32x}"
def process_result_value(self, value, dialect): def process_result_value(self, value, dialect):
if value is None: if value is None:
@ -51,7 +52,8 @@ class User(ModelBase):
""" """
Represents a single user of Amanuensis. Represents a single user of Amanuensis.
""" """
__tablename__ = 'user'
__tablename__ = "user"
############# #############
# User info # # User info #
@ -73,14 +75,14 @@ class User(ModelBase):
email = Column(String, nullable=False) email = Column(String, nullable=False)
# Whether the user can access site admin functions # Whether the user can access site admin functions
is_site_admin = Column(Boolean, nullable=False, server_default=text('FALSE')) is_site_admin = Column(Boolean, nullable=False, server_default=text("FALSE"))
#################### ####################
# History tracking # # History tracking #
#################### ####################
# The timestamp the user was created # The timestamp the user was created
created = Column(DateTime, nullable=False, server_default=text('CURRENT_TIMESTAMP')) created = Column(DateTime, nullable=False, server_default=text("CURRENT_TIMESTAMP"))
# The timestamp the user last logged in # The timestamp the user last logged in
# This is NULL if the user has never logged in # This is NULL if the user has never logged in
@ -94,17 +96,18 @@ class User(ModelBase):
# Foreign key relationships # # Foreign key relationships #
############################# #############################
memberships = relationship('Membership', back_populates='user') memberships = relationship("Membership", back_populates="user")
characters = relationship('Character', back_populates='user') characters = relationship("Character", back_populates="user")
articles = relationship('Article', back_populates='user') articles = relationship("Article", back_populates="user")
posts = relationship('Post', back_populates='user') posts = relationship("Post", back_populates="user")
class Lexicon(ModelBase): class Lexicon(ModelBase):
""" """
Represents a single game of Lexicon. Represents a single game of Lexicon.
""" """
__tablename__ = 'lexicon'
__tablename__ = "lexicon"
############# #############
# Game info # # Game info #
@ -128,10 +131,12 @@ class Lexicon(ModelBase):
#################### ####################
# The timestamp the lexicon was created # The timestamp the lexicon was created
created = Column(DateTime, nullable=False, server_default=text('CURRENT_TIMESTAMP')) created = Column(DateTime, nullable=False, server_default=text("CURRENT_TIMESTAMP"))
# The timestamp of the last change in game state # The timestamp of the last change in game state
last_updated = Column(DateTime, nullable=False, server_default=text('CURRENT_TIMESTAMP')) last_updated = Column(
DateTime, nullable=False, server_default=text("CURRENT_TIMESTAMP")
)
# The timestamp the first turn was started # The timestamp the first turn was started
# This is NULL until the game starts # This is NULL until the game starts
@ -221,23 +226,22 @@ class Lexicon(ModelBase):
# Foreign key relationships # # Foreign key relationships #
############################# #############################
memberships = relationship('Membership', back_populates='lexicon') memberships = relationship("Membership", back_populates="lexicon")
characters = relationship('Character', back_populates='lexicon') characters = relationship("Character", back_populates="lexicon")
articles = relationship('Article', back_populates='lexicon') articles = relationship("Article", back_populates="lexicon")
indexes = relationship('ArticleIndex', back_populates='lexicon') indexes = relationship("ArticleIndex", back_populates="lexicon")
index_rules = relationship('ArticleIndexRule', back_populates='lexicon') index_rules = relationship("ArticleIndexRule", back_populates="lexicon")
content_rules = relationship('ArticleContentRule', back_populates='lexicon') content_rules = relationship("ArticleContentRule", back_populates="lexicon")
posts = relationship('Post', back_populates='lexicon') posts = relationship("Post", back_populates="lexicon")
class Membership(ModelBase): class Membership(ModelBase):
""" """
Represents a user's participation in a Lexicon game. Represents a user's participation in a Lexicon game.
""" """
__tablename__ = 'membership'
__table_args__ = ( __tablename__ = "membership"
UniqueConstraint('user_id', 'lexicon_id'), __table_args__ = (UniqueConstraint("user_id", "lexicon_id"),)
)
################### ###################
# Membership keys # # Membership keys #
@ -247,17 +251,17 @@ class Membership(ModelBase):
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
# The user who is a member of a lexicon # The user who is a member of a lexicon
user_id = Column(Integer, ForeignKey('user.id'), nullable=False) user_id = Column(Integer, ForeignKey("user.id"), nullable=False)
# The lexicon of which the user is a member # The lexicon of which the user is a member
lexicon_id = Column(Integer, ForeignKey('lexicon.id'), nullable=False) lexicon_id = Column(Integer, ForeignKey("lexicon.id"), nullable=False)
#################### ####################
# History tracking # # History tracking #
#################### ####################
# Timestamp the user joined the game # Timestamp the user joined the game
joined = Column(DateTime, nullable=False, server_default=text('CURRENT_TIMESTAMP')) joined = Column(DateTime, nullable=False, server_default=text("CURRENT_TIMESTAMP"))
# Timestamp of the last time the user viewed the post feed # Timestamp of the last time the user viewed the post feed
# This is NULL if the player has never viewed posts # This is NULL if the player has never viewed posts
@ -268,7 +272,7 @@ class Membership(ModelBase):
################### ###################
# Whether the user can access editor functions # Whether the user can access editor functions
is_editor = Column(Boolean, nullable=False, server_default=text('FALSE')) is_editor = Column(Boolean, nullable=False, server_default=text("FALSE"))
######################### #########################
# Notification settings # # Notification settings #
@ -287,15 +291,16 @@ class Membership(ModelBase):
# Foreign key relationships # # Foreign key relationships #
############################# #############################
user = relationship('User', back_populates='memberships') user = relationship("User", back_populates="memberships")
lexicon = relationship('Lexicon', back_populates='memberships') lexicon = relationship("Lexicon", back_populates="memberships")
class Character(ModelBase): class Character(ModelBase):
""" """
Represents a character played by a uaser in a Lexicon game. Represents a character played by a uaser in a Lexicon game.
""" """
__tablename__ = 'character'
__tablename__ = "character"
################## ##################
# Character info # # Character info #
@ -308,10 +313,10 @@ class Character(ModelBase):
public_id = Column(Uuid, nullable=False, unique=True, default=uuid.uuid4) public_id = Column(Uuid, nullable=False, unique=True, default=uuid.uuid4)
# The lexicon to which this character belongs # The lexicon to which this character belongs
lexicon_id = Column(Integer, ForeignKey('lexicon.id'), nullable=False) lexicon_id = Column(Integer, ForeignKey("lexicon.id"), nullable=False)
# The user to whom this character belongs # The user to whom this character belongs
user_id = Column(Integer, ForeignKey('user.id'), nullable=False) user_id = Column(Integer, ForeignKey("user.id"), nullable=False)
# The character's name # The character's name
name = Column(String, nullable=False) name = Column(String, nullable=False)
@ -323,16 +328,17 @@ class Character(ModelBase):
# Foreign key relationships # # Foreign key relationships #
############################# #############################
user = relationship('User', back_populates='characters') user = relationship("User", back_populates="characters")
lexicon = relationship('Lexicon', back_populates='characters') lexicon = relationship("Lexicon", back_populates="characters")
articles = relationship('Article', back_populates='character') articles = relationship("Article", back_populates="character")
index_rules = relationship('ArticleIndexRule', back_populates='character') index_rules = relationship("ArticleIndexRule", back_populates="character")
class ArticleState(enum.Enum): class ArticleState(enum.Enum):
""" """
The step of the editorial process an article is in. The step of the editorial process an article is in.
""" """
DRAFT = 0 DRAFT = 0
SUBMITTED = 1 SUBMITTED = 1
APPROVED = 2 APPROVED = 2
@ -342,7 +348,8 @@ class Article(ModelBase):
""" """
Represents a single article in a lexicon. Represents a single article in a lexicon.
""" """
__tablename__ = 'article'
__tablename__ = "article"
################ ################
# Article info # # Article info #
@ -355,17 +362,17 @@ class Article(ModelBase):
public_id = Column(Uuid, nullable=False, unique=True, default=uuid.uuid4) public_id = Column(Uuid, nullable=False, unique=True, default=uuid.uuid4)
# The lexicon to which this article belongs # The lexicon to which this article belongs
lexicon_id = Column(Integer, ForeignKey('lexicon.id'), nullable=False) lexicon_id = Column(Integer, ForeignKey("lexicon.id"), nullable=False)
# The character who is the author of this article # The character who is the author of this article
# If this is NULL, the article is written by Ersatz Scrivener # If this is NULL, the article is written by Ersatz Scrivener
character_id = Column(Integer, ForeignKey('character.id'), nullable=True) character_id = Column(Integer, ForeignKey("character.id"), nullable=True)
# The user who owns this article # The user who owns this article
user_id = Column(Integer, ForeignKey('user.id'), nullable=False) user_id = Column(Integer, ForeignKey("user.id"), nullable=False)
# The article to which this is an addendum # The article to which this is an addendum
addendum_to = Column(Integer, ForeignKey('article.id'), nullable=True) addendum_to = Column(Integer, ForeignKey("article.id"), nullable=True)
################# #################
# Article state # # Article state #
@ -386,7 +393,9 @@ class Article(ModelBase):
#################### ####################
# Timestamp the content of the article was last updated # Timestamp the content of the article was last updated
last_updated = Column(DateTime, nullable=False, server_default=text('CURRENT_TIMESTAMP')) last_updated = Column(
DateTime, nullable=False, server_default=text("CURRENT_TIMESTAMP")
)
# Timestamp the article was last submitted # Timestamp the article was last submitted
# This is NULL until the article is submitted # This is NULL until the article is submitted
@ -410,16 +419,17 @@ class Article(ModelBase):
# Foreign key relationships # # Foreign key relationships #
############################# #############################
lexicon = relationship('Lexicon', back_populates='articles') lexicon = relationship("Lexicon", back_populates="articles")
character = relationship('Character', back_populates='articles') character = relationship("Character", back_populates="articles")
user = relationship('User', back_populates='articles') user = relationship("User", back_populates="articles")
addenda = relationship('Article', backref=backref('parent', remote_side=[id])) addenda = relationship("Article", backref=backref("parent", remote_side=[id]))
class IndexType(enum.Enum): class IndexType(enum.Enum):
""" """
The title-matching behavior of an article index. The title-matching behavior of an article index.
""" """
CHAR = 0 CHAR = 0
RANGE = 1 RANGE = 1
PREFIX = 2 PREFIX = 2
@ -430,7 +440,8 @@ class ArticleIndex(ModelBase):
""" """
Represents an index definition. Represents an index definition.
""" """
__tablename__ = 'article_index'
__tablename__ = "article_index"
############## ##############
# Index info # # Index info #
@ -440,7 +451,7 @@ class ArticleIndex(ModelBase):
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
# The lexicon this index is in # The lexicon this index is in
lexicon_id = Column(Integer, ForeignKey('lexicon.id'), nullable=False) lexicon_id = Column(Integer, ForeignKey("lexicon.id"), nullable=False)
# The index type # The index type
index_type = Column(Enum(IndexType), nullable=False) index_type = Column(Enum(IndexType), nullable=False)
@ -462,8 +473,8 @@ class ArticleIndex(ModelBase):
# Foreign key relationships # # Foreign key relationships #
############################# #############################
lexicon = relationship('Lexicon', back_populates='indexes') lexicon = relationship("Lexicon", back_populates="indexes")
index_rules = relationship('ArticleIndexRule', back_populates='index') index_rules = relationship("ArticleIndexRule", back_populates="index")
class ArticleIndexRule(ModelBase): class ArticleIndexRule(ModelBase):
@ -472,7 +483,8 @@ class ArticleIndexRule(ModelBase):
A character with multiple index rules may write in any index that satisfies A character with multiple index rules may write in any index that satisfies
a rule. A character with no index rules may write in any index. a rule. A character with no index rules may write in any index.
""" """
__tablename__ = 'article_index_rule'
__tablename__ = "article_index_rule"
################### ###################
# Index rule info # # Index rule info #
@ -482,17 +494,17 @@ class ArticleIndexRule(ModelBase):
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
# The lexicon of this index rule # The lexicon of this index rule
lexicon_id = Column(Integer, ForeignKey('lexicon.id'), nullable=False) lexicon_id = Column(Integer, ForeignKey("lexicon.id"), nullable=False)
#################### ####################
# Index rule scope # # Index rule scope #
#################### ####################
# The character to whom this rule applies # The character to whom this rule applies
character_id = Column(Integer, ForeignKey('character.id'), nullable=False) character_id = Column(Integer, ForeignKey("character.id"), nullable=False)
# The index to which the character is restricted # The index to which the character is restricted
index_id = Column(Integer, ForeignKey('article_index.id'), nullable=False) index_id = Column(Integer, ForeignKey("article_index.id"), nullable=False)
# The turn in which this rule applies # The turn in which this rule applies
turn = Column(Integer, nullable=False) turn = Column(Integer, nullable=False)
@ -501,15 +513,16 @@ class ArticleIndexRule(ModelBase):
# Foreign key relationships # # Foreign key relationships #
############################# #############################
lexicon = relationship('Lexicon', back_populates='index_rules') lexicon = relationship("Lexicon", back_populates="index_rules")
index = relationship('ArticleIndex', back_populates='index_rules') index = relationship("ArticleIndex", back_populates="index_rules")
character = relationship('Character', back_populates='index_rules') character = relationship("Character", back_populates="index_rules")
class ArticleContentRuleType(enum.Enum): class ArticleContentRuleType(enum.Enum):
""" """
The possible article content rules. The possible article content rules.
""" """
# Whether characters can cite themselves # Whether characters can cite themselves
ALLOW_SELF_CITE = 0 ALLOW_SELF_CITE = 0
# Whether characters can write new articles instead of phantoms # Whether characters can write new articles instead of phantoms
@ -543,7 +556,8 @@ class ArticleContentRule(ModelBase):
""" """
Represents a restriction on the content of an article for a turn. Represents a restriction on the content of an article for a turn.
""" """
__tablename__ = 'article_content_rule'
__tablename__ = "article_content_rule"
##################### #####################
# Content rule info # # Content rule info #
@ -553,7 +567,7 @@ class ArticleContentRule(ModelBase):
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
# The lexicon of this content rule # The lexicon of this content rule
lexicon_id = Column(Integer, ForeignKey('lexicon.id'), nullable=False) lexicon_id = Column(Integer, ForeignKey("lexicon.id"), nullable=False)
###################### ######################
# Content rule scope # # Content rule scope #
@ -577,14 +591,15 @@ class ArticleContentRule(ModelBase):
# Foreign key relationships # # Foreign key relationships #
############################# #############################
lexicon = relationship('Lexicon', back_populates='content_rules') lexicon = relationship("Lexicon", back_populates="content_rules")
class Post(ModelBase): class Post(ModelBase):
""" """
Represents a post in the game feed. Represents a post in the game feed.
""" """
__tablename__ = 'post'
__tablename__ = "post"
############# #############
# Post info # # Post info #
@ -594,18 +609,18 @@ class Post(ModelBase):
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
# The lexicon in which the post was made # The lexicon in which the post was made
lexicon_id = Column(Integer, ForeignKey('lexicon.id'), nullable=False) lexicon_id = Column(Integer, ForeignKey("lexicon.id"), nullable=False)
# The user who made the post # The user who made the post
# This may be NULL if the post was made by Amanuensis # This may be NULL if the post was made by Amanuensis
user_id = Column(Integer, ForeignKey('user.id'), nullable=True) user_id = Column(Integer, ForeignKey("user.id"), nullable=True)
################ ################
# Post content # # Post content #
################ ################
# The timestamp the post was created # The timestamp the post was created
created = Column(DateTime, nullable=False, server_default=text('CURRENT_TIMESTAMP')) created = Column(DateTime, nullable=False, server_default=text("CURRENT_TIMESTAMP"))
# The body of the post # The body of the post
body = Column(Text, nullable=False) body = Column(Text, nullable=False)
@ -614,5 +629,5 @@ class Post(ModelBase):
# Foreign key relationships # # Foreign key relationships #
############################# #############################
user = relationship('User', back_populates='posts') user = relationship("User", back_populates="posts")
lexicon = relationship('Lexicon', back_populates='posts') lexicon = relationship("Lexicon", back_populates="posts")

View File

@ -2,6 +2,7 @@
Submodule of custom exception types Submodule of custom exception types
""" """
class AmanuensisError(Exception): class AmanuensisError(Exception):
"""Base class for exceptions in amanuensis""" """Base class for exceptions in amanuensis"""

4
mypy.ini Normal file
View File

@ -0,0 +1,4 @@
[mypy]
ignore_missing_imports = true
exclude = "amanuensis/cli/.*|amanuensis/config/.*|amanuensis/lexicon/.*|amanuensis/log/.*|amanuensis/models/.*|amanuensis/parser/.*|amanuensis/resources/.*|amanuensis/server/.*|amanuensis/user/.*|amanuensis/__main__.py"
; mypy stable doesn't support pyproject.toml yet

487
poetry.lock generated
View File

@ -1,3 +1,11 @@
[[package]]
name = "appdirs"
version = "1.4.4"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev"
optional = false
python-versions = "*"
[[package]] [[package]]
name = "atomicwrites" name = "atomicwrites"
version = "1.4.0" version = "1.4.0"
@ -8,17 +16,39 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]] [[package]]
name = "attrs" name = "attrs"
version = "20.3.0" version = "21.2.0"
description = "Classes Without Boilerplate" description = "Classes Without Boilerplate"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[package.extras] [package.extras]
dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"] dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"]
docs = ["furo", "sphinx", "zope.interface"] docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"]
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"] tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"]
[[package]]
name = "black"
version = "21.5b2"
description = "The uncompromising code formatter."
category = "dev"
optional = false
python-versions = ">=3.6.2"
[package.dependencies]
appdirs = "*"
click = ">=7.1.2"
mypy-extensions = ">=0.4.3"
pathspec = ">=0.8.1,<1"
regex = ">=2020.1.8"
toml = ">=0.10.1"
[package.extras]
colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.6.0)", "aiohttp-cors (>=0.4.0)"]
python2 = ["typed-ast (>=1.4.2)"]
uvloop = ["uvloop (>=0.15.2)"]
[[package]] [[package]]
name = "click" name = "click"
@ -38,17 +68,17 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]] [[package]]
name = "flask" name = "flask"
version = "1.1.2" version = "1.1.4"
description = "A simple framework for building complex web applications." description = "A simple framework for building complex web applications."
category = "main" category = "main"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[package.dependencies] [package.dependencies]
click = ">=5.1" click = ">=5.1,<8.0"
itsdangerous = ">=0.24" itsdangerous = ">=0.24,<2.0"
Jinja2 = ">=2.10.1" Jinja2 = ">=2.10.1,<3.0"
Werkzeug = ">=0.15" Werkzeug = ">=0.15,<2.0"
[package.extras] [package.extras]
dev = ["pytest", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"] dev = ["pytest", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"]
@ -81,7 +111,7 @@ WTForms = "*"
[[package]] [[package]]
name = "greenlet" name = "greenlet"
version = "1.0.0" version = "1.1.0"
description = "Lightweight in-process concurrent programming" description = "Lightweight in-process concurrent programming"
category = "main" category = "main"
optional = false optional = false
@ -114,20 +144,44 @@ i18n = ["Babel (>=0.8)"]
[[package]] [[package]]
name = "markupsafe" name = "markupsafe"
version = "1.1.1" version = "2.0.1"
description = "Safely add untrusted strings to HTML/XML markup." description = "Safely add untrusted strings to HTML/XML markup."
category = "main" category = "main"
optional = false optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" python-versions = ">=3.6"
[[package]] [[package]]
name = "more-itertools" name = "more-itertools"
version = "8.7.0" version = "8.8.0"
description = "More routines for operating on iterables, beyond itertools" description = "More routines for operating on iterables, beyond itertools"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
[[package]]
name = "mypy"
version = "0.812"
description = "Optional static typing for Python"
category = "dev"
optional = false
python-versions = ">=3.5"
[package.dependencies]
mypy-extensions = ">=0.4.3,<0.5.0"
typed-ast = ">=1.4.0,<1.5.0"
typing-extensions = ">=3.7.4"
[package.extras]
dmypy = ["psutil (>=4.0)"]
[[package]]
name = "mypy-extensions"
version = "0.4.3"
description = "Experimental type system extensions for programs checked with the mypy typechecker."
category = "dev"
optional = false
python-versions = "*"
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "20.9" version = "20.9"
@ -139,6 +193,14 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.dependencies] [package.dependencies]
pyparsing = ">=2.0.2" pyparsing = ">=2.0.2"
[[package]]
name = "pathspec"
version = "0.8.1"
description = "Utility library for gitignore style pattern matching of file paths."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]] [[package]]
name = "pluggy" name = "pluggy"
version = "0.13.1" version = "0.13.1"
@ -188,9 +250,17 @@ wcwidth = "*"
checkqa-mypy = ["mypy (==v0.761)"] checkqa-mypy = ["mypy (==v0.761)"]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
[[package]]
name = "regex"
version = "2021.4.4"
description = "Alternative regular expression module, to replace re."
category = "dev"
optional = false
python-versions = "*"
[[package]] [[package]]
name = "sqlalchemy" name = "sqlalchemy"
version = "1.4.12" version = "1.4.17"
description = "Database Abstraction Library" description = "Database Abstraction Library"
category = "main" category = "main"
optional = false optional = false
@ -219,6 +289,30 @@ postgresql_psycopg2cffi = ["psycopg2cffi"]
pymysql = ["pymysql (<1)", "pymysql"] pymysql = ["pymysql (<1)", "pymysql"]
sqlcipher = ["sqlcipher3-binary"] sqlcipher = ["sqlcipher3-binary"]
[[package]]
name = "toml"
version = "0.10.2"
description = "Python Library for Tom's Obvious, Minimal Language"
category = "dev"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "typed-ast"
version = "1.4.3"
description = "a fork of Python 2 and 3 ast modules with type comment support"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "typing-extensions"
version = "3.10.0.0"
description = "Backported and Experimental Type Hints for Python 3.5+"
category = "dev"
optional = false
python-versions = "*"
[[package]] [[package]]
name = "wcwidth" name = "wcwidth"
version = "0.2.5" version = "0.2.5"
@ -258,16 +352,24 @@ locale = ["Babel (>=1.3)"]
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.8" python-versions = "^3.8"
content-hash = "48928fcd093c025ed3b9ed6153fd54310cbdefdaba133a57b4a2bfb6c9f5941f" content-hash = "8c38b0703447e638ee8181a4e449f0eab57858e171cd0de9d4e9fe07c61d0071"
[metadata.files] [metadata.files]
appdirs = [
{file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
{file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
]
atomicwrites = [ atomicwrites = [
{file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
{file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
] ]
attrs = [ attrs = [
{file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"}, {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"},
{file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"}, {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"},
]
black = [
{file = "black-21.5b2-py3-none-any.whl", hash = "sha256:e5cf21ebdffc7a9b29d73912b6a6a9a4df4ce70220d523c21647da2eae0751ef"},
{file = "black-21.5b2.tar.gz", hash = "sha256:1fc0e0a2c8ae7d269dfcf0c60a89afa299664f3e811395d40b1922dff8f854b5"},
] ]
click = [ click = [
{file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"},
@ -278,8 +380,8 @@ colorama = [
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
] ]
flask = [ flask = [
{file = "Flask-1.1.2-py2.py3-none-any.whl", hash = "sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557"}, {file = "Flask-1.1.4-py2.py3-none-any.whl", hash = "sha256:c34f04500f2cbbea882b1acb02002ad6fe6b7ffa64a6164577995657f50aed22"},
{file = "Flask-1.1.2.tar.gz", hash = "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060"}, {file = "Flask-1.1.4.tar.gz", hash = "sha256:0fbeb6180d383a9186d0d6ed954e0042ad9f18e0e8de088b2b419d526927d196"},
] ]
flask-login = [ flask-login = [
{file = "Flask-Login-0.5.0.tar.gz", hash = "sha256:6d33aef15b5bcead780acc339464aae8a6e28f13c90d8b1cf9de8b549d1c0b4b"}, {file = "Flask-Login-0.5.0.tar.gz", hash = "sha256:6d33aef15b5bcead780acc339464aae8a6e28f13c90d8b1cf9de8b549d1c0b4b"},
@ -290,49 +392,55 @@ flask-wtf = [
{file = "Flask_WTF-0.14.3-py2.py3-none-any.whl", hash = "sha256:57b3faf6fe5d6168bda0c36b0df1d05770f8e205e18332d0376ddb954d17aef2"}, {file = "Flask_WTF-0.14.3-py2.py3-none-any.whl", hash = "sha256:57b3faf6fe5d6168bda0c36b0df1d05770f8e205e18332d0376ddb954d17aef2"},
] ]
greenlet = [ greenlet = [
{file = "greenlet-1.0.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:1d1d4473ecb1c1d31ce8fd8d91e4da1b1f64d425c1dc965edc4ed2a63cfa67b2"}, {file = "greenlet-1.1.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:60848099b76467ef09b62b0f4512e7e6f0a2c977357a036de602b653667f5f4c"},
{file = "greenlet-1.0.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:cfd06e0f0cc8db2a854137bd79154b61ecd940dce96fad0cba23fe31de0b793c"}, {file = "greenlet-1.1.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f42ad188466d946f1b3afc0a9e1a266ac8926461ee0786c06baac6bd71f8a6f3"},
{file = "greenlet-1.0.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:eb333b90036358a0e2c57373f72e7648d7207b76ef0bd00a4f7daad1f79f5203"}, {file = "greenlet-1.1.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:76ed710b4e953fc31c663b079d317c18f40235ba2e3d55f70ff80794f7b57922"},
{file = "greenlet-1.0.0-cp27-cp27m-win32.whl", hash = "sha256:1a1ada42a1fd2607d232ae11a7b3195735edaa49ea787a6d9e6a53afaf6f3476"}, {file = "greenlet-1.1.0-cp27-cp27m-win32.whl", hash = "sha256:b33b51ab057f8a20b497ffafdb1e79256db0c03ef4f5e3d52e7497200e11f821"},
{file = "greenlet-1.0.0-cp27-cp27m-win_amd64.whl", hash = "sha256:f6f65bf54215e4ebf6b01e4bb94c49180a589573df643735107056f7a910275b"}, {file = "greenlet-1.1.0-cp27-cp27m-win_amd64.whl", hash = "sha256:ed1377feed808c9c1139bdb6a61bcbf030c236dd288d6fca71ac26906ab03ba6"},
{file = "greenlet-1.0.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:f59eded163d9752fd49978e0bab7a1ff21b1b8d25c05f0995d140cc08ac83379"}, {file = "greenlet-1.1.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:da862b8f7de577bc421323714f63276acb2f759ab8c5e33335509f0b89e06b8f"},
{file = "greenlet-1.0.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:875d4c60a6299f55df1c3bb870ebe6dcb7db28c165ab9ea6cdc5d5af36bb33ce"}, {file = "greenlet-1.1.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:5f75e7f237428755d00e7460239a2482fa7e3970db56c8935bd60da3f0733e56"},
{file = "greenlet-1.0.0-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:1bb80c71de788b36cefb0c3bb6bfab306ba75073dbde2829c858dc3ad70f867c"}, {file = "greenlet-1.1.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:258f9612aba0d06785143ee1cbf2d7361801c95489c0bd10c69d163ec5254a16"},
{file = "greenlet-1.0.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b5f1b333015d53d4b381745f5de842f19fe59728b65f0fbb662dafbe2018c3a5"}, {file = "greenlet-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d928e2e3c3906e0a29b43dc26d9b3d6e36921eee276786c4e7ad9ff5665c78a"},
{file = "greenlet-1.0.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:5352c15c1d91d22902582e891f27728d8dac3bd5e0ee565b6a9f575355e6d92f"}, {file = "greenlet-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cc407b68e0a874e7ece60f6639df46309376882152345508be94da608cc0b831"},
{file = "greenlet-1.0.0-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:2c65320774a8cd5fdb6e117c13afa91c4707548282464a18cf80243cf976b3e6"}, {file = "greenlet-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c557c809eeee215b87e8a7cbfb2d783fb5598a78342c29ade561440abae7d22"},
{file = "greenlet-1.0.0-cp35-cp35m-manylinux2014_ppc64le.whl", hash = "sha256:111cfd92d78f2af0bc7317452bd93a477128af6327332ebf3c2be7df99566683"}, {file = "greenlet-1.1.0-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:3d13da093d44dee7535b91049e44dd2b5540c2a0e15df168404d3dd2626e0ec5"},
{file = "greenlet-1.0.0-cp35-cp35m-win32.whl", hash = "sha256:cdb90267650c1edb54459cdb51dab865f6c6594c3a47ebd441bc493360c7af70"}, {file = "greenlet-1.1.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b3090631fecdf7e983d183d0fad7ea72cfb12fa9212461a9b708ff7907ffff47"},
{file = "greenlet-1.0.0-cp35-cp35m-win_amd64.whl", hash = "sha256:eac8803c9ad1817ce3d8d15d1bb82c2da3feda6bee1153eec5c58fa6e5d3f770"}, {file = "greenlet-1.1.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:06ecb43b04480e6bafc45cb1b4b67c785e183ce12c079473359e04a709333b08"},
{file = "greenlet-1.0.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:c93d1a71c3fe222308939b2e516c07f35a849c5047f0197442a4d6fbcb4128ee"}, {file = "greenlet-1.1.0-cp35-cp35m-win32.whl", hash = "sha256:944fbdd540712d5377a8795c840a97ff71e7f3221d3fddc98769a15a87b36131"},
{file = "greenlet-1.0.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:122c63ba795fdba4fc19c744df6277d9cfd913ed53d1a286f12189a0265316dd"}, {file = "greenlet-1.1.0-cp35-cp35m-win_amd64.whl", hash = "sha256:c767458511a59f6f597bfb0032a1c82a52c29ae228c2c0a6865cfeaeaac4c5f5"},
{file = "greenlet-1.0.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:c5b22b31c947ad8b6964d4ed66776bcae986f73669ba50620162ba7c832a6b6a"}, {file = "greenlet-1.1.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:2325123ff3a8ecc10ca76f062445efef13b6cf5a23389e2df3c02a4a527b89bc"},
{file = "greenlet-1.0.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:4365eccd68e72564c776418c53ce3c5af402bc526fe0653722bc89efd85bf12d"}, {file = "greenlet-1.1.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:598bcfd841e0b1d88e32e6a5ea48348a2c726461b05ff057c1b8692be9443c6e"},
{file = "greenlet-1.0.0-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:da7d09ad0f24270b20f77d56934e196e982af0d0a2446120cb772be4e060e1a2"}, {file = "greenlet-1.1.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:be9768e56f92d1d7cd94185bab5856f3c5589a50d221c166cc2ad5eb134bd1dc"},
{file = "greenlet-1.0.0-cp36-cp36m-win32.whl", hash = "sha256:647ba1df86d025f5a34043451d7c4a9f05f240bee06277a524daad11f997d1e7"}, {file = "greenlet-1.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfe7eac0d253915116ed0cd160a15a88981a1d194c1ef151e862a5c7d2f853d3"},
{file = "greenlet-1.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:e6e9fdaf6c90d02b95e6b0709aeb1aba5affbbb9ccaea5502f8638e4323206be"}, {file = "greenlet-1.1.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a6b035aa2c5fcf3dbbf0e3a8a5bc75286fc2d4e6f9cfa738788b433ec894919"},
{file = "greenlet-1.0.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:62afad6e5fd70f34d773ffcbb7c22657e1d46d7fd7c95a43361de979f0a45aef"}, {file = "greenlet-1.1.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca1c4a569232c063615f9e70ff9a1e2fee8c66a6fb5caf0f5e8b21a396deec3e"},
{file = "greenlet-1.0.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d3789c1c394944084b5e57c192889985a9f23bd985f6d15728c745d380318128"}, {file = "greenlet-1.1.0-cp36-cp36m-win32.whl", hash = "sha256:3096286a6072553b5dbd5efbefc22297e9d06a05ac14ba017233fedaed7584a8"},
{file = "greenlet-1.0.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:f5e2d36c86c7b03c94b8459c3bd2c9fe2c7dab4b258b8885617d44a22e453fb7"}, {file = "greenlet-1.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c35872b2916ab5a240d52a94314c963476c989814ba9b519bc842e5b61b464bb"},
{file = "greenlet-1.0.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:292e801fcb3a0b3a12d8c603c7cf340659ea27fd73c98683e75800d9fd8f704c"}, {file = "greenlet-1.1.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:b97c9a144bbeec7039cca44df117efcbeed7209543f5695201cacf05ba3b5857"},
{file = "greenlet-1.0.0-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:f3dc68272990849132d6698f7dc6df2ab62a88b0d36e54702a8fd16c0490e44f"}, {file = "greenlet-1.1.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:16183fa53bc1a037c38d75fdc59d6208181fa28024a12a7f64bb0884434c91ea"},
{file = "greenlet-1.0.0-cp37-cp37m-win32.whl", hash = "sha256:7cd5a237f241f2764324396e06298b5dee0df580cf06ef4ada0ff9bff851286c"}, {file = "greenlet-1.1.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6b1d08f2e7f2048d77343279c4d4faa7aef168b3e36039cba1917fffb781a8ed"},
{file = "greenlet-1.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:0ddd77586553e3daf439aa88b6642c5f252f7ef79a39271c25b1d4bf1b7cbb85"}, {file = "greenlet-1.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14927b15c953f8f2d2a8dffa224aa78d7759ef95284d4c39e1745cf36e8cdd2c"},
{file = "greenlet-1.0.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:90b6a25841488cf2cb1c8623a53e6879573010a669455046df5f029d93db51b7"}, {file = "greenlet-1.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bdcff4b9051fb1aa4bba4fceff6a5f770c6be436408efd99b76fc827f2a9319"},
{file = "greenlet-1.0.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:ed1d1351f05e795a527abc04a0d82e9aecd3bdf9f46662c36ff47b0b00ecaf06"}, {file = "greenlet-1.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c70c7dd733a4c56838d1f1781e769081a25fade879510c5b5f0df76956abfa05"},
{file = "greenlet-1.0.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:94620ed996a7632723a424bccb84b07e7b861ab7bb06a5aeb041c111dd723d36"}, {file = "greenlet-1.1.0-cp37-cp37m-win32.whl", hash = "sha256:0de64d419b1cb1bfd4ea544bedea4b535ef3ae1e150b0f2609da14bbf48a4a5f"},
{file = "greenlet-1.0.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:f97d83049715fd9dec7911860ecf0e17b48d8725de01e45de07d8ac0bd5bc378"}, {file = "greenlet-1.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:8833e27949ea32d27f7e96930fa29404dd4f2feb13cce483daf52e8842ec246a"},
{file = "greenlet-1.0.0-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:0a77691f0080c9da8dfc81e23f4e3cffa5accf0f5b56478951016d7cfead9196"}, {file = "greenlet-1.1.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:c1580087ab493c6b43e66f2bdd165d9e3c1e86ef83f6c2c44a29f2869d2c5bd5"},
{file = "greenlet-1.0.0-cp38-cp38-win32.whl", hash = "sha256:e1128e022d8dce375362e063754e129750323b67454cac5600008aad9f54139e"}, {file = "greenlet-1.1.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:ad80bb338cf9f8129c049837a42a43451fc7c8b57ad56f8e6d32e7697b115505"},
{file = "greenlet-1.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:5d4030b04061fdf4cbc446008e238e44936d77a04b2b32f804688ad64197953c"}, {file = "greenlet-1.1.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:a9017ff5fc2522e45562882ff481128631bf35da444775bc2776ac5c61d8bcae"},
{file = "greenlet-1.0.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:f8450d5ef759dbe59f84f2c9f77491bb3d3c44bc1a573746daf086e70b14c243"}, {file = "greenlet-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7920e3eccd26b7f4c661b746002f5ec5f0928076bd738d38d894bb359ce51927"},
{file = "greenlet-1.0.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:df8053867c831b2643b2c489fe1d62049a98566b1646b194cc815f13e27b90df"}, {file = "greenlet-1.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:408071b64e52192869129a205e5b463abda36eff0cebb19d6e63369440e4dc99"},
{file = "greenlet-1.0.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:df3e83323268594fa9755480a442cabfe8d82b21aba815a71acf1bb6c1776218"}, {file = "greenlet-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be13a18cec649ebaab835dff269e914679ef329204704869f2f167b2c163a9da"},
{file = "greenlet-1.0.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:181300f826625b7fd1182205b830642926f52bd8cdb08b34574c9d5b2b1813f7"}, {file = "greenlet-1.1.0-cp38-cp38-win32.whl", hash = "sha256:22002259e5b7828b05600a762579fa2f8b33373ad95a0ee57b4d6109d0e589ad"},
{file = "greenlet-1.0.0-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:58ca0f078d1c135ecf1879d50711f925ee238fe773dfe44e206d7d126f5bc664"}, {file = "greenlet-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:206295d270f702bc27dbdbd7651e8ebe42d319139e0d90217b2074309a200da8"},
{file = "greenlet-1.0.0-cp39-cp39-win32.whl", hash = "sha256:5f297cb343114b33a13755032ecf7109b07b9a0020e841d1c3cedff6602cc139"}, {file = "greenlet-1.1.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:096cb0217d1505826ba3d723e8981096f2622cde1eb91af9ed89a17c10aa1f3e"},
{file = "greenlet-1.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:5d69bbd9547d3bc49f8a545db7a0bd69f407badd2ff0f6e1a163680b5841d2b0"}, {file = "greenlet-1.1.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:03f28a5ea20201e70ab70518d151116ce939b412961c33827519ce620957d44c"},
{file = "greenlet-1.0.0.tar.gz", hash = "sha256:719e169c79255816cdcf6dccd9ed2d089a72a9f6c42273aae12d55e8d35bdcf8"}, {file = "greenlet-1.1.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:7db68f15486d412b8e2cfcd584bf3b3a000911d25779d081cbbae76d71bd1a7e"},
{file = "greenlet-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70bd1bb271e9429e2793902dfd194b653221904a07cbf207c3139e2672d17959"},
{file = "greenlet-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f92731609d6625e1cc26ff5757db4d32b6b810d2a3363b0ff94ff573e5901f6f"},
{file = "greenlet-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06d7ac89e6094a0a8f8dc46aa61898e9e1aec79b0f8b47b2400dd51a44dbc832"},
{file = "greenlet-1.1.0-cp39-cp39-win32.whl", hash = "sha256:adb94a28225005890d4cf73648b5131e885c7b4b17bc762779f061844aabcc11"},
{file = "greenlet-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:aa4230234d02e6f32f189fd40b59d5a968fe77e80f59c9c933384fe8ba535535"},
{file = "greenlet-1.1.0.tar.gz", hash = "sha256:c87df8ae3f01ffb4483c796fe1b15232ce2b219f0b18126948616224d3f658ee"},
] ]
itsdangerous = [ itsdangerous = [
{file = "itsdangerous-1.1.0-py2.py3-none-any.whl", hash = "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"}, {file = "itsdangerous-1.1.0-py2.py3-none-any.whl", hash = "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"},
@ -343,48 +451,81 @@ jinja2 = [
{file = "Jinja2-2.11.3.tar.gz", hash = "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"}, {file = "Jinja2-2.11.3.tar.gz", hash = "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"},
] ]
markupsafe = [ markupsafe = [
{file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"},
{file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"},
{file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"},
{file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"},
{file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"},
{file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"},
{file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"},
{file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"},
{file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"},
{file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"},
{file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"},
{file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"},
{file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"},
{file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"},
{file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"},
{file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"},
{file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"},
{file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"},
{file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"},
{file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"},
{file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"},
{file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"},
{file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"},
{file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"},
] ]
more-itertools = [ more-itertools = [
{file = "more-itertools-8.7.0.tar.gz", hash = "sha256:c5d6da9ca3ff65220c3bfd2a8db06d698f05d4d2b9be57e1deb2be5a45019713"}, {file = "more-itertools-8.8.0.tar.gz", hash = "sha256:83f0308e05477c68f56ea3a888172c78ed5d5b3c282addb67508e7ba6c8f813a"},
{file = "more_itertools-8.7.0-py3-none-any.whl", hash = "sha256:5652a9ac72209ed7df8d9c15daf4e1aa0e3d2ccd3c87f8265a0673cd9cbc9ced"}, {file = "more_itertools-8.8.0-py3-none-any.whl", hash = "sha256:2cf89ec599962f2ddc4d568a05defc40e0a587fbc10d5989713638864c36be4d"},
]
mypy = [
{file = "mypy-0.812-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a26f8ec704e5a7423c8824d425086705e381b4f1dfdef6e3a1edab7ba174ec49"},
{file = "mypy-0.812-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:28fb5479c494b1bab244620685e2eb3c3f988d71fd5d64cc753195e8ed53df7c"},
{file = "mypy-0.812-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:9743c91088d396c1a5a3c9978354b61b0382b4e3c440ce83cf77994a43e8c521"},
{file = "mypy-0.812-cp35-cp35m-win_amd64.whl", hash = "sha256:d7da2e1d5f558c37d6e8c1246f1aec1e7349e4913d8fb3cb289a35de573fe2eb"},
{file = "mypy-0.812-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4eec37370483331d13514c3f55f446fc5248d6373e7029a29ecb7b7494851e7a"},
{file = "mypy-0.812-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d65cc1df038ef55a99e617431f0553cd77763869eebdf9042403e16089fe746c"},
{file = "mypy-0.812-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:61a3d5b97955422964be6b3baf05ff2ce7f26f52c85dd88db11d5e03e146a3a6"},
{file = "mypy-0.812-cp36-cp36m-win_amd64.whl", hash = "sha256:25adde9b862f8f9aac9d2d11971f226bd4c8fbaa89fb76bdadb267ef22d10064"},
{file = "mypy-0.812-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:552a815579aa1e995f39fd05dde6cd378e191b063f031f2acfe73ce9fb7f9e56"},
{file = "mypy-0.812-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:499c798053cdebcaa916eef8cd733e5584b5909f789de856b482cd7d069bdad8"},
{file = "mypy-0.812-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:5873888fff1c7cf5b71efbe80e0e73153fe9212fafdf8e44adfe4c20ec9f82d7"},
{file = "mypy-0.812-cp37-cp37m-win_amd64.whl", hash = "sha256:9f94aac67a2045ec719ffe6111df543bac7874cee01f41928f6969756e030564"},
{file = "mypy-0.812-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d23e0ea196702d918b60c8288561e722bf437d82cb7ef2edcd98cfa38905d506"},
{file = "mypy-0.812-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:674e822aa665b9fd75130c6c5f5ed9564a38c6cea6a6432ce47eafb68ee578c5"},
{file = "mypy-0.812-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:abf7e0c3cf117c44d9285cc6128856106183938c68fd4944763003decdcfeb66"},
{file = "mypy-0.812-cp38-cp38-win_amd64.whl", hash = "sha256:0d0a87c0e7e3a9becdfbe936c981d32e5ee0ccda3e0f07e1ef2c3d1a817cf73e"},
{file = "mypy-0.812-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7ce3175801d0ae5fdfa79b4f0cfed08807af4d075b402b7e294e6aa72af9aa2a"},
{file = "mypy-0.812-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:b09669bcda124e83708f34a94606e01b614fa71931d356c1f1a5297ba11f110a"},
{file = "mypy-0.812-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:33f159443db0829d16f0a8d83d94df3109bb6dd801975fe86bacb9bf71628e97"},
{file = "mypy-0.812-cp39-cp39-win_amd64.whl", hash = "sha256:3f2aca7f68580dc2508289c729bd49ee929a436208d2b2b6aab15745a70a57df"},
{file = "mypy-0.812-py3-none-any.whl", hash = "sha256:2f9b3407c58347a452fc0736861593e105139b905cca7d097e413453a1d650b4"},
{file = "mypy-0.812.tar.gz", hash = "sha256:cd07039aa5df222037005b08fbbfd69b3ab0b0bd7a07d7906de75ae52c4e3119"},
]
mypy-extensions = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
] ]
packaging = [ packaging = [
{file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"},
{file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"},
] ]
pathspec = [
{file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"},
{file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"},
]
pluggy = [ pluggy = [
{file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
{file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
@ -401,41 +542,121 @@ pytest = [
{file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"}, {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"},
{file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"},
] ]
regex = [
{file = "regex-2021.4.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:619d71c59a78b84d7f18891fe914446d07edd48dc8328c8e149cbe0929b4e000"},
{file = "regex-2021.4.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:47bf5bf60cf04d72bf6055ae5927a0bd9016096bf3d742fa50d9bf9f45aa0711"},
{file = "regex-2021.4.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:281d2fd05555079448537fe108d79eb031b403dac622621c78944c235f3fcf11"},
{file = "regex-2021.4.4-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:bd28bc2e3a772acbb07787c6308e00d9626ff89e3bfcdebe87fa5afbfdedf968"},
{file = "regex-2021.4.4-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:7c2a1af393fcc09e898beba5dd59196edaa3116191cc7257f9224beaed3e1aa0"},
{file = "regex-2021.4.4-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c38c71df845e2aabb7fb0b920d11a1b5ac8526005e533a8920aea97efb8ec6a4"},
{file = "regex-2021.4.4-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:96fcd1888ab4d03adfc9303a7b3c0bd78c5412b2bfbe76db5b56d9eae004907a"},
{file = "regex-2021.4.4-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:ade17eb5d643b7fead300a1641e9f45401c98eee23763e9ed66a43f92f20b4a7"},
{file = "regex-2021.4.4-cp36-cp36m-win32.whl", hash = "sha256:e8e5b509d5c2ff12f8418006d5a90e9436766133b564db0abaec92fd27fcee29"},
{file = "regex-2021.4.4-cp36-cp36m-win_amd64.whl", hash = "sha256:11d773d75fa650cd36f68d7ca936e3c7afaae41b863b8c387a22aaa78d3c5c79"},
{file = "regex-2021.4.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d3029c340cfbb3ac0a71798100ccc13b97dddf373a4ae56b6a72cf70dfd53bc8"},
{file = "regex-2021.4.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:18c071c3eb09c30a264879f0d310d37fe5d3a3111662438889ae2eb6fc570c31"},
{file = "regex-2021.4.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:4c557a7b470908b1712fe27fb1ef20772b78079808c87d20a90d051660b1d69a"},
{file = "regex-2021.4.4-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:01afaf2ec48e196ba91b37451aa353cb7eda77efe518e481707e0515025f0cd5"},
{file = "regex-2021.4.4-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:3a9cd17e6e5c7eb328517969e0cb0c3d31fd329298dd0c04af99ebf42e904f82"},
{file = "regex-2021.4.4-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:90f11ff637fe8798933fb29f5ae1148c978cccb0452005bf4c69e13db951e765"},
{file = "regex-2021.4.4-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:919859aa909429fb5aa9cf8807f6045592c85ef56fdd30a9a3747e513db2536e"},
{file = "regex-2021.4.4-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:339456e7d8c06dd36a22e451d58ef72cef293112b559010db3d054d5560ef439"},
{file = "regex-2021.4.4-cp37-cp37m-win32.whl", hash = "sha256:67bdb9702427ceddc6ef3dc382455e90f785af4c13d495f9626861763ee13f9d"},
{file = "regex-2021.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:32e65442138b7b76dd8173ffa2cf67356b7bc1768851dded39a7a13bf9223da3"},
{file = "regex-2021.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1e1c20e29358165242928c2de1482fb2cf4ea54a6a6dea2bd7a0e0d8ee321500"},
{file = "regex-2021.4.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:314d66636c494ed9c148a42731b3834496cc9a2c4251b1661e40936814542b14"},
{file = "regex-2021.4.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6d1b01031dedf2503631d0903cb563743f397ccaf6607a5e3b19a3d76fc10480"},
{file = "regex-2021.4.4-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:741a9647fcf2e45f3a1cf0e24f5e17febf3efe8d4ba1281dcc3aa0459ef424dc"},
{file = "regex-2021.4.4-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:4c46e22a0933dd783467cf32b3516299fb98cfebd895817d685130cc50cd1093"},
{file = "regex-2021.4.4-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:e512d8ef5ad7b898cdb2d8ee1cb09a8339e4f8be706d27eaa180c2f177248a10"},
{file = "regex-2021.4.4-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:980d7be47c84979d9136328d882f67ec5e50008681d94ecc8afa8a65ed1f4a6f"},
{file = "regex-2021.4.4-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:ce15b6d103daff8e9fee13cf7f0add05245a05d866e73926c358e871221eae87"},
{file = "regex-2021.4.4-cp38-cp38-win32.whl", hash = "sha256:a91aa8619b23b79bcbeb37abe286f2f408d2f2d6f29a17237afda55bb54e7aac"},
{file = "regex-2021.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:c0502c0fadef0d23b128605d69b58edb2c681c25d44574fc673b0e52dce71ee2"},
{file = "regex-2021.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:598585c9f0af8374c28edd609eb291b5726d7cbce16be6a8b95aa074d252ee17"},
{file = "regex-2021.4.4-cp39-cp39-manylinux1_i686.whl", hash = "sha256:ee54ff27bf0afaf4c3b3a62bcd016c12c3fdb4ec4f413391a90bd38bc3624605"},
{file = "regex-2021.4.4-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7d9884d86dd4dd489e981d94a65cd30d6f07203d90e98f6f657f05170f6324c9"},
{file = "regex-2021.4.4-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:bf5824bfac591ddb2c1f0a5f4ab72da28994548c708d2191e3b87dd207eb3ad7"},
{file = "regex-2021.4.4-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:563085e55b0d4fb8f746f6a335893bda5c2cef43b2f0258fe1020ab1dd874df8"},
{file = "regex-2021.4.4-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9c3db21af35e3b3c05764461b262d6f05bbca08a71a7849fd79d47ba7bc33ed"},
{file = "regex-2021.4.4-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:3916d08be28a1149fb97f7728fca1f7c15d309a9f9682d89d79db75d5e52091c"},
{file = "regex-2021.4.4-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:fd45ff9293d9274c5008a2054ecef86a9bfe819a67c7be1afb65e69b405b3042"},
{file = "regex-2021.4.4-cp39-cp39-win32.whl", hash = "sha256:fa4537fb4a98fe8fde99626e4681cc644bdcf2a795038533f9f711513a862ae6"},
{file = "regex-2021.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:97f29f57d5b84e73fbaf99ab3e26134e6687348e95ef6b48cfd2c06807005a07"},
{file = "regex-2021.4.4.tar.gz", hash = "sha256:52ba3d3f9b942c49d7e4bc105bb28551c44065f139a65062ab7912bef10c9afb"},
]
sqlalchemy = [ sqlalchemy = [
{file = "SQLAlchemy-1.4.12-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:8c71a80a5474e6e9c9bbf1957ab1c73cdece9d33cfb26d9ea6e7aed41f535cd6"}, {file = "SQLAlchemy-1.4.17-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:c367ed95d41df584f412a9419b5ece85b0d6c2a08a51ae13ae47ef74ff9a9349"},
{file = "SQLAlchemy-1.4.12-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:b1d513ebb16a204c87296d774c2317950191583b34032540948f20096b63efe4"}, {file = "SQLAlchemy-1.4.17-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fdad4a33140b77df61d456922b7974c1f1bb2c35238f6809f078003a620c4734"},
{file = "SQLAlchemy-1.4.12-cp27-cp27m-win32.whl", hash = "sha256:4b749cdedf1afb613c3d31235258110e1f36231c15df9b8b63b3f13c712e4790"}, {file = "SQLAlchemy-1.4.17-cp27-cp27m-win32.whl", hash = "sha256:f1c68f7bd4a57ffdb85eab489362828dddf6cd565a4c18eda4c446c1d5d3059d"},
{file = "SQLAlchemy-1.4.12-cp27-cp27m-win_amd64.whl", hash = "sha256:b58f09f4ea42a92e0a8923f4598001f8935bd2ed0c4c6abb9903c5b4cd0d4015"}, {file = "SQLAlchemy-1.4.17-cp27-cp27m-win_amd64.whl", hash = "sha256:ee6e7ca09ff274c55d19a1e15ee6f884fa0230c0d9b8d22a456e249d08dee5bf"},
{file = "SQLAlchemy-1.4.12-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:b4bf83b05056349265b40de37c836517649ea9edd174301072f5a58c7b374f94"}, {file = "SQLAlchemy-1.4.17-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a5f00a2be7d777119e15ccfb5ba0b2a92e8a193959281089d79821a001095f80"},
{file = "SQLAlchemy-1.4.12-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:c94fe5ec27dec6a994293d1f194a97fcb904252526bbe72698229ec62c0f7281"}, {file = "SQLAlchemy-1.4.17-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:1dd77acbc19bee9c0ba858ff5e4e5d5c60895495c83b4df9bcdf4ad5e9b74f21"},
{file = "SQLAlchemy-1.4.12-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ac4a48e49e863a4d00d8a5ec94ff5540de1f5bcf96d8d54273a75c3278d8b4af"}, {file = "SQLAlchemy-1.4.17-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5732858e56d32fa7e02468f4fd2d8f01ddf709e5b93d035c637762890f8ed8b6"},
{file = "SQLAlchemy-1.4.12-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:e815a729b427bd997d681711dc0b22330e445a0a0c47e16b05d2038e814bd29f"}, {file = "SQLAlchemy-1.4.17-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:949ac299903d2ed8419086f81847381184e2264f3431a33af4679546dcc87f01"},
{file = "SQLAlchemy-1.4.12-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:aeb389136f3a39399ebb8e8ee17beba18d361cde9638059cfbf7e896354412b7"}, {file = "SQLAlchemy-1.4.17-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:196fb6bb2733834e506c925d7532f8eabad9d2304deef738a40846e54c31e236"},
{file = "SQLAlchemy-1.4.12-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:0c839000817201310a51af390545d7b316fafd6969ef250dad0a6d28c025214d"}, {file = "SQLAlchemy-1.4.17-cp36-cp36m-win32.whl", hash = "sha256:bde055c019e6e449ebc4ec61abd3e08690abeb028c7ada2a3b95d8e352b7b514"},
{file = "SQLAlchemy-1.4.12-cp36-cp36m-win32.whl", hash = "sha256:1e8a884d766fcc918199576bf37f1870327582640fa3302489d7415d815be8a9"}, {file = "SQLAlchemy-1.4.17-cp36-cp36m-win_amd64.whl", hash = "sha256:b0ad951a6e590bbcfbfeadc5748ef5ec8ede505a8119a71b235f7481cc08371c"},
{file = "SQLAlchemy-1.4.12-cp36-cp36m-win_amd64.whl", hash = "sha256:e11ccaa08975e414df6a16466377bb11af692b2a62255c3a70c0993cb2d7f2d7"}, {file = "SQLAlchemy-1.4.17-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:82922a320d38d7d6aa3a8130523ec7e8c70fa95f7ca7d0fd6ec114b626e4b10b"},
{file = "SQLAlchemy-1.4.12-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:deef50c730ddfb4169417a3a3b6393f1e90b0d5c1e62e1d090c1eb1132529f3f"}, {file = "SQLAlchemy-1.4.17-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e133e2551fa99c75849848a4ac08efb79930561eb629dd7d2dc9b7ee05256e6"},
{file = "SQLAlchemy-1.4.12-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:a21f41c4cdb76d7f68a6986b9f5c56bdc8eafbc366893d1031df0c367e832388"}, {file = "SQLAlchemy-1.4.17-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7e45043fe11d503e1c3f9dcf5b42f92d122a814237cd9af68a11dae46ecfcae1"},
{file = "SQLAlchemy-1.4.12-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:aec20f0ec5788bee91ecf667e9e30e5ed0add9233b63b0e34e916b21eb5bc850"}, {file = "SQLAlchemy-1.4.17-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:461a4ea803ce0834822f372617a68ac97f9fa1281f2a984624554c651d7c3ae1"},
{file = "SQLAlchemy-1.4.12-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:d5da8fff36593ac96dd3d60a4eb9495a142fb6d3f0ed23baf5567c0ef7aa9b47"}, {file = "SQLAlchemy-1.4.17-cp37-cp37m-win32.whl", hash = "sha256:4d93b62e98248e3e1ac1e91c2e6ee1e7316f704be1f734338b350b6951e6c175"},
{file = "SQLAlchemy-1.4.12-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:a4c9c947fc08d2ac48116c64b7dfbac22b9896619cb74923ba59876504ff6256"}, {file = "SQLAlchemy-1.4.17-cp37-cp37m-win_amd64.whl", hash = "sha256:a2d225c8863a76d15468896dc5af36f1e196b403eb9c7e0151e77ffab9e7df57"},
{file = "SQLAlchemy-1.4.12-cp37-cp37m-win32.whl", hash = "sha256:4c8c335b072967da27fef54fb53e74fadadd7d2167c5eb98f0bfb4bfeb3a6948"}, {file = "SQLAlchemy-1.4.17-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:b59b2c0a3b1d93027f6b6b8379a50c354483fe1ebe796c6740e157bb2e06d39a"},
{file = "SQLAlchemy-1.4.12-cp37-cp37m-win_amd64.whl", hash = "sha256:01b610951c83452ee5e7d912c4ed9db4538b15d66e96ca6696ec38f0c5ce2908"}, {file = "SQLAlchemy-1.4.17-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7222f3236c280fab3a2d76f903b493171f0ffc29667538cc388a5d5dd0216a88"},
{file = "SQLAlchemy-1.4.12-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:6b77880e23d3758db7ad65732304ab1c3a42f0cd20505f4a211750862563a161"}, {file = "SQLAlchemy-1.4.17-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4b09191ed22af149c07a880f309b7740f3f782ff13325bae5c6168a6aa57e715"},
{file = "SQLAlchemy-1.4.12-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:f04acd3840bcf33f941b049e24aeef0be5145b2cd5489a89559c11be2d25e262"}, {file = "SQLAlchemy-1.4.17-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216ff28fe803885ceb5b131dcee6507d28d255808dd5bcffcb3b5fa75be2e102"},
{file = "SQLAlchemy-1.4.12-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:691568d8238c756011d97a655a76820715cbc0295b7d294aa2f1d62fb0be4361"}, {file = "SQLAlchemy-1.4.17-cp38-cp38-win32.whl", hash = "sha256:dde05ae0987e43ec84e64d6722ce66305eda2a5e2b7d6fda004b37aabdfbb909"},
{file = "SQLAlchemy-1.4.12-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:0646a4caab207279532ffd3f173b4756ae3863f3a94e369b7d1b82831a7ad433"}, {file = "SQLAlchemy-1.4.17-cp38-cp38-win_amd64.whl", hash = "sha256:bc89e37c359dcd4d75b744e5e81af128ba678aa2ecea4be957e80e6e958a1612"},
{file = "SQLAlchemy-1.4.12-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:2b35206c11c415448caf5b7abddbfac6acbe37f79832ae2d1be013f0dfe252ea"}, {file = "SQLAlchemy-1.4.17-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:4c5e20666b33b03bf7f14953f0deb93007bf8c1342e985bd7c7cf25f46fac579"},
{file = "SQLAlchemy-1.4.12-cp38-cp38-win32.whl", hash = "sha256:89e755688476b7a925554a1e8a756e0dd6124dfb8fac80470a90cd8424326bee"}, {file = "SQLAlchemy-1.4.17-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f63e1f531a8bf52184e2afb53648511f3f8534decb7575b483a583d3cd8d13ed"},
{file = "SQLAlchemy-1.4.12-cp38-cp38-win_amd64.whl", hash = "sha256:1bc9ea9e54bbaf65fece8b719f56472748f75777806f4f5fadd8112a165eab19"}, {file = "SQLAlchemy-1.4.17-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7dc3d3285fb682316d580d84e6e0840fdd8ffdc05cb696db74b9dd746c729908"},
{file = "SQLAlchemy-1.4.12-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:1bdf65dc5263be4651aa34ebe07aa035c61421f145b0d43f4c0b1f3c33bec673"}, {file = "SQLAlchemy-1.4.17-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58c02d1771bb0e61bc9ced8f3b36b5714d9ece8fd4bdbe2a44a892574c3bbc3c"},
{file = "SQLAlchemy-1.4.12-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f90a42db44427bf98128d823502e0af3f4b83f208e09a3d51df5c2cd7f2a76cf"}, {file = "SQLAlchemy-1.4.17-cp39-cp39-win32.whl", hash = "sha256:6fe1c8dc26bc0005439cb78ebc78772a22cccc773f5a0e67cb3002d791f53f0f"},
{file = "SQLAlchemy-1.4.12-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:c9047989b8645d8830067dddb2bda544c625419b22b0f546660fd0bfe73341f6"}, {file = "SQLAlchemy-1.4.17-cp39-cp39-win_amd64.whl", hash = "sha256:7eb55d5583076c03aaf1510473fad2a61288490809049cb31028af56af7068ee"},
{file = "SQLAlchemy-1.4.12-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b7ed6ce2e32a68a3b417a848a409ed5b7e4c8e5fa8911b06c77a6be1cc767658"}, {file = "SQLAlchemy-1.4.17.tar.gz", hash = "sha256:651cdb3adcee13624ba22d5ff3e96f91e16a115d2ca489ddc16a8e4c217e8509"},
{file = "SQLAlchemy-1.4.12-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:5ffbd23ac4324e64a100310cd2cab6534f972ecf26bf3652e6847187c2e9e72d"}, ]
{file = "SQLAlchemy-1.4.12-cp39-cp39-win32.whl", hash = "sha256:ac7db7276c0807db73b58984d630404ab294c4ca59cf16157fdc15894dec4507"}, toml = [
{file = "SQLAlchemy-1.4.12-cp39-cp39-win_amd64.whl", hash = "sha256:ce5fc1099d194fbecc8d7c038c927d9daf75cbb83b3b314df3e43e308d67c33e"}, {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "SQLAlchemy-1.4.12.tar.gz", hash = "sha256:968e8cf7f269eaeed1b753cb5df4112be998c933df39421229fc7726c413672c"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
typed-ast = [
{file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"},
{file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"},
{file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"},
{file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"},
{file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"},
{file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"},
{file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"},
{file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"},
{file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"},
{file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"},
{file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"},
{file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"},
{file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"},
{file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"},
{file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"},
{file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"},
{file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"},
{file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"},
{file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"},
{file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"},
{file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"},
{file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"},
{file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"},
{file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"},
{file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"},
{file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"},
{file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"},
{file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"},
{file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"},
{file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"},
]
typing-extensions = [
{file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"},
{file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"},
{file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"},
] ]
wcwidth = [ wcwidth = [
{file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},

View File

@ -13,6 +13,15 @@ SQLAlchemy = "^1.4.12"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
pytest = "^5.2" pytest = "^5.2"
black = "^21.5b2"
mypy = "^0.812"
[tool.black]
extend-exclude = "^/amanuensis/cli/.*|^/amanuensis/config/.*|^/amanuensis/lexicon/.*|^/amanuensis/log/.*|^/amanuensis/models/.*|^/amanuensis/parser/.*|^/amanuensis/resources/.*|^/amanuensis/server/.*|^/amanuensis/user/.*|^/amanuensis/__main__.py"
[tool.mypy]
ignore_missing_imports = true
exclude = "amanuensis/cli/.*|amanuensis/config/.*|amanuensis/lexicon/.*|amanuensis/log/.*|amanuensis/models/.*|amanuensis/parser/.*|amanuensis/resources/.*|amanuensis/server/.*|amanuensis/user/.*|amanuensis/__main__.py"
[tool.pytest.ini_options] [tool.pytest.ini_options]
addopts = "--show-capture=log" addopts = "--show-capture=log"

View File

@ -11,9 +11,9 @@ import amanuensis.backend.user as userq
@pytest.fixture @pytest.fixture
def db(): def db() -> DbContext:
"""Provides an initialized database in memory.""" """Provides an initialized database in memory."""
db = DbContext('sqlite:///:memory:', debug=False) db = DbContext("sqlite:///:memory:", debug=False)
db.create_all() db.create_all()
return db return db
@ -21,58 +21,66 @@ def db():
@pytest.fixture @pytest.fixture
def make_user(db: DbContext): def make_user(db: DbContext):
"""Provides a factory function for creating users, with valid default values.""" """Provides a factory function for creating users, with valid default values."""
def user_factory(state={'nonce': 1}, **kwargs):
def user_factory(state={"nonce": 1}, **kwargs):
default_kwargs = { default_kwargs = {
'username': f'test_user_{state["nonce"]}', "username": f'test_user_{state["nonce"]}',
'password': 'password', "password": "password",
'display_name': None, "display_name": None,
'email': 'user@example.com', "email": "user@example.com",
'is_site_admin': False, "is_site_admin": False,
} }
state['nonce'] += 1 state["nonce"] += 1
updated_kwargs = {**default_kwargs, **kwargs} updated_kwargs = {**default_kwargs, **kwargs}
return userq.create(db, **updated_kwargs) return userq.create(db, **updated_kwargs)
return user_factory return user_factory
@pytest.fixture @pytest.fixture
def make_lexicon(db: DbContext): def make_lexicon(db: DbContext):
"""Provides a factory function for creating lexicons, with valid default values.""" """Provides a factory function for creating lexicons, with valid default values."""
def lexicon_factory(state={'nonce': 1}, **kwargs):
def lexicon_factory(state={"nonce": 1}, **kwargs):
default_kwargs = { default_kwargs = {
'name': f'Test_{state["nonce"]}', "name": f'Test_{state["nonce"]}',
'title': None, "title": None,
'prompt': f'Test Lexicon game {state["nonce"]}' "prompt": f'Test Lexicon game {state["nonce"]}',
} }
state['nonce'] += 1 state["nonce"] += 1
updated_kwargs = {**default_kwargs, **kwargs} updated_kwargs = {**default_kwargs, **kwargs}
return lexiq.create(db, **updated_kwargs) return lexiq.create(db, **updated_kwargs)
return lexicon_factory return lexicon_factory
@pytest.fixture @pytest.fixture
def make_membership(db: DbContext): def make_membership(db: DbContext):
"""Provides a factory function for creating memberships, with valid default values.""" """Provides a factory function for creating memberships, with valid default values."""
def membership_factory(**kwargs): def membership_factory(**kwargs):
default_kwargs = { default_kwargs = {
'is_editor': False, "is_editor": False,
} }
updated_kwargs = {**default_kwargs, **kwargs} updated_kwargs = {**default_kwargs, **kwargs}
return memq.create(db, **updated_kwargs) return memq.create(db, **updated_kwargs)
return membership_factory return membership_factory
@pytest.fixture @pytest.fixture
def make_character(db: DbContext): def make_character(db: DbContext):
"""Provides a factory function for creating characters, with valid default values.""" """Provides a factory function for creating characters, with valid default values."""
def character_factory(state={'nonce': 1}, **kwargs):
def character_factory(state={"nonce": 1}, **kwargs):
default_kwargs = { default_kwargs = {
'name': f'Character {state["nonce"]}', "name": f'Character {state["nonce"]}',
'signature': None, "signature": None,
} }
state['nonce'] += 1 state["nonce"] += 1
updated_kwargs = {**default_kwargs, **kwargs} updated_kwargs = {**default_kwargs, **kwargs}
return charq.create(db, **updated_kwargs) return charq.create(db, **updated_kwargs)
return character_factory return character_factory
@ -87,11 +95,8 @@ class TestFactory:
@pytest.fixture @pytest.fixture
def make( def make(
db: DbContext, db: DbContext, make_user, make_lexicon, make_membership, make_character
make_user, ) -> TestFactory:
make_lexicon,
make_membership,
make_character) -> TestFactory:
"""Fixture that groups all factory fixtures together.""" """Fixture that groups all factory fixtures together."""
return TestFactory( return TestFactory(
db, db,
@ -109,6 +114,8 @@ def lexicon_with_editor(make):
assert editor assert editor
lexicon = make.lexicon() lexicon = make.lexicon()
assert lexicon assert lexicon
membership = make.membership(user_id=editor.id, lexicon_id=lexicon.id, is_editor=True) membership = make.membership(
user_id=editor.id, lexicon_id=lexicon.id, is_editor=True
)
assert membership assert membership
return (lexicon, editor) return (lexicon, editor)

View File

@ -1,6 +1,7 @@
import pytest import pytest
from amanuensis.db import DbContext from amanuensis.db import DbContext
from amanuensis.db.models import Character, Lexicon, User
import amanuensis.backend.article as artiq import amanuensis.backend.article as artiq
from amanuensis.errors import ArgumentError from amanuensis.errors import ArgumentError
@ -9,18 +10,18 @@ from amanuensis.errors import ArgumentError
def test_create_article(db: DbContext, make): def test_create_article(db: DbContext, make):
"""Test new article creation""" """Test new article creation"""
# Create two users in a shared lexicon # Create two users in a shared lexicon
user1 = make.user() user1: User = make.user()
user2 = make.user() user2: User = make.user()
lexicon1 = make.lexicon() lexicon1: Lexicon = make.lexicon()
make.membership(user_id=user1.id, lexicon_id=lexicon1.id) make.membership(user_id=user1.id, lexicon_id=lexicon1.id)
make.membership(user_id=user2.id, lexicon_id=lexicon1.id) make.membership(user_id=user2.id, lexicon_id=lexicon1.id)
char1_1 = make.character(lexicon_id=lexicon1.id, user_id=user1.id) char1_1: Character = make.character(lexicon_id=lexicon1.id, user_id=user1.id)
char1_2 = make.character(lexicon_id=lexicon1.id, user_id=user2.id) char1_2: Character = make.character(lexicon_id=lexicon1.id, user_id=user2.id)
# Create a lexicon that only one user is in # Create a lexicon that only one user is in
lexicon2 = make.lexicon() lexicon2: Lexicon = make.lexicon()
make.membership(user_id=user2.id, lexicon_id=lexicon2.id) make.membership(user_id=user2.id, lexicon_id=lexicon2.id)
char2_2 = make.character(lexicon_id=lexicon2.id, user_id=user2.id) char2_2: Character = make.character(lexicon_id=lexicon2.id, user_id=user2.id)
# User cannot create article for another user's character # User cannot create article for another user's character
with pytest.raises(ArgumentError): with pytest.raises(ArgumentError):

View File

@ -7,35 +7,44 @@ from amanuensis.errors import ArgumentError
def test_create_character(db: DbContext, lexicon_with_editor, make): def test_create_character(db: DbContext, lexicon_with_editor, make):
"""Test creating a character.""" """Test creating a character."""
lexicon: Lexicon
user: User
lexicon, user = lexicon_with_editor lexicon, user = lexicon_with_editor
kwargs = { defaults: dict = {
'db': db, "db": db,
'user_id': user.id, "user_id": user.id,
'lexicon_id': lexicon.id, "lexicon_id": lexicon.id,
'name': 'Character Name', "name": "Character Name",
'signature': 'Signature', "signature": "Signature",
} }
kwargs: dict
# Bad argument types # Bad argument types
with pytest.raises(ArgumentError): with pytest.raises(ArgumentError):
charq.create(**{**kwargs, 'name': b'bytestring'}) kwargs = {**defaults, "name": b"bytestring"}
charq.create(**kwargs)
with pytest.raises(ArgumentError): with pytest.raises(ArgumentError):
charq.create(**{**kwargs, 'name': None}) kwargs = {**defaults, "name": None}
charq.create(**kwargs)
with pytest.raises(ArgumentError): with pytest.raises(ArgumentError):
charq.create(**{**kwargs, 'signature': b'bytestring'}) kwargs = {**defaults, "signature": b"bytestring"}
charq.create(**kwargs)
# Bad character name # Bad character name
with pytest.raises(ArgumentError): with pytest.raises(ArgumentError):
charq.create(**{**kwargs, 'name': ' '}) kwargs = {**defaults, "name": " "}
charq.create(**kwargs)
# Signature is auto-populated # Signature is auto-populated
char = charq.create(**{**kwargs, 'signature': None}) kwargs = {**defaults, "signature": None}
char = charq.create(**kwargs)
assert char.signature is not None assert char.signature is not None
# User must be in lexicon # User must be in lexicon
new_user = make.user() new_user: User = make.user()
with pytest.raises(ArgumentError): with pytest.raises(ArgumentError):
charq.create(**{**kwargs, 'user_id': new_user.id}) kwargs = {**defaults, "user_id": new_user.id}
charq.create(**kwargs)
def test_character_limits(db: DbContext, lexicon_with_editor): def test_character_limits(db: DbContext, lexicon_with_editor):
@ -47,27 +56,33 @@ def test_character_limits(db: DbContext, lexicon_with_editor):
# Set character limit to one and create a character # Set character limit to one and create a character
lexicon.character_limit = 1 lexicon.character_limit = 1
db.session.commit() db.session.commit()
char1 = charq.create(db, lexicon.id, user.id, 'Test Character 1', signature=None) char1: Character = charq.create(
assert char1.id, 'Failed to create character 1' db, lexicon.id, user.id, "Test Character 1", signature=None
)
assert char1.id, "Failed to create character 1"
# Creating a second character should fail # Creating a second character should fail
with pytest.raises(ArgumentError): with pytest.raises(ArgumentError):
char2 = charq.create(db, lexicon.id, user.id, 'Test Character 2', signature=None) char2: Character = charq.create(
db, lexicon.id, user.id, "Test Character 2", signature=None
)
assert char2 assert char2
# Raising the limit to 2 should allow a second character # Raising the limit to 2 should allow a second character
lexicon.character_limit = 2 lexicon.character_limit = 2
db.session.commit() db.session.commit()
char2 = charq.create(db, lexicon.id, user.id, 'Test Character 2', signature=None) char2 = charq.create(db, lexicon.id, user.id, "Test Character 2", signature=None)
assert char2.id, 'Failed to create character 2' assert char2.id, "Failed to create character 2"
# Creating a third character should fail # Creating a third character should fail
with pytest.raises(ArgumentError): with pytest.raises(ArgumentError):
char3 = charq.create(db, lexicon.id, user.id, 'Test Character 3', signature=None) char3: Character = charq.create(
db, lexicon.id, user.id, "Test Character 3", signature=None
)
assert char3 assert char3
# Setting the limit to null should allow a third character # Setting the limit to null should allow a third character
lexicon.character_limit = None lexicon.character_limit = None
db.session.commit() db.session.commit()
char3 = charq.create(db, lexicon.id, user.id, 'Test Character 3', signature=None) char3 = charq.create(db, lexicon.id, user.id, "Test Character 3", signature=None)
assert char3.id, 'Failed to create character 3' assert char3.id, "Failed to create character 3"

50
tests/test_index.py Normal file
View File

@ -0,0 +1,50 @@
from amanuensis.db.models import IndexType
import pytest
import amanuensis.backend.index as indq
from amanuensis.db import DbContext, Lexicon, User
from amanuensis.errors import ArgumentError
def test_create_index(db: DbContext, make):
"""Test new index creation"""
lexicon: Lexicon = make.lexicon()
defaults: dict = {
"db": db,
"lexicon_id": lexicon.id,
"index_type": IndexType.ETC,
"pattern": "&c.",
"logical_order": 0,
"display_order": 0,
"capacity": 0,
}
kwargs: dict
# Character indexes require nonempty patterns
with pytest.raises(ArgumentError):
kwargs = {**defaults, "index_type": IndexType.CHAR, "pattern": ""}
indq.create(**kwargs)
kwargs = {**defaults, "index_type": IndexType.CHAR, "pattern": "ABC"}
assert indq.create(**kwargs)
# Range indexes must follow the 1-2 format
with pytest.raises(ArgumentError):
kwargs = {**defaults, "index_type": IndexType.RANGE, "pattern": "ABC"}
indq.create(**kwargs)
kwargs = {**defaults, "index_type": IndexType.RANGE, "pattern": "A-F"}
assert indq.create(**kwargs)
# Prefix indexes require nonempty patterns
with pytest.raises(ArgumentError):
kwargs = {**defaults, "index_type": IndexType.CHAR, "pattern": ""}
indq.create(**kwargs)
kwargs = {**defaults, "index_type": IndexType.CHAR, "pattern": "Prefix:"}
assert indq.create(**kwargs)
# Etc indexes require nonempty patterns
with pytest.raises(ArgumentError):
kwargs = {**defaults, "index_type": IndexType.CHAR, "pattern": ""}
indq.create(**kwargs)
kwargs = {**defaults, "index_type": IndexType.CHAR, "pattern": "&c."}
assert indq.create(**kwargs)

View File

@ -1,3 +1,4 @@
from amanuensis.db.models import Lexicon
import datetime import datetime
import pytest import pytest
@ -9,28 +10,37 @@ from amanuensis.errors import ArgumentError
def test_create_lexicon(db: DbContext): def test_create_lexicon(db: DbContext):
"""Test new game creation.""" """Test new game creation."""
kwargs = { defaults: dict = {
'name': 'Test', "db": db,
'title': None, "name": "Test",
'prompt': 'A test Lexicon game' "title": None,
"prompt": "A test Lexicon game",
} }
kwargs: dict
# Test name constraints # Test name constraints
with pytest.raises(ArgumentError): with pytest.raises(ArgumentError):
lexiq.create(db, **{**kwargs, 'name': None}) kwargs = {**defaults, "name": None}
lexiq.create(**kwargs)
with pytest.raises(ArgumentError): with pytest.raises(ArgumentError):
lexiq.create(db, **{**kwargs, 'name': ''}) kwargs = {**defaults, "name": ""}
lexiq.create(**kwargs)
with pytest.raises(ArgumentError): with pytest.raises(ArgumentError):
lexiq.create(db, **{**kwargs, 'name': ' '}) kwargs = {**defaults, "name": " "}
lexiq.create(**kwargs)
with pytest.raises(ArgumentError): with pytest.raises(ArgumentError):
lexiq.create(db, **{**kwargs, 'name': '..'}) kwargs = {**defaults, "name": ".."}
lexiq.create(**kwargs)
with pytest.raises(ArgumentError): with pytest.raises(ArgumentError):
lexiq.create(db, **{**kwargs, 'name': '\x00'}) kwargs = {**defaults, "name": "\x00"}
lexiq.create(**kwargs)
with pytest.raises(ArgumentError): with pytest.raises(ArgumentError):
lexiq.create(db, **{**kwargs, 'name': 'space in name'}) kwargs = {**defaults, "name": "space in name"}
lexiq.create(**kwargs)
# Validate that creation populates fields, including timestamps # Validate that creation populates fields, including timestamps
before = datetime.datetime.utcnow() - datetime.timedelta(seconds=1) before = datetime.datetime.utcnow() - datetime.timedelta(seconds=1)
new_lexicon = lexiq.create(db, **kwargs) new_lexicon: Lexicon = lexiq.create(**defaults)
after = datetime.datetime.utcnow() + datetime.timedelta(seconds=1) after = datetime.datetime.utcnow() + datetime.timedelta(seconds=1)
assert new_lexicon assert new_lexicon
assert new_lexicon.id is not None assert new_lexicon.id is not None
@ -40,5 +50,4 @@ def test_create_lexicon(db: DbContext):
# No duplicate lexicon names # No duplicate lexicon names
with pytest.raises(ArgumentError): with pytest.raises(ArgumentError):
duplicate = lexiq.create(db, **kwargs) lexiq.create(**defaults)
assert duplicate

View File

@ -10,21 +10,21 @@ import amanuensis.backend.membership as memq
def test_create_membership(db: DbContext, make): def test_create_membership(db: DbContext, make):
"""Test joining a game.""" """Test joining a game."""
# Set up a user and a lexicon # Set up a user and a lexicon
new_user = make.user() new_user: User = make.user()
assert new_user.id, 'Failed to create user' assert new_user.id, "Failed to create user"
new_lexicon = make.lexicon() new_lexicon: Lexicon = make.lexicon()
assert new_lexicon.id, 'Failed to create lexicon' assert new_lexicon.id, "Failed to create lexicon"
# Add the user to the lexicon as an editor # Add the user to the lexicon as an editor
mem = memq.create(db, new_user.id, new_lexicon.id, True) mem: Membership = memq.create(db, new_user.id, new_lexicon.id, True)
assert mem, 'Failed to create membership' assert mem, "Failed to create membership"
# Check that the user and lexicon are mutually visible in the ORM relationships # Check that the user and lexicon are mutually visible in the ORM relationships
assert any(map(lambda mem: mem.lexicon == new_lexicon, new_user.memberships)) assert any(map(lambda mem: mem.lexicon == new_lexicon, new_user.memberships))
assert any(map(lambda mem: mem.user == new_user, new_lexicon.memberships)) assert any(map(lambda mem: mem.user == new_user, new_lexicon.memberships))
# Check that the editor flag was set properly # Check that the editor flag was set properly
editor = db( editor: User = db(
select(User) select(User)
.join(User.memberships) .join(User.memberships)
.join(Membership.lexicon) .join(Membership.lexicon)
@ -37,5 +37,4 @@ def test_create_membership(db: DbContext, make):
# Check that joining twice is not allowed # Check that joining twice is not allowed
with pytest.raises(ArgumentError): with pytest.raises(ArgumentError):
mem2 = memq.create(db, new_user.id, new_lexicon.id, False) memq.create(db, new_user.id, new_lexicon.id, False)
assert mem2

View File

@ -11,38 +11,47 @@ def test_create_post(db: DbContext, lexicon_with_editor):
lexicon, editor = lexicon_with_editor lexicon, editor = lexicon_with_editor
# argument dictionary for post object # argument dictionary for post object
kwargs = { defaults: dict = {
'lexicon_id': lexicon.id, "db": db,
'user_id': editor.id, "lexicon_id": lexicon.id,
'body': 'body' "user_id": editor.id,
"body": "body",
} }
kwargs: dict
# ids are integers # ids are integers
with pytest.raises(ArgumentError): with pytest.raises(ArgumentError):
postq.create(db, **{**kwargs, 'user_id': 'zero'}) kwargs = {**defaults, "user_id": "zero"}
postq.create(**kwargs)
with pytest.raises(ArgumentError): with pytest.raises(ArgumentError):
postq.create(db, **{**kwargs, 'lexicon_id': 'zero'}) kwargs = {**defaults, "lexicon_id": "zero"}
postq.create(**kwargs)
# empty arguments don't work # empty arguments don't work
with pytest.raises(ArgumentError): with pytest.raises(ArgumentError):
postq.create(db, **{**kwargs, 'lexicon_id': ''}) kwargs = {**defaults, "lexicon_id": ""}
postq.create(**kwargs)
with pytest.raises(ArgumentError): with pytest.raises(ArgumentError):
postq.create(db, **{**kwargs, 'user_id': ''}) kwargs = {**defaults, "user_id": ""}
postq.create(**kwargs)
with pytest.raises(ArgumentError): with pytest.raises(ArgumentError):
postq.create(db, **{**kwargs, 'body': ''}) kwargs = {**defaults, "body": ""}
postq.create(**kwargs)
# post with only whitespace doesn't work # post with only whitespace doesn't work
with pytest.raises(ArgumentError): with pytest.raises(ArgumentError):
postq.create(db, **{**kwargs, 'body': ' '}) kwargs = {**defaults, "body": " "}
postq.create(**kwargs)
# post creation works and populates fields # post creation works and populates fields
new_post = postq.create(db, **kwargs) new_post = postq.create(**defaults)
assert new_post assert new_post
assert new_post.lexicon_id is not None assert new_post.lexicon_id is not None
assert new_post.user_id is not None assert new_post.user_id is not None
assert new_post.body is not None assert new_post.body is not None
# post creation works when user is None # post creation works when user is None
new_post = postq.create(db, **{**kwargs, 'user_id': None}) kwargs = {**defaults, "user_id": None}
new_post = postq.create(**kwargs)
assert new_post assert new_post
assert new_post.user_id is None assert new_post.user_id is None

View File

@ -1,3 +1,4 @@
from amanuensis.db.models import User
import pytest import pytest
from amanuensis.db import DbContext from amanuensis.db import DbContext
@ -7,37 +8,45 @@ from amanuensis.errors import ArgumentError
def test_create_user(db: DbContext): def test_create_user(db: DbContext):
"""Test new user creation.""" """Test new user creation."""
kwargs = { defaults: dict = {
'username': 'username', "db": db,
'password': 'password', "username": "username",
'display_name': 'User Name', "password": "password",
'email': 'user@example.com', "display_name": "User Name",
'is_site_admin': False "email": "user@example.com",
"is_site_admin": False,
} }
kwargs: dict
# Test length constraints # Test length constraints
with pytest.raises(ArgumentError): with pytest.raises(ArgumentError):
userq.create(db, **{**kwargs, 'username': 'me'}) kwargs = {**defaults, "username": "me"}
userq.create(**kwargs)
with pytest.raises(ArgumentError): with pytest.raises(ArgumentError):
userq.create(db, **{**kwargs, 'username': 'the right honorable user-name, esquire'}) kwargs = {**defaults, "username": "the right honorable user-name, esquire"}
userq.create(**kwargs)
# Test allowed characters # Test allowed characters
with pytest.raises(ArgumentError): with pytest.raises(ArgumentError):
userq.create(db, **{**kwargs, 'username': 'user name'}) kwargs = {**defaults, "username": "user name"}
userq.create(**kwargs)
# No password # No password
with pytest.raises(ArgumentError): with pytest.raises(ArgumentError):
userq.create(db, **{**kwargs, 'password': None}) kwargs = {**defaults, "password": None}
userq.create(**kwargs)
# Valid creation works and populates fields # Valid creation works and populates fields
new_user = userq.create(db, **kwargs) new_user = userq.create(**defaults)
assert new_user assert new_user
assert new_user.id is not None assert new_user.id is not None
assert new_user.created is not None assert new_user.created is not None
# No duplicate usernames # No duplicate usernames
with pytest.raises(ArgumentError): with pytest.raises(ArgumentError):
duplicate = userq.create(db, **kwargs) userq.create(**defaults)
# Missing display name populates with username # Missing display name populates with username
user2_kw = {**kwargs, 'username': 'user2', 'display_name': None} user2_kw: dict = {**defaults, "username": "user2", "display_name": None}
user2 = userq.create(db, **user2_kw) user2: User = userq.create(**user2_kw)
assert user2.display_name is not None assert user2.display_name is not None