Memigrasikan Blobstore App Engine ke Cloud Storage

Panduan ini membahas cara melakukan migrasi dari Blobstore App Engine ke Cloud Storage.

Cloud Storage memiliki kesamaan dengan Blobstore App Engine di mana Anda dapat menggunakan Cloud Storage untuk menyajikan objek data besar (blob), seperti file video atau gambar, dan memungkinkan pengguna mengupload file data berukuran besar. Meskipun Blobstore App Engine hanya dapat diakses melalui layanan paket lama App Engine, Cloud Storage adalah produk mandiri Google Cloud yang diakses melalui Library Klien Cloud. Cloud Storage menawarkan solusi penyimpanan objek yang lebih modern pada aplikasi Anda dan memberi Anda fleksibilitas untuk bermigrasi ke Cloud Run atau platform hosting aplikasi Google Cloud lainnya di kemudian hari.

Untuk project Google Cloud yang dibuat setelah November 2016, Blobstore menggunakan bucket Cloud Storage di balik layar. Ini berarti bahwa saat Anda memigrasikan aplikasi ke Cloud Storage, semua objek dan izin yang ada di bucket Cloud Storage saat ini tetap tidak berubah. Anda juga dapat mulai mengakses bucket yang ada menggunakan Library Klien Cloud untuk Cloud Storage.

Perbedaan dan kesamaan utama

Cloud Storage tidak mencakup dependensi dan batasan Blobstore berikut:

  • Blobstore API untuk Python 2 memiliki dependensi di aplikasi web.
  • Blobstore API untuk Python 3 menggunakan class utilitas untuk menggunakan pengendali Blobstore.
  • Untuk Blobstore, jumlah file maksimum yang dapat diupload ke Blobstore adalah 500. Tidak ada batasan jumlah objek yang dapat Anda buat di bucket Cloud Storage.

Cloud Storage tidak mendukung:

  • Class pengendali Blobstore
  • Objek Blobstore

Kesamaan Cloud Storage dan Blobstore App Engine:

  • Mampu membaca dan menulis objek data besar di lingkungan runtime, serta menyimpan dan menyajikan objek data besar statis, seperti film, gambar, atau konten statis lainnya. Batas ukuran objek untuk Cloud Storage adalah 5 TiB.
  • Memungkinkan Anda menyimpan objek di bucket Cloud Storage.
  • Memiliki paket gratis.

Sebelum memulai

  • Anda perlu meninjau serta memahami harga dan kuota Cloud Storage:
    • Cloud Storage adalah layanan bayar sesuai penggunaan dan memiliki harganya sendiri untuk penyimpanan data berdasarkan kelas penyimpanan data Anda dan lokasi bucket Anda.
    • Kuota Cloud Storage memiliki beberapa perbedaan dengan kuota dan batas Blobstore App Engine, yang dapat memengaruhi kuota permintaan App Engine Anda.
  • Memiliki aplikasi App Engine Python 2 atau Python 3 yang sudah ada dan menggunakan Blobstore.
  • Contoh dalam panduan ini menunjukkan aplikasi yang bermigrasi ke Cloud Storage menggunakan framework Flask. Perlu diperhatikan bahwa Anda dapat menggunakan framework web apa pun, termasuk tetap menggunakan webapp2, saat bermigrasi ke Cloud Storage.

Ringkasan

Secara umum, proses migrasi ke Cloud Storage dari Blobstore App Engine terdiri dari langkah-langkah berikut:

  1. Mengupdate file konfigurasi
  2. Mengupdate aplikasi Python:
    • Mengupdate framework web
    • Mengimpor dan melakukan inisialisasi Cloud Storage
    • Mengupdate pengendali Blobstore
    • Opsional: Mengupdate model data jika menggunakan Cloud NDB atau App Engine NDB
  3. Menguji dan men-deploy aplikasi

Mengupdate file konfigurasi

