Browse Source
feat(crawler): implement serializers, views, and routing for CrawlTask and CrawlRun
master
feat(crawler): implement serializers, views, and routing for CrawlTask and CrawlRun
master
4 changed files with 155 additions and 14 deletions
-
53backend/crawler/migrations/0001_initial.py
-
44backend/crawler/serializers.py
-
11backend/crawler/urls.py
-
53backend/crawler/views.py
@ -0,0 +1,53 @@ |
|||||
|
# Generated by Django 6.0.8 on 2026-08-08 07:53 |
||||
|
|
||||
|
import crawler.models |
||||
|
import django.db.models.deletion |
||||
|
import uuid |
||||
|
from django.db import migrations, models |
||||
|
|
||||
|
|
||||
|
class Migration(migrations.Migration): |
||||
|
|
||||
|
initial = True |
||||
|
|
||||
|
dependencies = [ |
||||
|
] |
||||
|
|
||||
|
operations = [ |
||||
|
migrations.CreateModel( |
||||
|
name='CrawlTask', |
||||
|
fields=[ |
||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), |
||||
|
('created_at', models.DateTimeField(auto_now_add=True)), |
||||
|
('updated_at', models.DateTimeField(auto_now=True)), |
||||
|
('title', models.CharField(max_length=255)), |
||||
|
('divar_url', models.URLField(validators=[crawler.models.validate_divar_url])), |
||||
|
('detection_prompt', models.TextField()), |
||||
|
('interval_minutes', models.IntegerField(choices=[(5, '5 Minutes'), (15, '15 Minutes'), (30, '30 Minutes'), (60, '1 Hour'), (120, '2 Hours'), (180, '3 Hours'), (240, '4 Hours'), (300, '5 Hours'), (360, '6 Hours')], default=60)), |
||||
|
('start_hour', models.TimeField()), |
||||
|
('end_hour', models.TimeField()), |
||||
|
('telegram_channel_id', models.CharField(blank=True, max_length=100, null=True)), |
||||
|
('is_active', models.BooleanField(default=True)), |
||||
|
], |
||||
|
options={ |
||||
|
'indexes': [models.Index(fields=['is_active'], name='crawler_cra_is_acti_b67d0a_idx'), models.Index(fields=['created_at'], name='crawler_cra_created_920fb7_idx'), models.Index(fields=['title'], name='crawler_cra_title_dfbb5f_idx')], |
||||
|
}, |
||||
|
), |
||||
|
migrations.CreateModel( |
||||
|
name='CrawlRun', |
||||
|
fields=[ |
||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), |
||||
|
('started_at', models.DateTimeField(auto_now_add=True)), |
||||
|
('finished_at', models.DateTimeField(blank=True, null=True)), |
||||
|
('status', models.CharField(choices=[('RUNNING', 'Running'), ('SUCCESS', 'Success'), ('FAILED', 'Failed')], default='RUNNING', max_length=20)), |
||||
|
('ads_fetched_count', models.IntegerField(default=0)), |
||||
|
('ads_evaluated_count', models.IntegerField(default=0)), |
||||
|
('ads_flagged_count', models.IntegerField(default=0)), |
||||
|
('error_log', models.TextField(blank=True, null=True)), |
||||
|
('crawl_task', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='runs', to='crawler.crawltask')), |
||||
|
], |
||||
|
options={ |
||||
|
'abstract': False, |
||||
|
}, |
||||
|
), |
||||
|
] |
||||
@ -0,0 +1,44 @@ |
|||||
|
from rest_framework import serializers |
||||
|
from .models import CrawlTask, CrawlRun |
||||
|
|
||||
|
class CrawlTaskSerializer(serializers.ModelSerializer): |
||||
|
class Meta: |
||||
|
model = CrawlTask |
||||
|
fields = [ |
||||
|
'id', |
||||
|
'title', |
||||
|
'divar_url', |
||||
|
'detection_prompt', |
||||
|
'interval_minutes', |
||||
|
'start_hour', |
||||
|
'end_hour', |
||||
|
'telegram_channel_id', |
||||
|
'is_active', |
||||
|
'created_at', |
||||
|
'updated_at', |
||||
|
] |
||||
|
read_only_fields = ['id', 'created_at', 'updated_at'] |
||||
|
|
||||
|
def validate(self, attrs): |
||||
|
start_hour = attrs.get('start_hour') |
||||
|
end_hour = attrs.get('end_hour') |
||||
|
|
||||
|
# If hours are provided, ensure they make sense or do any other general task validations |
||||
|
return attrs |
||||
|
|
||||
|
|
||||
|
class CrawlRunSerializer(serializers.ModelSerializer): |
||||
|
class Meta: |
||||
|
model = CrawlRun |
||||
|
fields = [ |
||||
|
'id', |
||||
|
'crawl_task', |
||||
|
'started_at', |
||||
|
'finished_at', |
||||
|
'status', |
||||
|
'ads_fetched_count', |
||||
|
'ads_evaluated_count', |
||||
|
'ads_flagged_count', |
||||
|
'error_log', |
||||
|
] |
||||
|
read_only_fields = ['id', 'started_at', 'finished_at', 'status', 'ads_fetched_count', 'ads_evaluated_count', 'ads_flagged_count', 'error_log'] |
||||
@ -1,6 +1,11 @@ |
|||||
from django.urls import path |
|
||||
from .views import CrawlerView |
|
||||
|
from django.urls import path, include |
||||
|
from rest_framework.routers import DefaultRouter |
||||
|
from .views import CrawlTaskViewSet |
||||
|
|
||||
|
router = DefaultRouter() |
||||
|
router.register(r'', CrawlTaskViewSet, basename='crawlers') |
||||
|
|
||||
urlpatterns = [ |
urlpatterns = [ |
||||
path('', CrawlerView.as_view()), |
|
||||
|
path('', include(router.urls)), |
||||
] |
] |
||||
|
|
||||
@ -1,11 +1,50 @@ |
|||||
from django.shortcuts import render |
|
||||
from rest_framework.views import APIView |
|
||||
|
from rest_framework import viewsets, status |
||||
|
from rest_framework.decorators import action |
||||
from rest_framework.response import Response |
from rest_framework.response import Response |
||||
|
from .models import CrawlTask, CrawlRun |
||||
|
from .serializers import CrawlTaskSerializer, CrawlRunSerializer |
||||
|
|
||||
|
class CrawlTaskViewSet(viewsets.ModelViewSet): |
||||
|
""" |
||||
|
ViewSet for CrawlTask models. Supports CRUD and custom trigger/runs actions. |
||||
|
""" |
||||
|
queryset = CrawlTask.objects.all().order_by('-created_at') |
||||
|
serializer_class = CrawlTaskSerializer |
||||
|
|
||||
# Create your views here. |
|
||||
|
@action(detail=True, methods=['post'], url_path='trigger') |
||||
|
def trigger(self, request, pk=None): |
||||
|
task = self.get_object() |
||||
|
|
||||
#create the django rest framework view for crawling the data |
|
||||
class CrawlerView(APIView): |
|
||||
def get(self, request): |
|
||||
return Response({'message': 'Hello World'}) |
|
||||
|
# Create a new CrawlRun with status RUNNING |
||||
|
run = CrawlRun.objects.create( |
||||
|
crawl_task=task, |
||||
|
status='RUNNING' |
||||
|
) |
||||
|
|
||||
|
# Check if tasks can be imported. |
||||
|
# This will integrate with crawlers/tasks.py once it is written in a later step. |
||||
|
try: |
||||
|
from crawler.tasks import run_crawl_pipeline |
||||
|
# run asynchronously |
||||
|
run_crawl_pipeline.delay(str(run.id)) |
||||
|
is_queued = True |
||||
|
except ImportError: |
||||
|
# Fallback if tasks.py doesn't exist yet or run_crawl_pipeline is not defined |
||||
|
is_queued = False |
||||
|
|
||||
|
return Response({ |
||||
|
"status": "queued" if is_queued else "queued_stub", |
||||
|
"run_id": str(run.id) |
||||
|
}, status=status.HTTP_202_ACCEPTED) |
||||
|
|
||||
|
@action(detail=True, methods=['get'], url_path='runs') |
||||
|
def runs(self, request, pk=None): |
||||
|
task = self.get_object() |
||||
|
runs = task.runs.all().order_by('-started_at') |
||||
|
page = self.paginate_queryset(runs) |
||||
|
if page is not None: |
||||
|
serializer = CrawlRunSerializer(page, many=True) |
||||
|
return self.get_paginated_response(serializer.data) |
||||
|
|
||||
|
serializer = CrawlRunSerializer(runs, many=True) |
||||
|
return Response(serializer.data) |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue