Skip to content

Commit

Permalink
3062 historical endpoint fix (#738)
Browse files Browse the repository at this point in the history
* chore(api): accept iso date and use django filter

* fix(infra): add history specifc listener rule

* fix(api): evidence is None case

* chore(api): unneeded default value
  • Loading branch information
tim-schultz authored Nov 19, 2024
1 parent 4c5b2ea commit 0a16b0b
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 34 deletions.
60 changes: 28 additions & 32 deletions api/v2/api/api_stamps.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from typing import Any, Dict, List
from urllib.parse import urljoin

import django_filters
from django.conf import settings
from django.core.cache import cache
from ninja import Schema
Expand Down Expand Up @@ -105,28 +106,6 @@ 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.
Expand All @@ -144,6 +123,21 @@ def extract_score_data(event_data: Dict[str, Any]) -> Dict[str, Any]:
return event_data


class EventFilter(django_filters.FilterSet):
created_at__lte = django_filters.IsoDateTimeFilter(
field_name="created_at", lookup_expr="lte"
)
address = django_filters.CharFilter(field_name="address")
community__id = django_filters.NumberFilter(field_name="community__id")
action = django_filters.ChoiceFilter(
choices=Event.Action.choices, field_name="action"
)

class Meta:
model = Event
fields = ["created_at", "address", "community__id", "action"]


@api_router.get(
"/stamps/{scorer_id}/score/{address}/history",
auth=ApiKey(),
Expand Down Expand Up @@ -174,16 +168,18 @@ def get_score_history(
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=end_of_day)
.order_by("-created_at")
.first()
filterset = EventFilter(
data={
"community__id": community.id,
"action": Event.Action.SCORE_UPDATE,
"address": address,
"created_at__lte": created_at,
},
queryset=with_read_db(Event),
)

score_event = filterset.qs.order_by("-created_at").first()

if not score_event:
return NoScoreResponse(
address=address, status=f"No Score Found for {address} at {created_at}"
Expand All @@ -193,11 +189,11 @@ def get_score_history(
score_data = extract_score_data(score_event.data)

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

# Handle score extraction for both formats
if "evidence" in score_data and "rawScore" in score_data["evidence"]:
if "evidence" in score_data and "rawScore" in evidence:
score = score_data["evidence"]["rawScore"]
else:
score = score_data.get("score", "0")
Expand Down
62 changes: 61 additions & 1 deletion api/v2/test/test_historical_score_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,36 @@ def test_get_historical_score_missing_fields(
assert response_data["expiration_timestamp"] is None
assert response_data["stamp_scores"] is None

@freeze_time("2023-01-01")
def test_get_historical_score_ne_evidence(
self,
scorer_account,
scorer_api_key,
scorer_community_with_binary_scorer,
):
# Create score event with minimal data
Event.objects.create(
action=Event.Action.SCORE_UPDATE,
address=scorer_account.address,
community=scorer_community_with_binary_scorer,
data={"score": 78.762, "evidence": None},
)

client = Client()
response = client.get(
f"{self.base_url}/{scorer_community_with_binary_scorer.id}/score/{scorer_account.address}/history?created_at=2023-01-01",
HTTP_AUTHORIZATION="Token " + scorer_api_key,
)
response_data = response.json()

assert response.status_code == 200
assert response_data["score"] == "78.762"
assert response_data["threshold"] == "0"
assert response_data["passing_score"] is True
assert response_data["last_score_timestamp"] is None
assert response_data["expiration_timestamp"] is None
assert response_data["stamp_scores"] is None

def test_get_historical_score_no_score_found(
self,
scorer_account,
Expand Down Expand Up @@ -200,4 +230,34 @@ def test_get_historical_score_invalid_date(
HTTP_AUTHORIZATION="Token " + scorer_api_key,
)

assert response.status_code == 400
assert response.status_code == 200
assert (
response.json()["status"]
== "No Score Found for 0xB81C935D01e734b3D8bb233F5c4E1D72DBC30f6c at invalid-date"
)

def test_get_historical_score_valid_iso_date(
self,
scorer_account,
scorer_api_key,
scorer_community_with_binary_scorer,
):
client = Client()
# Test various valid ISO 8601 date formats
valid_dates = [
"2023-01-01",
"2023-01-01T00:00:00Z",
"2023-01-01T00:00:00+00:00",
"2023-01-01T12:34:56.789Z",
]

for date in valid_dates:
response = client.get(
f"{self.base_url}/{scorer_community_with_binary_scorer.id}/score/{scorer_account.address}/history?created_at={date}",
HTTP_AUTHORIZATION="Token " + scorer_api_key,
)

assert response.status_code == 200
response_data = response.json()
assert response_data["address"] == scorer_account.address
assert "No Score Found for" in response_data["status"]
34 changes: 33 additions & 1 deletion infra/lib/v2/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,11 +135,43 @@ export function createV2Api({
},
},
],
listenerPriority: 2022,
listenerPriority: 2023,
},
alarmConfigurations
);

const targetPassportRuleHistory = new ListenerRule(
`passport-v2-lrule-history`,
{
tags: { ...defaultTags, Name: "passport-v2-lrule-history" },
listenerArn: httpsListener.arn,
priority: 2022,
actions: [
{
type: "forward",
targetGroupArn: targetGroupRegistry.arn,
},
],
conditions: [
{
hostHeader: {
values: ["*.passport.xyz"],
},
},
{
pathPattern: {
values: ["/v2/stamps/*/score/*/history"],
},
},
{
httpRequestMethod: {
values: ["GET"],
},
},
],
}
);

const targetPassportRule = new ListenerRule(`passport-v2-lrule`, {
tags: { ...defaultTags, Name: "passport-v2-lrule" },
listenerArn: httpsListener.arn,
Expand Down

0 comments on commit 0a16b0b

Please sign in to comment.