예제 #1
0
        async def extra_template():
            display_rows = []
            for row in results.rows if results else []:
                display_row = []
                for column, value in zip(results.columns, row):
                    display_value = value
                    # Let the plugins have a go
                    # pylint: disable=no-member
                    plugin_display_value = None
                    for candidate in pm.hook.render_cell(
                            value=value,
                            column=column,
                            table=None,
                            database=database,
                            datasette=self.ds,
                    ):
                        candidate = await await_me_maybe(candidate)
                        if candidate is not None:
                            plugin_display_value = candidate
                            break
                    if plugin_display_value is not None:
                        display_value = plugin_display_value
                    else:
                        if value in ("", None):
                            display_value = Markup(" ")
                        elif is_url(str(display_value).strip()):
                            display_value = Markup(
                                '<a href="{url}">{url}</a>'.format(
                                    url=escape(value.strip())))
                        elif isinstance(display_value, bytes):
                            blob_url = path_with_format(
                                request=request,
                                format="blob",
                                extra_qs={
                                    "_blob_column":
                                    column,
                                    "_blob_hash":
                                    hashlib.sha256(display_value).hexdigest(),
                                },
                            )
                            display_value = Markup(
                                '<a class="blob-download" href="{}">&lt;Binary:&nbsp;{}&nbsp;byte{}&gt;</a>'
                                .format(
                                    blob_url,
                                    len(display_value),
                                    "" if len(value) == 1 else "s",
                                ))
                    display_row.append(display_value)
                display_rows.append(display_row)

            # Show 'Edit SQL' button only if:
            # - User is allowed to execute SQL
            # - SQL is an approved SELECT statement
            # - No magic parameters, so no :_ in the SQL string
            edit_sql_url = None
            is_validated_sql = False
            try:
                validate_sql_select(sql)
                is_validated_sql = True
            except InvalidSql:
                pass
            if allow_execute_sql and is_validated_sql and ":_" not in sql:
                edit_sql_url = (self.ds.urls.database(database) + "?" +
                                urlencode({
                                    **{
                                        "sql": sql,
                                    },
                                    **named_parameter_values,
                                }))

            show_hide_hidden = ""
            if metadata.get("hide_sql"):
                if bool(params.get("_show_sql")):
                    show_hide_link = path_with_removed_args(
                        request, {"_show_sql"})
                    show_hide_text = "hide"
                    show_hide_hidden = (
                        '<input type="hidden" name="_show_sql" value="1">')
                else:
                    show_hide_link = path_with_added_args(
                        request, {"_show_sql": 1})
                    show_hide_text = "show"
            else:
                if bool(params.get("_hide_sql")):
                    show_hide_link = path_with_removed_args(
                        request, {"_hide_sql"})
                    show_hide_text = "show"
                    show_hide_hidden = (
                        '<input type="hidden" name="_hide_sql" value="1">')
                else:
                    show_hide_link = path_with_added_args(
                        request, {"_hide_sql": 1})
                    show_hide_text = "hide"
            hide_sql = show_hide_text == "show"
            return {
                "display_rows": display_rows,
                "custom_sql": True,
                "named_parameter_values": named_parameter_values,
                "editable": editable,
                "canned_query": canned_query,
                "edit_sql_url": edit_sql_url,
                "metadata": metadata,
                "settings": self.ds.settings_dict(),
                "request": request,
                "show_hide_link": show_hide_link,
                "show_hide_text": show_hide_text,
                "show_hide_hidden": markupsafe.Markup(show_hide_hidden),
                "hide_sql": hide_sql,
            }
