From 24a2027a902b1b73ad80371e6a5a12acbd2077f5 Mon Sep 17 00:00:00 2001 From: photon0205 Date: Tue, 15 Aug 2023 12:00:00 +0530 Subject: [PATCH 1/4] feat: add ui to build APIs --- api_builder/__init__.py | 0 api_builder/admin.py | 3 ++ api_builder/apps.py | 6 +++ api_builder/migrations/0001_initial.py | 26 +++++++++ api_builder/migrations/__init__.py | 0 api_builder/models.py | 11 ++++ api_builder/serializers.py | 20 +++++++ api_builder/tests.py | 3 ++ api_builder/urls.py | 7 +++ api_builder/utils.py | 46 ++++++++++++++++ api_builder/views.py | 75 ++++++++++++++++++++++++++ core/settings.py | 3 +- core/urls.py | 1 + 13 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 api_builder/__init__.py create mode 100644 api_builder/admin.py create mode 100644 api_builder/apps.py create mode 100644 api_builder/migrations/0001_initial.py create mode 100644 api_builder/migrations/__init__.py create mode 100644 api_builder/models.py create mode 100644 api_builder/serializers.py create mode 100644 api_builder/tests.py create mode 100644 api_builder/urls.py create mode 100644 api_builder/utils.py create mode 100644 api_builder/views.py diff --git a/api_builder/__init__.py b/api_builder/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api_builder/admin.py b/api_builder/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/api_builder/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/api_builder/apps.py b/api_builder/apps.py new file mode 100644 index 00000000..2e13e8bb --- /dev/null +++ b/api_builder/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ApiBuilderConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'api_builder' diff --git a/api_builder/migrations/0001_initial.py b/api_builder/migrations/0001_initial.py new file mode 100644 index 00000000..744c464b --- /dev/null +++ b/api_builder/migrations/0001_initial.py @@ -0,0 +1,26 @@ +# Generated by Django 4.1.5 on 2023-09-02 01:17 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('datahub', '0040_alter_datasetv2_name_alter_policy_description_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='API', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('endpoint', models.CharField(max_length=100)), + ('selected_columns', models.JSONField()), + ('access_key', models.CharField(blank=True, max_length=100, null=True)), + ('dataset_file', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='apis', to='datahub.datasetv2file')), + ], + ), + ] diff --git a/api_builder/migrations/__init__.py b/api_builder/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api_builder/models.py b/api_builder/models.py new file mode 100644 index 00000000..bbc21d56 --- /dev/null +++ b/api_builder/models.py @@ -0,0 +1,11 @@ +from django.db import models +from datahub.models import DatasetV2File + +class API(models.Model): + dataset_file = models.ForeignKey(DatasetV2File, on_delete=models.CASCADE, related_name='apis') + endpoint = models.CharField(max_length=100) + selected_columns = models.JSONField() + access_key = models.CharField(max_length=100, blank=True, null=True) + + def __str__(self): + return self.endpoint diff --git a/api_builder/serializers.py b/api_builder/serializers.py new file mode 100644 index 00000000..1988a5c4 --- /dev/null +++ b/api_builder/serializers.py @@ -0,0 +1,20 @@ +from rest_framework import serializers +from api_builder.models import API +from datahub.models import DatasetV2File + +class DatasetV2FileSerializer(serializers.ModelSerializer): + dataset = serializers.SerializerMethodField() + + class Meta: + model = DatasetV2File + fields = '__all__' + + def get_dataset(self, obj): + return obj.dataset.name + +class APISerializer(serializers.ModelSerializer): + dataset_file = DatasetV2FileSerializer() + + class Meta: + model = API + fields = '__all__' diff --git a/api_builder/tests.py b/api_builder/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/api_builder/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/api_builder/urls.py b/api_builder/urls.py new file mode 100644 index 00000000..d3686753 --- /dev/null +++ b/api_builder/urls.py @@ -0,0 +1,7 @@ +from django.urls import path +from api_builder.views import APIViewWithData, CreateAPIView + +urlpatterns = [ + path('create/', CreateAPIView.as_view(), name='create_api'), + path('//', APIViewWithData.as_view(), name='api-with-data'), +] diff --git a/api_builder/utils.py b/api_builder/utils.py new file mode 100644 index 00000000..6a903180 --- /dev/null +++ b/api_builder/utils.py @@ -0,0 +1,46 @@ +import pandas as pd +import logging +import uuid +from rest_framework.authentication import BaseAuthentication +from rest_framework.exceptions import AuthenticationFailed +from .models import API + +class APIKeyAuthentication(BaseAuthentication): + def authenticate(self, request): + api_key = request.headers.get('Authorization', '') + + if not api_key.strip() or api_key.lower() == 'none': + raise AuthenticationFailed('No API key provided.') + + api_key = api_key.split()[-1] + + try: + api = API.objects.get(access_key=api_key) + except API.DoesNotExist: + raise AuthenticationFailed('API key is invalid.') + + return (api, None) + +def generate_api_key(): + return str(uuid.uuid4()) + +def read_columns_from_csv_or_xlsx_file(file_path): + """This function reads the file and returns the column names as a list.""" + try: + if file_path.endswith(".xlsx") or file_path.endswith(".xls"): + content = pd.read_excel(file_path, index_col=None, nrows=21).head(2) if file_path else None + else: + content = pd.read_csv(file_path, index_col=False, nrows=21).head(2) if file_path else None + + if content is not None: + content = content.drop(content.filter(regex='Unnamed').columns, axis=1) + content = content.fillna("") + column_names = content.columns.astype(str).tolist() + else: + column_names = [] + + except Exception as error: + logging.error("Invalid file ERROR: %s", error) + column_names = [] + + return column_names diff --git a/api_builder/views.py b/api_builder/views.py new file mode 100644 index 00000000..cc691169 --- /dev/null +++ b/api_builder/views.py @@ -0,0 +1,75 @@ +import os +import json +from django.http import JsonResponse +from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly +from rest_framework import status +from rest_framework.views import APIView +from rest_framework.response import Response +from api_builder.serializers import APISerializer +from api_builder.utils import APIKeyAuthentication, generate_api_key, read_columns_from_csv_or_xlsx_file +from core import settings +from core.utils import read_contents_from_csv_or_xlsx_file +from .models import API +from datahub.models import DatasetV2File + +class CreateAPIView(APIView): + permission_classes = [IsAuthenticated] + def post(self, request): + user = request.user + endpoint = request.data.get('endpoint', None) + final_endpoint = f"/api/{user.id}/{endpoint}" + dataset_file_id = request.data.get('dataset_file_id', None) + print(dataset_file_id) + try: + dataset_file = DatasetV2File.objects.get(id=dataset_file_id) + except DatasetV2File.DoesNotExist: + return JsonResponse({'error': 'Dataset file not found'}, status=404) + + access_key = generate_api_key() + dataset_file_path = os.path.join(settings.DATASET_FILES_URL, str(dataset_file.standardised_file)) + selected_columns_json = request.data.get('selected_columns', '[]') + + try: + selected_columns = json.loads(selected_columns_json) + if not isinstance(selected_columns, list): + raise ValueError('Selected columns must be a list') + except json.JSONDecodeError: + return JsonResponse({'error': 'Invalid JSON for selected_columns'}, status=400) + + existing_columns = read_columns_from_csv_or_xlsx_file(dataset_file_path) + + if not existing_columns: + return JsonResponse({'error': 'Failed to read dataset file'}, status=500) + + missing_columns = [col for col in selected_columns if col not in existing_columns] + + if missing_columns: + return JsonResponse({'error': f'Selected columns do not exist: {", ".join(missing_columns)}'}, status=400) + + api = API.objects.create( + dataset_file=dataset_file, + endpoint=final_endpoint, + selected_columns=selected_columns, + access_key=access_key + ) + serializer = APISerializer(api) + return JsonResponse({'message': 'API created successfully', 'api': serializer.data}, status=status.HTTP_200_OK) + +class APIViewWithData(APIView): + authentication_classes = [APIKeyAuthentication] + permission_classes = [IsAuthenticatedOrReadOnly] + + def get(self, request, endpoint_name, user_id): + api_key = request.headers.get('Authorization', '').split()[-1] + final_endpoint = f"/api/{user_id}/{endpoint_name}" + try: + api = API.objects.get(access_key=api_key, endpoint=final_endpoint) + except API.DoesNotExist: + return Response({'error': 'API endpoint not found or unauthorized.'}, status=404) + + selected_columns = api.selected_columns + file_path = api.dataset_file.file.path + content = read_contents_from_csv_or_xlsx_file(file_path) + filtered_content = [{col: row[col] for col in selected_columns} for row in content] + + return Response({'data': filtered_content}, status=status.HTTP_200_OK) diff --git a/core/settings.py b/core/settings.py index 1b56c325..43a73174 100644 --- a/core/settings.py +++ b/core/settings.py @@ -67,7 +67,8 @@ "datahub", "participant", "microsite", - "connectors" + "connectors", + "api_builder" ] # Use nose to run all tests TEST_RUNNER = "django_nose.NoseTestSuiteRunner" diff --git a/core/urls.py b/core/urls.py index 1de88489..23b4f663 100644 --- a/core/urls.py +++ b/core/urls.py @@ -63,6 +63,7 @@ path("connectors/", include("connectors.urls")), path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), path('protected-media/', protected_media_view), + path("api/", include("api_builder.urls")), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + static(settings.PROTECTED_MEDIA_URL, document_root=settings.PROTECTED_MEDIA_ROOT) From f508f875a19fd62fbdec34b2e44bf2aa17804b6f Mon Sep 17 00:00:00 2001 From: photon0205 Date: Tue, 22 Aug 2023 12:00:00 +0530 Subject: [PATCH 2/4] feat: add all APIs view to the dashboard --- api_builder/urls.py | 3 ++- api_builder/views.py | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/api_builder/urls.py b/api_builder/urls.py index d3686753..cdabff7f 100644 --- a/api_builder/urls.py +++ b/api_builder/urls.py @@ -1,7 +1,8 @@ from django.urls import path -from api_builder.views import APIViewWithData, CreateAPIView +from api_builder.views import APIViewWithData, CreateAPIView, ListUserAPIsView urlpatterns = [ path('create/', CreateAPIView.as_view(), name='create_api'), path('//', APIViewWithData.as_view(), name='api-with-data'), + path('list/', ListUserAPIsView.as_view(), name='list-user-apis'), ] diff --git a/api_builder/views.py b/api_builder/views.py index cc691169..604e4c82 100644 --- a/api_builder/views.py +++ b/api_builder/views.py @@ -12,6 +12,14 @@ from .models import API from datahub.models import DatasetV2File +class ListUserAPIsView(APIView): + permission_classes = [IsAuthenticated] + def get(self, request): + user = request.user + apis = API.objects.filter(endpoint__startswith=f"/api/{user.id}/") + serializer = APISerializer(apis, many=True) + return JsonResponse({'apis': serializer.data}, status=200) + class CreateAPIView(APIView): permission_classes = [IsAuthenticated] def post(self, request): From 52949c7a880f057735255ba9863ae37f9c214151 Mon Sep 17 00:00:00 2001 From: photon0205 Date: Mon, 28 Aug 2023 12:00:00 +0100 Subject: [PATCH 3/4] docs: add documentation --- ...alter_api_access_key_alter_api_endpoint.py | 23 +++++++ api_builder/models.py | 20 +++++- api_builder/serializers.py | 27 +++++++- api_builder/urls.py | 6 +- api_builder/utils.py | 33 ++++++++-- api_builder/views.py | 66 +++++++++++++------ 6 files changed, 140 insertions(+), 35 deletions(-) create mode 100644 api_builder/migrations/0002_alter_api_access_key_alter_api_endpoint.py diff --git a/api_builder/migrations/0002_alter_api_access_key_alter_api_endpoint.py b/api_builder/migrations/0002_alter_api_access_key_alter_api_endpoint.py new file mode 100644 index 00000000..e80abbec --- /dev/null +++ b/api_builder/migrations/0002_alter_api_access_key_alter_api_endpoint.py @@ -0,0 +1,23 @@ +# Generated by Django 4.1.5 on 2023-09-07 01:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api_builder', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='api', + name='access_key', + field=models.CharField(max_length=100), + ), + migrations.AlterField( + model_name='api', + name='endpoint', + field=models.CharField(max_length=100, unique=True), + ), + ] diff --git a/api_builder/models.py b/api_builder/models.py index bbc21d56..005ed1a0 100644 --- a/api_builder/models.py +++ b/api_builder/models.py @@ -1,11 +1,25 @@ from django.db import models from datahub.models import DatasetV2File + class API(models.Model): - dataset_file = models.ForeignKey(DatasetV2File, on_delete=models.CASCADE, related_name='apis') - endpoint = models.CharField(max_length=100) + """ + API Model - Represents an API configuration. + + Attributes: + - dataset_file: ForeignKey to the associated DatasetV2File. + - endpoint: The API endpoint (unique). + - selected_columns: JSONField for storing selected columns. + - access_key: API access key. + """ + + dataset_file = models.ForeignKey(DatasetV2File, on_delete=models.CASCADE, related_name="apis") + endpoint = models.CharField(max_length=100, unique=True) selected_columns = models.JSONField() - access_key = models.CharField(max_length=100, blank=True, null=True) + access_key = models.CharField(max_length=100) def __str__(self): + """ + Returns a string representation of the API instance. + """ return self.endpoint diff --git a/api_builder/serializers.py b/api_builder/serializers.py index 1988a5c4..2a592be2 100644 --- a/api_builder/serializers.py +++ b/api_builder/serializers.py @@ -2,19 +2,42 @@ from api_builder.models import API from datahub.models import DatasetV2File + class DatasetV2FileSerializer(serializers.ModelSerializer): + """ + Serializer for DatasetV2File model. + + Serializes DatasetV2File fields and includes a method for getting the dataset name. + """ + dataset = serializers.SerializerMethodField() class Meta: model = DatasetV2File - fields = '__all__' + fields = "__all__" def get_dataset(self, obj): + """ + Get the dataset name associated with the DatasetV2File. + + Args: + obj: The DatasetV2File instance. + + Returns: + The dataset name. + """ return obj.dataset.name + class APISerializer(serializers.ModelSerializer): + """ + Serializer for the API model. + + Includes serialization of the associated DatasetV2File using DatasetV2FileSerializer. + """ + dataset_file = DatasetV2FileSerializer() class Meta: model = API - fields = '__all__' + fields = "__all__" diff --git a/api_builder/urls.py b/api_builder/urls.py index cdabff7f..6be308ad 100644 --- a/api_builder/urls.py +++ b/api_builder/urls.py @@ -2,7 +2,7 @@ from api_builder.views import APIViewWithData, CreateAPIView, ListUserAPIsView urlpatterns = [ - path('create/', CreateAPIView.as_view(), name='create_api'), - path('//', APIViewWithData.as_view(), name='api-with-data'), - path('list/', ListUserAPIsView.as_view(), name='list-user-apis'), + path("create/", CreateAPIView.as_view(), name="create_api"), + path("//", APIViewWithData.as_view(), name="api-with-data"), + path("list/", ListUserAPIsView.as_view(), name="list-user-apis"), ] diff --git a/api_builder/utils.py b/api_builder/utils.py index 6a903180..63698310 100644 --- a/api_builder/utils.py +++ b/api_builder/utils.py @@ -5,27 +5,48 @@ from rest_framework.exceptions import AuthenticationFailed from .models import API + class APIKeyAuthentication(BaseAuthentication): + """ + Custom authentication class for API key authentication. + """ + def authenticate(self, request): - api_key = request.headers.get('Authorization', '') + api_key = request.headers.get("Authorization", "") - if not api_key.strip() or api_key.lower() == 'none': - raise AuthenticationFailed('No API key provided.') + if not api_key.strip() or api_key.lower() == "none": + raise AuthenticationFailed("No API key provided.") api_key = api_key.split()[-1] try: api = API.objects.get(access_key=api_key) except API.DoesNotExist: - raise AuthenticationFailed('API key is invalid.') + raise AuthenticationFailed("API key is invalid.") return (api, None) + def generate_api_key(): + """ + Generate a random API access key. + + Returns: + A randomly generated API access key. + """ return str(uuid.uuid4()) + def read_columns_from_csv_or_xlsx_file(file_path): - """This function reads the file and returns the column names as a list.""" + """ + Read column names from a CSV or XLSX file. + + Args: + file_path: The path to the file. + + Returns: + A list of column names. + """ try: if file_path.endswith(".xlsx") or file_path.endswith(".xls"): content = pd.read_excel(file_path, index_col=None, nrows=21).head(2) if file_path else None @@ -33,7 +54,7 @@ def read_columns_from_csv_or_xlsx_file(file_path): content = pd.read_csv(file_path, index_col=False, nrows=21).head(2) if file_path else None if content is not None: - content = content.drop(content.filter(regex='Unnamed').columns, axis=1) + content = content.drop(content.filter(regex="Unnamed").columns, axis=1) content = content.fillna("") column_names = content.columns.astype(str).tolist() else: diff --git a/api_builder/views.py b/api_builder/views.py index 604e4c82..d4637d5e 100644 --- a/api_builder/views.py +++ b/api_builder/views.py @@ -1,5 +1,6 @@ import os import json +from django.db import IntegrityError from django.http import JsonResponse from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly from rest_framework import status @@ -12,72 +13,95 @@ from .models import API from datahub.models import DatasetV2File + class ListUserAPIsView(APIView): + """ + View for listing user-specific APIs. + """ + permission_classes = [IsAuthenticated] + def get(self, request): user = request.user apis = API.objects.filter(endpoint__startswith=f"/api/{user.id}/") serializer = APISerializer(apis, many=True) - return JsonResponse({'apis': serializer.data}, status=200) + return JsonResponse({"apis": serializer.data}, status=200) + class CreateAPIView(APIView): + """ + View for creating an API. + """ + permission_classes = [IsAuthenticated] + def post(self, request): user = request.user - endpoint = request.data.get('endpoint', None) + endpoint = request.data.get("endpoint", None) final_endpoint = f"/api/{user.id}/{endpoint}" - dataset_file_id = request.data.get('dataset_file_id', None) - print(dataset_file_id) + dataset_file_id = request.data.get("dataset_file_id", None) try: dataset_file = DatasetV2File.objects.get(id=dataset_file_id) except DatasetV2File.DoesNotExist: - return JsonResponse({'error': 'Dataset file not found'}, status=404) + return JsonResponse({"error": "Dataset file not found"}, status=404) access_key = generate_api_key() dataset_file_path = os.path.join(settings.DATASET_FILES_URL, str(dataset_file.standardised_file)) - selected_columns_json = request.data.get('selected_columns', '[]') + selected_columns_json = request.data.get("selected_columns", "[]") try: selected_columns = json.loads(selected_columns_json) if not isinstance(selected_columns, list): - raise ValueError('Selected columns must be a list') + raise ValueError("Selected columns must be a list") except json.JSONDecodeError: - return JsonResponse({'error': 'Invalid JSON for selected_columns'}, status=400) + return JsonResponse({"error": "Invalid JSON for selected_columns"}, status=400) existing_columns = read_columns_from_csv_or_xlsx_file(dataset_file_path) if not existing_columns: - return JsonResponse({'error': 'Failed to read dataset file'}, status=500) + return JsonResponse({"error": "Failed to read dataset file"}, status=500) missing_columns = [col for col in selected_columns if col not in existing_columns] if missing_columns: - return JsonResponse({'error': f'Selected columns do not exist: {", ".join(missing_columns)}'}, status=400) + return JsonResponse({"error": f'Selected columns do not exist: {", ".join(missing_columns)}'}, status=400) + + try: + # Attempt to create the API + api = API.objects.create( + dataset_file=dataset_file, + endpoint=final_endpoint, + selected_columns=selected_columns, + access_key=access_key, + ) + serializer = APISerializer(api) + return JsonResponse( + {"message": "API created successfully", "api": serializer.data}, status=status.HTTP_200_OK + ) + except IntegrityError as e: + # Handle the IntegrityError (duplicate endpoint) + return JsonResponse({"error": "API endpoint already exists"}, status=400) - api = API.objects.create( - dataset_file=dataset_file, - endpoint=final_endpoint, - selected_columns=selected_columns, - access_key=access_key - ) - serializer = APISerializer(api) - return JsonResponse({'message': 'API created successfully', 'api': serializer.data}, status=status.HTTP_200_OK) class APIViewWithData(APIView): + """ + View for accessing API data with authentication. + """ + authentication_classes = [APIKeyAuthentication] permission_classes = [IsAuthenticatedOrReadOnly] def get(self, request, endpoint_name, user_id): - api_key = request.headers.get('Authorization', '').split()[-1] + api_key = request.headers.get("Authorization", "").split()[-1] final_endpoint = f"/api/{user_id}/{endpoint_name}" try: api = API.objects.get(access_key=api_key, endpoint=final_endpoint) except API.DoesNotExist: - return Response({'error': 'API endpoint not found or unauthorized.'}, status=404) + return Response({"error": "API endpoint not found or unauthorized."}, status=404) selected_columns = api.selected_columns file_path = api.dataset_file.file.path content = read_contents_from_csv_or_xlsx_file(file_path) filtered_content = [{col: row[col] for col in selected_columns} for row in content] - return Response({'data': filtered_content}, status=status.HTTP_200_OK) + return Response({"data": filtered_content}, status=status.HTTP_200_OK) From f99f348ceede96f4c3f61efbb335a8371b14effd Mon Sep 17 00:00:00 2001 From: photon0205 Date: Thu, 7 Sep 2023 20:00:00 +0100 Subject: [PATCH 4/4] feat: add tests --- api_builder/tests/test_models.py | 88 ++++++++++++++++++ api_builder/tests/test_urls.py | 26 ++++++ api_builder/tests/test_views.py | 149 +++++++++++++++++++++++++++++++ 3 files changed, 263 insertions(+) create mode 100644 api_builder/tests/test_models.py create mode 100644 api_builder/tests/test_urls.py create mode 100644 api_builder/tests/test_views.py diff --git a/api_builder/tests/test_models.py b/api_builder/tests/test_models.py new file mode 100644 index 00000000..5d2da0d0 --- /dev/null +++ b/api_builder/tests/test_models.py @@ -0,0 +1,88 @@ +from django.test import Client, TestCase +from accounts.models import User, UserRole +from api_builder.models import API +from datahub.models import ( + DatasetV2, + DatasetV2File, + Organization, + UserOrganizationMap, +) + +datasets_dump_data = { + "name": "dump_datasets1", + "description": "dataset description", + "geography": "tpt", + "constantly_update": False, +} + +class APITestModels(TestCase): + @classmethod + def setUpClass(cls): + """ + Set up the test environment for the APITestModels test case. + + This method is called once before any test methods in this class run. + It creates necessary test objects and clients. + """ + super().setUpClass() + cls.user = Client() + cls.client_admin = Client() + cls.admin_role = UserRole.objects.create(id="1", role_name="datahub_admin") + cls.admin_user = User.objects.create( + email="sp.code2003@gmail.com", + role_id=cls.admin_role.id, + ) + cls.admin_org = Organization.objects.create( + org_email="admin_org@dg.org", + name="admin org", + phone_number="+91 83602-11483", + website="htttps://google.com", + address=({"city": "Bangalore"}), + ) + cls.admin_map = UserOrganizationMap.objects.create( + user_id=cls.admin_user.id, + organization_id=cls.admin_org.id, + ) + cls.dataset = DatasetV2.objects.create(user_map=cls.admin_map, **datasets_dump_data) + cls.dataset_id = cls.dataset.id + cls.dataset_file = DatasetV2File.objects.create( + file="api_builder/tests/test_data/File.csv", dataset=cls.dataset + ) + + def test_create_api(self): + """ + Test the creation of an API object. + + This method creates an API object and asserts that its attributes + are correctly set. + """ + api = API.objects.create( + dataset_file=self.dataset_file, + endpoint="test_endpoint", + selected_columns=["column1", "column2"], + access_key="test_key", + ) + self.assertEqual(api.endpoint, "test_endpoint") + self.assertEqual(api.dataset_file, self.dataset_file) + self.assertEqual(api.access_key, "test_key") + + def test_duplicate_endpoint(self): + """ + Test the prevention of duplicate endpoint creation. + + This method creates an API object with a duplicate endpoint and + asserts that it raises an exception. + """ + API.objects.create( + dataset_file=self.dataset_file, + endpoint="test_endpoint", + selected_columns=["Period", "Data_value"], + access_key="test_key", + ) + with self.assertRaises(Exception): + API.objects.create( + dataset_file=self.dataset_file, + endpoint="test_endpoint", + selected_columns=["Period", "Data_value"], + access_key="test_key2", + ) diff --git a/api_builder/tests/test_urls.py b/api_builder/tests/test_urls.py new file mode 100644 index 00000000..ce25935e --- /dev/null +++ b/api_builder/tests/test_urls.py @@ -0,0 +1,26 @@ +from django.test import TestCase +from django.urls import reverse, resolve +from api_builder import views + +class APITestUrls(TestCase): + def test_create_api_url(self): + """ + Test that the 'create_api' URL resolves to the CreateAPIView view. + """ + url = reverse("create_api") + self.assertEqual(resolve(url).func.view_class, views.CreateAPIView) + + def test_api_with_data_url(self): + """ + Test that the 'api-with-data' URL with arguments 'user_id' and 'endpoint_name' + resolves to the APIViewWithData view. + """ + url = reverse("api-with-data", args=["user_id", "endpoint_name"]) + self.assertEqual(resolve(url).func.view_class, views.APIViewWithData) + + def test_list_user_apis_url(self): + """ + Test that the 'list-user-apis' URL resolves to the ListUserAPIsView view. + """ + url = reverse("list-user-apis") + self.assertEqual(resolve(url).func.view_class, views.ListUserAPIsView) diff --git a/api_builder/tests/test_views.py b/api_builder/tests/test_views.py new file mode 100644 index 00000000..daac8bc7 --- /dev/null +++ b/api_builder/tests/test_views.py @@ -0,0 +1,149 @@ +import os +from django.conf import settings +from django.test import TestCase, Client +from django.urls import reverse +from accounts.models import User, UserRole +from api_builder.models import API +from rest_framework import status +from datahub.models import DatasetV2, DatasetV2File, Organization, UserOrganizationMap +from participant.tests.test_util import TestUtils +from django.core.files.uploadedfile import SimpleUploadedFile + +datasets_dump_data = { + "name": "dump_datasets3", + "description": "dataset description", + "geography": "tpt", + "constantly_update": False, +} +auth = {"token": "null"} +auth_co_steward = {"token": "null"} +auth_participant = {"token": "null"} + +class APITestViews(TestCase): + """ + Test cases for the views in the 'api_builder' app. + """ + + @classmethod + def setUpClass(self): + """ + Set up test data and initialize the test client. + """ + super().setUpClass() + self.client_admin = Client() + self.admin_role = UserRole.objects.create(id="3", role_name="datahub_admin") + self.admin_user = User.objects.create( + email="sahajpreets12@gmail.com", + role_id=self.admin_role.id, + ) + self.admin_org = Organization.objects.create( + org_email="sahajpreets12@gmail.com", + name="admin org", + phone_number="+91 83602-11483", + website="htttps://google.com", + address=({"city": "Banglore"}), + ) + self.admin_map = UserOrganizationMap.objects.create( + user_id=self.admin_user.id, + organization_id=self.admin_org.id, + ) + self.dataset = DatasetV2.objects.create(user_map=self.admin_map, **datasets_dump_data) + self.dataset_id = self.dataset.id + with open("api_builder/tests/test_data/File.csv", "rb") as file: + file_obj = file.read() + file = SimpleUploadedFile("File.csv", file_obj) + self.dataset_file = DatasetV2File.objects.create(file=file, dataset=self.dataset, standardised_file=file) + auth["token"] = TestUtils.create_token_for_user(self.admin_user, self.admin_map) + admin_header = self.set_auth_headers(self) # type:ignore + self.client_admin.defaults["HTTP_AUTHORIZATION"] = admin_header[0] + self.client_admin.defaults["CONTENT_TYPE"] = admin_header[1] + + def set_auth_headers(self): + """ + Set the authentication headers for API requests. + """ + headers = {"Content-Type": "application/json", "Authorization": f'Bearer {auth["token"]}'} + return headers["Authorization"], headers["Content-Type"] + + def test_create_api_view(self): + """ + Test the 'create_api' view. + """ + api_data = { + "endpoint": "test_endpoint", + "dataset_file_id": self.dataset_file.id, + "selected_columns": '["Period", "Data_value"]', + } + response = self.client_admin.post(reverse("create_api"), api_data, format="json") + data = response.json() + print(data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertTrue(API.objects.filter(endpoint=data["api"]["endpoint"]).exists()) + + def test_list_user_apis_view(self): + """ + Test the 'list-user-apis' view. + """ + api = API.objects.create( + dataset_file=self.dataset_file, + endpoint=f"/api/{self.admin_user.id}/test_endpoint", + selected_columns=["Period", "Data_value"], + access_key="test_key", + ) + + self.client_admin.force_login(self.admin_user) + response = self.client_admin.get(reverse("list-user-apis"), format="json") + data = response.json() + print(data, API.objects.all()[0].endpoint) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(data["apis"]), 1) + self.assertEqual(data["apis"][0]["endpoint"], api.endpoint) + + def test_access_api_with_valid_key(self): + """ + Test accessing an API with a valid access key. + """ + api = API.objects.create( + dataset_file=self.dataset_file, + endpoint=f"/api/{self.admin_user.id}/test_endpoint", + selected_columns=["Period", "Data_value"], + access_key="test_key", + ) + response = self.client_admin.get( + reverse("api-with-data", args=[str(self.admin_user.id), "test_endpoint"]), + format="json", + HTTP_AUTHORIZATION=f"{api.access_key}", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_access_api_with_invalid_key(self): + """ + Test accessing an API with an invalid access key. + """ + api = API.objects.create( + dataset_file=self.dataset_file, + endpoint=f"/api/{self.admin_user.id}/test_endpoint", + selected_columns=["Period", "Data_value"], + access_key="test_key", + ) + + response = self.client.get( + reverse("api-with-data", args=[str(self.admin_user.id), "test_endpoint"]), + format="json", + HTTP_AUTHORIZATION="invalid_key", + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + @classmethod + def tearDownClass(cls): + """ + Clean up resources and database records after running the tests. + """ + for dataset_file in DatasetV2File.objects.all(): + file_path = os.path.join(settings.MEDIA_ROOT, str(dataset_file.file)) + standardised_file_path = os.path.join(settings.MEDIA_ROOT, str(dataset_file.standardised_file)) + if os.path.exists(file_path): + os.remove(file_path) + if os.path.exists(standardised_file_path): + os.remove(standardised_file_path) + super(APITestViews, cls).tearDownClass()