Sebelum mengubah kode aplikasi untuk beralih dari Blobstore ke Cloud Storage, update file konfigurasi Anda agar dapat menggunakan library Cloud Storage.

  1. Update file app.yaml: Ikuti petunjuk untuk versi Python Anda:

    Python 2

    Untuk aplikasi Python 2:

    1. Hapus bagian handlers dan dependensi aplikasi web yang tidak diperlukan di bagian libraries.
    2. Jika Anda menggunakan Library Klien Cloud, tambahkan versi terbaru library grpcio dan setuptools.
    3. Tambahkan library ssl karena diperlukan oleh Cloud Storage.

    Berikut adalah contoh file app.yaml dengan perubahan yang dibuat:

    runtime: python27
    threadsafe: yes
    api_version: 1
    
    handlers:
    - url: /.*
      script: main.app
    
    libraries:
    - name: grpcio
      version: latest
    - name: setuptools
      version: latest
    - name: ssl
      version: latest
    

    Python 3

    Untuk aplikasi Python 3, hapus semua baris kecuali untuk elemen runtime. Contoh:

    runtime: python310 # or another support version
    

    Runtime Python 3 menginstal library secara otomatis, sehingga Anda tidak perlu menentukan library bawaan dari runtime Python 2 sebelumnya. Jika aplikasi Python 3 Anda menggunakan layanan paket lama lain saat bermigrasi ke Cloud Storage, biarkan file app.yaml apa adanya.

  2. Update filerequirements.txt: Ikuti petunjuk untuk versi python Anda:

    Python 2

    Tambahkan Library Klien Cloud untuk Cloud Storage ke daftar dependensi dalam file requirements.txt.

    google-cloud-storage
    

    Kemudian, jalankan pip install -t lib -r requirements.txt untuk memperbarui daftar library yang tersedia untuk aplikasi Anda.

    Python 3

    Tambahkan Library Klien Cloud untuk Cloud Storage ke daftar dependensi dalam file requirements.txt.

    google-cloud-storage
    

    App Engine otomatis menginstal dependensi ini selama deployment aplikasi di runtime Python 3, jadi hapus folder lib jika ada.

  3. Untuk aplikasi Python 2, jika aplikasi Anda menggunakan library bawaan atau yang disalin, Anda harus menentukan jalur tersebut dalam file appengine_config.py:

    import pkg_resources
    from google.appengine.ext import vendor
    
    # Set PATH to your libraries folder.
    PATH = 'lib'
    # Add libraries installed in the PATH folder.
    vendor.add(PATH)
    # Add libraries to pkg_resources working set to find the distribution.
    pkg_resources.working_set.add_entry(PATH)
    

Mengupdate aplikasi Python

Setelah memodifikasi file konfigurasi, update aplikasi Python Anda.

Mengupdate framework web Python 2

Untuk aplikasi Python 2 yang menggunakan framework webapp2, sebaiknya lakukan migrasi dari framework webapp2 yang sudah tidak berlaku. Lihat Jadwal dukungan runtime untuk mengetahui tanggal akhir dukungan Python 2.

Anda dapat bermigrasi ke framework web lain seperti Flask, Django, atau WSGI. Karena Cloud Storage tidak menyertakan dependensi di webapp2 dan pengendali Blobstore tidak didukung, Anda dapat menghapus atau mengganti library lain yang terkait dengan aplikasi web.

Jika Anda memilih untuk terus menggunakan webapp2, perhatikan bahwa contoh dalam panduan ini menggunakan Cloud Storage dengan Flask.

Jika berencana menggunakan layanan Google Cloud selain Cloud Storage, atau untuk mendapatkan akses ke versi runtime terbaru, sebaiknya upgrade aplikasi Anda ke runtime Python 3. Untuk informasi selengkapnya, lihat Ringkasan migrasi Python 2 ke Python 3.

Mengimpor dan melakukan inisialisasi Cloud Storage

Ubah file aplikasi Anda dengan memperbarui baris impor dan inisialisasi:

  1. Hapus pernyataan impor Blobstore, seperti berikut:

    import webapp2
    from google.appengine.ext import blobstore
    from google.appengine.ext.webapp import blobstore_handlers
    
  2. Tambahkan pernyataan impor untuk Cloud Storage dan library Autentikasi Google, seperti berikut:

    import io
    from flask import (Flask, abort, redirect, render_template,
    request, send_file, url_for)
    from google.cloud import storage
    import google.auth
    

    Library Autentikasi Google diperlukan untuk mendapatkan project ID yang sama dengan yang digunakan di Blobstore untuk Cloud Storage. Impor library lain seperti Cloud NBD jika berlaku untuk aplikasi Anda.

  3. Buat klien baru untuk Cloud Storage dan tentukan bucket yang digunakan di Blobstore. Contoh:

    gcs_client = storage.Client()
    _, PROJECT_ID = google.auth.default()
    BUCKET = '%s.appspot.com' % PROJECT_ID
    

    Untuk project Google Cloud setelah November 2016, Blobstore menulis ke bucket Cloud Storage yang diberi nama berdasarkan URL aplikasi Anda dan mengikuti format PROJECT_ID.appspot.com. Anda menggunakan autentikasi Google untuk mendapatkan project ID guna menentukan bucket Cloud Storage yang digunakan untuk menyimpan blob di Blobstore.

