Skip to content

Commit 2fba592

Browse files
committed
Add events API
This commit adds a new API endpoint to retrieve the latest events. It allows users to specify pagination parameters such as 'from_id', 'to_id', and 'size'. Both 'from_id' and 'to_id' could refer to the timestamp in milliseconds where the event was created. Signed-off-by: Jose Javier Merchante <jjmerchante@bitergia.com>
1 parent 0f88339 commit 2fba592

9 files changed

Lines changed: 396 additions & 0 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
title: Events API endpoint
3+
category: added
4+
author: Jose Javier Merchante <jjmerchante@bitergia.com>
5+
issue: null
6+
notes: >
7+
Add a new API endpoint for retrieving the latest events
8+
created. Users can control pagination using parameters
9+
like 'from_id', 'to_id', and 'size'. Both 'from_id' and
10+
'to_id' accept timestamps in milliseconds corresponding
11+
to when each event was created.

src/grimoirelab/core/app/urls.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
from grimoirelab.core.scheduler.urls import urlpatterns as sched_urlpatterns
1717
from grimoirelab.core.datasources.urls import ecosystems_urlpatterns
18+
from grimoirelab.core.events.urls import events_urlpatterns
1819

1920
urlpatterns = [
2021
path("login", api_login, name="api_login"),
@@ -34,6 +35,8 @@
3435
SortingHatGraphQLView.as_view(graphiql=settings.DEBUG, schema=schema)
3536
),
3637
),
38+
# Events API
39+
path("events/", include(events_urlpatterns)),
3740
]
3841
),
3942
),

src/grimoirelab/core/events/__init__.py

Whitespace-only changes.

src/grimoirelab/core/events/api.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Copyright (C) GrimoireLab Contributors
4+
#
5+
# This program is free software; you can redistribute it and/or modify
6+
# it under the terms of the GNU General Public License as published by
7+
# the Free Software Foundation; either version 3 of the License, or
8+
# (at your option) any later version.
9+
#
10+
# This program is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU General Public License
16+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
#
18+
19+
import json
20+
21+
from django.conf import settings
22+
from rest_framework import response, views
23+
from django_rq import get_connection
24+
from rest_framework.exceptions import ValidationError
25+
from rest_framework import serializers
26+
from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes
27+
28+
29+
MAX_PAGE_SIZE = 100
30+
DEFAULT_PAGE_SIZE = 25
31+
32+
33+
class EventSerializer(serializers.Serializer):
34+
entry_id = serializers.CharField()
35+
event = serializers.JSONField()
36+
37+
38+
class EventStream(views.APIView):
39+
"""
40+
API endpoint that allows to get the latest events in the stream.
41+
By default, it returns the latest events in the stream.
42+
43+
Old events are removed from the stream based on the retention policy configured.
44+
"""
45+
46+
serializer_class = EventSerializer
47+
48+
@extend_schema(
49+
responses=EventSerializer(many=True),
50+
parameters=[
51+
OpenApiParameter(
52+
name="from_id",
53+
type=OpenApiTypes.STR,
54+
description=(
55+
"Start ID (inclusive) to read events from. "
56+
"Can be a Redis Stream ID (e.g. '1609459200000-0') or a "
57+
"timestamp in milliseconds. Use for paginating forward."
58+
),
59+
required=False,
60+
),
61+
OpenApiParameter(
62+
name="to_id",
63+
type=OpenApiTypes.STR,
64+
description=(
65+
"End ID (inclusive) to read events up to. "
66+
"Can be a Redis Stream ID or a timestamp in milliseconds."
67+
),
68+
required=False,
69+
),
70+
OpenApiParameter(
71+
name="size",
72+
default=25,
73+
type=OpenApiTypes.INT,
74+
description=f"Maximum number of events to return (default {DEFAULT_PAGE_SIZE}, max {MAX_PAGE_SIZE}).",
75+
required=False,
76+
),
77+
],
78+
)
79+
def get(self, request, format=None):
80+
events_stream = settings.GRIMOIRELAB_EVENTS_STREAM_NAME
81+
from_id = request.query_params.get("from_id", None)
82+
to_id = request.query_params.get("to_id", None)
83+
try:
84+
size = int(request.query_params.get("size", DEFAULT_PAGE_SIZE))
85+
if size < 1 or size > MAX_PAGE_SIZE:
86+
raise ValueError
87+
except ValueError:
88+
raise ValidationError("'size' parameter must be an integer between 1 and 100.")
89+
90+
connection = get_connection()
91+
if from_id is None and to_id is None:
92+
# Get the latest events
93+
entries = connection.xrevrange(events_stream, max="+", min="-", count=size)
94+
entries = reversed(entries)
95+
else:
96+
entries = connection.xrange(
97+
events_stream, min=from_id or "-", max=to_id or "+", count=size
98+
)
99+
100+
events = []
101+
for entry in entries:
102+
entry_id = entry[0].decode("utf-8")
103+
entry_data = json.loads(entry[1][b"data"])
104+
event = {"entry_id": entry_id, "event": entry_data}
105+
events.append(event)
106+
107+
return response.Response(events)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Copyright (C) GrimoireLab Contributors
4+
#
5+
# This program is free software; you can redistribute it and/or modify
6+
# it under the terms of the GNU General Public License as published by
7+
# the Free Software Foundation; either version 3 of the License, or
8+
# (at your option) any later version.
9+
#
10+
# This program is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU General Public License
16+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
#
18+
19+
import os
20+
21+
from django.apps import AppConfig
22+
23+
24+
class EventsConfig(AppConfig):
25+
name = "grimoirelab.core.events"
26+
path = os.path.dirname(os.path.abspath(__file__))

src/grimoirelab/core/events/migrations/__init__.py

Whitespace-only changes.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Copyright (C) GrimoireLab Contributors
4+
#
5+
# This program is free software; you can redistribute it and/or modify
6+
# it under the terms of the GNU General Public License as published by
7+
# the Free Software Foundation; either version 3 of the License, or
8+
# (at your option) any later version.
9+
#
10+
# This program is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU General Public License
16+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
#
18+
19+
from django.urls import path
20+
21+
from . import api
22+
23+
events_urlpatterns = [
24+
path("stream/", api.EventStream.as_view(), name="event-stream"),
25+
]

tests/unit/events/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)