İçeriğe geç

Django Import-Export Model

Herkese merhabalar. 2021’nin ilk yazısı. Umarım herkes için güzel bir yıl olur. Bugün ki konu Django ekosisteminin önemli paketlerinden olan django-import-export.

Adındanda anlaşılacağı üzere bu paket yardımı ile Django modelleri üzerinde import ve export işlemlerini yönetebiliyoruz. Bir çok format desteklenmekte. Öne çıkanları sayacak olursak (csv,json,yaml,excel). Ayrıca tablib paketi üzerine inşa edildiği için tablib paketinde desteklenen diğer tüm formatlar ile import-export işlemlerini de gerçekleştirebilirsiniz. Ayrıca projenizde Django admin kullanıyor iseniz işlemleri panelden yönetebilmeniz adına bir eklenti de django-import-export paketi tarafından sağlanmaktadır.

Basit bir Django projesi oluşturduğunuzu varsayarak devam edelim.

Kurulum.

pip install django-import-export

Daha sonra ise Django uygulamalarına kayıt edelim.

INSTALLED_APPS = [
    #other django stuff
    "example", #example app
    "import_export",
]
IMPORT_EXPORT_USE_TRANSACTIONS = True

Import işlemi sırasında veritabanı transaction’larını kullanması gerektiğini belirtiyoruz. Böylece olası bir hata oluşması durumunda transaction rollback edilecek ve veri tutarsızlıklarının önüne geçmiş olacağız.

Kurulum işlemleri tamamlandı.


Model ve yönetim komutu.

#example/models.py
from django.db import models

class Driver(models.Model):
    name = models.CharField(max_length=80)
    plate_no = models.CharField(max_length=20)
    is_banned = models.BooleanField(default=False)
    email = models.EmailField(blank=False, null=False)
    started = models.DateField()

Basit bir Driver modeli kurguladık. Import ve export işlemlerimizi bu model üzerinde gerçekleştireceğiz.

Daha sonra random data üretmek için basit bir management komutu yazalım.

İlk olarak Faker paketini dahil edelim. Random data üretirken favorim 🙂

pip install faker
#example/management/commands/create_drivers.py
from django.core.management import BaseCommand
from example.models import Driver
import faker


class Command(BaseCommand):
    help = "Creates drivers."

    def add_arguments(self, parser):
        parser.add_argument("total", type=int, help="How many drivers will be created?")

    def handle(self, *args, **kwargs):
        total = kwargs.get("total")
        f = faker.Faker("tr_TR")
        drivers = [
            Driver(
                name=f.name(),
                plate_no=f.license_plate(),
                is_banned=f.boolean(),
                email=f.email(),
                started=f.date(),
            )
            for _ in range(total)
        ]
        Driver.objects.bulk_create(drivers)
        self.stdout.write(f"{total} drivers created.")

Management komutları ile Django uygulamanızı yönetebilirsiniz. Bu random data üretmekte olabilir, toplu aksiyonların kolayca alınması, veya test sırasında bazı dataların içeriye aktarılması da olabilir. Buradaki kullanım verdiğimiz limit kadar bize sürücü kaydı yapması. Daha detaylı bir bilgi için sizi şu yazıya davet edebilirim. 🙂


Resources.

django-import-export Resource adlı bir konsept ile çalışır. Resource’lar yerleşik Django ModelForm veya DRF ModelSerializer kavramına benzerlik gösterir. Resource’lar ile import ve export işlemlerini yönetiriz. Dökümantasyonda gerekli tanımlamaların, üzerinde çalışılan uygulamanın admin.py dosyasına konulması gerektiği söylenmekte. Fakat Django admin kullanılmayan bir uygulamada yeni bir resources.py dosyası oluşturup orda tanımlamak yönetilebilir bir uygulama için güzel bir adım. Biz de öyle yapacağız.

#example/resources.py

from import_export import resources
from example.models import Driver

class DriverResource(resources.ModelResource):
    class Meta:
        model = Driver

En basit hali ile bir Resource oluşturduk. Bu tanımla modelin tüm alanları işlemler sırasında kullanılacaktır. Size daha özelleştirilmiş bir kullanım lazım ise, (örneğin model fieldlarının sıralamasının değiştirilmesi, modelde doğrudan bulunmayan fakat hesaplayarak eklemek istediğiniz kısımlar, export sıralamasının değiştirilmesi vs) dökümantasyondaki şu alana göz atabilirsiniz.