예제 #2
0
 async def stream_fn(r):
     nonlocal data
     writer = csv.writer(LimitedWriter(r,
                                       self.ds.setting("max_csv_mb")))
     first = True
     next = None
     while first or (next and stream):
         try:
             if next:
                 kwargs["_next"] = next
             if not first:
                 data, _, _ = await self.data(request, database, hash,
                                              **kwargs)
             if first:
                 await writer.writerow(headings)
                 first = False
             next = data.get("next")
             for row in data["rows"]:
                 if any(isinstance(r, bytes) for r in row):
                     new_row = []
                     for column, cell in zip(headings, row):
                         if isinstance(cell, bytes):
                             # If this is a table page, use .urls.row_blob()
                             if data.get("table"):
                                 pks = data.get("primary_keys") or []
                                 cell = self.ds.absolute_url(
                                     request,
                                     self.ds.urls.row_blob(
                                         database,
                                         data["table"],
                                         path_from_row_pks(
                                             row, pks, not pks),
                                         column,
                                     ),
                                 )
                             else:
                                 # Otherwise generate URL for this query
                                 cell = self.ds.absolute_url(
                                     request,
                                     path_with_format(
                                         request=request,
                                         format="blob",
                                         extra_qs={
                                             "_blob_column":
                                             column,
                                             "_blob_hash":
                                             hashlib.sha256(
                                                 cell).hexdigest(),
                                         },
                                         replace_format="csv",
                                     ),
                                 )
                         new_row.append(cell)
                     row = new_row
                 if not expanded_columns:
                     # Simple path
                     await writer.writerow(row)
                 else:
                     # Look for {"value": "label": } dicts and expand
                     new_row = []
                     for heading, cell in zip(data["columns"], row):
                         if heading in expanded_columns:
                             if cell is None:
                                 new_row.extend(("", ""))
                             else:
                                 assert isinstance(cell, dict)
                                 new_row.append(cell["value"])
                                 new_row.append(cell["label"])
                         else:
                             new_row.append(cell)
                     await writer.writerow(new_row)
         except Exception as e:
             print("caught this", e)
             await r.write(str(e))
             return
예제 #3
0
파일: base.py 프로젝트: backwardn/datasette
    async def view_get(self, request, database, hash, correct_hash_provided,
                       **kwargs):
        _format, kwargs = await self.get_format(request, database, kwargs)

        if _format == "csv":
            return await self.as_csv(request, database, hash, **kwargs)

        if _format is None:
            # HTML views default to expanding all foreign key labels
            kwargs["default_labels"] = True

        extra_template_data = {}
        start = time.time()
        status_code = 200
        templates = []
        try:
            response_or_template_contexts = await self.data(
                request, database, hash, **kwargs)
            if isinstance(response_or_template_contexts, Response):
                return response_or_template_contexts

            else:
                data, extra_template_data, templates = response_or_template_contexts
        except QueryInterrupted:
            raise DatasetteError(
                """
                SQL query took too long. The time limit is controlled by the
                <a href="https://datasette.readthedocs.io/en/stable/config.html#sql-time-limit-ms">sql_time_limit_ms</a>
                configuration option.
            """,
                title="SQL Interrupted",
                status=400,
                messagge_is_html=True,
            )
        except (sqlite3.OperationalError, InvalidSql) as e:
            raise DatasetteError(str(e), title="Invalid SQL", status=400)

        except (sqlite3.OperationalError) as e:
            raise DatasetteError(str(e))

        except DatasetteError:
            raise

        end = time.time()
        data["query_ms"] = (end - start) * 1000
        for key in ("source", "source_url", "license", "license_url"):
            value = self.ds.metadata(key)
            if value:
                data[key] = value

        # Special case for .jsono extension - redirect to _shape=objects
        if _format == "jsono":
            return self.redirect(
                request,
                path_with_added_args(
                    request,
                    {"_shape": "objects"},
                    path=request.path.rsplit(".jsono", 1)[0] + ".json",
                ),
                forward_querystring=False,
            )

        if _format in self.ds.renderers.keys():
            # Dispatch request to the correct output format renderer
            # (CSV is not handled here due to streaming)
            result = call_with_supported_arguments(
                self.ds.renderers[_format][0],
                datasette=self.ds,
                columns=data.get("columns") or [],
                rows=data.get("rows") or [],
                sql=data.get("query", {}).get("sql", None),
                query_name=data.get("query_name"),
                database=database,
                table=data.get("table"),
                request=request,
                view_name=self.name,
                # These will be deprecated in Datasette 1.0:
                args=request.args,
                data=data,
            )
            if asyncio.iscoroutine(result):
                result = await result
            if result is None:
                raise NotFound("No data")

            r = Response(
                body=result.get("body"),
                status=result.get("status_code", 200),
                content_type=result.get("content_type", "text/plain"),
                headers=result.get("headers"),
            )
        else:
            extras = {}
            if callable(extra_template_data):
                extras = extra_template_data()
                if asyncio.iscoroutine(extras):
                    extras = await extras
            else:
                extras = extra_template_data
            url_labels_extra = {}
            if data.get("expandable_columns"):
                url_labels_extra = {"_labels": "on"}

            renderers = {}
            for key, (_, can_render) in self.ds.renderers.items():
                it_can_render = call_with_supported_arguments(
                    can_render,
                    datasette=self.ds,
                    columns=data.get("columns") or [],
                    rows=data.get("rows") or [],
                    sql=data.get("query", {}).get("sql", None),
                    query_name=data.get("query_name"),
                    database=database,
                    table=data.get("table"),
                    request=request,
                    view_name=self.name,
                )
                if asyncio.iscoroutine(it_can_render):
                    it_can_render = await it_can_render
                if it_can_render:
                    renderers[key] = path_with_format(request, key,
                                                      {**url_labels_extra})

            url_csv_args = {"_size": "max", **url_labels_extra}
            url_csv = path_with_format(request, "csv", url_csv_args)
            url_csv_path = url_csv.split("?")[0]
            context = {
                **data,
                **extras,
                **{
                    "renderers":
                    renderers,
                    "url_csv":
                    url_csv,
                    "url_csv_path":
                    url_csv_path,
                    "url_csv_hidden_args": [(key, value) for key, value in urllib.parse.parse_qsl(request.query_string) if key not in ("_labels", "_facet", "_size")] + [("_size", "max")],
                    "datasette_version":
                    __version__,
                    "config":
                    self.ds.config_dict(),
                },
            }
            if "metadata" not in context:
                context["metadata"] = self.ds.metadata
            r = await self.render(templates, request=request, context=context)
            r.status = status_code

        ttl = request.args.get("_ttl", None)
        if ttl is None or not ttl.isdigit():
            if correct_hash_provided:
                ttl = self.ds.config("default_cache_ttl_hashed")
            else:
                ttl = self.ds.config("default_cache_ttl")

        return self.set_response_headers(r, ttl)