Mengupdate pengendali Blobstore

Karena Cloud Storage tidak mendukung pengendali upload dan download Blobstore, Anda harus menggunakan kombinasi fungsi Cloud Storage, modul library standar io, framework web, dan utilitas Python untuk mengupload serta mendownload objek (blob) di Cloud Storage.

Berikut cara mengupdate pengendali Blobstore menggunakan Flask sebagai contoh framework web:

  1. Ganti class pengendali upload Blobstore Anda dengan fungsi upload di Flask. Ikuti petunjuk untuk versi Python Anda:

    Python 2

    Pengendali Blobstore di Python 2 adalah class webapp2 seperti yang ditunjukkan dalam contoh Blobstore berikut:

    class UploadHandler(blobstore_handlers.BlobstoreUploadHandler):
        'Upload blob (POST) handler'
        def post(self):
            uploads = self.get_uploads()
            blob_id = uploads[0].key() if uploads else None
            store_visit(self.request.remote_addr, self.request.user_agent, blob_id)
            self.redirect('/', code=307)
    ...
    app = webapp2.WSGIApplication([
        ('/', MainHandler),
        ('/upload', UploadHandler),
        ('/view/([^/]+)?', ViewBlobHandler),
    ], debug=True)
    

    Untuk menggunakan Cloud Storage:

    1. Ganti class upload Webapp dengan fungsi upload Flask.
    2. Ganti pengendali upload dan pemilihan rute dengan metode POST Flask yang dilengkapi dengan pemilihan rute.

    Contoh kode yang diupdate:

    @app.route('/upload', methods=['POST'])
    def upload():
        'Upload blob (POST) handler'
        fname = None
        upload = request.files.get('file', None)
        if upload:
            fname = secure_filename(upload.filename)
            blob = gcs_client.bucket(BUCKET).blob(fname)
            blob.upload_from_file(upload, content_type=upload.content_type)
        store_visit(request.remote_addr, request.user_agent, fname)
        return redirect(url_for('root'), code=307)
    

    Dalam contoh kode Cloud Storage yang diupdate, aplikasi sekarang mengidentifikasi artefak objek berdasarkan nama objeknya (fname) alih-alih blob_id. Pemilihan rute juga terjadi di bagian bawah file aplikasi.

    Untuk mendapatkan objek yang diupload, metode get_uploads() Blobstore diganti dengan metode request.files.get() Flask. Di Flask, Anda dapat menggunakan metode secure_filename() untuk mendapatkan nama tanpa karakter jalur, seperti /, untuk file, dan mengidentifikasi objek menggunakan gcs_client.bucket(BUCKET).blob(fname) untuk menentukan nama bucket dan nama objek.

    Panggilan upload_from_file() Cloud Storage menjalankan upload seperti yang ditunjukkan pada contoh yang diupdate.

    Python 3

    Class pengendali upload di Blobstore untuk Python 3 adalah class utilitas dan memerlukan penggunaan kamus environ WSGI sebagai parameter input, seperti ditunjukkan dalam Blobstore berikut contoh:

    class UploadHandler(blobstore.BlobstoreUploadHandler):
        'Upload blob (POST) handler'
        def post(self):
            uploads = self.get_uploads(request.environ)
            if uploads:
                blob_id = uploads[0].key()
                store_visit(request.remote_addr, request.user_agent, blob_id)
            return redirect('/', code=307)
    ...
    @app.route('/upload', methods=['POST'])
    def upload():
        """Upload handler called by blobstore when a blob is uploaded in the test."""
        return UploadHandler().post()
    

    Untuk menggunakan Cloud Storage, ganti metode get_uploads(request.environ) Blobstore dengan metode request.files.get() Flask.

    Contoh kode yang diupdate:

    @app.route('/upload', methods=['POST'])
    def upload():
        'Upload blob (POST) handler'
        fname = None
        upload = request.files.get('file', None)
        if upload:
            fname = secure_filename(upload.filename)
            blob = gcs_client.bucket(BUCKET).blob(fname)
            blob.upload_from_file(upload, content_type=upload.content_type)
        store_visit(request.remote_addr, request.user_agent, fname)
        return redirect(url_for('root'), code=307)
    

    Dalam contoh kode Cloud Storage yang diupdate, aplikasi sekarang mengidentifikasi artefak objek berdasarkan nama objeknya (fname) alih-alih blob_id. Pemilihan rute juga terjadi di bagian bawah file aplikasi.

    Untuk mendapatkan objek yang diupload, metode get_uploads() Blobstore diganti dengan metode request.files.get() Flask. Di Flask, Anda dapat menggunakan metode secure_filename() untuk mendapatkan nama tanpa karakter jalur, seperti /, untuk file, dan mengidentifikasi objek menggunakan gcs_client.bucket(BUCKET).blob(fname) untuk menentukan nama bucket dan nama objek.

    Metode upload_from_file() Cloud Storage melakukan upload seperti yang ditunjukkan pada contoh yang diupdate.

  2. Ganti class pengendali Download Blobstore Anda dengan fungsi download di Flask. Ikuti petunjuk untuk versi Python Anda:

    Python 2

    Contoh pengendali download berikut menunjukkan penggunaan class BlobstoreDownloadHandler, yang menggunakan webapp2:

    class ViewBlobHandler(blobstore_handlers.BlobstoreDownloadHandler):
        'view uploaded blob (GET) handler'
        def get(self, blob_key):
            self.send_blob(blob_key) if blobstore.get(blob_key) else self.error(404)
    ...
    app = webapp2.WSGIApplication([
        ('/', MainHandler),
        ('/upload', UploadHandler),
        ('/view/([^/]+)?', ViewBlobHandler),
    ], debug=True)
    

    Untuk menggunakan Cloud Storage:

    1. Perbarui metode send_blob() Blobstore untuk menggunakan metode download_as_bytes() Cloud Storage.
    2. Ubah pemilihan rute dari webapp2 ke Flask.

    Contoh kode yang diupdate:

    @app.route('/view/<path:fname>')
    def view(fname):
        'view uploaded blob (GET) handler'
        blob = gcs_client.bucket(BUCKET).blob(fname)
        try:
            media = blob.download_as_bytes()
        except exceptions.NotFound:
            abort(404)
        return send_file(io.BytesIO(media), mimetype=blob.content_type)
    

    Dalam contoh kode Cloud Storage yang diupdate, Flask melengkapi rute pada fungsi Flask dan mengidentifikasi objek menggunakan '/view/<path:fname>'. Cloud Storage mengidentifikasi objek blob berdasarkan nama objek dan nama bucket, serta menggunakan metode download_as_bytes() untuk mendownload objek dalam ukuran byte, alih-alih menggunakan metode send_blob dari Blobstore. Jika artefak tidak ditemukan, aplikasi akan menampilkan error 404 HTTP.

    Python 3

    Seperti pengendali upload, class pengendali download di Blobstore untuk Python 3 adalah class utilitas dan memerlukan penggunaan kamus environ WSGI sebagai parameter input, seperti ditunjukkan dalam contoh Blobstore berikut:

    class ViewBlobHandler(blobstore.BlobstoreDownloadHandler):
        'view uploaded blob (GET) handler'
        def get(self, blob_key):
            if not blobstore.get(blob_key):
                return "Photo key not found", 404
            else:
                headers = self.send_blob(request.environ, blob_key)
    
            # Prevent Flask from setting a default content-type.
            # GAE sets it to a guessed type if the header is not set.
            headers['Content-Type'] = None
            return '', headers
    ...
    @app.route('/view/<blob_key>')
    def view_photo(blob_key):
        """View photo given a key."""
        return ViewBlobHandler().get(blob_key)
    

    Untuk menggunakan Cloud Storage, ganti send_blob(request.environ, blob_key) Blobstore dengan metode blob.download_as_bytes() Cloud Storage.

    Contoh kode yang diupdate:

    @app.route('/view/<path:fname>')
    def view(fname):
        'view uploaded blob (GET) handler'
        blob = gcs_client.bucket(BUCKET).blob(fname)
        try:
            media = blob.download_as_bytes()
        except exceptions.NotFound:
            abort(404)
        return send_file(io.BytesIO(media), mimetype=blob.content_type)
    

    Dalam contoh kode Cloud Storage yang diupdate, blob_key diganti dengan fname, dan Flask mengidentifikasi objek menggunakan URL '/view/<path:fname>'. Metode gcs_client.bucket(BUCKET).blob(fname) digunakan untuk menemukan nama file dan nama bucket. Metode download_as_bytes() Cloud Storage mendownload objek dalam ukuran byte, alih-alih menggunakan metode send_blob() dari Blobstore.

  3. Jika aplikasi Anda menggunakan pengendali utama, ganti class MainHandler dengan fungsi root() di Flask. Ikuti petunjuk untuk versi Python Anda:

    Python 2

    Berikut adalah contoh penggunaan class MainHandler Blobstore:

    class MainHandler(BaseHandler):
        'main application (GET/POST) handler'
        def get(self):
            self.render_response('index.html',
                    upload_url=blobstore.create_upload_url('/upload'))
    
        def post(self):
            visits = fetch_visits(10)
            self.render_response('index.html', visits=visits)
    
    app = webapp2.WSGIApplication([
        ('/', MainHandler),
        ('/upload', UploadHandler),
        ('/view/([^/]+)?', ViewBlobHandler),
    ], debug=True)
    

    Untuk menggunakan Cloud Storage:

    1. Hapus class MainHandler(BaseHandler), karena Flask menangani pemilihan rute untuk Anda.
    2. Sederhanakan kode Blobstore dengan Flask.
    3. Hapus pemilihan rute aplikasi web di bagian akhir.

    Contoh kode yang diupdate:

    @app.route('/', methods=['GET', 'POST'])
    def root():
        'main application (GET/POST) handler'
        context = {}
        if request.method == 'GET':
            context['upload_url'] = url_for('upload')
        else:
            context['visits'] = fetch_visits(10)
        return render_template('index.html', **context)
    

    Python 3

    Jika menggunakan Flask, Anda tidak akan memiliki class MainHandler, tetapi fungsi root Flask Anda perlu diperbarui jika blobstore digunakan. Contoh berikut menggunakan fungsi blobstore.create_upload_url('/upload'):

    @app.route('/', methods=['GET', 'POST'])
    def root():
        'main application (GET/POST) handler'
        context = {}
        if request.method == 'GET':
            context['upload_url'] = blobstore.create_upload_url('/upload')
        else:
            context['visits'] = fetch_visits(10)
        return render_template('index.html', **context)
    

    Untuk menggunakan Cloud Storage, ganti fungsi blobstore.create_upload_url('/upload') dengan metode url_for() Flask guna mendapatkan URL untuk fungsi upload().

    Contoh kode yang diupdate:

    @app.route('/', methods=['GET', 'POST'])
    def root():
        'main application (GET/POST) handler'
        context = {}
        if request.method == 'GET':
            context['upload_url'] = url_for('upload') # Updated to use url_for
        else:
            context['visits'] = fetch_visits(10)
        return render_template('index.html', **context)
    

