Services

drf-commons includes two production-ready service classes for file-based data operations: file export and file import.

File Export Service

ExportService

from drf_commons.services.export_file import ExportService

Generates file exports in CSV, XLSX, and PDF formats from structured data.

Supported formats:

Format

Requirement

Notes

csv

Core (no extras)

UTF-8 encoded, suitable for data exchange

xlsx

drf-commons[export]

Full Excel workbook with configurable column widths and headers

pdf

drf-commons[export]

Formatted table via weasyprint; suitable for human-readable reports

Direct usage:

from drf_commons.services.export_file import ExportService

service = ExportService()
data = ArticleSerializer(queryset, many=True).data

# CSV
response = service.export_csv(data, field_config={"title": "Title", "published": "Published"})

# XLSX
response = service.export_xlsx(
    data,
    field_config={"title": "Title", "created_at": "Date"},
    column_config={"title": {"width": 40}},
)

# PDF
response = service.export_pdf(data, field_config={"title": "Title"})

Via FileExportMixin (recommended):

class ArticleViewSet(BaseViewSet):
    export_field_config = {
        "title": "Article Title",
        "published": "Status",
        "created_at": "Date Created",
        "created_by__username": "Author",
    }

# POST /articles/export/
{
  "file_type": "xlsx",
  "includes": ["title", "published", "created_at"],
  "column_config": {
    "title": {"width": 50, "header": "Article Title"}
  }
}

The exported file is returned as an HttpResponse with appropriate Content-Disposition and MIME type headers.

File Import Service

FileImportService

from drf_commons.services.import_from_file import FileImportService

Processes file imports from CSV, XLS, and XLSX formats. Supports multi-model imports with dependency ordering, foreign key resolution, data transformation hooks, and progress callbacks.

Import Configuration Reference

The import_file_config dict (set on the ViewSet or passed directly to the service) defines the complete import specification:

import_file_config = {
    # Required: file format
    "file_format": "xlsx",              # "csv", "xls", "xlsx"

    # Required: processing order for models (dependency-first)
    "order": ["country", "city", "person"],

    # Required: model definitions
    "models": {
        "country": {
            "model": Country,           # Django model class
            "fields": ["name", "code"], # Columns to import
            "unique_fields": ["code"],  # Fields for upsert lookup
        },
        "city": {
            "model": City,
            "fields": ["name", "country_code"],
            "unique_fields": ["name", "country_code"],
            "foreign_keys": {
                # Map a column to a related model
                "country": {
                    "model": Country,
                    "lookup_field": "code",
                    "source_field": "country_code",
                }
            },
        },
        "person": {
            "model": Person,
            "fields": ["first_name", "last_name", "email", "city_name"],
            "unique_fields": ["email"],
            "foreign_keys": {
                "city": {
                    "model": City,
                    "lookup_field": "name",
                    "source_field": "city_name",
                }
            },
        },
    },

    # Optional: process large files in chunks
    "chunk_size": 500,
}

Transformation Hooks

The import_transforms dict on the ViewSet applies per-field transformations before data is written to the database:

class PersonViewSet(ImportableViewSet):
    import_file_config = { ... }

    import_transforms = {
        "email": lambda v: v.strip().lower(),
        "first_name": lambda v: v.strip().title(),
        "last_name": lambda v: v.strip().title(),
        "phone": lambda v: re.sub(r"\D", "", v),  # digits only
    }

Progress Callbacks

For long-running imports, pass a progress callback to the service:

def on_progress(processed: int, total: int, errors: list):
    logger.info(f"Import progress: {processed}/{total} rows ({len(errors)} errors)")

service = FileImportService(
    config=import_file_config,
    transforms=import_transforms,
)
result = service.import_file(
    file_obj=uploaded_file,
    progress_callback=on_progress,
)

Import Result

The service returns a structured result dict:

{
    "success": True,
    "processed": 1200,
    "failed": 3,
    "errors": [
        {"row": 45, "field": "email", "message": "Email already exists"},
        {"row": 89, "field": "country_code", "message": "Country 'XX' not found"},
    ],
    "created": 800,
    "updated": 397,
}

Import Template Generation

The generate_import_template management command generates a sample import file based on the ViewSet’s import_file_config:

python manage.py generate_import_template PersonViewSet

This generates a file (XLSX by default) with:

  • One sheet per model in the import config

  • Column headers matching the fields list

  • Example data in the first data row

Chunk-Based Processing

For very large files, set chunk_size in the import config. The service reads the file in chunks and processes each chunk in its own transaction:

import_file_config = {
    "file_format": "xlsx",
    "chunk_size": 250,          # Process 250 rows per transaction
    "order": ["person"],
    "models": { ... },
}

This bounds peak memory usage and ensures that a failure in one chunk does not roll back the entire import.

DataProcessor

The import pipeline internal components:

Component

Responsibility

FileReader

Reads CSV/XLS/XLSX into a pandas DataFrame

ConfigValidator

Validates the import_file_config at service construction

DataProcessor

Applies transforms, resolves foreign keys, validates rows

BulkOperations

Executes bulk_create() / bulk_update() for each model chunk

These components are internal to the service and are not part of the public API. Customize import behavior through the configuration interface, transforms, and progress callbacks.