예제 #4
0
def test_path_with_format(path, format, extra_qs, expected):
    request = Request.fake(path)
    actual = utils.path_with_format(request=request,
                                    format=format,
                                    extra_qs=extra_qs)
    assert expected == actual
예제 #5
0
def test_path_with_format(path, format, extra_qs, expected):
    request = Request(path.encode('utf8'), {}, '1.1', 'GET', None)
    actual = utils.path_with_format(request, format, extra_qs)
    assert expected == actual
예제 #6
0
    async def view_get(self, request, database, hash, correct_hash_provided,
                       **kwargs):
        # If ?_format= is provided, use that as the format
        _format = request.args.get("_format", None)
        if not _format:
            _format = (kwargs.pop("as_format", None) or "").lstrip(".")
        if "table_and_format" in kwargs:

            async def async_table_exists(t):
                return await self.ds.table_exists(database, t)

            table, _ext_format = await resolve_table_and_format(
                table_and_format=urllib.parse.unquote_plus(
                    kwargs["table_and_format"]),
                table_exists=async_table_exists)
            _format = _format or _ext_format
            kwargs["table"] = table
            del kwargs["table_and_format"]
        elif "table" in kwargs:
            kwargs["table"] = urllib.parse.unquote_plus(kwargs["table"])

        if _format == "csv":
            return await self.as_csv(request, database, hash, **kwargs)

        if _format is None:
            # HTML views default to expanding all forign key labels
            kwargs['default_labels'] = True

        extra_template_data = {}
        start = time.time()
        status_code = 200
        templates = []
        try:
            response_or_template_contexts = await self.data(
                request, database, hash, **kwargs)
            if isinstance(response_or_template_contexts,
                          response.HTTPResponse):
                return response_or_template_contexts

            else:
                data, extra_template_data, templates = response_or_template_contexts
        except InterruptedError as e:
            raise DatasetteError("""
                SQL query took too long. The time limit is controlled by the
                <a href="https://datasette.readthedocs.io/en/stable/config.html#sql-time-limit-ms">sql_time_limit_ms</a>
                configuration option.
            """,
                                 title="SQL Interrupted",
                                 status=400,
                                 messagge_is_html=True)
        except (sqlite3.OperationalError, InvalidSql) as e:
            raise DatasetteError(str(e), title="Invalid SQL", status=400)

        except (sqlite3.OperationalError) as e:
            raise DatasetteError(str(e))

        except DatasetteError:
            raise

        end = time.time()
        data["query_ms"] = (end - start) * 1000
        for key in ("source", "source_url", "license", "license_url"):
            value = self.ds.metadata(key)
            if value:
                data[key] = value
        if _format in ("json", "jsono"):
            # Special case for .jsono extension - redirect to _shape=objects
            if _format == "jsono":
                return self.redirect(
                    request,
                    path_with_added_args(
                        request,
                        {"_shape": "objects"},
                        path=request.path.rsplit(".jsono", 1)[0] + ".json",
                    ),
                    forward_querystring=False,
                )

            # Handle the _json= parameter which may modify data["rows"]
            json_cols = []
            if "_json" in request.args:
                json_cols = request.args["_json"]
            if json_cols and "rows" in data and "columns" in data:
                data["rows"] = convert_specific_columns_to_json(
                    data["rows"],
                    data["columns"],
                    json_cols,
                )

            # unless _json_infinity=1 requested, replace infinity with None
            if "rows" in data and not value_as_boolean(
                    request.args.get("_json_infinity", "0")):
                data["rows"] = [remove_infinites(row) for row in data["rows"]]

            # Deal with the _shape option
            shape = request.args.get("_shape", "arrays")
            if shape == "arrayfirst":
                data = [row[0] for row in data["rows"]]
            elif shape in ("objects", "object", "array"):
                columns = data.get("columns")
                rows = data.get("rows")
                if rows and columns:
                    data["rows"] = [dict(zip(columns, row)) for row in rows]
                if shape == "object":
                    error = None
                    if "primary_keys" not in data:
                        error = "_shape=object is only available on tables"
                    else:
                        pks = data["primary_keys"]
                        if not pks:
                            error = "_shape=object not available for tables with no primary keys"
                        else:
                            object_rows = {}
                            for row in data["rows"]:
                                pk_string = path_from_row_pks(
                                    row, pks, not pks)
                                object_rows[pk_string] = row
                            data = object_rows
                    if error:
                        data = {
                            "ok": False,
                            "error": error,
                            "database": database,
                        }
                elif shape == "array":
                    data = data["rows"]
            elif shape == "arrays":
                pass
            else:
                status_code = 400
                data = {
                    "ok": False,
                    "error": "Invalid _shape: {}".format(shape),
                    "status": 400,
                    "title": None,
                }
            headers = {}
            if self.ds.cors:
                headers["Access-Control-Allow-Origin"] = "*"
            # Handle _nl option for _shape=array
            nl = request.args.get("_nl", "")
            if nl and shape == "array":
                body = "\n".join(json.dumps(item) for item in data)
                content_type = "text/plain"
            else:
                body = json.dumps(data, cls=CustomJSONEncoder)
                content_type = "application/json"
            r = response.HTTPResponse(
                body,
                status=status_code,
                content_type=content_type,
                headers=headers,
            )
        else:
            extras = {}
            if callable(extra_template_data):
                extras = extra_template_data()
                if asyncio.iscoroutine(extras):
                    extras = await extras
            else:
                extras = extra_template_data
            url_labels_extra = {}
            if data.get("expandable_columns"):
                url_labels_extra = {"_labels": "on"}
            url_csv_args = {"_size": "max", **url_labels_extra}
            url_csv = path_with_format(request, "csv", url_csv_args)
            url_csv_path = url_csv.split('?')[0]
            context = {
                **data,
                **extras,
                **{
                    "url_json":
                    path_with_format(request, "json", {
                        **url_labels_extra,
                    }),
                    "url_csv":
                    url_csv,
                    "url_csv_path":
                    url_csv_path,
                    "url_csv_hidden_args": [(key, value) for key, value in urllib.parse.parse_qsl(request.query_string) if key not in ("_labels", "_facet", "_size")] + [("_size", "max")],
                    "datasette_version":
                    __version__,
                    "config":
                    self.ds.config_dict(),
                }
            }
            if "metadata" not in context:
                context["metadata"] = self.ds.metadata
            r = self.render(templates, **context)
            r.status = status_code
        # Set far-future cache expiry
        if self.ds.cache_headers and r.status == 200:
            ttl = request.args.get("_ttl", None)
            if ttl is None or not ttl.isdigit():
                if correct_hash_provided:
                    ttl = self.ds.config("default_cache_ttl_hashed")
                else:
                    ttl = self.ds.config("default_cache_ttl")
            else:
                ttl = int(ttl)
            if ttl == 0:
                ttl_header = 'no-cache'
            else:
                ttl_header = 'max-age={}'.format(ttl)
            r.headers["Cache-Control"] = ttl_header
        r.headers["Referrer-Policy"] = "no-referrer"
        return r