Menguji dan men-deploy aplikasi

Server pengembangan lokal dapat digunakan untuk menguji apakah aplikasi Anda berjalan, tetapi tidak akan dapat menguji Cloud Storage sampai Anda men-deploy versi baru karena semua permintaan Cloud Storage harus dikirim melalui internet ke server bucket Cloud Storage sebenarnya. Baca artikel Menguji dan men-deploy aplikasi Anda untuk cara menjalankan aplikasi secara lokal. Kemudian, deploy versi baru untuk mengonfirmasi bahwa aplikasi terlihat sama seperti sebelumnya.

Aplikasi yang menggunakan App Engine NDB atau Cloud NDB

Anda harus memperbarui model data Datastore jika aplikasi Anda menggunakan App Engine NDB atau Cloud NDB untuk menyertakan properti terkait Blobstore.

Mengupdate model data

Karena properti BlobKey dari NDB tidak didukung oleh Cloud Storage, Anda perlu mengubah baris yang terkait dengan Blobstore agar dapat menggunakan padanan bawaan dari NDB, framework web, atau di tempat lain.

Untuk mengupdate model data Anda:

  1. Temukan baris yang menggunakan BlobKey dalam model data, seperti berikut:

    class Visit(ndb.Model):
        'Visit entity registers visitor IP address & timestamp'
        visitor   = ndb.StringProperty()
        timestamp = ndb.DateTimeProperty(auto_now_add=True)
        file_blob = ndb.BlobKeyProperty()
    
  2. Ganti ndb.BlobKeyProperty() dengan ndb.StringProperty():

    class Visit(ndb.Model):
        'Visit entity registers visitor IP address & timestamp'
        visitor   = ndb.StringProperty()
        timestamp = ndb.DateTimeProperty(auto_now_add=True)
        file_blob = ndb.StringProperty() # Modified from ndb.BlobKeyProperty()
    
  3. Jika Anda juga melakukan upgrade dari App Engine NDB ke Cloud NDB selama migrasi, lihat panduan migrasi Cloud NDB untuk panduan tentang cara memfaktorkan ulang kode NDB untuk menggunakan pengelola konteks Python.

