| """ |
| API views for datastore access. |
| """ |
|
|
| import logging |
|
|
| from django.http import JsonResponse, HttpRequest |
| from django.views.decorators.csrf import csrf_exempt |
| from django.views.decorators.http import require_http_methods |
|
|
| from core.models import DataStore, DataStoreEntry |
| from core.views.api.decorators import api_token_required, add_cors_headers |
|
|
| logger = logging.getLogger(__name__) |
|
|
| |
| DEFAULT_PAGE_SIZE = 50 |
| MAX_PAGE_SIZE = 100 |
|
|
|
|
| @csrf_exempt |
| @require_http_methods(["GET", "OPTIONS"]) |
| @api_token_required |
| def list_datastores(request: HttpRequest) -> JsonResponse: |
| """ |
| List all datastores accessible to the token. |
| |
| GET /api/v1/datastores/ |
| |
| Returns: |
| { |
| "datastores": [ |
| { |
| "name": "store_name", |
| "description": "...", |
| "entry_count": 42, |
| "created_at": "2026-01-15T10:30:00Z", |
| "updated_at": "2026-02-10T14:22:00Z" |
| } |
| ], |
| "count": 1 |
| } |
| """ |
| if request.method == "OPTIONS": |
| response = JsonResponse({}) |
| return add_cors_headers(response) |
|
|
| token = request.api_token |
|
|
| if token.datastore: |
| |
| datastores = [token.datastore] |
| else: |
| |
| datastores = DataStore.objects.all() |
|
|
| data = { |
| "datastores": [ |
| { |
| "name": ds.name, |
| "description": ds.description, |
| "entry_count": ds.entry_count, |
| "created_at": ds.created_at.isoformat(), |
| "updated_at": ds.updated_at.isoformat(), |
| } |
| for ds in datastores |
| ], |
| "count": len(datastores) if token.datastore else datastores.count(), |
| } |
|
|
| response = JsonResponse(data) |
| return add_cors_headers(response) |
|
|
|
|
| @csrf_exempt |
| @require_http_methods(["GET", "OPTIONS"]) |
| @api_token_required |
| def get_datastore(request: HttpRequest, name: str) -> JsonResponse: |
| """ |
| Get datastore metadata. |
| |
| GET /api/v1/datastores/<name>/ |
| |
| Returns: |
| { |
| "name": "store_name", |
| "description": "...", |
| "entry_count": 42, |
| "created_at": "2026-01-15T10:30:00Z", |
| "updated_at": "2026-02-10T14:22:00Z" |
| } |
| """ |
| if request.method == "OPTIONS": |
| response = JsonResponse({}) |
| return add_cors_headers(response) |
|
|
| datastore = _get_authorized_datastore(request, name) |
| if isinstance(datastore, JsonResponse): |
| return datastore |
|
|
| data = { |
| "name": datastore.name, |
| "description": datastore.description, |
| "entry_count": datastore.entry_count, |
| "created_at": datastore.created_at.isoformat(), |
| "updated_at": datastore.updated_at.isoformat(), |
| } |
|
|
| response = JsonResponse(data) |
| return add_cors_headers(response) |
|
|
|
|
| @csrf_exempt |
| @require_http_methods(["GET", "OPTIONS"]) |
| @api_token_required |
| def list_entries(request: HttpRequest, name: str) -> JsonResponse: |
| """ |
| List entries in a datastore with pagination. |
| |
| GET /api/v1/datastores/<name>/entries/ |
| Query params: |
| - page: Page number (default: 1) |
| - page_size: Items per page (default: 50, max: 100) |
| |
| Returns: |
| { |
| "entries": [ |
| { |
| "key": "config", |
| "value": {"retries": 3}, |
| "created_at": "2026-01-20T08:00:00Z", |
| "updated_at": "2026-02-01T12:00:00Z" |
| } |
| ], |
| "count": 42, |
| "page": 1, |
| "page_size": 50, |
| "total_pages": 1 |
| } |
| """ |
| if request.method == "OPTIONS": |
| response = JsonResponse({}) |
| return add_cors_headers(response) |
|
|
| datastore = _get_authorized_datastore(request, name) |
| if isinstance(datastore, JsonResponse): |
| return datastore |
|
|
| |
| try: |
| page = max(1, int(request.GET.get("page", 1))) |
| except (ValueError, TypeError): |
| page = 1 |
|
|
| try: |
| page_size = min(MAX_PAGE_SIZE, max(1, int(request.GET.get("page_size", DEFAULT_PAGE_SIZE)))) |
| except (ValueError, TypeError): |
| page_size = DEFAULT_PAGE_SIZE |
|
|
| |
| total_count = datastore.entries.count() |
| total_pages = max(1, (total_count + page_size - 1) // page_size) |
|
|
| |
| page = min(page, total_pages) |
|
|
| |
| offset = (page - 1) * page_size |
| entries = datastore.entries.all()[offset:offset + page_size] |
|
|
| data = { |
| "entries": [ |
| { |
| "key": entry.key, |
| "value": entry.get_value(), |
| "created_at": entry.created_at.isoformat(), |
| "updated_at": entry.updated_at.isoformat(), |
| } |
| for entry in entries |
| ], |
| "count": total_count, |
| "page": page, |
| "page_size": page_size, |
| "total_pages": total_pages, |
| } |
|
|
| response = JsonResponse(data) |
| return add_cors_headers(response) |
|
|
|
|
| @csrf_exempt |
| @require_http_methods(["GET", "OPTIONS"]) |
| @api_token_required |
| def get_entry(request: HttpRequest, name: str, key: str) -> JsonResponse: |
| """ |
| Get a single entry by key. |
| |
| GET /api/v1/datastores/<name>/entries/<key>/ |
| |
| Returns: |
| { |
| "key": "config", |
| "value": {"retries": 3}, |
| "created_at": "2026-01-20T08:00:00Z", |
| "updated_at": "2026-02-01T12:00:00Z" |
| } |
| """ |
| if request.method == "OPTIONS": |
| response = JsonResponse({}) |
| return add_cors_headers(response) |
|
|
| datastore = _get_authorized_datastore(request, name) |
| if isinstance(datastore, JsonResponse): |
| return datastore |
|
|
| try: |
| entry = DataStoreEntry.objects.get(datastore=datastore, key=key) |
| except DataStoreEntry.DoesNotExist: |
| response = JsonResponse( |
| {"error": {"code": "NOT_FOUND", "message": f"Entry '{key}' not found"}}, |
| status=404, |
| ) |
| return add_cors_headers(response) |
|
|
| data = { |
| "key": entry.key, |
| "value": entry.get_value(), |
| "created_at": entry.created_at.isoformat(), |
| "updated_at": entry.updated_at.isoformat(), |
| } |
|
|
| response = JsonResponse(data) |
| return add_cors_headers(response) |
|
|
|
|
| def _get_authorized_datastore(request: HttpRequest, name: str): |
| """ |
| Get datastore if token has access. |
| |
| Returns the DataStore object or a JsonResponse error. |
| """ |
| token = request.api_token |
|
|
| try: |
| datastore = DataStore.objects.get(name=name) |
| except DataStore.DoesNotExist: |
| response = JsonResponse( |
| {"error": {"code": "NOT_FOUND", "message": f"Datastore '{name}' not found"}}, |
| status=404, |
| ) |
| return add_cors_headers(response) |
|
|
| |
| if token.datastore and token.datastore != datastore: |
| response = JsonResponse( |
| {"error": {"code": "FORBIDDEN", "message": "Token does not have access to this datastore"}}, |
| status=403, |
| ) |
| return add_cors_headers(response) |
|
|
| return datastore |
|
|