diff --git a/src/apps/core/views.py b/src/apps/core/views.py index 056376c..16bc377 100644 --- a/src/apps/core/views.py +++ b/src/apps/core/views.py @@ -13,6 +13,7 @@ from typing import Any from django.conf import settings from django.db import connection +from drf_yasg.utils import swagger_auto_schema from rest_framework import status from rest_framework.permissions import AllowAny from rest_framework.request import Request @@ -21,29 +22,29 @@ from rest_framework.views import APIView logger = logging.getLogger(__name__) +# Swagger теги +HEALTH_TAG = "Мониторинг" +JOBS_TAG = "Фоновые задачи" + class HealthCheckView(APIView): """ - Comprehensive health check endpoint. + Комплексная проверка состояния системы. - GET /api/health/ - Returns detailed status of all dependencies. - - Response: - { - "status": "healthy" | "degraded" | "unhealthy", - "version": "1.0.0", - "checks": { - "database": {"status": "up", "latency_ms": 5}, - "redis": {"status": "up", "latency_ms": 2}, - "celery": {"status": "up"} - } - } + Возвращает статус всех зависимостей (БД, Redis, Celery). """ permission_classes = [AllowAny] authentication_classes = [] # No auth required + @swagger_auto_schema( + tags=[HEALTH_TAG], + operation_summary="Проверка состояния", + operation_description=( + "Комплексная проверка всех зависимостей системы.\n" + "Возвращает статус: healthy, degraded или unhealthy." + ), + ) def get(self, request: Request) -> Response: """Run all health checks and return status.""" checks = {} @@ -131,15 +132,19 @@ class HealthCheckView(APIView): class LivenessView(APIView): """ - Kubernetes liveness probe endpoint. + Kubernetes liveness probe. - GET /api/health/live/ - Returns 200 if the application is running. + Проверяет, запущено ли приложение. """ permission_classes = [AllowAny] authentication_classes = [] + @swagger_auto_schema( + tags=[HEALTH_TAG], + operation_summary="Liveness probe", + operation_description="Возвращает 200 если приложение запущено.", + ) def get(self, request: Request) -> Response: """Simple liveness check.""" return Response({"status": "alive"}, status=status.HTTP_200_OK) @@ -147,15 +152,21 @@ class LivenessView(APIView): class ReadinessView(APIView): """ - Kubernetes readiness probe endpoint. + Kubernetes readiness probe. - GET /api/health/ready/ - Returns 200 if the application is ready to serve traffic. + Проверяет, готово ли приложение обрабатывать запросы. """ permission_classes = [AllowAny] authentication_classes = [] + @swagger_auto_schema( + tags=[HEALTH_TAG], + operation_summary="Readiness probe", + operation_description=( + "Возвращает 200 если приложение готово обрабатывать запросы." + ), + ) def get(self, request: Request) -> Response: """Check if app is ready to serve traffic.""" # Check database connection @@ -177,26 +188,21 @@ class BackgroundJobStatusView(APIView): """ Получение статуса фоновой задачи. - GET /api/v1/jobs/{task_id}/ Возвращает статус, прогресс и результат задачи. - - Response: - { - "id": "uuid", - "task_id": "celery-task-id", - "status": "pending|started|success|failure|revoked", - "progress": 75, - "progress_message": "Обработка данных...", - "result": {...}, - "error": "", - "is_finished": false - } """ from rest_framework.permissions import IsAuthenticated permission_classes = [IsAuthenticated] + @swagger_auto_schema( + tags=[JOBS_TAG], + operation_summary="Статус задачи", + operation_description=( + "Возвращает статус конкретной фоновой задачи.\n" + "Доступно только владельцу задачи или администратору." + ), + ) def get(self, request: Request, task_id: str) -> Response: """Получить статус задачи по task_id.""" from apps.core.serializers import BackgroundJobSerializer @@ -219,18 +225,21 @@ class BackgroundJobListView(APIView): """ Список фоновых задач пользователя. - GET /api/v1/jobs/ - Возвращает список задач текущего пользователя. - - Query params: - status: Фильтр по статусу (pending, started, success, failure) - limit: Количество записей (по умолчанию 50) + Возвращает список задач текущего пользователя с фильтрацией. """ from rest_framework.permissions import IsAuthenticated permission_classes = [IsAuthenticated] + @swagger_auto_schema( + tags=[JOBS_TAG], + operation_summary="Список задач", + operation_description=( + "Возвращает список фоновых задач текущего пользователя.\n" + "Поддерживает фильтрацию по статусу (status) и лимит (limit)." + ), + ) def get(self, request: Request) -> Response: """Получить список задач пользователя.""" from apps.core.serializers import BackgroundJobListSerializer diff --git a/src/apps/parsers/serializers.py b/src/apps/parsers/serializers.py index ae3f3f6..c914d1f 100644 --- a/src/apps/parsers/serializers.py +++ b/src/apps/parsers/serializers.py @@ -1,13 +1,164 @@ """ Сериализаторы для приложения парсеров. + +Все сериализаторы read-only, так как данные загружаются только через парсеры. """ -from apps.parsers.models import FinancialReport, FinancialReportLine +from apps.parsers.models import ( + FinancialReport, + FinancialReportLine, + IndustrialCertificateRecord, + InspectionRecord, + ManufacturerRecord, + ParserLoadLog, + ProcurementRecord, + Proxy, +) from rest_framework import serializers +# ============================================================================= +# Минпромторг - Сертификаты промышленного производства +# ============================================================================= + + +class IndustrialCertificateSerializer(serializers.ModelSerializer): + """ + Сертификат промышленного производства РФ. + + Данные загружаются из Минпромторга. + """ + + class Meta: + model = IndustrialCertificateRecord + fields = [ + "id", + "load_batch", + "issue_date", + "certificate_number", + "expiry_date", + "certificate_file_url", + "organisation_name", + "inn", + "ogrn", + "created_at", + "updated_at", + ] + read_only_fields = fields + + +# ============================================================================= +# Минпромторг - Реестр производителей +# ============================================================================= + + +class ManufacturerSerializer(serializers.ModelSerializer): + """ + Производитель из реестра Минпромторга. + + Данные загружаются из Минпромторга. + """ + + class Meta: + model = ManufacturerRecord + fields = [ + "id", + "load_batch", + "full_legal_name", + "inn", + "ogrn", + "address", + "created_at", + "updated_at", + ] + read_only_fields = fields + + +# ============================================================================= +# Единый реестр проверок (proverki.gov.ru) +# ============================================================================= + + +class InspectionSerializer(serializers.ModelSerializer): + """ + Проверка из Единого реестра проверок. + + Поддерживает ФЗ-294 и ФЗ-248. + """ + + class Meta: + model = InspectionRecord + fields = [ + "id", + "load_batch", + "registration_number", + "inn", + "ogrn", + "organisation_name", + "control_authority", + "inspection_type", + "inspection_form", + "start_date", + "end_date", + "status", + "legal_basis", + "result", + "is_federal_law_248", + "data_year", + "data_month", + "created_at", + "updated_at", + ] + read_only_fields = fields + + +# ============================================================================= +# Государственные закупки (zakupki.gov.ru) +# ============================================================================= + + +class ProcurementSerializer(serializers.ModelSerializer): + """ + Государственная закупка из ЕИС zakupki.gov.ru. + + Поддерживает 44-ФЗ и 223-ФЗ. + """ + + class Meta: + model = ProcurementRecord + fields = [ + "id", + "load_batch", + "purchase_number", + "purchase_name", + "customer_inn", + "customer_kpp", + "customer_ogrn", + "customer_name", + "max_price", + "currency_code", + "placement_method", + "publish_date", + "end_date", + "status", + "law_type", + "purchase_object_info", + "href", + "region_code", + "data_year", + "data_month", + "created_at", + "updated_at", + ] + read_only_fields = fields + + +# ============================================================================= +# ФНС - Бухгалтерская отчетность +# ============================================================================= + class FinancialReportLineSerializer(serializers.ModelSerializer): - """Сериализатор строки финансового отчета.""" + """Строка финансового отчета.""" class Meta: model = FinancialReportLine @@ -23,7 +174,11 @@ class FinancialReportLineSerializer(serializers.ModelSerializer): class FinancialReportSerializer(serializers.ModelSerializer): - """Сериализатор финансового отчета.""" + """ + Финансовый отчет ФНС. + + Данные загружаются из Excel файлов. + """ lines_count = serializers.SerializerMethodField() @@ -50,7 +205,7 @@ class FinancialReportSerializer(serializers.ModelSerializer): class FinancialReportDetailSerializer(FinancialReportSerializer): - """Сериализатор финансового отчета с детализацией строк.""" + """Финансовый отчет с детализацией строк.""" lines = FinancialReportLineSerializer(many=True, read_only=True) @@ -59,7 +214,11 @@ class FinancialReportDetailSerializer(FinancialReportSerializer): class FNSFileUploadSerializer(serializers.Serializer): - """Сериализатор для загрузки файлов FNS.""" + """ + Сериализатор для загрузки файлов FNS. + + Принимает список Excel файлов в формате fin_{id}_{ogrn}.xlsx + """ files = serializers.ListField( child=serializers.FileField(), @@ -81,3 +240,55 @@ class FNSFileUploadSerializer(serializers.Serializer): ) return value + + +# ============================================================================= +# Служебные модели +# ============================================================================= + + +class ParserLoadLogSerializer(serializers.ModelSerializer): + """ + Лог загрузки парсера. + + Информация о каждой загрузке данных из внешнего источника. + """ + + source_display = serializers.CharField(source="get_source_display", read_only=True) + + class Meta: + model = ParserLoadLog + fields = [ + "id", + "batch_id", + "source", + "source_display", + "records_count", + "status", + "error_message", + "created_at", + "updated_at", + ] + read_only_fields = fields + + +class ProxySerializer(serializers.ModelSerializer): + """ + Прокси-сервер для парсеров. + + Используется для обхода блокировок при парсинге. + """ + + class Meta: + model = Proxy + fields = [ + "id", + "address", + "is_active", + "last_used_at", + "fail_count", + "description", + "created_at", + "updated_at", + ] + read_only_fields = fields diff --git a/src/apps/parsers/urls.py b/src/apps/parsers/urls.py index 04002d6..12c5438 100644 --- a/src/apps/parsers/urls.py +++ b/src/apps/parsers/urls.py @@ -1,10 +1,66 @@ -from apps.parsers.views import FinancialReportViewSet, FNSReportUploadView +""" +URL конфигурация для приложения парсеров. + +Все эндпоинты только для чтения (GET, GET list). +""" + +from apps.parsers.views import ( + FinancialReportViewSet, + FNSReportUploadView, + IndustrialCertificateViewSet, + InspectionViewSet, + ManufacturerViewSet, + ParserLoadLogViewSet, + ProcurementViewSet, + ProxyViewSet, +) from django.urls import include, path from rest_framework.routers import DefaultRouter app_name = "parsers" -# FNS router +# ============================================================================= +# Минпромторг: /api/v1/minpromtorg/ +# ============================================================================= + +minpromtorg_router = DefaultRouter() +minpromtorg_router.register( + r"certificates", IndustrialCertificateViewSet, basename="certificates" +) +minpromtorg_router.register( + r"manufacturers", ManufacturerViewSet, basename="manufacturers" +) + +minpromtorg_urlpatterns = [ + path("", include(minpromtorg_router.urls)), +] + +# ============================================================================= +# Единый реестр проверок: /api/v1/proverki/ +# ============================================================================= + +proverki_router = DefaultRouter() +proverki_router.register(r"", InspectionViewSet, basename="inspections") + +proverki_urlpatterns = [ + path("", include(proverki_router.urls)), +] + +# ============================================================================= +# Государственные закупки: /api/v1/zakupki/ +# ============================================================================= + +zakupki_router = DefaultRouter() +zakupki_router.register(r"", ProcurementViewSet, basename="procurements") + +zakupki_urlpatterns = [ + path("", include(zakupki_router.urls)), +] + +# ============================================================================= +# ФНС - Бухгалтерская отчетность: /api/v1/fns/ +# ============================================================================= + fns_router = DefaultRouter() fns_router.register(r"reports", FinancialReportViewSet, basename="fns-reports") @@ -13,4 +69,20 @@ fns_urlpatterns = [ path("", include(fns_router.urls)), ] +# ============================================================================= +# Системные (логи, прокси): /api/v1/system/ +# ============================================================================= + +system_router = DefaultRouter() +system_router.register(r"logs", ParserLoadLogViewSet, basename="parser-logs") +system_router.register(r"proxies", ProxyViewSet, basename="proxies") + +system_urlpatterns = [ + path("", include(system_router.urls)), +] + +# ============================================================================= +# Legacy urlpatterns (пусто, используется app_name) +# ============================================================================= + urlpatterns = [] diff --git a/src/apps/parsers/views.py b/src/apps/parsers/views.py index 1170911..567bc37 100644 --- a/src/apps/parsers/views.py +++ b/src/apps/parsers/views.py @@ -1,53 +1,296 @@ """ Views для приложения парсеров. + +Все ViewSets только для чтения (GET, GET list). +Добавление и удаление данных - через парсеры и админку. """ import hashlib from pathlib import Path -from apps.parsers.models import FinancialReport +from apps.parsers.models import ( + FinancialReport, + IndustrialCertificateRecord, + InspectionRecord, + ManufacturerRecord, + ParserLoadLog, + ProcurementRecord, + Proxy, +) from apps.parsers.serializers import ( FinancialReportDetailSerializer, FinancialReportSerializer, FNSFileUploadSerializer, + IndustrialCertificateSerializer, + InspectionSerializer, + ManufacturerSerializer, + ParserLoadLogSerializer, + ProcurementSerializer, + ProxySerializer, ) from apps.parsers.tasks import process_fns_file from django.conf import settings +from drf_yasg import openapi from drf_yasg.utils import swagger_auto_schema from rest_framework import status from rest_framework.parsers import MultiPartParser +from rest_framework.permissions import IsAdminUser, IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView from rest_framework.viewsets import ReadOnlyModelViewSet +# ============================================================================= +# Swagger Tags (для группировки в документации) +# ============================================================================= + +MINPROMTORG_TAG = "Минпромторг" +PROVERKI_TAG = "Единый реестр проверок" +ZAKUPKI_TAG = "Государственные закупки" FNS_TAG = "ФНС - Бухгалтерская отчетность" +SYSTEM_TAG = "Системные" + + +# ============================================================================= +# Минпромторг - Сертификаты промышленного производства +# ============================================================================= + + +class IndustrialCertificateViewSet(ReadOnlyModelViewSet): + """ + API для просмотра сертификатов промышленного производства. + + Данные загружаются из Минпромторга. + Только чтение - добавление через парсер/админку. + """ + + queryset = IndustrialCertificateRecord.objects.all().order_by("-created_at") + serializer_class = IndustrialCertificateSerializer + permission_classes = [IsAuthenticated] + filterset_fields = ["inn", "ogrn", "certificate_number", "load_batch"] + search_fields = ["organisation_name", "certificate_number", "inn", "ogrn"] + + @swagger_auto_schema( + tags=[MINPROMTORG_TAG], + operation_summary="Список сертификатов", + operation_description=( + "Возвращает список сертификатов промышленного производства.\n" + "Поддерживает фильтрацию по: inn, ogrn, certificate_number, load_batch.\n" + "Поддерживает поиск по: organisation_name, certificate_number, inn, ogrn." + ), + ) + def list(self, request, *args, **kwargs): + return super().list(request, *args, **kwargs) + + @swagger_auto_schema( + tags=[MINPROMTORG_TAG], + operation_summary="Детали сертификата", + operation_description="Возвращает информацию о конкретном сертификате.", + ) + def retrieve(self, request, *args, **kwargs): + return super().retrieve(request, *args, **kwargs) + + +# ============================================================================= +# Минпромторг - Реестр производителей +# ============================================================================= + + +class ManufacturerViewSet(ReadOnlyModelViewSet): + """ + API для просмотра реестра производителей. + + Данные загружаются из Минпромторга. + Только чтение - добавление через парсер/админку. + """ + + queryset = ManufacturerRecord.objects.all().order_by("-created_at") + serializer_class = ManufacturerSerializer + permission_classes = [IsAuthenticated] + filterset_fields = ["inn", "ogrn", "load_batch"] + search_fields = ["full_legal_name", "inn", "ogrn", "address"] + + @swagger_auto_schema( + tags=[MINPROMTORG_TAG], + operation_summary="Список производителей", + operation_description=( + "Возвращает список производителей из реестра Минпромторга.\n" + "Поддерживает фильтрацию по: inn, ogrn, load_batch.\n" + "Поддерживает поиск по: full_legal_name, inn, ogrn, address." + ), + ) + def list(self, request, *args, **kwargs): + return super().list(request, *args, **kwargs) + + @swagger_auto_schema( + tags=[MINPROMTORG_TAG], + operation_summary="Детали производителя", + operation_description="Возвращает информацию о конкретном производителе.", + ) + def retrieve(self, request, *args, **kwargs): + return super().retrieve(request, *args, **kwargs) + + +# ============================================================================= +# Единый реестр проверок (proverki.gov.ru) +# ============================================================================= + + +class InspectionViewSet(ReadOnlyModelViewSet): + """ + API для просмотра проверок из Единого реестра проверок. + + Данные из ФГИС "Единый реестр проверок" (Генпрокуратура РФ). + Поддерживает ФЗ-294 (традиционные) и ФЗ-248 (новые с 2021). + Только чтение - добавление через парсер/админку. + """ + + queryset = InspectionRecord.objects.all().order_by("-created_at") + serializer_class = InspectionSerializer + permission_classes = [IsAuthenticated] + filterset_fields = [ + "inn", + "ogrn", + "registration_number", + "is_federal_law_248", + "data_year", + "data_month", + "load_batch", + ] + search_fields = [ + "organisation_name", + "registration_number", + "inn", + "ogrn", + "control_authority", + ] + + @swagger_auto_schema( + tags=[PROVERKI_TAG], + operation_summary="Список проверок", + operation_description=( + "Возвращает список проверок из Единого реестра.\n" + "Поддерживает фильтрацию по: inn, ogrn, registration_number, " + "is_federal_law_248, data_year, data_month, load_batch.\n" + "Поддерживает поиск по: organisation_name, registration_number, " + "inn, ogrn, control_authority." + ), + ) + def list(self, request, *args, **kwargs): + return super().list(request, *args, **kwargs) + + @swagger_auto_schema( + tags=[PROVERKI_TAG], + operation_summary="Детали проверки", + operation_description="Возвращает информацию о конкретной проверке.", + ) + def retrieve(self, request, *args, **kwargs): + return super().retrieve(request, *args, **kwargs) + + +# ============================================================================= +# Государственные закупки (zakupki.gov.ru) +# ============================================================================= + + +class ProcurementViewSet(ReadOnlyModelViewSet): + """ + API для просмотра государственных закупок. + + Данные из ЕИС zakupki.gov.ru. + Поддерживает 44-ФЗ и 223-ФЗ. + Только чтение - добавление через парсер/админку. + """ + + queryset = ProcurementRecord.objects.all().order_by("-created_at") + serializer_class = ProcurementSerializer + permission_classes = [IsAuthenticated] + filterset_fields = [ + "customer_inn", + "customer_ogrn", + "purchase_number", + "law_type", + "status", + "region_code", + "data_year", + "data_month", + "load_batch", + ] + search_fields = [ + "purchase_name", + "purchase_number", + "customer_name", + "customer_inn", + "customer_ogrn", + ] + + @swagger_auto_schema( + tags=[ZAKUPKI_TAG], + operation_summary="Список закупок", + operation_description=( + "Возвращает список государственных закупок.\n" + "Поддерживает фильтрацию по: customer_inn, customer_ogrn, " + "purchase_number, law_type, status, region_code, " + "data_year, data_month, load_batch.\n" + "Поддерживает поиск по: purchase_name, purchase_number, " + "customer_name, customer_inn, customer_ogrn." + ), + ) + def list(self, request, *args, **kwargs): + return super().list(request, *args, **kwargs) + + @swagger_auto_schema( + tags=[ZAKUPKI_TAG], + operation_summary="Детали закупки", + operation_description="Возвращает информацию о конкретной закупке.", + ) + def retrieve(self, request, *args, **kwargs): + return super().retrieve(request, *args, **kwargs) + + +# ============================================================================= +# ФНС - Бухгалтерская отчетность +# ============================================================================= class FinancialReportViewSet(ReadOnlyModelViewSet): """ API для просмотра финансовых отчетов ФНС. - list: - Получить список всех отчетов. - Поддерживает фильтрацию по: ogrn, external_id, status. - - retrieve: - Получить детальную информацию об отчете, включая все строки. + Данные загружаются из Excel файлов (fin_{id}_{ogrn}.xlsx). + Только чтение - добавление через загрузку файлов. """ queryset = FinancialReport.objects.all().order_by("-created_at") - filterset_fields = ["ogrn", "external_id", "status", "source"] + permission_classes = [IsAuthenticated] + filterset_fields = ["ogrn", "external_id", "status", "source", "load_batch"] + search_fields = ["ogrn", "external_id", "file_name"] def get_serializer_class(self): if self.action == "retrieve": return FinancialReportDetailSerializer return FinancialReportSerializer - @swagger_auto_schema(tags=[FNS_TAG]) + @swagger_auto_schema( + tags=[FNS_TAG], + operation_summary="Список отчетов", + operation_description=( + "Возвращает список финансовых отчетов ФНС.\n" + "Поддерживает фильтрацию по: ogrn, external_id, status, " + "source, load_batch.\n" + "Поддерживает поиск по: ogrn, external_id, file_name." + ), + ) def list(self, request, *args, **kwargs): return super().list(request, *args, **kwargs) - @swagger_auto_schema(tags=[FNS_TAG]) + @swagger_auto_schema( + tags=[FNS_TAG], + operation_summary="Детали отчета", + operation_description=( + "Возвращает детальную информацию об отчете, " + "включая все строки бухгалтерской отчетности." + ), + ) def retrieve(self, request, *args, **kwargs): return super().retrieve(request, *args, **kwargs) @@ -56,25 +299,59 @@ class FNSReportUploadView(APIView): """ API для загрузки файлов бухгалтерской отчетности ФНС. - POST: - Пакетная загрузка файлов. - Файлы сохраняются во временную директорию и ставятся в очередь - на обработку через Celery. - - Request: - multipart/form-data с полем 'files' (можно несколько файлов) - - Response: - { - "queued": 3, - "skipped": 1, - "task_ids": ["uuid1", "uuid2", "uuid3"] - } + Файлы сохраняются во временную директорию и ставятся в очередь + на обработку через Celery. """ parser_classes = [MultiPartParser] + permission_classes = [IsAuthenticated] - @swagger_auto_schema(tags=[FNS_TAG], request_body=FNSFileUploadSerializer) + @swagger_auto_schema( + tags=[FNS_TAG], + operation_summary="Загрузка файлов", + operation_description=( + "Пакетная загрузка файлов бухгалтерской отчетности.\n\n" + "**Формат файла:** fin_{id}_{ogrn}.xlsx\n\n" + "**Ответ:**\n" + "- queued: количество файлов в очереди\n" + "- skipped: количество пропущенных (дубликаты)\n" + "- task_ids: ID задач для отслеживания статуса" + ), + manual_parameters=[ + openapi.Parameter( + name="files", + in_=openapi.IN_FORM, + type=openapi.TYPE_FILE, + required=True, + description="Файл(ы) для загрузки (fin_*.xlsx)", + ), + ], + consumes=["multipart/form-data"], + responses={ + 202: openapi.Response( + description="Файлы приняты в обработку", + schema=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + "queued": openapi.Schema( + type=openapi.TYPE_INTEGER, + description="Количество файлов в очереди", + ), + "skipped": openapi.Schema( + type=openapi.TYPE_INTEGER, + description="Количество пропущенных", + ), + "task_ids": openapi.Schema( + type=openapi.TYPE_ARRAY, + items=openapi.Schema(type=openapi.TYPE_STRING), + description="ID задач Celery", + ), + }, + ), + ), + 400: "Ошибка валидации файлов", + }, + ) def post(self, request): serializer = FNSFileUploadSerializer(data=request.data) serializer.is_valid(raise_exception=True) @@ -120,3 +397,76 @@ class FNSReportUploadView(APIView): }, status=status.HTTP_202_ACCEPTED, ) + + +# ============================================================================= +# Системные (логи загрузок, прокси) +# ============================================================================= + + +class ParserLoadLogViewSet(ReadOnlyModelViewSet): + """ + API для просмотра логов загрузок парсеров. + + Информация о каждой загрузке данных из внешних источников. + Только для администраторов. + """ + + queryset = ParserLoadLog.objects.all().order_by("-created_at") + serializer_class = ParserLoadLogSerializer + permission_classes = [IsAdminUser] + filterset_fields = ["source", "status", "batch_id"] + + @swagger_auto_schema( + tags=[SYSTEM_TAG], + operation_summary="Список логов загрузок", + operation_description=( + "Возвращает историю загрузок данных парсерами.\n" + "Доступно только администраторам.\n" + "Поддерживает фильтрацию по: source, status, batch_id." + ), + ) + def list(self, request, *args, **kwargs): + return super().list(request, *args, **kwargs) + + @swagger_auto_schema( + tags=[SYSTEM_TAG], + operation_summary="Детали загрузки", + operation_description="Возвращает информацию о конкретной загрузке.", + ) + def retrieve(self, request, *args, **kwargs): + return super().retrieve(request, *args, **kwargs) + + +class ProxyViewSet(ReadOnlyModelViewSet): + """ + API для просмотра списка прокси-серверов. + + Используется для отладки и мониторинга парсеров. + Только для администраторов. + """ + + queryset = Proxy.objects.all().order_by("-last_used_at") + serializer_class = ProxySerializer + permission_classes = [IsAdminUser] + filterset_fields = ["is_active"] + + @swagger_auto_schema( + tags=[SYSTEM_TAG], + operation_summary="Список прокси", + operation_description=( + "Возвращает список прокси-серверов для парсеров.\n" + "Доступно только администраторам.\n" + "Поддерживает фильтрацию по: is_active." + ), + ) + def list(self, request, *args, **kwargs): + return super().list(request, *args, **kwargs) + + @swagger_auto_schema( + tags=[SYSTEM_TAG], + operation_summary="Детали прокси", + operation_description="Возвращает информацию о конкретном прокси.", + ) + def retrieve(self, request, *args, **kwargs): + return super().retrieve(request, *args, **kwargs) diff --git a/src/apps/user/views.py b/src/apps/user/views.py index 0259014..989dcf7 100644 --- a/src/apps/user/views.py +++ b/src/apps/user/views.py @@ -20,14 +20,26 @@ from .serializers import ( ) from .services import ProfileService, UserService +# Swagger теги для группировки +AUTH_TAG = "Аутентификация" +USER_TAG = "Пользователь" + class RegisterView(APIView): - """Регистрация нового пользователя""" + """ + Регистрация нового пользователя. + + Создаёт учётную запись и возвращает JWT токены. + """ permission_classes = [AllowAny] @swagger_auto_schema( - request_body=UserRegistrationSerializer, responses={201: UserSerializer} + tags=[AUTH_TAG], + operation_summary="Регистрация", + operation_description="Создание новой учётной записи пользователя.", + request_body=UserRegistrationSerializer, + responses={201: UserSerializer}, ) def post(self, request): serializer = UserRegistrationSerializer(data=request.data) @@ -49,11 +61,21 @@ class RegisterView(APIView): class LoginView(APIView): - """Вход пользователя""" + """ + Вход пользователя. + + Возвращает access и refresh токены для авторизации. + """ permission_classes = [AllowAny] - @swagger_auto_schema(request_body=LoginSerializer, responses={200: TokenSerializer}) + @swagger_auto_schema( + tags=[AUTH_TAG], + operation_summary="Вход", + operation_description="Аутентификация по email и паролю. Возвращает JWT токены.", + request_body=LoginSerializer, + responses={200: TokenSerializer}, + ) def post(self, request): serializer = LoginSerializer(data=request.data) if serializer.is_valid(): @@ -74,20 +96,18 @@ class LoginView(APIView): class LogoutView(APIView): - """Выход пользователя""" + """ + Выход пользователя. + + Добавляет refresh токен в черный список (при наличии). + """ permission_classes = [IsAuthenticated] @swagger_auto_schema( - manual_parameters=[ - openapi.Parameter( - "Authorization", - openapi.IN_HEADER, - description="Bearer ", - type=openapi.TYPE_STRING, - required=True, - ) - ], + tags=[AUTH_TAG], + operation_summary="Выход", + operation_description="Инвалидация refresh токена.", responses={200: "Успешный выход"}, ) def post(self, request): @@ -104,20 +124,14 @@ class LogoutView(APIView): class CurrentUserView(APIView): - """Получение данных текущего пользователя""" + """Получение данных текущего пользователя.""" permission_classes = [IsAuthenticated] @swagger_auto_schema( - manual_parameters=[ - openapi.Parameter( - "Authorization", - openapi.IN_HEADER, - description="Bearer ", - type=openapi.TYPE_STRING, - required=True, - ) - ], + tags=[USER_TAG], + operation_summary="Текущий пользователь", + operation_description="Возвращает данные авторизованного пользователя.", responses={200: UserSerializer}, ) def get(self, request): @@ -126,21 +140,15 @@ class CurrentUserView(APIView): class UserUpdateView(APIView): - """Обновление данных пользователя""" + """Обновление данных пользователя.""" permission_classes = [IsAuthenticated] @swagger_auto_schema( + tags=[USER_TAG], + operation_summary="Обновить данные", + operation_description="Частичное обновление данных пользователя.", request_body=UserUpdateSerializer, - manual_parameters=[ - openapi.Parameter( - "Authorization", - openapi.IN_HEADER, - description="Bearer ", - type=openapi.TYPE_STRING, - required=True, - ) - ], responses={200: UserSerializer}, ) def patch(self, request): @@ -153,7 +161,7 @@ class UserUpdateView(APIView): class ProfileDetailView(generics.RetrieveUpdateAPIView): - """Получение и обновление профиля пользователя""" + """Получение и обновление профиля пользователя.""" permission_classes = [IsAuthenticated] serializer_class = ProfileUpdateSerializer @@ -168,15 +176,9 @@ class ProfileDetailView(generics.RetrieveUpdateAPIView): return profile @swagger_auto_schema( - manual_parameters=[ - openapi.Parameter( - "Authorization", - openapi.IN_HEADER, - description="Bearer ", - type=openapi.TYPE_STRING, - required=True, - ) - ] + tags=[USER_TAG], + operation_summary="Получить профиль", + operation_description="Возвращает профиль текущего пользователя.", ) def get(self, request, *args, **kwargs): profile = self.get_object() @@ -184,16 +186,10 @@ class ProfileDetailView(generics.RetrieveUpdateAPIView): return Response(serializer.data) @swagger_auto_schema( + tags=[USER_TAG], + operation_summary="Обновить профиль", + operation_description="Частичное обновление профиля пользователя.", request_body=ProfileUpdateSerializer, - manual_parameters=[ - openapi.Parameter( - "Authorization", - openapi.IN_HEADER, - description="Bearer ", - type=openapi.TYPE_STRING, - required=True, - ) - ], ) def patch(self, request, *args, **kwargs): profile = self.get_object() @@ -207,21 +203,15 @@ class ProfileDetailView(generics.RetrieveUpdateAPIView): class PasswordChangeView(APIView): - """Смена пароля""" + """Смена пароля пользователя.""" permission_classes = [IsAuthenticated] @swagger_auto_schema( + tags=[USER_TAG], + operation_summary="Сменить пароль", + operation_description="Смена пароля. Требуется текущий пароль для подтверждения.", request_body=PasswordChangeSerializer, - manual_parameters=[ - openapi.Parameter( - "Authorization", - openapi.IN_HEADER, - description="Bearer ", - type=openapi.TYPE_STRING, - required=True, - ) - ], responses={200: "Пароль успешно изменен"}, ) def post(self, request): @@ -246,20 +236,29 @@ class PasswordChangeView(APIView): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) +@swagger_auto_schema( + method="get", + tags=[USER_TAG], + operation_summary="Полный профиль", + operation_description="Расширенная информация о пользователе и профиле.", +) @api_view(["GET"]) @permission_classes([IsAuthenticated]) def user_profile_detail(request): - """Получение полных данных профиля пользователя""" + """Получение полных данных профиля пользователя.""" profile_data = ProfileService.get_full_profile_data(request.user.id) return Response(profile_data) class TokenRefreshView(APIView): - """Обновление access токена через refresh токен""" + """Обновление access токена через refresh токен.""" permission_classes = [AllowAny] @swagger_auto_schema( + tags=[AUTH_TAG], + operation_summary="Обновить токен", + operation_description="Получение нового access токена по refresh токену.", request_body=openapi.Schema( type=openapi.TYPE_OBJECT, properties={ diff --git a/src/config/api_v1_urls.py b/src/config/api_v1_urls.py index 2c325c3..119b82a 100644 --- a/src/config/api_v1_urls.py +++ b/src/config/api_v1_urls.py @@ -1,22 +1,49 @@ """ API v1 URL configuration. -All API endpoints are versioned under /api/v1/ +Все API эндпоинты версионированы под /api/v1/ + +Структура: +- /api/v1/users/ - Аутентификация и пользователи +- /api/v1/jobs/ - Фоновые задачи +- /api/v1/minpromtorg/ - Минпромторг (сертификаты, производители) +- /api/v1/proverki/ - Единый реестр проверок +- /api/v1/zakupki/ - Государственные закупки +- /api/v1/fns/ - ФНС (бухгалтерская отчетность) +- /api/v1/system/ - Системные (логи, прокси) - только для админов """ from apps.core.views import BackgroundJobListView, BackgroundJobStatusView -from apps.parsers.urls import fns_urlpatterns +from apps.parsers.urls import ( + fns_urlpatterns, + minpromtorg_urlpatterns, + proverki_urlpatterns, + system_urlpatterns, + zakupki_urlpatterns, +) from django.urls import include, path app_name = "api_v1" +# Фоновые задачи jobs_urlpatterns = [ path("", BackgroundJobListView.as_view(), name="job-list"), path("/", BackgroundJobStatusView.as_view(), name="job-status"), ] urlpatterns = [ + # Аутентификация и пользователи path("users/", include("apps.user.urls")), - path("fns/", include((fns_urlpatterns, "fns"))), + # Фоновые задачи path("jobs/", include((jobs_urlpatterns, "jobs"))), + # Парсеры - Минпромторг + path("minpromtorg/", include((minpromtorg_urlpatterns, "minpromtorg"))), + # Парсеры - Единый реестр проверок + path("proverki/", include((proverki_urlpatterns, "proverki"))), + # Парсеры - Государственные закупки + path("zakupki/", include((zakupki_urlpatterns, "zakupki"))), + # Парсеры - ФНС бухгалтерская отчетность + path("fns/", include((fns_urlpatterns, "fns"))), + # Системные (только админы) + path("system/", include((system_urlpatterns, "system"))), ] diff --git a/src/config/settings/base.py b/src/config/settings/base.py index a9acf15..ee5a6db 100644 --- a/src/config/settings/base.py +++ b/src/config/settings/base.py @@ -344,6 +344,24 @@ if isinstance(CORS_ALLOWED_ORIGINS, str): CORS_ALLOWED_ORIGINS = CORS_ALLOWED_ORIGINS.split(",") CORS_ALLOW_CREDENTIALS = True +# ============================================================================= +# SWAGGER SETTINGS (drf-yasg) +# ============================================================================= +SWAGGER_SETTINGS = { + "SECURITY_DEFINITIONS": { + "Bearer": { + "type": "apiKey", + "name": "Authorization", + "in": "header", + "description": "JWT авторизация. Формат: Bearer ", + } + }, + "USE_SESSION_AUTH": True, + "PERSIST_AUTH": True, + "REFETCH_SCHEMA_WITH_AUTH": True, + "REFETCH_SCHEMA_ON_LOGOUT": True, +} + # Logging configuration LOGGING = { "version": 1, diff --git a/src/config/urls.py b/src/config/urls.py index aca36e0..d6d698d 100644 --- a/src/config/urls.py +++ b/src/config/urls.py @@ -17,8 +17,21 @@ schema_view = get_schema_view( openapi.Info( title="Mostovik API", default_version="v1", - description="API documentation for Mostovik project", - terms_of_service="https://www.google.com/policies/terms/", + description=""" +## API документация для проекта Mostovik + +### Авторизация +Для доступа к защищённым эндпоинтам используйте JWT токен: +1. Получите токен через `POST /api/v1/users/login/` +2. Добавьте заголовок: `Authorization: Bearer ` + +### Обновление токена +Используйте `POST /api/v1/users/token/refresh/` с refresh токеном. + +### Парсеры +API предоставляет только чтение данных (GET, GET list). +Добавление и удаление записей происходит через парсеры и админку. + """, contact=openapi.Contact(email="contact@mostovik.local"), license=openapi.License(name="BSD License"), ),