#35 Add flake8 linter and address issues

Fusionné
getty a fusionné 8 commits à partir de getty/gdritter/add-linter vers getty/master il y a 4 ans
22 fichiers modifiés avec 377 ajouts et 251 suppressions
  1. 5 0
      .drone.yml
  2. 2 0
      .flake8
  3. 1 3
      lament-configuration.py
  4. 47 30
      lc/app.py
  5. 3 2
      lc/config.py
  6. 3 3
      lc/error.py
  7. 47 31
      lc/model.py
  8. 14 3
      lc/request.py
  9. 6 3
      lc/view.py
  10. 11 7
      lc/web.py
  11. 0 2
      migrations/__init__.py
  12. 1 0
      migrations/m_0001_add_meta_table.py
  13. 162 109
      poetry.lock
  14. 2 1
      pyproject.toml
  15. 2 2
      scripts/migrate.py
  16. 4 3
      scripts/populate.py
  17. 1 1
      stubs/peewee.py
  18. 1 1
      stubs/pystache/__init__.py
  19. 14 6
      tasks.py
  20. 5 0
      tests/config.py
  21. 29 29
      tests/model.py
  22. 17 15
      tests/routes.py

+ 5 - 0
.drone.yml

@@ -18,6 +18,11 @@ steps:
   commands:
   - inv tc
 
+- name: lint
+  image: gdritter/lament-configuration-env:latest
+  commands:
+  - inv lint
+
 - name: test
   image: gdritter/lament-configuration-env:latest
   commands:

+ 2 - 0
.flake8

@@ -0,0 +1,2 @@
+[flake8]
+max-line-length = 100

+ 1 - 3
lament-configuration.py

@@ -1,6 +1,4 @@
-import os
-
-from lc.app import app
+from lc.app import app  # noqa: F401
 import lc.config
 import lc.model
 

+ 47 - 30
lc/app.py

@@ -1,7 +1,4 @@
-import contextlib
-import os
 import flask
-import sys
 
 import lc.config as c
 import lc.error as e
@@ -23,7 +20,11 @@ class Index(Endpoint):
 
         return render(
             "main",
-            v.Page(title="main", content=render("linklist", linklist), user=self.user,),
+            v.Page(
+                title="main",
+                content=render("linklist", linklist),
+                user=self.user,
+            ),
         )
 
 
@@ -39,7 +40,12 @@ class Auth(Endpoint):
 class Login(Endpoint):
     def html(self):
         return render(
-            "main", v.Page(title="login", content=render("login"), user=self.user,)
+            "main",
+            v.Page(
+                title="login",
+                content=render("login"),
+                user=self.user,
+            ),
         )
 
 
@@ -70,7 +76,9 @@ class CreateUser(Endpoint):
         return render(
             "main",
             v.Page(
-                title="add user", user=self.user, content=render("add_user", add_user),
+                title="add user",
+                user=self.user,
+                content=render("add_user", add_user),
             ),
         )
 
@@ -152,65 +160,72 @@ class CreateLink(Endpoint):
         url = flask.request.args.get("url", "")
         name = flask.request.args.get("name", "")
         tags = u.get_tags()
-        defaults = v.AddLinkDefaults(user=user, name=name, url=url, all_tags=tags,)
+        defaults = v.AddLinkDefaults(
+            user=user,
+            name=name,
+            url=url,
+            all_tags=tags,
+        )
         return render(
             "main",
             v.Page(
-                title="login", content=render("add_link", defaults), user=self.user,
+                title="login",
+                content=render("add_link", defaults),
+                user=self.user,
             ),
         )
 
     def api_post(self, user: str):
         u = self.require_authentication(user)
         req = self.request_data(r.Link)
-        l = m.Link.from_request(u, req)
-        return self.api_ok(l.link_url(), l.to_dict())
+        link = m.Link.from_request(u, req)
+        return self.api_ok(link.link_url(), link.to_dict())
 
 
-@endpoint("/u/<string:user>/l/<string:link>")
+@endpoint("/u/<string:user>/l/<string:link_id>")
 class GetLink(Endpoint):
-    def api_get(self, user: str, link: str):
+    def api_get(self, user: str, link_id: str):
         u = self.require_authentication(user)
-        l = u.get_link(int(link))
-        return self.api_ok(l.link_url(), l.to_dict())
+        link = u.get_link(int(link_id))
+        return self.api_ok(link.link_url(), link.to_dict())
 
-    def api_post(self, user: str, link: str):
+    def api_post(self, user: str, link_id: str):
         u = self.require_authentication(user)
-        l = u.get_link(int(link))
+        link = u.get_link(int(link_id))
         req = self.request_data(r.Link)
-        l.update_from_request(u, req)
-        raise e.LCRedirect(l.link_url())
+        link.update_from_request(u, req)
+        raise e.LCRedirect(link.link_url())
 
-    def api_delete(self, user: str, link: str):
+    def api_delete(self, user: str, link_id: str):
         u = self.require_authentication(user)
-        u.get_link(int(link)).delete_instance()
+        u.get_link(int(link_id)).delete_instance()
         return self.api_ok(u.base_url())
 
-    def html(self, user: str, link: str):
-        l = m.User.by_slug(user).get_link(int(link))
+    def html(self, user: str, link_id: str):
+        link = m.User.by_slug(user).get_link(int(link_id))
         return render(
             "main",
             v.Page(
-                title=f"link {l.name}",
+                title=f"link {link.name}",
                 content=render(
-                    "linklist", v.LinkList([l.to_view(self.user)], [], user=user)
+                    "linklist", v.LinkList([link.to_view(self.user)], [], user=user)
                 ),
                 user=self.user,
             ),
         )
 
 
-@endpoint("/u/<string:slug>/l/<string:link>/edit")
+@endpoint("/u/<string:slug>/l/<string:link_id>/edit")
 class EditLink(Endpoint):
-    def html(self, slug: str, link: str):
+    def html(self, slug: str, link_id: str):
         u = self.require_authentication(slug)
         all_tags = u.get_tags()
-        l = u.get_link(int(link))
+        link = u.get_link(int(link_id))
         return render(
             "main",
             v.Page(
                 title="login",
-                content=render("edit_link", v.SingleLink(l, all_tags)),
+                content=render("edit_link", v.SingleLink(link, all_tags)),
                 user=self.user,
             ),
         )
@@ -256,11 +271,13 @@ class GetStringSearch(Endpoint):
 @endpoint("/u/<string:user>/import")
 class PinboardImport(Endpoint):
     def html(self, user: str):
-        u = self.require_authentication(user)
+        _ = self.require_authentication(user)
         return render(
             "main",
             v.Page(
-                title=f"import pinboard data", content=render("import"), user=self.user,
+                title="import pinboard data",
+                content=render("import"),
+                user=self.user,
             ),
         )
 

+ 3 - 2
lc/config.py

