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="{}"><Binary: {} byte{}></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, }
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
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)
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
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
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
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(" ") 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="{}"><Binary: {} byte{}></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, }
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