Skip to content

Commit

Permalink
refactor: store entire score object and return same score response as… (
Browse files Browse the repository at this point in the history
#714)

* refactor: store entire score object and return same score response as other endpoints

* chore: remaining tests for updated historical endpoint

* chore: test score serialization
  • Loading branch information
tim-schultz authored Oct 29, 2024
1 parent ad17dc2 commit db51bf8
Show file tree
Hide file tree
Showing 5 changed files with 297 additions and 65 deletions.
7 changes: 6 additions & 1 deletion api/registry/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,16 @@ class InvalidLimitException(APIException):
default_detail = "Invalid limit."


class CreatedAtIsRequired(APIException):
class CreatedAtIsRequiredException(APIException):
status_code = status.HTTP_400_BAD_REQUEST
default_detail = "You must provide created_at as a query param."


class CreatedAtMalFormedException(APIException):
status_code = status.HTTP_400_BAD_REQUEST
default_detail = "Created at must be in the format YYYY-MM-DD."


class NoRequiredPermissionsException(APIException):
status_code = status.HTTP_403_FORBIDDEN
default_detail = "You are not allowed to access this endpoint."
Expand Down
20 changes: 16 additions & 4 deletions api/registry/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import json
from enum import Enum

from django.core import serializers
from django.db import models
from django.db.models.signals import pre_save
from django.dispatch import receiver
Expand Down Expand Up @@ -87,19 +89,29 @@ def __str__(self):
return f"Score #{self.id}, score={self.score}, last_score_timestamp={self.last_score_timestamp}, status={self.status}, error={self.error}, evidence={self.evidence}, passport_id={self.passport_id}"


def serialize_score(score: Score):
json_score = {}
try:
serialized_score = serializers.serialize("json", [score])
json_score = json.loads(serialized_score)[0]
except:
json_score["error"] = "Error serializing score"

return json_score


@receiver(pre_save, sender=Score)
def score_updated(sender, instance, **kwargs):
if instance.status != Score.Status.DONE:
return instance

json_score = serialize_score(instance)

Event.objects.create(
action=Event.Action.SCORE_UPDATE,
address=instance.passport.address,
community=instance.passport.community,
data={
"score": float(instance.score) if instance.score is not None else 0,
"evidence": instance.evidence,
},
data=json_score,
)

return instance
Expand Down
46 changes: 46 additions & 0 deletions api/registry/test/test_score_updated_serialization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from unittest.mock import patch

import pytest

from registry.models import serialize_score


@pytest.mark.django_db
class TestSerializeScore:
def test_successful_serialization(self, scorer_score):
"""Test successful serialization of a score object"""
# Use the existing scorer_score fixture
score = scorer_score

# Serialize the score
result = serialize_score(score)

# Verify the structure and content
assert isinstance(result, dict)
assert "model" in result
assert "fields" in result
assert "error" not in result

# Verify the model name
assert (
result["model"] == "registry.score"
) # adjust if your model is named differently

# Verify key fields are present
assert "status" in result["fields"]
assert "passport" in result["fields"]

@patch("django.core.serializers.serialize")
def test_serialization_error(self, mock_serialize, scorer_score):
"""Test handling of serialization errors"""
# Use the existing scorer_score fixture
score = scorer_score

# Make serialize throw an exception
mock_serialize.side_effect = Exception("Serialization failed")

# Attempt to serialize
result = serialize_score(score)

# Verify error handling
assert result == {"error": "Error serializing score"}
92 changes: 70 additions & 22 deletions api/v2/api/api_stamps.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from datetime import datetime
from datetime import datetime, time
from decimal import Decimal
from typing import List
from typing import Any, Dict, List
from urllib.parse import urljoin

from django.conf import settings
from django.core.cache import cache
from ninja import Schema
from ninja_extra.exceptions import APIException

import api_logging as logging
Expand Down Expand Up @@ -32,7 +33,8 @@
fetch_all_stamp_metadata,
)
from registry.exceptions import (
CreatedAtIsRequired,
CreatedAtIsRequiredException,
CreatedAtMalFormedException,
InternalServerErrorException,
InvalidAddressException,
InvalidAPIKeyPermissions,
Expand Down Expand Up @@ -104,11 +106,50 @@ async def a_submit_passport(request, scorer_id: int, address: str) -> V2ScoreRes
raise InternalServerErrorException("Unexpected error while submitting passport")


def process_date_parameter(date_str: str) -> datetime:
"""
Convert a date string (YYYY-MM-DD) to a datetime object set to the end of that day.
Args:
date_str: String in format 'YYYY-MM-DD'
Returns:
datetime: Datetime object set to 23:59:59 of the given date
Raises:
ValueError: If date string is not in correct format
"""
try:
# Parse the date string
date_obj = datetime.strptime(date_str, "%Y-%m-%d")
# Set time to end of day (23:59:59)
return datetime.combine(date_obj.date(), time(23, 59, 59))
except Exception:
raise CreatedAtMalFormedException()


def extract_score_data(event_data: Dict[str, Any]) -> Dict[str, Any]:
"""
Extract score data from either the legacy or new data structure.
Args:
event_data: Dictionary containing score event data
Returns:
Dictionary with normalized score data
"""
# Handle legacy format (with 'fields' key)
if "fields" in event_data:
return event_data["fields"]
# Handle new format (direct score data)
return event_data


@api_router.get(
"/stamps/{int:scorer_id}/score/{str:address}/history",
auth=ApiKey(),
response={
200: DetailedScoreResponse | NoScoreResponse,
200: V2ScoreResponse | NoScoreResponse,
401: ErrorMessageResponse,
400: ErrorMessageResponse,
404: ErrorMessageResponse,
Expand All @@ -122,35 +163,30 @@ async def a_submit_passport(request, scorer_id: int, address: str) -> V2ScoreRes
\n
To access this endpoint, you must submit your use case and be approved by the Passport team. To do so, please fill out the following form, making sure to provide a detailed description of your use case. The Passport team typically reviews and responds to form responses within 48 hours. <a href="https://forms.gle/4GyicBfhtHW29eEu8" target="_blank">https://forms.gle/4GyicBfhtHW29eEu8</a>
""",
include_in_schema=False,
tags=["Stamp Analysis"],
)
@track_apikey_usage(track_response=False)
def get_score_history(
request,
scorer_id: int,
address: str,
created_at: str = "",
created_at: str,
):
if not request.api_key.historical_endpoint:
raise InvalidAPIKeyPermissions()

if not created_at:
raise CreatedAtIsRequired()
raise CreatedAtIsRequiredException()

check_rate_limit(request)

community = api_get_object_or_404(Community, id=scorer_id, account=request.auth)

try:
end_of_day = process_date_parameter(created_at)
base_query = with_read_db(Event).filter(
community__id=community.id, action=Event.Action.SCORE_UPDATE
)

score_event = (
base_query.filter(
address=address, created_at__lte=datetime.fromisoformat(created_at)
)
base_query.filter(address=address, created_at__lte=end_of_day)
.order_by("-created_at")
.first()
)
Expand All @@ -160,16 +196,28 @@ def get_score_history(
address=address, status=f"No Score Found for {address} at {created_at}"
)

# TODO: geri this is not correct, we need to review the return structure and value here
return DetailedScoreResponse(
# Extract and normalize score data from either format
score_data = extract_score_data(score_event.data)

# Get evidence data, defaulting to empty dict if not present
evidence = score_data.get("evidence", {})
threshold = evidence.get("threshold", "0")

# Handle score extraction for both formats
if "evidence" in score_data and "rawScore" in score_data["evidence"]:
score = score_data["evidence"]["rawScore"]
else:
score = score_data.get("score", "0")

return V2ScoreResponse(
address=address,
score=score_event.data["score"],
status=Score.Status.DONE,
last_score_timestamp=score_event.data["created_at"],
expiration_date=score_event.data["expiration_date"],
evidence=score_event.data["evidence"],
error=None,
stamp_scores=None,
score=score,
passing_score=(Decimal(score) >= Decimal(threshold) if score else False),
threshold=threshold,
last_score_timestamp=score_data.get("last_score_timestamp"),
expiration_timestamp=score_data.get("expiration_date"),
error=score_data.get("error"),
stamp_scores=score_data.get("stamp_scores"),
)

except Exception as e:
Expand Down
Loading

0 comments on commit db51bf8

Please sign in to comment.