@@ -29,7 +29,8 @@ class App:
     def from_env() -> "App":
         config = environ.to_config(Config)
         app = flask.Flask(
-            __name__, static_folder=os.path.join(os.getcwd(), config.static_path),
+            __name__,
+            static_folder=os.path.join(os.getcwd(), config.static_path),
         )
         app.secret_key = config.secret_key
         return App(
@@ -45,7 +46,7 @@ class App:
     def in_memory_db(self):
         try:
             self.db.close()
-        except:
+        except Exception:
             pass
         self.db.init(":memory:")
 

+ 3 - 3
lc/error.py

@@ -72,7 +72,7 @@ class BadPassword(LCException):
 @dataclass
 class NotImplemented(LCException):
     def __str__(self):
-        return f"Bad request: no handler for route."
+        return "Bad request: no handler for route."
 
     def http_code(self) -> int:
         return 404
@@ -81,7 +81,7 @@ class NotImplemented(LCException):
 @dataclass
 class BadPermissions(LCException):
     def __str__(self):
-        return f"Insufficient permissions."
+        return "Insufficient permissions."
 
     def http_code(self) -> int:
         return 403
@@ -123,7 +123,7 @@ class AlreadyUsedInvite(LCException):
 @dataclass
 class MismatchedPassword(LCException):
     def __str__(self):
-        return f"Provided passwords do not match. Please check your passwords."
+        return "Provided passwords do not match. Please check your passwords."
 
     def http_code(self) -> int:
         return 400

+ 47 - 31
lc/model.py

@@ -1,5 +1,4 @@
 from contextlib import contextmanager
-from dataclasses import dataclass
 import datetime
 import json
 from passlib.apps import custom_app_context as pwd
@@ -34,7 +33,7 @@ class Meta(Model):
     def fetch():
         try:
             return Meta.get(id=0)
-        except:
+        except Exception:
             meta = Meta.create(id=0)
             return meta
 
@@ -52,7 +51,10 @@ class User(Model):
     def from_request(user: r.User) -> "User":
         passhash = pwd.hash(user.password)
         try:
-            return User.create(name=user.name, passhash=passhash,)
+            return User.create(
+                name=user.name,
+                passhash=passhash,
+            )
         except peewee.IntegrityError:
             raise e.UserExists(name=user.name)
 
@@ -104,10 +106,11 @@ class User(Model):
         self, as_user: Optional["User"], page: int
     ) -> Tuple[List[v.Link], v.Pagination]:
         query = Link.select().where(
-            (Link.user == self) & ((self == as_user) | (Link.private == False))
+            (Link.user == self)
+            & ((self == as_user) | (Link.private == False))  # noqa: E712
         )
         links = query.order_by(-Link.created).paginate(page, c.app.per_page)
-        link_views = [l.to_view(as_user) for l in links]
+        link_views = [link.to_view(as_user) for link in links]
         pagination = v.Pagination.from_total(page, query.count())
         return link_views, pagination
 
@@ -140,38 +143,40 @@ class User(Model):
     def import_pinboard_data(self, stream):
         try:
             links = json.load(stream)
-        except json.decoder.JSONDecodeError as exn:
+        except json.decoder.JSONDecodeError:
             raise e.BadFileUpload("could not parse file as JSON")
 
         if not isinstance(links, list):
-            raise e.BadFileUpload(f"expected a list")
+            raise e.BadFileUpload("expected a list")
 
         # create and (for this session) cache the tags
         tags = {}
-        for l in links:
-            if "tags" not in l:
+        for link in links:
+            if "tags" not in link:
                 raise e.BadFileUpload("missing key {exn.args[0]}")
-            for t in l["tags"].split():
+            for t in link["tags"].split():
                 if t in tags:
                     continue
 
                 tags[t] = Tag.get_or_create_tag(self, t)
 
         with self.atomic():
-            for l in links:
+            for link in links:
                 try:
-                    time = datetime.datetime.strptime(l["time"], "%Y-%m-%dT%H:%M:%SZ")
+                    time = datetime.datetime.strptime(
+                        link["time"], "%Y-%m-%dT%H:%M:%SZ"
+                    )
                     ln = Link.create(
-                        url=l["href"],
-                        name=l["description"],
-                        description=l["extended"],
-                        private=l["shared"] == "no",
+                        url=link["href"],
+                        name=link["description"],
+                        description=link["extended"],
+                        private=link["shared"] == "no",
                         created=time,
                         user=self,
                     )
                 except KeyError as exn:
                     raise e.BadFileUpload(f"missing key {exn.args[0]}")
-                for t in l["tags"].split():
+                for t in link["tags"].split():
                     HasTag.get_or_create(link=ln, tag=tags[t])
 
     def get_tags(self) -> List[v.Tag]:
@@ -181,7 +186,9 @@ class User(Model):
         )
 
     def get_related_tags(self, tag: "Tag") -> List[v.Tag]:
-        # SELECT * from has_tag t1, has_tag t2, link l WHERE t1.link_id == l.id AND t2.link_id == l.id AND t1.id != t2.id AND t1 = self
+        # SELECT * from has_tag t1, has_tag t2, link l
+        #   WHERE t1.link_id == l.id AND t2.link_id == l.id
+        #   AND t1.id != t2.id AND t1 = self
         SelfTag = HasTag.alias()
         query = (
             HasTag.select(HasTag.tag)
@@ -190,7 +197,10 @@ class User(Model):
             .where((SelfTag.tag == tag) & (SelfTag.id != HasTag.id))
             .group_by(HasTag.tag)
         )