Random data.

50 tane sürücü oluşturalım.

$ ./manage.py create_drivers 50
50 drivers created.

Export.

Django shelle düştükten export mekanizmasını anlayalım.

Csv export.

>>> from pprint import pprint as pp
>>> from example.resources import DriverResource
>>> dataset = DriverResource().export()
>>> pp(dataset.csv)
('id,name,plate_no,is_banned,email,started\r\n'
 '1,Bayan Hayrünnisa Demirel Zengin,75 F '
 '9273,1,utkucanhayrioglu@petkim.com,2010-07-17\r\n'
 '2,Taylak Demir,15 FH 3506,0,ashaninonu@ergul.com,2015-10-02\r\n'
### continues

Json export.

pp(dataset.json)
('[{"id": 1, "name": "Bayan Hayrünnisa Demirel Zengin", "plate_no": "75 F '
 '9273", "is_banned": "1", "email": "utkucanhayrioglu@petkim.com", "started": '
 '"2010-07-17"}, {"id": 2, "name": "Taylak Demir", "plate_no": "15 FH 3506", '
 '"is_banned": "0", "email": "ashaninonu@ergul.com", "started": "2015-10-02"}, '
#continues

Genel yapı ve kullanım hakkında bir fikir vermiştir diye düşünüyorum. Fakat yine de üzerinden geçelim. Gerekli resource nesnesini örnekledikten sonra bu nesne üzerinden export metodunu çağırıyoruz. Export metodu default olarak ilgili Resource meta sınıfında tanımlı modelin tüm kayıtlarını çağırıyor. Belirtilen alanlara göre serialize etmeye hazır hale geliyor ve verileri tutan bir dataset nesnesi oluşturuyor. (Ek bir tanımlama yapılmaz ise modelin tüm alanları). Daha sonra istenen çıktı tipine göre dataset serialize ediliyor. Kolay değil mi?

Export edilecek veriyi filtreleme.

İstenilmesi durumunda export edilecek veri filtrelenebilir.

qs = Driver.objects.filter(is_banned=False)
dataset = DriverResource().export(qs)
dataset.csv

Views’larda export.

Öncelikle export tipini seçebileceğimiz basit bir template oluşturalım.

<!-- example/templates/example/export.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Export</title>
</head>
<body>
    <form action="" method="POST">
        {% csrf_token %}
        <select name="file_type" id="">
            <option selected value="csv">CSV</option>
            <option value="json">JSON</option>
            <option value="excel">EXCEL</option>
        </select>
        <button type="submit">Export</button>
    </form>
</body>
</html>

Daha sonra da gerekli view’ı oluşturalım.

from django.views import View
from django.http import HttpResponse, JsonResponse
from django.shortcuts import render
from example.resources import DriverResource

class Export(View):
    template_name = "example/export.html"

    def get(self, request, *args, **kwargs):
        return render(request, self.template_name, {})

    def post(self, request, *args, **kwargs):
        file_type = request.POST.get("file_type")
        dataset = DriverResource().export()
        if file_type == "csv":
            response = HttpResponse(dataset.csv, content_type="text/csv")
            response["Content-Disposition"] = 'attachment; filename="driver.csv"'
            return response
        elif file_type == "json":
            response = HttpResponse(dataset.json, content_type="text/json")
            response["Content-Disposition"] = 'attachment; filename="driver.json"'
        elif file_type == "excel":
            response = HttpResponse(
                dataset.xlsx,
                content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
            )
            response["Content-Disposition"] = 'attachment; filename="driver.xlsx"'
        else:
            response = JsonResponse({"message": "unsupported file type"}, status=400)
        return response

Formdan gelen isteğe göre dataseti serialize ediyor ve ilgili cevabı dönüyoruz. Serialize edilecek ve dönülecek olan dosya formatını, dönülecek cevabın header kısmına content_type olarak ekliyoruz. content-disposition headerı ile de indirelecek dosyayı cevaba iliştiriyoruz. Export alabileceğiniz diğer formatlar ve bu formatlara karşılık gelen, HTTP protokolünün destek verdiği dosya tipleri için ilgili Mozilla web dökümantasyonunu buraya bırakıyorum.


Import işlemleri.

Import işlemini simüle edebilmek için şöyle bir import.csv dosyası hazırlayalım.

name,plate_no,is_banned,started,email
Egehan Gundogdu,48 EVO 048,1,2020-12-23,egehn.gundogdu@gmail.com
Jim Halpert,32 JM 097,0,2017-12-13,jim@halpert.com
Pam Beesly,44 PB 044,0,2019-07-11,pam@office.com

ID alanının dosyada yer almadığına dikkat edelim. Bu alan Django tarafından her yeni kayıt oluşturulduğunda otomatik olarak kayıt ediliyor.

İçeri aktarılacak olan dosya hazır gözüküyor. O zaman hızlıca dosya kabul eden bir forma sahip, template oluşturalım.

<!-- example/templates/example/import.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title></title>
</head>
<body>
        <form action="" method="POST" enctype="multipart/form-data">
            {% csrf_token %}
            <input type="file" required name="import_file" id="">
            <br><br>
            <button type="submit">Import the file.</button>
        </form>
</body>
</html>

Gerekli viewı oluşturarak devam edelim.

class Import(View):
    template_name = "example/import.html"

    def get(self, request, *args, **kwargs):
        return render(request, self.template_name, {})

    def post(self, request, *args, **kwargs):
        resource = DriverResource()
        new_drivers = request.FILES["import_file"]
        file_type = str(new_drivers).split(".")[-1]
        dataset = Dataset()
        imported_data = dataset.load(
            new_drivers.read().decode("utf-8"), format=file_type
        )
        result = resource.import_data(
            dataset, dry_run=True
        ) # testing data
        if not result.has_errors():
            resource.import_data(dataset, dry_run=False)
        else:
            return HttpResponse('some errors on your file.')
        return render(request, self.template_name, {})

Açıklayarak gidecek olursak, ilk olarak ilgili resource sınıfını örnekliyoruz. Daha sonra gelen dosyayı kabul ediyor ve uzantısına göre ayırıyoruz. Bu ayırma işlemi bize tek bir view üzerinden birden fazla import edilebilecek dosya tipine destek vermemizi sağlayacak. Daha sonra bir dataset oluşturuyor ve gelen dosyayı UTF-8 ile decode ediyor ve yine aynı datasete yüklüyoruz. Ayırdığımız dosya uzantısını yine format olarak dataset metoduna geçiriyoruz.

Daha sonra import_data metodu yardımı ile dosyayı içeri aktarmaya çalışıyoruz. Fakat dry_run parametresi bizim için önemli. Bu parametre yardımı ile içeri aktarmaya çalıştığımız dosyayı yalancı bir aktarma işlemi gibi gösterip kontrol ediyoruz. Herhangi bir uyumsuzluk ya da tutarsızlık olması durumunda veritabanını korumuş oluyoruz. Eğer bu işlem sırasında bir hata ile karşılaşılır ise de, yakalıyor ve kullanıcıya dosyada bir problem olduğunu dönüyoruz. Bir hata yok ise dry_run parametresini False olarak ayarlıyor ve veriyi gerçekten içeri aktarıyoruz.


Sonuç.

django-import-export kütüphanesi sağladıkları ve yetenekleri ile fazlası ile dikkate değer bir eklenti. İstemcilerinize anlamlı raporlar oluşturabilmek için fazlası ile kullanışlı. Burada basit bir giriş yapmak olsak da dökümantasyonunu karıştırmanızı öneriyorum.

Ayrıca çok büyük veri kümelerinde (örnegin dışarıya aktarmaya çalıştığınız 50k kullanıcı vs) bu yükü HTTP protokolünün olağan request-response döngüsünden çıkararak Celery yardımı ile asenkron workerlar sayesinde arkada işlemek, uygulama performansı ve kullanıcılara pürüzsüz bir uygulama deneyimi sunma açısından oldukça faydalı olacaktır. Umarım bu giriş ve küçük örnek proje size bir kaç fikir vermiştir.  Kodları daha detaylı incelemek isterseniz ilgili repo burada.

Herkese iyi günler dilerim!

Tarih:Blog

İlk Yorumu Siz Yapın

Bir cevap yazın

E-posta hesabınız yayımlanmayacak.

Göster
Gizle