mirror of
https://github.com/decompals/frogress.git
synced 2025-12-12 06:45:22 +00:00
Cleanup/reorg (#10)
* Cleanup/reorg WIP - serializers next * PR comments, fixes * fix URLs
This commit is contained in:
54
frog_api/exceptions.py
Normal file
54
frog_api/exceptions.py
Normal 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
|
||||
@@ -1,26 +1,31 @@
|
||||
from django.urls import path, re_path
|
||||
from frog_api import views
|
||||
from frog_api.views import data, structure
|
||||
|
||||
urlpatterns = [
|
||||
path("projects/", views.ProjectView.as_view()),
|
||||
re_path(
|
||||
"projects/(?P<project>.+)/(?P<version>.+)/$",
|
||||
views.AddNewCategoryView.as_view(),
|
||||
# structure (/project)
|
||||
path(
|
||||
"projects/",
|
||||
structure.ProjectStructureView.as_view(),
|
||||
),
|
||||
re_path(
|
||||
"data/(?P<project>.+)/(?P<version>.+)/(?P<category>.+)/$",
|
||||
views.CategoryDigestView.as_view(),
|
||||
"projects/(?P<project_slug>.+)/(?P<version_slug>.+)/$",
|
||||
structure.CategoryStructureView.as_view(),
|
||||
),
|
||||
# data (/data)
|
||||
re_path(
|
||||
"data/(?P<project_slug>.+)/(?P<version_slug>.+)/(?P<category_slug>.+)/$",
|
||||
data.CategoryDataView.as_view(),
|
||||
),
|
||||
re_path(
|
||||
"data/(?P<project>.+)/(?P<version>.+)/$",
|
||||
views.VersionDigestView.as_view(),
|
||||
"data/(?P<project_slug>.+)/(?P<version_slug>.+)/$",
|
||||
data.VersionDataView.as_view(),
|
||||
),
|
||||
re_path(
|
||||
"data/(?P<project>.+)/$",
|
||||
views.ProjectDigestView.as_view(),
|
||||
"data/(?P<project_slug>.+)/$",
|
||||
data.ProjectDataView.as_view(),
|
||||
),
|
||||
path(
|
||||
"data/",
|
||||
views.RootDigestView.as_view(),
|
||||
data.RootDataView.as_view(),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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
34
frog_api/views/common.py
Normal 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
173
frog_api/views/data.py
Normal 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)
|
||||
70
frog_api/views/structure.py
Normal file
70
frog_api/views/structure.py
Normal 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)
|
||||
Reference in New Issue
Block a user