-        return sorted((t.tag.to_view() for t in query), key=lambda t: t.name,)
+        return sorted(
+            (t.tag.to_view() for t in query),
+            key=lambda t: t.name,
+        )
 
     def get_string_search(
         self, needle: str, as_user: Optional["User"], page: int
@@ -200,11 +210,11 @@ class User(Model):
         """
         query = Link.select().where(
             (Link.user == self)
-            & ((self == as_user) | (Link.private == False))
+            & ((self == as_user) | (Link.private == False))  # noqa: E712
             & (Link.name.contains(needle) | Link.description.contains(needle))
         )
         links = query.order_by(-Link.created).paginate(page, c.app.per_page)
-        link_views = [l.to_view(as_user) for l in links]
+        link_views = [link.to_view(as_user) for link in links]
         pagination = v.Pagination.from_total(page, query.count())
         return link_views, pagination
 
@@ -237,17 +247,17 @@ class Link(Model):
     ) -> Tuple[List["Link"], v.Pagination]:
         links = (
             Link.select()
-            .where((Link.user == as_user) | (Link.private == False))
+            .where((Link.user == as_user) | (Link.private == False))  # noqa: E712
             .order_by(-Link.created)
             .paginate(page, c.app.per_page)
         )
-        link_views = [l.to_view(as_user) for l in links]
+        link_views = [link.to_view(as_user) for link in links]
         pagination = v.Pagination.from_total(page, Link.select().count())
         return link_views, pagination
 
     @staticmethod
     def from_request(user: User, link: r.Link) -> "Link":
-        l = Link.create(
+        new_link = Link.create(
             url=link.url,
             name=link.name,
             description=link.description,
@@ -257,8 +267,8 @@ class Link(Model):
         )
         for tag_name in link.tags:
             tag = Tag.get_or_create_tag(user, tag_name)
-            HasTag.get_or_create(link=l, tag=tag)
-        return l
+            HasTag.get_or_create(link=new_link, tag=tag)
+        return new_link
 
     def update_from_request(self, user: User, link: r.Link):
         with self.atomic():
@@ -324,7 +334,10 @@ class Tag(Model):
             .join(Link)
             .where(
                 (HasTag.tag == self)
-                & ((HasTag.link.user == as_user) | (HasTag.link.private == False))
+                & (
+                    (HasTag.link.user == as_user)
+                    | (HasTag.link.private == False)  # noqa: E712
+                )
             )
         )
         links = [
@@ -337,7 +350,7 @@ class Tag(Model):
     def get_family(self) -> Iterator["Tag"]:
         yield self
         p = self
-        while (p := p.parent) :
+        while p := p.parent:
             yield p
 
     BAD_TAG_CHARS = set("{}[]\\()#?")
@@ -348,7 +361,7 @@ class Tag(Model):
 
     @staticmethod
     def get_or_create_tag(user: User, tag_name: str) -> "Tag":
-        if (t := Tag.get_or_none(name=tag_name, user=user)) :
+        if t := Tag.get_or_none(name=tag_name, user=user):
             return t
 
         if not Tag.is_valid_tag_name(tag_name):
@@ -406,7 +419,7 @@ class UserInvite(Model):
 
     @staticmethod
     def by_code(token: str) -> "UserInvite":
-        if (u := UserInvite.get_or_none(token=token)) :
+        if u := UserInvite.get_or_none(token=token):
             return u
         raise e.NoSuchInvite(invite=token)
 
@@ -414,7 +427,10 @@ class UserInvite(Model):
     def manufacture(creator: User) -> "UserInvite":
         now = datetime.datetime.now()
         token = c.app.serialize_token(
-            {"created_at": now.timestamp(), "created_by": creator.name,}
+            {
+                "created_at": now.timestamp(),
+                "created_by": creator.name,
+            }
         )
         return UserInvite.create(
             token=token,

+ 14 - 3
lc/request.py

@@ -32,7 +32,10 @@ class User(Request):
 
     @classmethod
     def from_form(cls, form: Mapping[str, str]):
-        return cls(name=form["username"], password=form["password"],)
+        return cls(
+            name=form["username"],
+            password=form["password"],
+        )
 
     def to_token(self) -> str:
         return c.app.serialize_token({"name": self.name})
@@ -47,7 +50,11 @@ class NewUser(Request):
 
     @classmethod
     def from_form(cls, form: Mapping[str, str]):
-        return cls(name=form["username"], n1=form["n1"], n2=form["n2"],)
+        return cls(
+            name=form["username"],
+            n1=form["n1"],
+            n2=form["n2"],
+        )
 
     def to_user_request(self) -> User:
         if self.n1 != self.n2:
@@ -65,7 +72,11 @@ class PasswordChange(Request):
 
     @classmethod
     def from_form(cls, form: Mapping[str, str]):
-        return cls(old=form["old"], n1=form["n1"], n2=form["n2"],)
+        return cls(
+            old=form["old"],
+            n1=form["n1"],
+            n2=form["n2"],
+        )
 
     def require_match(self):
         if self.n1 != self.n2:

+ 6 - 3
lc/view.py

@@ -26,7 +26,10 @@ class Pagination(View):
 
     @classmethod
     def from_total(cls, current, total) -> "Pagination":
-        return cls(current=current, last=((total - 1) // c.app.per_page) + 1,)
+        return cls(
+            current=current,
+            last=((total - 1) // c.app.per_page) + 1,
+        )
 
 
 @dataclass
@@ -91,8 +94,8 @@ class HierTagList:
             else:
                 chunks = tag.split("/")
                 focus = groups[chunks[0]] = groups.get(chunks[0], {})
-                for c in chunks[1:]:
-                    focus[c] = focus = focus.get(c, {})
+                for chunk in chunks[1:]:
+                    focus[chunk] = focus = focus.get(chunk, {})
 
         return "\n".join(self.render_html(k, v) for k, v in groups.items())
 

+ 11 - 7
lc/web.py

@@ -27,7 +27,7 @@ class Endpoint:
         # try finding the token
         token = None
         # first check the HTTP headers
-        if (auth := flask.request.headers.get("Authorization", None)) :
+        if auth := flask.request.headers.get("Authorization", None):
             token = auth.split()[1]
         # if that fails, check the session
         elif flask.session.get("auth", None):
@@ -40,7 +40,7 @@ class Endpoint:
         # it contains a valid user password, too
         try:
             payload = c.app.load_token(token)
-        except:
+        except Exception:
             # TODO: be more specific about what errors we're catching
             # here!
             return
@@ -58,13 +58,18 @@ class Endpoint:
     def just_get_user() -> Optional[m.User]:
         try:
             return Endpoint().user
-        except:
+        except Exception:
             # this is going to catch everything on the off chance that
             # there's a bug in the user-validation code: this is used
             # in error handlers, so we should be resilient to that!
             return None
 
-    SHOULD_REDIRECT = set(("application/x-www-form-urlencoded", "multipart/form-data",))
+    SHOULD_REDIRECT = set(
+        (
+            "application/x-www-form-urlencoded",
+            "multipart/form-data",
+        )
+    )
 
     def api_ok(self, redirect: str, data: Optional[dict] = None) -> ApiOK:
         if data is None:
@@ -164,6 +169,7 @@ def endpoint(route: str):
     def do_endpoint(endpoint_class: Type[Endpoint]):
         # we'll just make that explicit here
         assert Endpoint in endpoint_class.__bases__
+
         # finally, we need a function that we'll give to Flask in
         # order to actually dispatch to. This is the actual routing
         # function, which is why it just creates an instance of the
@@ -200,7 +206,6 @@ def render(name: str, data: Optional[v.View] = None) -> str:
 
 @c.app.app.errorhandler(404)
 def handle_404(e):
-    user = Endpoint.just_get_user()
     url = flask.request.path
     error = v.Error(code=404, message=f"Page {url} not found")
     page = v.Page(title="not found", content=render("error", error), user=None)
@@ -209,8 +214,7 @@ def handle_404(e):
 
 @c.app.app.errorhandler(500)
 def handle_500(e):
-    user = Endpoint.just_get_user()
     c.log(f"Internal error: {e}")
-    error = v.Error(code=500, message=f"An unexpected error occurred")
+    error = v.Error(code=500, message="An unexpected error occurred")
     page = v.Page(title="500", content=render("error", error), user=None)
     return render("main", page)

+ 0 - 2
migrations/__init__.py

@@ -1,2 +0,0 @@
-
-from . import m_0001_add_meta_table

+ 1 - 0
migrations/m_0001_add_meta_table.py

@@ -3,6 +3,7 @@ from lc.migration import migration
 
 # This migration just ensures that the meta table is updated to version 1
 
+
 @migration
 def run(m):
     pass

+ 162 - 109
poetry.lock

@@ -4,7 +4,7 @@ description = "A small Python module for determining appropriate platform-specif
 name = "appdirs"
 optional = false
 python-versions = "*"
-version = "1.4.3"
+version = "1.4.4"
 
 [[package]]
 category = "dev"
@@ -13,7 +13,7 @@ marker = "sys_platform == \"win32\""
 name = "atomicwrites"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "1.3.0"
+version = "1.4.0"
 
 [[package]]
 category = "main"
@@ -21,13 +21,13 @@ description = "Classes Without Boilerplate"
 name = "attrs"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "19.3.0"
+version = "20.2.0"
 
 [package.extras]
-azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"]
-dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"]
-docs = ["sphinx", "zope.interface"]
-tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"]
+dev = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "sphinx-rtd-theme", "pre-commit"]
+docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"]
+tests = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"]
+tests_no_zope = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"]
 
 [[package]]
 category = "dev"
@@ -35,18 +35,20 @@ description = "The uncompromising code formatter."
 name = "black"
 optional = false
 python-versions = ">=3.6"
-version = "19.10b0"
+version = "20.8b1"
 
 [package.dependencies]
 appdirs = "*"
-attrs = ">=18.1.0"
-click = ">=6.5"
+click = ">=7.1.2"
+mypy-extensions = ">=0.4.3"
 pathspec = ">=0.6,<1"
-regex = "*"
-toml = ">=0.9.4"
+regex = ">=2020.1.8"
+toml = ">=0.10.1"
 typed-ast = ">=1.4.0"
+typing-extensions = ">=3.7.4"
 
 [package.extras]
+colorama = ["colorama (>=0.4.3)"]
 d = ["aiohttp (>=3.3.2)", "aiohttp-cors"]
 
 [[package]]
@@ -55,7 +57,7 @@ description = "Python package for providing Mozilla's CA Bundle."
 name = "certifi"
 optional = false
 python-versions = "*"
-version = "2020.4.5.1"
+version = "2020.6.20"
 
 [[package]]
 category = "dev"
@@ -71,7 +73,7 @@ description = "Composable command line interface toolkit"
 name = "click"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-version = "7.1.1"
+version = "7.1.2"
 
 [[package]]
 category = "dev"
@@ -88,7 +90,7 @@ description = "Easily serialize dataclasses to and from JSON"
 name = "dataclasses-json"
 optional = false
 python-versions = ">=3.6"
-version = "0.4.2"
+version = "0.4.5"
 
 [package.dependencies]
 marshmallow = ">=3.3.0,<4.0.0"
@@ -115,6 +117,19 @@ dev = ["pytest", "coverage", "sphinx", "sphinx-rtd-theme", "pre-commit"]
 docs = ["sphinx", "sphinx-rtd-theme"]
 tests = ["pytest", "coverage"]
 
+[[package]]
+category = "dev"
+description = "the modular source code checker: pep8 pyflakes and co"
+name = "flake8"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
+version = "3.8.3"
+
+[package.dependencies]
+mccabe = ">=0.6.0,<0.7.0"
+pycodestyle = ">=2.6.0a1,<2.7.0"
+pyflakes = ">=2.2.0,<2.3.0"
+
 [[package]]
 category = "main"
 description = "A simple framework for building complex web applications."
@@ -140,7 +155,7 @@ description = "Internationalized Domain Names in Applications (IDNA)"
 name = "idna"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "2.9"
+version = "2.10"
 
 [[package]]
 category = "dev"
@@ -164,7 +179,7 @@ description = "A very fast and expressive template engine."
 name = "jinja2"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-version = "2.11.1"
+version = "2.11.2"
 
 [package.dependencies]
 MarkupSafe = ">=0.23"
@@ -186,12 +201,12 @@ description = "A lightweight library for converting complex datatypes to and fro
 name = "marshmallow"
 optional = false
 python-versions = ">=3.5"
-version = "3.5.1"
+version = "3.8.0"
 
 [package.extras]
-dev = ["pytest", "pytz", "simplejson", "mypy (0.761)", "flake8 (3.7.9)", "flake8-bugbear (20.1.4)", "pre-commit (>=1.20,<3.0)", "tox"]
-docs = ["sphinx (2.4.3)", "sphinx-issues (1.2.0)", "alabaster (0.7.12)", "sphinx-version-warning (1.1.2)"]
-lint = ["mypy (0.761)", "flake8 (3.7.9)", "flake8-bugbear (20.1.4)", "pre-commit (>=1.20,<3.0)"]
+dev = ["pytest", "pytz", "simplejson", "mypy (0.782)", "flake8 (3.8.3)", "flake8-bugbear (20.1.4)", "pre-commit (>=2.4,<3.0)", "tox"]
+docs = ["sphinx (3.2.1)", "sphinx-issues (1.2.0)", "alabaster (0.7.12)", "sphinx-version-warning (1.1.2)", "autodocsumm (0.2.0)"]
+lint = ["mypy (0.782)", "flake8 (3.8.3)", "flake8-bugbear (20.1.4)", "pre-commit (>=2.4,<3.0)"]
 tests = ["pytest", "pytz", "simplejson"]
 
 [[package]]
@@ -205,13 +220,21 @@ version = "1.5.1"
 [package.dependencies]
 marshmallow = ">=2.0.0"
 
+[[package]]
+category = "dev"
+description = "McCabe checker, plugin for flake8"
+name = "mccabe"
+optional = false
+python-versions = "*"
+version = "0.6.1"
+
 [[package]]
 category = "dev"
 description = "More routines for operating on iterables, beyond itertools"
 name = "more-itertools"
 optional = false
 python-versions = ">=3.5"
-version = "8.2.0"
+version = "8.5.0"
 
 [[package]]
 category = "dev"
@@ -243,7 +266,7 @@ description = "Core utilities for Python packages"
 name = "packaging"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "20.3"
+version = "20.4"
 
 [package.dependencies]
 pyparsing = ">=2.0.2"
@@ -269,7 +292,7 @@ description = "Utility library for gitignore style pattern matching of file path
 name = "pathspec"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-version = "0.7.0"
+version = "0.8.0"
 
 [[package]]
 category = "main"
@@ -277,7 +300,7 @@ description = "a little orm"
 name = "peewee"
 optional = false
 python-versions = "*"
-version = "3.13.2"
+version = "3.13.3"
 
 [[package]]
 category = "dev"
@@ -296,7 +319,23 @@ description = "library with cross-python path, ini-parsing, io, code, log facili
 name = "py"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "1.8.1"
+version = "1.9.0"
+
+[[package]]
+category = "dev"
+description = "Python style guide checker"
+name = "pycodestyle"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+version = "2.6.0"
+
+[[package]]
+category = "dev"
+description = "passive checker of Python programs"
+name = "pyflakes"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+version = "2.2.0"
 
 [[package]]
 category = "dev"
@@ -320,7 +359,7 @@ description = "pytest: simple powerful testing with Python"
 name = "pytest"
 optional = false
 python-versions = ">=3.5"
-version = "5.4.1"
+version = "5.4.3"
 
 [package.dependencies]
 atomicwrites = ">=1.0"
@@ -342,7 +381,7 @@ description = "Alternative regular expression module, to replace re."
 name = "regex"
 optional = false
 python-versions = "*"
-version = "2020.4.4"
+version = "2020.7.14"
 
 [[package]]
 category = "dev"
@@ -350,7 +389,7 @@ description = "Python HTTP for Humans."
 name = "requests"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-version = "2.23.0"
+version = "2.24.0"
 
 [package.dependencies]
 certifi = ">=2017.4.17"
@@ -368,7 +407,7 @@ description = "Python 2 and 3 compatibility utilities"
 name = "six"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
-version = "1.14.0"
+version = "1.15.0"
 
 [[package]]
 category = "main"
@@ -384,7 +423,7 @@ description = "Python Library for Tom's Obvious, Minimal Language"
 name = "toml"
 optional = false
 python-versions = "*"
-version = "0.10.0"
+version = "0.10.1"
 
 [[package]]
 category = "dev"
@@ -400,7 +439,7 @@ description = "Backported and Experimental Type Hints for Python 3.5+"
 name = "typing-extensions"
 optional = false
 python-versions = "*"
-version = "3.7.4.2"
+version = "3.7.4.3"
 
 [[package]]
 category = "main"
@@ -408,7 +447,7 @@ description = "Runtime inspection utilities for typing module."
 name = "typing-inspect"
 optional = false
 python-versions = "*"
-version = "0.5.0"
+version = "0.6.0"
 
 [package.dependencies]
 mypy-extensions = ">=0.3.0"
@@ -420,11 +459,11 @@ description = "HTTP library with thread-safe connection pooling, file post, and
 name = "urllib3"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
-version = "1.25.8"
+version = "1.25.10"
 
 [package.extras]
 brotli = ["brotlipy (>=0.6.0)"]
-secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
+secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0.14)", "ipaddress"]
 socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"]
 
 [[package]]
@@ -433,15 +472,15 @@ description = "The uWSGI server"
 name = "uwsgi"
 optional = false
 python-versions = "*"
-version = "2.0.18"
+version = "2.0.19.1"
 
 [[package]]
 category = "dev"
-description = "Measures number of Terminal column cells of wide-character codes"
+description = "Measures the displayed width of unicode strings in a terminal"
 name = "wcwidth"
 optional = false
 python-versions = "*"
-version = "0.1.9"
+version = "0.2.5"
 
 [[package]]
 category = "main"
@@ -456,57 +495,60 @@ dev = ["pytest", "pytest-timeout", "coverage", "tox", "sphinx", "pallets-sphinx-
 watchdog = ["watchdog"]
 
 [metadata]
-content-hash = "f40a6c27fd58db00cd96ef01da21d52516a783004dcd49d72787d734218dd8a9"
+content-hash = "c3de105fc1fe9d28c6e1951f8842bfd9acfe746b33c46e3442391e91898b3c75"
 python-versions = "^3.8"
 
 [metadata.files]
 appdirs = [
-    {file = "appdirs-1.4.3-py2.py3-none-any.whl", hash = "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"},
-    {file = "appdirs-1.4.3.tar.gz", hash = "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92"},
+    {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
+    {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
 ]
 atomicwrites = [
-    {file = "atomicwrites-1.3.0-py2.py3-none-any.whl", hash = "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4"},
-    {file = "atomicwrites-1.3.0.tar.gz", hash = "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"},
+    {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
+    {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
 ]
 attrs = [
-    {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"},
-    {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"},
+    {file = "attrs-20.2.0-py2.py3-none-any.whl", hash = "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc"},
+    {file = "attrs-20.2.0.tar.gz", hash = "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594"},
 ]
 black = [
-    {file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"},
-    {file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"},
+    {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"},
 ]
 certifi = [
-    {file = "certifi-2020.4.5.1-py2.py3-none-any.whl", hash = "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304"},
-    {file = "certifi-2020.4.5.1.tar.gz", hash = "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519"},
+    {file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"},
+    {file = "certifi-2020.6.20.tar.gz", hash = "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"},
 ]
 chardet = [
     {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"},
     {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"},
 ]
 click = [
-    {file = "click-7.1.1-py2.py3-none-any.whl", hash = "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"},
-    {file = "click-7.1.1.tar.gz", hash = "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc"},
+    {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"},
+    {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"},
 ]
 colorama = [
     {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"},
     {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"},
 ]
 dataclasses-json = [
-    {file = "dataclasses-json-0.4.2.tar.gz", hash = "sha256:65ac9ae2f7ec152ee01bf42c8c024736d4cd6f6fb761502dec92bd553931e3d9"},
-    {file = "dataclasses_json-0.4.2-py3-none-any.whl", hash = "sha256:dbb53ebbac30ef45f44f5f436b21bd5726a80a14e1a193958864229100271372"},
+    {file = "dataclasses-json-0.4.5.tar.gz", hash = "sha256:8026790fc917437d2949cd0d78af650d7c1b2f5055f13336a3a4aa32bd61a293"},
+    {file = "dataclasses_json-0.4.5-py3-none-any.whl", hash = "sha256:1c500aece88738a559763572f728a16ed85fddc45cb83868b5637556347e50bd"},
 ]
 environ-config = [
     {file = "environ-config-20.1.0.tar.gz", hash = "sha256:3167feda073bd3cd3457a3e5fa7c2836b6574c046cd0dcd79385ce3284e837bd"},
     {file = "environ_config-20.1.0-py2.py3-none-any.whl", hash = "sha256:0e300307520c1e6a5424018b7f70246a2c7f4cb5cc5cbbebccc6a982eb1767cb"},
 ]
+flake8 = [
+    {file = "flake8-3.8.3-py2.py3-none-any.whl", hash = "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c"},
+    {file = "flake8-3.8.3.tar.gz", hash = "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208"},
+]
 flask = [
     {file = "Flask-1.1.2-py2.py3-none-any.whl", hash = "sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557"},
     {file = "Flask-1.1.2.tar.gz", hash = "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060"},
 ]
 idna = [
-    {file = "idna-2.9-py2.py3-none-any.whl", hash = "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"},
-    {file = "idna-2.9.tar.gz", hash = "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"},
+    {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"},
+    {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
 ]
 invoke = [
     {file = "invoke-1.4.1-py2-none-any.whl", hash = "sha256:93e12876d88130c8e0d7fd6618dd5387d6b36da55ad541481dfa5e001656f134"},
@@ -518,8 +560,8 @@ itsdangerous = [
     {file = "itsdangerous-1.1.0.tar.gz", hash = "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19"},
 ]
 jinja2 = [
-    {file = "Jinja2-2.11.1-py2.py3-none-any.whl", hash = "sha256:b0eaf100007721b5c16c1fc1eecb87409464edc10469ddc9a22a27a99123be49"},
-    {file = "Jinja2-2.11.1.tar.gz", hash = "sha256:93187ffbc7808079673ef52771baa950426fd664d3aad1d0fa3e95644360e250"},
+    {file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"},
+    {file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"},
 ]
 markupsafe = [
     {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"},
@@ -557,16 +599,20 @@ markupsafe = [
     {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"},
 ]
 marshmallow = [
-    {file = "marshmallow-3.5.1-py2.py3-none-any.whl", hash = "sha256:ac2e13b30165501b7d41fc0371b8df35944f5849769d136f20e2c5f6cdc6e665"},
-    {file = "marshmallow-3.5.1.tar.gz", hash = "sha256:90854221bbb1498d003a0c3cc9d8390259137551917961c8b5258c64026b2f85"},
+    {file = "marshmallow-3.8.0-py2.py3-none-any.whl", hash = "sha256:2272273505f1644580fbc66c6b220cc78f893eb31f1ecde2af98ad28011e9811"},
+    {file = "marshmallow-3.8.0.tar.gz", hash = "sha256:47911dd7c641a27160f0df5fd0fe94667160ffe97f70a42c3cc18388d86098cc"},
 ]
 marshmallow-enum = [
     {file = "marshmallow-enum-1.5.1.tar.gz", hash = "sha256:38e697e11f45a8e64b4a1e664000897c659b60aa57bfa18d44e226a9920b6e58"},
     {file = "marshmallow_enum-1.5.1-py2.py3-none-any.whl", hash = "sha256:57161ab3dbfde4f57adeb12090f39592e992b9c86d206d02f6bd03ebec60f072"},
 ]
+mccabe = [
+    {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
+    {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
+]
 more-itertools = [
-    {file = "more-itertools-8.2.0.tar.gz", hash = "sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507"},
-    {file = "more_itertools-8.2.0-py3-none-any.whl", hash = "sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c"},
+    {file = "more-itertools-8.5.0.tar.gz", hash = "sha256:6f83822ae94818eae2612063a5101a7311e68ae8002005b5e05f03fd74a86a20"},
+    {file = "more_itertools-8.5.0-py3-none-any.whl", hash = "sha256:9b30f12df9393f0d28af9210ff8efe48d10c94f73e5daf886f10c4b0b0b4f03c"},
 ]
 mypy = [
     {file = "mypy-0.770-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:a34b577cdf6313bf24755f7a0e3f3c326d5c1f4fe7422d1d06498eb25ad0c600"},
@@ -589,27 +635,35 @@ mypy-extensions = [
     {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
 ]
 packaging = [
-    {file = "packaging-20.3-py2.py3-none-any.whl", hash = "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752"},
-    {file = "packaging-20.3.tar.gz", hash = "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3"},
+    {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"},
+    {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"},
 ]
 passlib = [
     {file = "passlib-1.7.2-py2.py3-none-any.whl", hash = "sha256:68c35c98a7968850e17f1b6892720764cc7eed0ef2b7cb3116a89a28e43fe177"},
     {file = "passlib-1.7.2.tar.gz", hash = "sha256:8d666cef936198bc2ab47ee9b0410c94adf2ba798e5a84bf220be079ae7ab6a8"},
 ]
 pathspec = [
-    {file = "pathspec-0.7.0-py2.py3-none-any.whl", hash = "sha256:163b0632d4e31cef212976cf57b43d9fd6b0bac6e67c26015d611a647d5e7424"},
-    {file = "pathspec-0.7.0.tar.gz", hash = "sha256:562aa70af2e0d434367d9790ad37aed893de47f1693e4201fd1d3dca15d19b96"},
+    {file = "pathspec-0.8.0-py2.py3-none-any.whl", hash = "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0"},
+    {file = "pathspec-0.8.0.tar.gz", hash = "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"},
 ]
 peewee = [
-    {file = "peewee-3.13.2.tar.gz", hash = "sha256:85f6696b6691a315646047e0b19e9a28258b35612b7121bc4eb1b61ff53c760a"},
+    {file = "peewee-3.13.3.tar.gz", hash = "sha256:1269a9736865512bd4056298003aab190957afe07d2616cf22eaf56cb6398369"},
 ]
 pluggy = [
     {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
     {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
 ]
 py = [
-    {file = "py-1.8.1-py2.py3-none-any.whl", hash = "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"},
-    {file = "py-1.8.1.tar.gz", hash = "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa"},
+    {file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"},
+    {file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"},
+]
+pycodestyle = [
+    {file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"},
+    {file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"},
+]
+pyflakes = [
+    {file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"},
+    {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"},
 ]
 pyparsing = [
     {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
@@ -619,47 +673,46 @@ pystache = [
     {file = "pystache-0.5.4.tar.gz", hash = "sha256:f7bbc265fb957b4d6c7c042b336563179444ab313fb93a719759111eabd3b85a"},
 ]
 pytest = [
-    {file = "pytest-5.4.1-py3-none-any.whl", hash = "sha256:0e5b30f5cb04e887b91b1ee519fa3d89049595f428c1db76e73bd7f17b09b172"},
-    {file = "pytest-5.4.1.tar.gz", hash = "sha256:84dde37075b8805f3d1f392cc47e38a0e59518fb46a431cfdaf7cf1ce805f970"},
+    {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"},
+    {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"},
 ]
 regex = [
-    {file = "regex-2020.4.4-cp27-cp27m-win32.whl", hash = "sha256:90742c6ff121a9c5b261b9b215cb476eea97df98ea82037ec8ac95d1be7a034f"},
-    {file = "regex-2020.4.4-cp27-cp27m-win_amd64.whl", hash = "sha256:24f4f4062eb16c5bbfff6a22312e8eab92c2c99c51a02e39b4eae54ce8255cd1"},
-    {file = "regex-2020.4.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:08119f707f0ebf2da60d2f24c2f39ca616277bb67ef6c92b72cbf90cbe3a556b"},
-    {file = "regex-2020.4.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:c9423a150d3a4fc0f3f2aae897a59919acd293f4cb397429b120a5fcd96ea3db"},
-    {file = "regex-2020.4.4-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:c087bff162158536387c53647411db09b6ee3f9603c334c90943e97b1052a156"},
-    {file = "regex-2020.4.4-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:1cbe0fa0b7f673400eb29e9ef41d4f53638f65f9a2143854de6b1ce2899185c3"},
-    {file = "regex-2020.4.4-cp36-cp36m-win32.whl", hash = "sha256:0ce9537396d8f556bcfc317c65b6a0705320701e5ce511f05fc04421ba05b8a8"},
-    {file = "regex-2020.4.4-cp36-cp36m-win_amd64.whl", hash = "sha256:7e1037073b1b7053ee74c3c6c0ada80f3501ec29d5f46e42669378eae6d4405a"},
-    {file = "regex-2020.4.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:4385f12aa289d79419fede43f979e372f527892ac44a541b5446617e4406c468"},
-    {file = "regex-2020.4.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:a58dd45cb865be0ce1d5ecc4cfc85cd8c6867bea66733623e54bd95131f473b6"},
-    {file = "regex-2020.4.4-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:ccccdd84912875e34c5ad2d06e1989d890d43af6c2242c6fcfa51556997af6cd"},
-    {file = "regex-2020.4.4-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:ea4adf02d23b437684cd388d557bf76e3afa72f7fed5bbc013482cc00c816948"},
-    {file = "regex-2020.4.4-cp37-cp37m-win32.whl", hash = "sha256:2294f8b70e058a2553cd009df003a20802ef75b3c629506be20687df0908177e"},
-    {file = "regex-2020.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:e91ba11da11cf770f389e47c3f5c30473e6d85e06d7fd9dcba0017d2867aab4a"},
-    {file = "regex-2020.4.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5635cd1ed0a12b4c42cce18a8d2fb53ff13ff537f09de5fd791e97de27b6400e"},
-    {file = "regex-2020.4.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:23069d9c07e115537f37270d1d5faea3e0bdded8279081c4d4d607a2ad393683"},
-    {file = "regex-2020.4.4-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c162a21e0da33eb3d31a3ac17a51db5e634fc347f650d271f0305d96601dc15b"},
-    {file = "regex-2020.4.4-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:fb95debbd1a824b2c4376932f2216cc186912e389bdb0e27147778cf6acb3f89"},
-    {file = "regex-2020.4.4-cp38-cp38-win32.whl", hash = "sha256:2a3bf8b48f8e37c3a40bb3f854bf0121c194e69a650b209628d951190b862de3"},
-    {file = "regex-2020.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:5bfed051dbff32fd8945eccca70f5e22b55e4148d2a8a45141a3b053d6455ae3"},
-    {file = "regex-2020.4.4.tar.gz", hash = "sha256:295badf61a51add2d428a46b8580309c520d8b26e769868b922750cf3ce67142"},
+    {file = "regex-2020.7.14-cp27-cp27m-win32.whl", hash = "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7"},
+    {file = "regex-2020.7.14-cp27-cp27m-win_amd64.whl", hash = "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644"},
+    {file = "regex-2020.7.14-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc"},
+    {file = "regex-2020.7.14-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067"},
+    {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd"},
+    {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88"},
+    {file = "regex-2020.7.14-cp36-cp36m-win32.whl", hash = "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4"},
+    {file = "regex-2020.7.14-cp36-cp36m-win_amd64.whl", hash = "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f"},
+    {file = "regex-2020.7.14-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162"},
+    {file = "regex-2020.7.14-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf"},
+    {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7"},
+    {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89"},
+    {file = "regex-2020.7.14-cp37-cp37m-win32.whl", hash = "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6"},
+    {file = "regex-2020.7.14-cp37-cp37m-win_amd64.whl", hash = "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204"},
+    {file = "regex-2020.7.14-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99"},
+    {file = "regex-2020.7.14-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e"},
+    {file = "regex-2020.7.14-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e"},
+    {file = "regex-2020.7.14-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a"},
+    {file = "regex-2020.7.14-cp38-cp38-win32.whl", hash = "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341"},
+    {file = "regex-2020.7.14-cp38-cp38-win_amd64.whl", hash = "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840"},
+    {file = "regex-2020.7.14.tar.gz", hash = "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb"},
 ]
 requests = [
-    {file = "requests-2.23.0-py2.py3-none-any.whl", hash = "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee"},
-    {file = "requests-2.23.0.tar.gz", hash = "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"},
+    {file = "requests-2.24.0-py2.py3-none-any.whl", hash = "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"},
+    {file = "requests-2.24.0.tar.gz", hash = "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"},
 ]
 six = [
-    {file = "six-1.14.0-py2.py3-none-any.whl", hash = "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"},
-    {file = "six-1.14.0.tar.gz", hash = "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a"},
+    {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
+    {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"},
 ]
 stringcase = [
     {file = "stringcase-1.2.0.tar.gz", hash = "sha256:48a06980661908efe8d9d34eab2b6c13aefa2163b3ced26972902e3bdfd87008"},
 ]
 toml = [
-    {file = "toml-0.10.0-py2.7.egg", hash = "sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"},
-    {file = "toml-0.10.0-py2.py3-none-any.whl", hash = "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"},
-    {file = "toml-0.10.0.tar.gz", hash = "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c"},
+    {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"},
+    {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"},
 ]
 typed-ast = [
     {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"},
@@ -685,25 +738,25 @@ typed-ast = [
     {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"},
 ]
 typing-extensions = [
-    {file = "typing_extensions-3.7.4.2-py2-none-any.whl", hash = "sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392"},
-    {file = "typing_extensions-3.7.4.2-py3-none-any.whl", hash = "sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5"},
-    {file = "typing_extensions-3.7.4.2.tar.gz", hash = "sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae"},
+    {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"},
+    {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"},
+    {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"},
 ]
 typing-inspect = [
-    {file = "typing_inspect-0.5.0-py2-none-any.whl", hash = "sha256:75c97b7854426a129f3184c68588db29091ff58e6908ed520add1d52fc44df6e"},
-    {file = "typing_inspect-0.5.0-py3-none-any.whl", hash = "sha256:c6ed1cd34860857c53c146a6704a96da12e1661087828ce350f34addc6e5eee3"},
-    {file = "typing_inspect-0.5.0.tar.gz", hash = "sha256:811b44f92e780b90cfe7bac94249a4fae87cfaa9b40312765489255045231d9c"},
+    {file = "typing_inspect-0.6.0-py2-none-any.whl", hash = "sha256:de08f50a22955ddec353876df7b2545994d6df08a2f45d54ac8c05e530372ca0"},
+    {file = "typing_inspect-0.6.0-py3-none-any.whl", hash = "sha256:3b98390df4d999a28cf5b35d8b333425af5da2ece8a4ea9e98f71e7591347b4f"},
+    {file = "typing_inspect-0.6.0.tar.gz", hash = "sha256:8f1b1dd25908dbfd81d3bebc218011531e7ab614ba6e5bf7826d887c834afab7"},
 ]
 urllib3 = [
-    {file = "urllib3-1.25.8-py2.py3-none-any.whl", hash = "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc"},
-    {file = "urllib3-1.25.8.tar.gz", hash = "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"},
+    {file = "urllib3-1.25.10-py2.py3-none-any.whl", hash = "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"},
+    {file = "urllib3-1.25.10.tar.gz", hash = "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a"},
 ]
 uwsgi = [
-    {file = "uwsgi-2.0.18.tar.gz", hash = "sha256:4972ac538800fb2d421027f49b4a1869b66048839507ccf0aa2fda792d99f583"},
+    {file = "uWSGI-2.0.19.1.tar.gz", hash = "sha256:faa85e053c0b1be4d5585b0858d3a511d2cd10201802e8676060fd0a109e5869"},
 ]
 wcwidth = [
-    {file = "wcwidth-0.1.9-py2.py3-none-any.whl", hash = "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1"},
-    {file = "wcwidth-0.1.9.tar.gz", hash = "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1"},
+    {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
+    {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
 ]
 werkzeug = [
     {file = "Werkzeug-1.0.1-py2.py3-none-any.whl", hash = "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43"},

+ 2 - 1
pyproject.toml

@@ -17,10 +17,11 @@ environ-config = "^20.1.0"
 [tool.poetry.dev-dependencies]
 pytest = "^5.4.1"
 invoke = "^1.4.1"
-black = "^19.10b0"
+black = "^20.8b0"
 requests = "^2.23.0"
 mypy = "^0.770"
 uwsgi = "^2.0.18"
+flake8 = "^3.8.3"
 
 [build-system]
 requires = ["poetry>=0.12"]

+ 2 - 2
scripts/migrate.py

@@ -11,7 +11,7 @@ m.create_tables()
 meta = m.Meta.fetch()
 print(f"Current schema version is: {meta.version}")
 
-import migrations
+import migrations  # noqa: F401, E402
 
 runnable = filter(lambda m: m.version > meta.version, lc.migration.registered)
 
@@ -19,7 +19,7 @@ for migration in sorted(runnable, key=lambda m: m.version):
     print(f"{migration.version} - {migration.name}")
     try:
         migration.run(playhouse.migrate.SqliteMigrator(c.app.db))
-    except:
+    except Exception:
         sys.exit(1)
 
     meta.version = migration.version

+ 4 - 3
scripts/populate.py

@@ -1,7 +1,5 @@
 #!/usr/bin/env python3
 
-import datetime
-import json
 import os
 
 import lc.config as c
@@ -15,7 +13,10 @@ def main():
     u = m.User.get_or_none(name="gdritter")
     if not u:
         u = m.User.from_request(
-            r.User(name="gdritter", password=os.getenv("PASSWORD", "behest").strip(),)
+            r.User(
+                name="gdritter",
+                password=os.getenv("PASSWORD", "behest").strip(),
+            )
         )
         u.set_as_admin()
 

+ 1 - 1
stubs/peewee.py

@@ -1,4 +1,4 @@
-from typing import Any, Optional, TypeVar, Type, List
+from typing import Any, Optional, TypeVar, Type
 
 
 class Expression:

+ 1 - 1
stubs/pystache/__init__.py

@@ -1,6 +1,6 @@
 from typing import Any, List
 
-import pystache.loader
+import pystache.loader  # noqa: F401
 
 
 class Renderer:

+ 14 - 6
tasks.py

@@ -16,8 +16,8 @@ def run(c, port=8080, host="127.0.0.1"):
         env={
             "FLASK_APP": "lament-configuration.py",
             "LC_APP_PATH": f"http://{host}:{port}",
-            "LC_DB_PATH": f"test.db",
-            "LC_SECRET_KEY": f"TESTING_KEY",
+            "LC_DB_PATH": "test.db",
+            "LC_SECRET_KEY": "TESTING_KEY",
         },
     )
 
@@ -26,12 +26,12 @@ def run(c, port=8080, host="127.0.0.1"):
 def migrate(c, port=8080, host="127.0.0.1"):
     """Run migrations to update the database schema"""
     c.run(
-        f"PYTHONPATH=$(pwd) poetry run python3 scripts/migrate.py",
+        "PYTHONPATH=$(pwd) poetry run python3 scripts/migrate.py",
         env={
             "FLASK_APP": "lament-configuration.py",
             "LC_APP_PATH": f"http://{host}:{port}",
-            "LC_DB_PATH": f"test.db",
-            "LC_SECRET_KEY": f"TESTING_KEY",
+            "LC_DB_PATH": "test.db",
+            "LC_SECRET_KEY": "TESTING_KEY",
         },
     )
 
@@ -78,9 +78,17 @@ def tc(c):
     )
 
 
+@task
+def lint(c):
+    """Typecheck with mypy"""
+    c.run("poetry run flake8")
+
+
 @task
 def uwsgi(c, sock="lc.sock"):
     """Run a uwsgi server"""
     c.run(
-        f"poetry run uwsgi --socket {sock} --module lament-configuration:app --processes 4 --threads 2"
+        f"poetry run uwsgi --socket {sock} "
+        "--module lament-configuration:app "
+        "--processes 4 --threads 2"
     )

+ 5 - 0
tests/config.py

@@ -0,0 +1,5 @@
+import os
+
+os.environ["LC_DB_PATH"] = ":memory:"
+os.environ["LC_SECRET_KEY"] = "TEST_KEY"
+os.environ["LC_APP_PATH"] = "localhost"

+ 29 - 29
tests/model.py

@@ -1,10 +1,5 @@
-import os
-import peewee
 import pytest
-
-os.environ["LC_DB_PATH"] = ":memory:"
-os.environ["LC_SECRET_KEY"] = "TEST_KEY"
-os.environ["LC_APP_PATH"] = "localhost"
+import config  # noqa: F401
 
 import lc.config as c
 import lc.error as e
@@ -21,7 +16,12 @@ class Testdb:
         c.app.close_db()
 
     def mk_user(self, name="gdritter", password="foo") -> m.User:
-        return m.User.from_request(r.User(name=name, password=password,))
+        return m.User.from_request(
+            r.User(
+                name=name,
+                password=password,
+            )
+        )
 
     def test_create_user(self):
         name = "gdritter"
@@ -50,9 +50,9 @@ class Testdb:
 
     def test_no_duplicate_users(self):
         name = "gdritter"
-        u1 = self.mk_user(name=name)
+        self.mk_user(name=name)
         with pytest.raises(e.UserExists):
-            u2 = self.mk_user(name=name)
+            self.mk_user(name=name)
 
     def test_get_or_create_tag(self):
         u = self.mk_user()
@@ -99,16 +99,16 @@ class Testdb:
     def test_add_hierarchy(self):
         u = self.mk_user()
         req = r.Link("http://foo.com", "foo", "", False, ["food/bread/rye"])
-        l = m.Link.from_request(u, req)
-        assert l.name == req.name
-        tag_names = {t.tag.name for t in l.tags}  # type: ignore
+        link = m.Link.from_request(u, req)
+        assert link.name == req.name
+        tag_names = {t.tag.name for t in link.tags}  # type: ignore
         assert tag_names == {"food", "food/bread", "food/bread/rye"}
 
     def test_bad_tag(self):
         u = self.mk_user()
         req = r.Link("http://foo.com", "foo", "", False, ["foo{bar}"])
         with pytest.raises(e.BadTagName):
-            l = m.Link.from_request(u, req)
+            m.Link.from_request(u, req)
 
     def test_create_invite(self):
         u = self.mk_user()
@@ -153,32 +153,32 @@ class Testdb:
         with pytest.raises(e.NoSuchInvite):
             m.User.from_invite(r.User(name="u4", password="u4"), "a-non-existent-token")
 
-    def check_tags(self, l, tags):
-        present = set(map(lambda hastag: hastag.tag.name, l.tags))
+    def check_tags(self, link, tags):
+        present = set(map(lambda hastag: hastag.tag.name, link.tags))
         assert present == set(tags)
 
     def test_edit_link(self):
         u = self.mk_user()
 
         req = r.Link("http://foo.com", "foo", "", False, ["foo", "bar"])
-        l = m.Link.from_request(u, req)
-        assert l.name == req.name
-        assert l.tags == ["foo", "bar"]  # type: ignore
+        link = m.Link.from_request(u, req)
+        assert link.name == req.name
+        assert link.tags == ["foo", "bar"]  # type: ignore
 
         # check the in-place update
         req.name = "bar"
         req.tags = ["bar", "baz"]
         req.private = True
-        l.update_from_request(u, req)
-        assert l.name == req.name
-        assert l.private
-        assert l.created != req.created
-        self.check_tags(l, req.tags)
+        link.update_from_request(u, req)
+        assert link.name == req.name
+        assert link.private
+        assert link.created != req.created
+        self.check_tags(link, req.tags)
 
         # check that the link was persisted
-        l2 = m.Link.by_id(l.id)
-        assert l2
-        assert l2.name == req.name
-        assert l2.private
-        assert l2.created != req.created
-        self.check_tags(l2, req.tags)
+        link2 = m.Link.by_id(link.id)
+        assert link2
+        assert link2.name == req.name
+        assert link2.private
+        assert link2.created != req.created
+        self.check_tags(link2, req.tags)

+ 17 - 15
tests/routes.py

@@ -1,10 +1,4 @@
-import os
-import json
-
-os.environ["LC_DB_PATH"] = ":memory:"
-os.environ["LC_SECRET_KEY"] = "TEST_KEY"
-os.environ["LC_APP_PATH"] = "localhost"
-
+import config  # noqa: F401
 import lc.config as c
 import lc.model as m
 import lc.request as r
@@ -21,7 +15,12 @@ class TestRoutes:
         c.app.close_db()
 
     def mk_user(self, username="gdritter", password="foo") -> m.User:
-        return m.User.from_request(r.User(name=username, password=password,))
+        return m.User.from_request(
+            r.User(
+                name=username,
+                password=password,
+            )
+        )
 
     def test_index(self):
         result = self.app.get("/")
@@ -30,7 +29,7 @@ class TestRoutes:
     def test_successful_api_login(self):
         username = "gdritter"
         password = "bar"
-        u = self.mk_user(username=username, password=password)
+        self.mk_user(username=username, password=password)
         result = self.app.post("/auth", json={"name": username, "password": password})
         assert result.status == "200 OK"
         decoded_token = c.app.load_token(result.json["token"])
@@ -39,14 +38,14 @@ class TestRoutes:
     def test_failed_api_login(self):
         username = "gdritter"
         password = "bar"
-        u = self.mk_user(username=username, password=password)
+        self.mk_user(username=username, password=password)
         result = self.app.post("/auth", json={"name": username, "password": "foo"})
         assert result.status == "403 FORBIDDEN"
 
     def test_successful_web_login(self):
         username = "gdritter"
         password = "bar"
-        u = self.mk_user(username=username, password=password)
+        self.mk_user(username=username, password=password)
         result = self.app.post(
             "/auth",
             data={"username": username, "password": password},
@@ -57,7 +56,7 @@ class TestRoutes:
     def test_failed_web_login(self):
         username = "gdritter"
         password = "bar"
-        u = self.mk_user(username=username, password=password)
+        self.mk_user(username=username, password=password)
         result = self.app.post("/auth", data={"username": username, "password": "foo"})
         assert result.status == "403 FORBIDDEN"
 
@@ -132,19 +131,22 @@ class TestRoutes:
 
         # this should be fine
         check_link = self.app.get(
-            f"/u/{u.name}/l/{link_id}", headers={"Content-Type": "application/json"},
+            f"/u/{u.name}/l/{link_id}",
+            headers={"Content-Type": "application/json"},
         )
         assert check_link.status == "200 OK"
         assert check_link.json["url"] == sample_url
 
         # delete the link
         delete_link = self.app.delete(
-            f"/u/{u.name}/l/{link_id}", headers={"Authorization": f"Bearer {token}"},
+            f"/u/{u.name}/l/{link_id}",
+            headers={"Authorization": f"Bearer {token}"},
         )
         assert delete_link.status == "200 OK"
 
         # make sure it is gone
         bad_result = self.app.get(
-            f"/u/{u.name}/l/{link_id}", headers={"Content-Type": "application/json"},
+            f"/u/{u.name}/l/{link_id}",
+            headers={"Content-Type": "application/json"},
         )
         assert bad_result.status == "404 NOT FOUND"