pythongeometrygeopandaspy-shinygeopackage

How to download a geopackage file from geodataframe in a shiny app?


I have a shiny app that displays a number of layers on a map using folium. I want to give the user the possibility to download one of the layers (a linestring geodataframe) as a geopackage file. Here is the code I have so far:

# relevant imported packages
from shiny import App, render, ui, reactive
import pandas as pd
import geopandas as gpd

# code in the ui for the download button
ui.download_button(id="downloadRoute", label="Get the route file"),

# code in the server function
@output
@render.download
def downloadRoute():
    result_value = result()
    route = result_value['route_detailed']
    with io.BytesIO() as buffer:
        route.to_file(buffer, driver='GPKG')
        buffer.seek(0)
        return ui.download(filename="my_route.gpkg", file=buffer.read(), content_type="application/geopackage+sqlite3")

I have verified that route is actually a valid geodataframe. If I download it outside shiny, it is a valid geopackage.

In shiny, clicking the download button doesn't do anything on the UI. It only prints this error in the console:

500 Internal Server Error
ERROR:    Exception in ASGI application
Traceback (most recent call last):
  File "pyogrio\_io.pyx", line 1268, in pyogrio._io.ogr_create
  File "pyogrio\_err.pyx", line 177, in pyogrio._err.exc_wrap_pointer
pyogrio._err.CPLE_OpenFailedError: sqlite3_open(<_io.BytesIO object at 0x000002494BFCE3E0>) failed: unable to open database file   

 

What could be the thing I am doing wrong? Are there other ways I can achieve this?


Solution

  • As a side note, the @output decorator is no longer necessary since v0.6.0. And regarding the download issue, you (must?) yield the value of the buffer in the render.download, something like below :

    from io import BytesIO
    from shiny import App, render, ui
    import geopandas as gpd
    from shapely import LineString
    
    app_ui = ui.page_fluid(
        ui.download_button(id="downloadRoute", label="Download as GPKG")
    )
    
    route = gpd.GeoDataFrame(geometry=[LineString([(0, 0), (1, 0)])])
    
    
    def server(input):
        @render.download(
            filename="file.gpkg",
            # media_type="application/geopackage+sqlite3", # optional
        )
        def downloadRoute():
            with BytesIO() as buff:
                route.to_file(buff, driver="GPKG")
                yield buff.getvalue()
    
    
    app = App(app_ui, server)
    

    Demo at shinylive:

    enter image description here