예제 #7
0
        async def extra_template():
            display_rows = []
            for row in results.rows:
                display_row = []
                for column, value in zip(results.columns, row):
                    display_value = value
                    # Let the plugins have a go
                    # pylint: disable=no-member
                    plugin_value = pm.hook.render_cell(
                        value=value,
                        column=column,
                        table=None,
                        database=database,
                        datasette=self.ds,
                    )
                    if plugin_value is not None:
                        display_value = plugin_value
                    else:
                        if value in ("", None):
                            display_value = jinja2.Markup("&nbsp;")
                        elif is_url(str(display_value).strip()):
                            display_value = jinja2.Markup(
                                '<a href="{url}">{url}</a>'.format(
                                    url=jinja2.escape(value.strip())))
                        elif isinstance(display_value, bytes):
                            blob_url = path_with_format(
                                request=request,
                                format="blob",
                                extra_qs={
                                    "_blob_column":
                                    column,
                                    "_blob_hash":
                                    hashlib.sha256(display_value).hexdigest(),
                                },
                            )
                            display_value = jinja2.Markup(
                                '<a class="blob-download" href="{}">&lt;Binary:&nbsp;{}&nbsp;byte{}&gt;</a>'
                                .format(
                                    blob_url,
                                    len(display_value),
                                    "" if len(value) == 1 else "s",
                                ))
                    display_row.append(display_value)
                display_rows.append(display_row)

            # Show 'Edit SQL' button only if:
            # - User is allowed to execute SQL
            # - SQL is an approved SELECT statement
            # - No magic parameters, so no :_ in the SQL string
            edit_sql_url = None
            is_validated_sql = False
            try:
                validate_sql_select(sql)
                is_validated_sql = True
            except InvalidSql:
                pass
            if allow_execute_sql and is_validated_sql and ":_" not in sql:
                edit_sql_url = (self.ds.urls.database(database) + "?" +
                                urlencode({
                                    **{
                                        "sql": sql,
                                    },
                                    **named_parameter_values,
                                }))
            return {
                "display_rows": display_rows,
                "custom_sql": True,
                "named_parameter_values": named_parameter_values,
                "editable": editable,
                "canned_query": canned_query,
                "edit_sql_url": edit_sql_url,
                "metadata": metadata,
                "config": self.ds.config_dict(),
                "request": request,
                "path_with_added_args": path_with_added_args,
                "path_with_removed_args": path_with_removed_args,
                "hide_sql": "_hide_sql" in params,
            }
예제 #8
0
def test_path_with_format(path, format, extra_qs, expected):
    request = Request(path.encode("utf8"), {}, "1.1", "GET", None)
    actual = utils.path_with_format(request, format, extra_qs)
    assert expected == actual