Cleanup/reorg (#10)

* Cleanup/reorg WIP - serializers next

* PR comments, fixes

* fix URLs
This commit is contained in:
Ethan Roseman 2022-08-26 11:02:16 +09:00 committed by GitHub
parent 5790357ac4
commit 686e15385e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 375 additions and 396 deletions

54
frog_api/exceptions.py Normal file
View File

@ -0,0 +1,54 @@
from rest_framework.exceptions import APIException
from rest_framework import status
class NonexistentProjectException(APIException):
status_code = status.HTTP_404_NOT_FOUND
def __init__(self, project: str):
super().__init__(f"Project {project} not found")
class NonexistentVersionException(APIException):
status_code = status.HTTP_404_NOT_FOUND
def __init__(self, project: str, version: str):
super().__init__(f"Version '{version}' for project '{project}' not found")
class NonexistentCategoryException(APIException):
status_code = status.HTTP_404_NOT_FOUND
def __init__(self, project: str, version: str, category: str):
super().__init__(
f"Category '{category}' not found for project '{project}', version '{version}'"
)
class NoEntriesException(APIException):
status_code = status.HTTP_404_NOT_FOUND
def __init__(self, project: str, version: str, category: str):
super().__init__(
f"No data exists for project '{project}', version '{version}', and category '{category}'"
)
class MissingAPIKeyException(APIException):
status_code = status.HTTP_403_FORBIDDEN
default_detail = "No API key provided"
class InvalidAPIKeyException(APIException):
status_code = status.HTTP_403_FORBIDDEN
default_detail = "Incorrect API key provided"
class InvalidDataException(APIException):
status_code = status.HTTP_400_BAD_REQUEST
# Maybe?
class AlreadyExistsException(APIException):
status_code = status.HTTP_400_BAD_REQUEST

View File

@ -1,26 +1,31 @@
from django.urls import path, re_path from django.urls import path, re_path
from frog_api import views from frog_api.views import data, structure
urlpatterns = [ urlpatterns = [
path("projects/", views.ProjectView.as_view()), # structure (/project)
re_path( path(
"projects/(?P<project>.+)/(?P<version>.+)/$", "projects/",
views.AddNewCategoryView.as_view(), structure.ProjectStructureView.as_view(),
), ),
re_path( re_path(
"data/(?P<project>.+)/(?P<version>.+)/(?P<category>.+)/$", "projects/(?P<project_slug>.+)/(?P<version_slug>.+)/$",
views.CategoryDigestView.as_view(), structure.CategoryStructureView.as_view(),
),
# data (/data)
re_path(
"data/(?P<project_slug>.+)/(?P<version_slug>.+)/(?P<category_slug>.+)/$",
data.CategoryDataView.as_view(),
), ),
re_path( re_path(
"data/(?P<project>.+)/(?P<version>.+)/$", "data/(?P<project_slug>.+)/(?P<version_slug>.+)/$",
views.VersionDigestView.as_view(), data.VersionDataView.as_view(),
), ),
re_path( re_path(
"data/(?P<project>.+)/$", "data/(?P<project_slug>.+)/$",
views.ProjectDigestView.as_view(), data.ProjectDataView.as_view(),
), ),
path( path(
"data/", "data/",
views.RootDigestView.as_view(), data.RootDataView.as_view(),
), ),
] ]

View File

@ -1,271 +0,0 @@
from typing import Any, List
from rest_framework import status
from rest_framework.exceptions import APIException
from rest_framework.response import Response
from rest_framework.request import Request
from rest_framework.views import APIView
from django.db import models
import json
from frog_api.models import Category, Entry, Measure, Project, Version
from frog_api.serializers import (
ProjectSerializer,
TerseEntrySerializer,
)
class MissingModelException(APIException):
status_code = status.HTTP_404_NOT_FOUND
class InvalidAPIKeyException(APIException):
status_code = status.HTTP_403_FORBIDDEN
class InvalidDataException(APIException):
status_code = status.HTTP_400_BAD_REQUEST
# Maybe?
class AlreadyExistsException(APIException):
status_code = status.HTTP_400_BAD_REQUEST
class ProjectView(APIView):
"""
API endpoint that allows projects to be viewed.
"""
def get(self, request: Request) -> Response:
"""
Return a list of all projects.
"""
projects = Project.objects.all()
serializer = ProjectSerializer(projects, many=True)
return Response(serializer.data)
def get_latest_entry(project: str, version: str, category: str) -> dict[Any, Any]:
if not Project.objects.filter(slug=project).exists():
raise MissingModelException(f"Project {project} not found")
if not Version.objects.filter(slug=version, project__slug=project).exists():
raise MissingModelException(
f"Version '{version}' not found for project '{project}'"
)
if not Category.objects.filter(
slug=category, version__slug=version, version__project__slug=project
).exists():
raise MissingModelException(
f"Category '{category}' not found for project '{project}' and version '{version}'"
)
entry = Entry.objects.filter(
category__slug=category,
category__version__slug=version,
category__version__project__slug=project,
).first()
if entry is None:
raise MissingModelException(
f"No data exists for project '{project}', version '{version}', and category '{category}'"
)
# Re-format the measures (TODO: DRF-ify this?)
entry_data = TerseEntrySerializer(entry).data
entry_data["measures"] = {m["type"]: m["value"] for m in entry_data["measures"]}
return entry_data
def get_versions_digest_for_project(project: str) -> dict[Any, Any]:
versions = {}
for version in Version.objects.filter(project__slug=project):
entry = get_latest_entry(project, version.slug, "default")
if entry is not None:
versions[version.slug] = entry
return versions
class RootDigestView(APIView):
"""
API endpoint that returns the most recent entry for each version of each project.
"""
def get(self, request: Request) -> Response:
"""
Return the most recent entry for ovreall progress for each version of each project.
"""
projects = {}
for project in Project.objects.all():
versions = get_versions_digest_for_project(project.slug)
if len(versions) > 0:
projects[project.slug] = versions
return Response({"progress": projects})
class ProjectDigestView(APIView):
"""
API endpoint that returns the most recent entry for each version of a project.
"""
def get(self, request: Request, project: str) -> Response:
"""
Return the most recent entry for overall progress for each version of a project.
"""
if not Project.objects.filter(slug=project).exists():
raise MissingModelException(f"Project {project} not found")
projects = {}
versions = get_versions_digest_for_project(project)
if len(versions) > 0:
projects[project] = versions
return Response({"progress": projects})
def write_new_entries(request: Request, project: str, version: str) -> List[Any]:
found_project = Project.objects.filter(slug=project).first()
if not found_project:
raise MissingModelException(f"Project {project} not found")
found_version = Version.objects.filter(slug=version, project__slug=project).first()
if not found_version:
raise MissingModelException(
f"Version '{version}' not found for project '{project}'"
)
print(request.data)
input = request.data
if "api_key" not in input:
raise InvalidAPIKeyException(f"No api_key provided, cannot POST.")
if input["api_key"] != found_project.auth_key:
raise InvalidAPIKeyException(
f"Provided api_key does not match authorization, cannot POST."
)
to_save: List[models.Model] = []
for row in input["data"]:
timestamp = row["timestamp"]
git_hash = row["git_hash"]
for cat in row:
if cat in ["timestamp", "git_hash"]:
continue
if type(row[cat]) is not dict:
continue
category = Category.objects.filter(
slug=cat, version__slug=version, version__project__slug=project
).first()
if not category:
raise MissingModelException(
f"Attempted to write to Category '{cat}' not found in project '{project}', version '{version}'"
)
entry = Entry(category=category, timestamp=timestamp, git_hash=git_hash)
print(entry)
to_save.append(entry)
for measure_type in row[cat]:
value = row[cat][measure_type]
if type(value) != int:
raise InvalidDataException(f"{cat}:{measure_type} must be an int")
to_save.append(Measure(entry=entry, type=measure_type, value=value))
for s in to_save:
s.save()
return []
class VersionDigestView(APIView):
"""
API endpoint that returns the most recent entry for overall progress for a version of a project.
"""
def get(self, request: Request, project: str, version: str) -> Response:
"""
Return the most recent entry for overall progress for a version of a project.
"""
entry = get_latest_entry(project, version, "default")
return Response(entry)
def post(self, request: Request, project: str, version: str) -> Response:
result = write_new_entries(request, project, version)
return Response(result, status=status.HTTP_201_CREATED)
class CategoryDigestView(APIView):
"""
API endpoint that returns the most recent entry for a specific cagory and a version of a project.
"""
def get(
self, request: Request, project: str, version: str, category: str
) -> Response:
"""
Return the most recent entry for a specific cagory and a version of a project.
"""
entry = get_latest_entry(project, version, category)
return Response(entry)
def add_new_category(request: Request, project: str, version: str) -> List[Any]:
found_project = Project.objects.filter(slug=project).first()
if not found_project:
raise MissingModelException(f"Project {project} not found")
found_version = Version.objects.filter(slug=version, project__slug=project).first()
if not found_version:
raise MissingModelException(
f"Version '{version}' not found for project '{project}'"
)
print(request.data)
input = request.data
categories = input["data"]
if "api_key" not in input:
raise InvalidAPIKeyException(f"No api_key provided, cannot POST.")
if input["api_key"] != found_project.auth_key:
raise InvalidAPIKeyException(
f"Provided api_key does not match authorization, cannot POST."
)
to_save = []
for cat in categories:
if Category.objects.filter(
slug=cat, version__slug=version, version__project__slug=project
).exists():
raise AlreadyExistsException(
f"Category {cat} already exists for project '{project}', version '{version}'"
)
to_save.append(Category(version=found_version, slug=cat, name=categories[cat]))
for s in to_save:
s.save()
return []
class AddNewCategoryView(APIView):
"""
API endpoint for adding new categories
"""
def post(self, request: Request, project: str, version: str) -> Response:
result = add_new_category(request, project, version)
return Response(result, status=status.HTTP_201_CREATED)

34
frog_api/views/common.py Normal file
View File

@ -0,0 +1,34 @@
from frog_api.exceptions import (
InvalidAPIKeyException,
NonexistentCategoryException,
NonexistentProjectException,
NonexistentVersionException,
)
from frog_api.models import Category, Project, Version
def get_project(slug: str) -> Project:
ret = Project.objects.filter(slug=slug).first()
if not ret:
raise NonexistentProjectException(slug)
return ret
def get_version(slug: str, project: Project) -> Version:
ret = Version.objects.filter(slug=slug, project=project).first()
if not ret:
raise NonexistentVersionException(project.slug, slug)
return ret
def get_category(slug: str, version: Version) -> Category:
ret = Category.objects.filter(slug=slug, version=version).first()
if not ret:
raise NonexistentCategoryException(version.project.slug, version.slug, slug)
return ret
def validate_api_key(key: str, project: Project) -> bool:
if key != project.auth_key:
raise InvalidAPIKeyException()
return True

173
frog_api/views/data.py Normal file
View File

@ -0,0 +1,173 @@
from typing import Any, List
from django.db import models
from frog_api.exceptions import (
InvalidDataException,
MissingAPIKeyException,
NoEntriesException,
)
from frog_api.models import Entry, Measure, Project, Version
from frog_api.serializers import TerseEntrySerializer
from frog_api.views.common import (
get_category,
get_project,
get_version,
validate_api_key,
)
from rest_framework import status
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView
def get_latest_entry(
project_slug: str, version_slug: str, category_slug: str
) -> dict[str, Any]:
project = get_project(project_slug)
version = get_version(version_slug, project)
category = get_category(category_slug, version)
entry = Entry.objects.filter(category=category).first()
if entry is None:
raise NoEntriesException(project_slug, version_slug, category_slug)
# Re-format the measures (TODO: handle this in a DRF serializer)
entry_data = TerseEntrySerializer(entry).data
entry_data["measures"] = {m["type"]: m["value"] for m in entry_data["measures"]}
return entry_data
def get_versions_digest_for_project(project: Project) -> dict[Any, Any]:
versions = {}
for version in Version.objects.filter(project=project):
entry = get_latest_entry(project.slug, version.slug, "default")
if entry is not None:
versions[version.slug] = entry
return versions
class RootDataView(APIView):
"""
API endpoint that returns the most recent entry for overall progress of each version of each project.
"""
def get(self, request: Request) -> Response:
"""
Return the most recent entry for overall progress of each version of each project.
"""
projects = {}
for project in Project.objects.all():
versions = get_versions_digest_for_project(project)
if len(versions) > 0:
projects[project.slug] = versions
return Response({"progress": projects})
class ProjectDataView(APIView):
"""
API endpoint that returns the most recent entry for each version of a project.
"""
def get(self, request: Request, project_slug: str) -> Response:
"""
Return the most recent entry for overall progress for each version of a project.
"""
project = get_project(project_slug)
versions = get_versions_digest_for_project(project)
projects = {}
if len(versions) > 0:
projects[project_slug] = versions
return Response({"progress": projects})
class VersionDataView(APIView):
"""
API endpoint that returns the most recent entry for overall progress for a version of a project.
"""
@staticmethod
def create_entries(
req_data: dict[str, Any], project_slug: str, version_slug: str
) -> int:
project = get_project(project_slug)
version = get_version(version_slug, project)
if "api_key" not in req_data:
raise MissingAPIKeyException()
validate_api_key(req_data["api_key"], project)
to_save: List[models.Model] = []
for entry in req_data["data"]:
timestamp = entry["timestamp"]
git_hash = entry["git_hash"]
for cat in entry:
if cat in ["timestamp", "git_hash"]:
continue
if type(entry[cat]) is not dict:
continue
category = get_category(cat, version)
entry = Entry(category=category, timestamp=timestamp, git_hash=git_hash)
to_save.append(entry)
for measure_type in entry[cat]:
value = entry[cat][measure_type]
if type(value) != int:
raise InvalidDataException(
f"{cat}:{measure_type} must be an integer"
)
to_save.append(Measure(entry=entry, type=measure_type, value=value))
for s in to_save:
s.save()
return len(to_save)
def get(self, request: Request, project_slug: str, version_slug: str) -> Response:
"""
Return the most recent entry for overall progress for a version of a project.
"""
entry = get_latest_entry(project_slug, version_slug, "default")
return Response(entry)
def post(self, request: Request, project_slug: str, version_slug: str) -> Response:
result = VersionDataView.create_entries(
request.data, project_slug, version_slug
)
success_data = {
"result": "success",
"wrote": result,
}
return Response(success_data, status=status.HTTP_201_CREATED)
class CategoryDataView(APIView):
"""
API endpoint that returns data for a specific cagory and a version of a project.
"""
def get(
self, request: Request, project_slug: str, version_slug: str, category_slug: str
) -> Response:
"""
Return data for a specific cagory and a version of a project.
"""
entry = get_latest_entry(project_slug, version_slug, category_slug)
return Response(entry)

View File

@ -0,0 +1,70 @@
from typing import Any
from django.db import models
from frog_api.exceptions import AlreadyExistsException, MissingAPIKeyException
from frog_api.models import Category, Project
from frog_api.serializers import ProjectSerializer
from frog_api.views.common import get_project, get_version, validate_api_key
from rest_framework import status
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView
class ProjectStructureView(APIView):
"""
API endpoint that allows projects to be viewed.
"""
def get(self, request: Request) -> Response:
"""
Return a list of all projects.
"""
projects = Project.objects.all()
serializer = ProjectSerializer(projects, many=True)
return Response(serializer.data)
class CategoryStructureView(APIView):
"""
API endpoint for adding new categories
"""
@staticmethod
def create_categories(
req_data: dict[str, Any], project_slug: str, version_slug: str
) -> int:
project = get_project(project_slug)
version = get_version(version_slug, project)
if "api_key" not in req_data:
raise MissingAPIKeyException()
validate_api_key(req_data["api_key"], project)
categories = req_data["data"]
to_save: list[models.Model] = []
for cat in categories:
if Category.objects.filter(slug=cat, version=version).exists():
raise AlreadyExistsException(
f"Category {cat} already exists for project '{project_slug}', version '{version_slug}'"
)
to_save.append(Category(version=version, slug=cat, name=categories[cat]))
for s in to_save:
s.save()
return len(to_save)
def post(self, request: Request, project_slug: str, version_slug: str) -> Response:
result = CategoryStructureView.create_categories(
request.data, project_slug, version_slug
)
success_data = {
"result": "success",
"wrote": result,
}
return Response(success_data, status=status.HTTP_201_CREATED)

140
poetry.lock generated
View File

@ -7,7 +7,7 @@ optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
[package.extras] [package.extras]
tests = ["mypy (>=0.800)", "pytest-asyncio", "pytest"] tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"]
[[package]] [[package]]
name = "backports.zoneinfo" name = "backports.zoneinfo"
@ -37,10 +37,10 @@ tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""}
typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}
[package.extras] [package.extras]
uvloop = ["uvloop (>=0.15.2)"]
jupyter = ["tokenize-rt (>=3.2.0)", "ipython (>=7.8.0)"]
d = ["aiohttp (>=3.7.4)"]
colorama = ["colorama (>=0.4.3)"] colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.7.4)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"]
[[package]] [[package]]
name = "certifi" name = "certifi"
@ -120,8 +120,8 @@ sqlparse = ">=0.2.2"
tzdata = {version = "*", markers = "sys_platform == \"win32\""} tzdata = {version = "*", markers = "sys_platform == \"win32\""}
[package.extras] [package.extras]
bcrypt = ["bcrypt"]
argon2 = ["argon2-cffi (>=19.1.0)"] argon2 = ["argon2-cffi (>=19.1.0)"]
bcrypt = ["bcrypt"]
[[package]] [[package]]
name = "django-nested-admin" name = "django-nested-admin"
@ -252,9 +252,9 @@ tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
typing-extensions = ">=3.10" typing-extensions = ">=3.10"
[package.extras] [package.extras]
reports = ["lxml"]
python2 = ["typed-ast (>=1.4.0,<2)"]
dmypy = ["psutil (>=4.0)"] dmypy = ["psutil (>=4.0)"]
python2 = ["typed-ast (>=1.4.0,<2)"]
reports = ["lxml"]
[[package]] [[package]]
name = "mypy-extensions" name = "mypy-extensions"
@ -281,8 +281,8 @@ optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
[package.extras] [package.extras]
test = ["pytest (>=6)", "pytest-mock (>=3.6)", "pytest-cov (>=2.7)", "appdirs (==1.4.4)"] docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"]
docs = ["sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)", "proselint (>=0.10.2)", "furo (>=2021.7.5b38)"] test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"]
[[package]] [[package]]
name = "python-monkey-business" name = "python-monkey-business"
@ -318,8 +318,8 @@ idna = ">=2.5,<4"
urllib3 = ">=1.21.1,<1.27" urllib3 = ">=1.21.1,<1.27"
[package.extras] [package.extras]
use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"]
socks = ["PySocks (>=1.5.6,!=1.5.7)"] socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"]
[[package]] [[package]]
name = "six" name = "six"
@ -435,24 +435,7 @@ asgiref = [
{file = "asgiref-3.5.2-py3-none-any.whl", hash = "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4"}, {file = "asgiref-3.5.2-py3-none-any.whl", hash = "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4"},
{file = "asgiref-3.5.2.tar.gz", hash = "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424"}, {file = "asgiref-3.5.2.tar.gz", hash = "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424"},
] ]
"backports.zoneinfo" = [ "backports.zoneinfo" = []
{file = "backports.zoneinfo-0.2.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc"},
{file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722"},
{file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546"},
{file = "backports.zoneinfo-0.2.1-cp36-cp36m-win32.whl", hash = "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08"},
{file = "backports.zoneinfo-0.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7"},
{file = "backports.zoneinfo-0.2.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac"},
{file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf"},
{file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570"},
{file = "backports.zoneinfo-0.2.1-cp37-cp37m-win32.whl", hash = "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b"},
{file = "backports.zoneinfo-0.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582"},
{file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"},
{file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"},
{file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"},
{file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"},
{file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"},
{file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"},
]
black = [ black = [
{file = "black-22.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f586c26118bc6e714ec58c09df0157fe2d9ee195c764f630eb0d8e7ccce72e69"}, {file = "black-22.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f586c26118bc6e714ec58c09df0157fe2d9ee195c764f630eb0d8e7ccce72e69"},
{file = "black-22.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b270a168d69edb8b7ed32c193ef10fd27844e5c60852039599f9184460ce0807"}, {file = "black-22.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b270a168d69edb8b7ed32c193ef10fd27844e5c60852039599f9184460ce0807"},
@ -482,10 +465,7 @@ certifi = [
{file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"}, {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"},
{file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"}, {file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"},
] ]
charset-normalizer = [ charset-normalizer = []
{file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"},
{file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"},
]
click = [ click = [
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
@ -502,22 +482,10 @@ coreschema = [
{file = "coreschema-0.0.4-py2-none-any.whl", hash = "sha256:5e6ef7bf38c1525d5e55a895934ab4273548629f16aed5c0a6caa74ebf45551f"}, {file = "coreschema-0.0.4-py2-none-any.whl", hash = "sha256:5e6ef7bf38c1525d5e55a895934ab4273548629f16aed5c0a6caa74ebf45551f"},
{file = "coreschema-0.0.4.tar.gz", hash = "sha256:9503506007d482ab0867ba14724b93c18a33b22b6d19fb419ef2d239dd4a1607"}, {file = "coreschema-0.0.4.tar.gz", hash = "sha256:9503506007d482ab0867ba14724b93c18a33b22b6d19fb419ef2d239dd4a1607"},
] ]
django = [ django = []
{file = "Django-4.1-py3-none-any.whl", hash = "sha256:031ccb717782f6af83a0063a1957686e87cb4581ea61b47b3e9addf60687989a"}, django-nested-admin = []
{file = "Django-4.1.tar.gz", hash = "sha256:032f8a6fc7cf05ccd1214e4a2e21dfcd6a23b9d575c6573cacc8c67828dbe642"}, django-stubs = []
] django-stubs-ext = []
django-nested-admin = [
{file = "django-nested-admin-3.4.0.tar.gz", hash = "sha256:fbcf20d75a73dcbcc6285793ff936eff8df4deba5b169e0c1ab765394c562805"},
{file = "django_nested_admin-3.4.0-py2.py3-none-any.whl", hash = "sha256:c6852c5ac632f4e698b6beda455006fd464c852459e5e858a6db832cdb23d9e1"},
]
django-stubs = [
{file = "django-stubs-1.12.0.tar.gz", hash = "sha256:ea8b35d0da49f7b2ee99a79125f1943e033431dd114726d6643cc35de619230e"},
{file = "django_stubs-1.12.0-py3-none-any.whl", hash = "sha256:0dff8ec0ba3abe046450b3d8a29ce9e72629893d2c1ef679189cc2bfdb6d2f64"},
]
django-stubs-ext = [
{file = "django-stubs-ext-0.5.0.tar.gz", hash = "sha256:9bd7418376ab00b7f88d6d56be9fece85bfa0c7c348ac621155fa4d7a91146f2"},
{file = "django_stubs_ext-0.5.0-py3-none-any.whl", hash = "sha256:c5d8db53d29c756e7e3d0820a5a079a43bc38d8fab0e1b8bd5df2f3366c54b5a"},
]
djangorestframework = [ djangorestframework = [
{file = "djangorestframework-3.13.1-py3-none-any.whl", hash = "sha256:24c4bf58ed7e85d1fe4ba250ab2da926d263cd57d64b03e8dcef0ac683f8b1aa"}, {file = "djangorestframework-3.13.1-py3-none-any.whl", hash = "sha256:24c4bf58ed7e85d1fe4ba250ab2da926d263cd57d64b03e8dcef0ac683f8b1aa"},
{file = "djangorestframework-3.13.1.tar.gz", hash = "sha256:0c33407ce23acc68eca2a6e46424b008c9c02eceb8cf18581921d0092bc1f2ee"}, {file = "djangorestframework-3.13.1.tar.gz", hash = "sha256:0c33407ce23acc68eca2a6e46424b008c9c02eceb8cf18581921d0092bc1f2ee"},
@ -580,31 +548,7 @@ markupsafe = [
{file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"},
{file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"},
] ]
mypy = [ mypy = []
{file = "mypy-0.971-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2899a3cbd394da157194f913a931edfd4be5f274a88041c9dc2d9cdcb1c315c"},
{file = "mypy-0.971-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:98e02d56ebe93981c41211c05adb630d1d26c14195d04d95e49cd97dbc046dc5"},
{file = "mypy-0.971-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:19830b7dba7d5356d3e26e2427a2ec91c994cd92d983142cbd025ebe81d69cf3"},
{file = "mypy-0.971-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:02ef476f6dcb86e6f502ae39a16b93285fef97e7f1ff22932b657d1ef1f28655"},
{file = "mypy-0.971-cp310-cp310-win_amd64.whl", hash = "sha256:25c5750ba5609a0c7550b73a33deb314ecfb559c350bb050b655505e8aed4103"},
{file = "mypy-0.971-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d3348e7eb2eea2472db611486846742d5d52d1290576de99d59edeb7cd4a42ca"},
{file = "mypy-0.971-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3fa7a477b9900be9b7dd4bab30a12759e5abe9586574ceb944bc29cddf8f0417"},
{file = "mypy-0.971-cp36-cp36m-win_amd64.whl", hash = "sha256:2ad53cf9c3adc43cf3bea0a7d01a2f2e86db9fe7596dfecb4496a5dda63cbb09"},
{file = "mypy-0.971-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:855048b6feb6dfe09d3353466004490b1872887150c5bb5caad7838b57328cc8"},
{file = "mypy-0.971-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:23488a14a83bca6e54402c2e6435467a4138785df93ec85aeff64c6170077fb0"},
{file = "mypy-0.971-cp37-cp37m-win_amd64.whl", hash = "sha256:4b21e5b1a70dfb972490035128f305c39bc4bc253f34e96a4adf9127cf943eb2"},
{file = "mypy-0.971-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:9796a2ba7b4b538649caa5cecd398d873f4022ed2333ffde58eaf604c4d2cb27"},
{file = "mypy-0.971-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5a361d92635ad4ada1b1b2d3630fc2f53f2127d51cf2def9db83cba32e47c856"},
{file = "mypy-0.971-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b793b899f7cf563b1e7044a5c97361196b938e92f0a4343a5d27966a53d2ec71"},
{file = "mypy-0.971-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d1ea5d12c8e2d266b5fb8c7a5d2e9c0219fedfeb493b7ed60cd350322384ac27"},
{file = "mypy-0.971-cp38-cp38-win_amd64.whl", hash = "sha256:23c7ff43fff4b0df93a186581885c8512bc50fc4d4910e0f838e35d6bb6b5e58"},
{file = "mypy-0.971-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1f7656b69974a6933e987ee8ffb951d836272d6c0f81d727f1d0e2696074d9e6"},
{file = "mypy-0.971-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d2022bfadb7a5c2ef410d6a7c9763188afdb7f3533f22a0a32be10d571ee4bbe"},
{file = "mypy-0.971-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef943c72a786b0f8d90fd76e9b39ce81fb7171172daf84bf43eaf937e9f220a9"},
{file = "mypy-0.971-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d744f72eb39f69312bc6c2abf8ff6656973120e2eb3f3ec4f758ed47e414a4bf"},
{file = "mypy-0.971-cp39-cp39-win_amd64.whl", hash = "sha256:77a514ea15d3007d33a9e2157b0ba9c267496acf12a7f2b9b9f8446337aac5b0"},
{file = "mypy-0.971-py3-none-any.whl", hash = "sha256:0d054ef16b071149917085f51f89555a576e2618d5d9dd70bd6eea6410af3ac9"},
{file = "mypy-0.971.tar.gz", hash = "sha256:40b0f21484238269ae6a57200c807d80debc6459d444c0489a102d7c6a75fa56"},
]
mypy-extensions = [ mypy-extensions = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {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"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
@ -617,22 +561,13 @@ platformdirs = [
{file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
{file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
] ]
python-monkey-business = [ python-monkey-business = []
{file = "python-monkey-business-1.0.0.tar.gz", hash = "sha256:9976522989766f00b2aaa24ec96eacb91a6de7b7001d1452079323b071988e0e"}, pytz = []
{file = "python_monkey_business-1.0.0-py2.py3-none-any.whl", hash = "sha256:6d4cf47f011945db838ccf04643acd49b82f7ad6ab7ecba4c8165385687a828a"},
]
pytz = [
{file = "pytz-2022.2.1-py2.py3-none-any.whl", hash = "sha256:220f481bdafa09c3955dfbdddb7b57780e9a94f5127e35456a48589b9e0c0197"},
{file = "pytz-2022.2.1.tar.gz", hash = "sha256:cea221417204f2d1a2aa03ddae3e867921971d0d76f14d87abb4414415bbdcf5"},
]
requests = [ requests = [
{file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"},
{file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"},
] ]
six = [ six = []
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
sqlparse = [ sqlparse = [
{file = "sqlparse-0.4.2-py3-none-any.whl", hash = "sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d"}, {file = "sqlparse-0.4.2-py3-none-any.whl", hash = "sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d"},
{file = "sqlparse-0.4.2.tar.gz", hash = "sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae"}, {file = "sqlparse-0.4.2.tar.gz", hash = "sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae"},
@ -641,39 +576,18 @@ tomli = [
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
] ]
types-markdown = [ types-markdown = []
{file = "types-Markdown-3.4.1.tar.gz", hash = "sha256:cda9bfd1fcb11e8133a037f8a184e3059ae7389a5f5cc0b53117bf2902aca10d"}, types-pytz = []
{file = "types_Markdown-3.4.1-py3-none-any.whl", hash = "sha256:2d1e5bfff192c78d6644bc3820fea9c7c7cb42dc87558020728ec5b728448ce2"}, types-pyyaml = []
] types-requests = []
types-pytz = [ types-urllib3 = []
{file = "types-pytz-2022.2.1.0.tar.gz", hash = "sha256:47cfb19c52b9f75896440541db392fd312a35b279c6307a531db71152ea63e2b"},
{file = "types_pytz-2022.2.1.0-py3-none-any.whl", hash = "sha256:50ead2254b524a3d4153bc65d00289b66898060d2938e586170dce918dbaf3b3"},
]
types-pyyaml = [
{file = "types-PyYAML-6.0.11.tar.gz", hash = "sha256:7f7da2fd11e9bc1e5e9eb3ea1be84f4849747017a59fc2eee0ea34ed1147c2e0"},
{file = "types_PyYAML-6.0.11-py3-none-any.whl", hash = "sha256:8f890028123607379c63550179ddaec4517dc751f4c527a52bb61934bf495989"},
]
types-requests = [
{file = "types-requests-2.28.9.tar.gz", hash = "sha256:feaf581bd580497a47fe845d506fa3b91b484cf706ff27774e87659837de9962"},
{file = "types_requests-2.28.9-py3-none-any.whl", hash = "sha256:86cb66d3de2f53eac5c09adc42cf6547eefbd0c7e1210beca1ee751c35d96083"},
]
types-urllib3 = [
{file = "types-urllib3-1.26.23.tar.gz", hash = "sha256:b78e819f0e350221d0689a5666162e467ba3910737bafda14b5c2c85e9bb1e56"},
{file = "types_urllib3-1.26.23-py3-none-any.whl", hash = "sha256:333e675b188a1c1fd980b4b352f9e40572413a4c1ac689c23cd546e96310070a"},
]
typing-extensions = [ typing-extensions = [
{file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"},
{file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"}, {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"},
] ]
tzdata = [ tzdata = []
{file = "tzdata-2022.2-py2.py3-none-any.whl", hash = "sha256:c3119520447d68ef3eb8187a55a4f44fa455f30eb1b4238fa5691ba094f2b05b"},
{file = "tzdata-2022.2.tar.gz", hash = "sha256:21f4f0d7241572efa7f7a4fdabb052e61b55dc48274e6842697ccdf5253e5451"},
]
uritemplate = [ uritemplate = [
{file = "uritemplate-4.1.1-py2.py3-none-any.whl", hash = "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e"}, {file = "uritemplate-4.1.1-py2.py3-none-any.whl", hash = "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e"},
{file = "uritemplate-4.1.1.tar.gz", hash = "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0"}, {file = "uritemplate-4.1.1.tar.gz", hash = "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0"},
] ]
urllib3 = [ urllib3 = []
{file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"},
{file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"},
]