Kompatibilitas mundur untuk model data Datastore

Di bagian sebelumnya, mengganti ndb.BlobKeyProperty dengan ndb.StringProperty akan membuat aplikasi tidak kompatibel dengan versi sebelumnya, yang berarti aplikasi tidak akan dapat memproses entri lama yang dibuat oleh Blobstore. Jika Anda perlu mempertahankan data lama, buat kolom tambahan untuk entri Cloud Storage baru, alih-alih mengupdate kolom ndb.BlobKeyProperty, dan buat fungsi untuk menormalisasi data.

Dari contoh di bagian sebelumnya, buat perubahan berikut:

  1. Buat dua kolom properti terpisah saat menentukan model data Anda. Gunakan properti file_blob untuk mengidentifikasi objek yang dibuat Blobstore dan properti file_gcs untuk mengidentifikasi objek yang dibuat Cloud Storage:

    class Visit(ndb.Model):
        'Visit entity registers visitor IP address & timestamp'
        visitor   = ndb.StringProperty()
        timestamp = ndb.DateTimeProperty(auto_now_add=True)
        file_blob = ndb.BlobKeyProperty()  # backwards-compatibility
        file_gcs  = ndb.StringProperty()
    
  2. Cari baris yang merujuk kunjungan baru, seperti berikut:

    def store_visit(remote_addr, user_agent, upload_key):
        'create new Visit entity in Datastore'
        with ds_client.context():
            Visit(visitor='{}: {}'.format(remote_addr, user_agent),
                    file_blob=upload_key).put()
    
  3. Ubah kode Anda sehingga file_gcs digunakan untuk entri terbaru. Contoh:

    def store_visit(remote_addr, user_agent, upload_key):
        'create new Visit entity in Datastore'
        with ds_client.context():
            Visit(visitor='{}: {}'.format(remote_addr, user_agent),
                    file_gcs=upload_key).put() # change file_blob to file_gcs for new requests
    
  4. Buat fungsi baru untuk menormalisasi data. Contoh berikut menunjukkan penggunaan ekstraksi, transformasi, dan pemuatan (ETL) untuk melakukan loop melalui semua kunjungan, serta mengambil data pengunjung dan stempel waktu untuk memeriksa apakah file_gcs atau file_gcs ada:

    def etl_visits(visits):
        return [{
                'visitor': v.visitor,
                'timestamp': v.timestamp,
                'file_blob': v.file_gcs if hasattr(v, 'file_gcs') \
                        and v.file_gcs else v.file_blob
                } for v in visits]
    
  5. Cari baris yang merujuk ke fungsi fetch_visits():

    @app.route('/', methods=['GET', 'POST'])
    def root():
        'main application (GET/POST) handler'
        context = {}
        if request.method == 'GET':
            context['upload_url'] = url_for('upload')
        else:
            context['visits'] = fetch_visits(10)
        return render_template('index.html', **context)
    
  6. Gabungkan fetch_visits() di dalam fungsi etl_visits(), misalnya:

    @app.route('/', methods=['GET', 'POST'])
    def root():
        'main application (GET/POST) handler'
        context = {}
        if request.method == 'GET':
            context['upload_url'] = url_for('upload')
        else:
            context['visits'] = etl_visits(fetch_visits(10)) # etl_visits wraps around fetch_visits
        return render_template('index.html', **context)
    

Contoh

Langkah berikutnya