Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat api rate limiter #241

Merged
merged 14 commits into from
Nov 1, 2024
5 changes: 4 additions & 1 deletion deployment/docker/requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,7 @@ pytest-django
requests-mock

# memory profiler
memory-profiler
memory-profiler

# fakeredis
fakeredis==2.26.1
17 changes: 16 additions & 1 deletion django_project/core/tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,26 @@
.. note:: Common class for unit tests.
"""

from django.test import TestCase
from fakeredis import FakeConnection
from django.test import TestCase, override_settings
from rest_framework.test import APIRequestFactory

from core.factories import UserF


@override_settings(
CACHES={
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': [
'redis://127.0.0.1:6379',
],
'OPTIONS': {
'connection_class': FakeConnection
}
}
}
)
class BaseAPIViewTest(TestCase):
"""Base class for API test."""

Expand Down
34 changes: 33 additions & 1 deletion django_project/gap_api/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,23 @@
"""

import random
import json
from django.contrib import admin
from django.db.models import Count, TextField
from django.db.models.fields.json import KeyTextTransform
from django.db.models.functions import TruncDay, Cast
from django.http import HttpResponse
from django.core.serializers.json import DjangoJSONEncoder
from rest_framework_tracking.admin import APIRequestLogAdmin
from rest_framework_tracking.models import APIRequestLog as BaseAPIRequestLog

from gap.models import DatasetType
from gap_api.models import APIRequestLog, DatasetTypeAPIConfig, Location
from gap_api.models import (
APIRequestLog,
DatasetTypeAPIConfig,
Location,
APIRateLimiter
)


admin.site.unregister(BaseAPIRequestLog)
Expand Down Expand Up @@ -182,6 +190,30 @@
list_filter = ('user',)


@admin.action(description='Export rate limiter as json')
def export_rate_limiter_as_json(modeladmin, request, queryset):
"""Download rate limiter."""
fields_to_include = [

Check warning on line 196 in django_project/gap_api/admin.py

View check run for this annotation

Codecov / codecov/patch

django_project/gap_api/admin.py#L196

Added line #L196 was not covered by tests
'pk', 'user_id', 'minute_limit', 'hour_limit', 'day_limit']
data = list(queryset.all().values(*fields_to_include))

Check warning on line 198 in django_project/gap_api/admin.py

View check run for this annotation

Codecov / codecov/patch

django_project/gap_api/admin.py#L198

Added line #L198 was not covered by tests

# Convert the data to JSON
response_data = json.dumps(data, cls=DjangoJSONEncoder)

Check warning on line 201 in django_project/gap_api/admin.py

View check run for this annotation

Codecov / codecov/patch

django_project/gap_api/admin.py#L201

Added line #L201 was not covered by tests

# Create the HttpResponse with the correct content_type for JSON
response = HttpResponse(response_data, content_type='application/json')
response['Content-Disposition'] = 'attachment; filename=rate_limiter.json'
return response

Check warning on line 206 in django_project/gap_api/admin.py

View check run for this annotation

Codecov / codecov/patch

django_project/gap_api/admin.py#L204-L206

Added lines #L204 - L206 were not covered by tests


class APIRateLimiterAdmin(admin.ModelAdmin):
"""Admin class for APIRateLimiter."""

list_display = ('config_name', 'minute_limit', 'hour_limit', 'day_limit',)
actions = (export_rate_limiter_as_json,)


admin.site.register(APIRequestLog, GapAPIRequestLogAdmin)
admin.site.register(DatasetTypeAPIConfig, GapAPIDatasetTypeConfigAdmin)
admin.site.register(Location, LocationAdmin)
admin.site.register(APIRateLimiter, APIRateLimiterAdmin)
4 changes: 3 additions & 1 deletion django_project/gap_api/api_views/crop_insight.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
CropInsightSerializer, CropInsightGeojsonSerializer
)
from gap_api.utils.helper import ApiTag
from gap_api.mixins import GAPAPILoggingMixin, CounterSlidingWindowThrottle


def default_fields():
Expand All @@ -41,10 +42,11 @@ def default_fields():
return []


class CropPlanAPI(APIView):
class CropPlanAPI(GAPAPILoggingMixin, APIView):
"""API class for crop plan data."""

