Django Befehle

Ernesto Rico Schmidt am 4. Februar 2018

Eine sehr mächtige Möglichkeit, die Django bietet, sind die sogenannten management commands.

Die management commands sind einfache Python-Skripte, die im Unterverzeichnis management/commands einer Django-Applikation (in diesem Fall convocatorias) leben.

convocatorias/management/commands/
├── download_documentos.py
├── import_convocatorias.py
└── __init__.py

Der Spider convocatorias gibt die Ergebnisse als JSON-Datei aus.

[
    {
        "departamento": "La Paz",
        "entidad": "Ucep Mi Riego",
        "cuce": "18-0086-10-817156-1-1",
        "objeto": "Adquisici\u00f3n de equipos de computaci\u00f3n y accesorios para los balances hidricos",
        "tipo": "Bienes",
        "modalidad": "OF",
        "publicada": "2018-01-18",
        "presentada": "2018-01-29",
        "monto_bob": 95600.0,
        "monto_usd": 13835.71,
        "monto_eur": 12646.55,
        "contacto": "...",
        "arch": "MTEzMjE3Mw=="
    },
    ...
]

Diese können direkt in Django importiert werden, denn wir haben im Spider bereits, wenn notwendig, unformatiert.

Import von Daten aus JSON-Dateien

from django.core.management.base import BaseCommand
from django.db import IntegrityError

from convocatorias.models import Convocatoria

import json
from datetime import datetime

Wir definieren die Klasse Command, abgeleitet von der Klasse BaseCommand, mit der Hilfe help, und mit der Methode add_argument(self, parse) definieren wir, dass das Befehl ein Argument convocatorias.json hat, den wir später beim Ausführen des Befehls als options['convocatorias.json'] ansprechen können.

class Command(BaseCommand):
    help = 'Importa las convocatorias encontradas por la araña en infosicoes'

    def add_arguments(self, parser):
        parser.add_argument('convocatorias.json', type=str)

In der Methode handle(self, *args, **options) definieren wir, was das Befehl macht und was wir mit dem Argument des Befehls machen.

Zunächst laden wir die JSON-Datei als JSON-Liste, so dass wir einfach über die Elemente der Liste iterieren können.

    def handle(self, *args, **options):
        with open(options['convocatorias.json']) as f:
            convocatorias = json.load(f)

Wir erzeugen eine Instanz vom Convocatoria-Objekt und versuchen diese zu speichern.

Falls diese bereits in der Datenbank vorhanden ist, wird eine Ausnahme ausgelöst und wir erhöhen den Zähler der bereits vorhanden Ausschreibungen, bzw. wenn die Instanz gespeichert werden kann, erhöhen wir den Zähler der neue gespeicherten Ausschreibungen.

        counts = [0, 0]
        for convocatoria in convocatorias:
            try:
                instance = Convocatoria(departamento=convocatoria['departamento'],
                                        entidad=convocatoria['entidad'],
                                        slug=convocatoria['cuce'],
                                        objeto=convocatoria['objeto'],
                                        tipo=convocatoria['tipo'],
                                        modalidad=convocatoria['modalidad'],
                                        añadida=datetime.now(),
                                        publicada=convocatoria['publicada'],
                                        presentada=convocatoria['presentada'],
                                        monto_bob=convocatoria['monto_bob'],
                                        monto_usd=convocatoria['monto_usd'],
                                        monto_eur=convocatoria['monto_eur'],
                                        contacto=convocatoria['contacto'],
                                        arch=convocatoria['arch'])
                instance.save()
            except IntegrityError:
                counts[0] += 1
            else:
                counts[1] += 1

Zum Schluss geben wir beide Zähler einfach aus.

        self.stdout.write(self.style.NOTICE('%s convocatorias ya estaban en el sistema ' % counts[0]))
        self.stdout.write(self.style.SUCCESS('%s convocatorias introducidas al sistema ' % counts[1]))

In der Applikation kann man markieren, dass eine Ausschreibung entweder keine weitere Überprüfung benötigt, dass die Ausschreibung ein Dokument für die Überprüfung benötigt, dass das Dokument für die Ausschreibung Überprüfung benötigt, oder sich in Überprüfung befindet, und schließlich dass die Ausschreibung frei von proprietäre Software oder damit kontaminiert ist.

Bei Ausschreibungen, die markiert sind, dass sie ein Dokument benötigen, werden diese herunter geladen. Dazu verwenden wir requests.

Download von Dokumenten

from datetime import datetime
import os

import requests
from convocatorias.models import Convocatoria
from django.core.files import File
from django.core.management.base import BaseCommand

URL = 'https://www.infosicoes.com/contenido/paginas/procesos/archivo.php'

Wir definieren wieder die Klasse Command, abgeleitet von der Klasse BaseCommand, mit der Hilfe help, und da wir keine Argumente benötigen, brauchen wir keine Methode handle(self, *args, **options) zu defiieren.

class Command(BaseCommand):
    help = 'Descarga los documentos base de contratación requeridos para ser evaluados'

Wir iterieren einfach über die Convocatoria-Objekte, die den weitere Dokumente benötigen (estatus=3) und setzten ein POST-Request mit einem einzigen Argument: arch den wir vorher gespeichert haben.

    def handle(self, *args, **options):
        convocatorias = Convocatoria.objects.filter(estatus=3)  # Convocatoria requiere documento para su revisión

        count = 0
        for convocatoria in convocatorias:
            response = requests.post(URL, data={'arch': convocatoria.arch})

Wir können als Antwort erhalten, dass das Limit an Downloads bereits überschritten ist, dann erhalten wir kein Dokument als Antwort.

            if response.text == 'Limite de descargas excedido!':
                self.stderr.write(self.style.ERROR(response.text))
                break

Wir können aber auch das Dokument als Antwort erhalten und wir können es zunächst speichern, anschließend im zugehörigen Convocatoria-Objekt speichern und den Status der Ausschreibung entsprechend ändern, und so markieren dass das Dokument Überprüfung benötigt.

            if response.ok:
                filename = response.headers['Content-Disposition'][21:-1]
                with open(filename, 'wb') as f:
                    f.write(response.content)

                with open(filename, 'rb') as f:
                    convocatoria.documento.save(filename, File(f))

                os.remove(filename)
                count += 1

                convocatoria.añadido = datetime.now()
                convocatoria.estatus = 4  # Documento requiere revisión
                convocatoria.save()

Zum Schluss geben wir einfach die Anzahl der herunter geladenen Dokumente aus.

        self.stdout.write(self.style.SUCCESS('%i documentos descargados' % count))

Das Django-Projekt sowie das gesamte Projekt ist auf GitHub zu finden