permission_classes = [IsAuthenticated]
throttle_classes = [CounterSlidingWindowThrottle]
outputs = [
'json',
'geojson',
Expand Down
3 changes: 2 additions & 1 deletion django_project/gap_api/api_views/location.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from gap_api.serializers.common import APIErrorSerializer
from gap_api.serializers.location import LocationSerializer
from gap_api.utils.helper import ApiTag
from gap_api.mixins import GAPAPILoggingMixin
from gap_api.mixins import GAPAPILoggingMixin, CounterSlidingWindowThrottle
from gap_api.utils.fiona import (
validate_shapefile_zip,
validate_collection_crs,
Expand All @@ -42,6 +42,7 @@ class LocationAPI(GAPAPILoggingMixin, APIView):
"""API class for uploading location."""

permission_classes = [IsAuthenticated]
throttle_classes = [CounterSlidingWindowThrottle]
parser_classes = (MultiPartParser,)
api_parameters = [
openapi.Parameter(
Expand Down
3 changes: 2 additions & 1 deletion django_project/gap_api/api_views/measurement.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
from gap_api.models import DatasetTypeAPIConfig, Location
from gap_api.serializers.common import APIErrorSerializer
from gap_api.utils.helper import ApiTag
from gap_api.mixins import GAPAPILoggingMixin
from gap_api.mixins import GAPAPILoggingMixin, CounterSlidingWindowThrottle


def product_type_list():
Expand Down Expand Up @@ -87,6 +87,7 @@ class MeasurementAPI(GAPAPILoggingMixin, APIView):
date_format = '%Y-%m-%d'
time_format = '%H:%M:%S'
permission_classes = [IsAuthenticated]
throttle_classes = [CounterSlidingWindowThrottle]
api_parameters = [
openapi.Parameter(
'product', openapi.IN_QUERY,
Expand Down
4 changes: 3 additions & 1 deletion django_project/gap_api/api_views/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@
from gap_api.serializers.common import APIErrorSerializer
from gap_api.serializers.user import UserInfoSerializer
from gap_api.utils.helper import ApiTag
from gap_api.mixins import GAPAPILoggingMixin, CounterSlidingWindowThrottle


class UserInfo(APIView):
class UserInfo(GAPAPILoggingMixin, APIView):
"""API to return user info."""

permission_classes = [IsAuthenticated]
throttle_classes = [CounterSlidingWindowThrottle]

@swagger_auto_schema(
operation_id='user-info',
Expand Down
14 changes: 13 additions & 1 deletion django_project/gap_api/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from django.contrib.gis.geos import Polygon, MultiPolygon

from core.factories import UserF
from gap_api.models import APIRequestLog, Location
from gap_api.models import APIRequestLog, Location, APIRateLimiter


class APIRequestLogFactory(DjangoModelFactory):
Expand Down Expand Up @@ -51,3 +51,15 @@ class Meta: # noqa
)
)
created_on = factory.Faker('date_time')


class APIRateLimiterFactory(DjangoModelFactory):
"""Factory class for APIRateLimiter model."""

class Meta: # noqa
model = APIRateLimiter

user = factory.SubFactory(UserF)
minute_limit = 10
hour_limit = 100
day_limit = 1000
12 changes: 12 additions & 0 deletions django_project/gap_api/fixtures/2.apiratelimiter.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[
{
"model": "gap_api.apiratelimiter",
"pk": 1,
"fields": {
"user": null,
"minute_limit": 1000,
"hour_limit": 10000,
"day_limit": 100000
}
}
]
26 changes: 26 additions & 0 deletions django_project/gap_api/migrations/0004_apiratelimiter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated by Django 4.2.7 on 2024-10-30 11:51

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('gap_api', '0003_location_location_user_locationname'),
]

operations = [
migrations.CreateModel(
name='APIRateLimiter',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('minute_limit', models.IntegerField()),
('hour_limit', models.IntegerField()),
('day_limit', models.IntegerField()),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
),
]
1 change: 1 addition & 0 deletions django_project/gap_api/mixins/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from gap_api.mixins.logging import * # noqa
from gap_api.mixins.rate_limiter import * # noqa
Loading
Loading