def _get_screenshot(self) -> ScreenshotData: """ Get a chart or dashboard screenshot :raises: ReportScheduleScreenshotFailedError """ screenshot: Optional[BaseScreenshot] = None if self._report_schedule.chart: url = self._get_url(standalone="true") screenshot = ChartScreenshot( url, self._report_schedule.chart.digest, window_size=app.config["WEBDRIVER_WINDOW"]["slice"], thumb_size=app.config["WEBDRIVER_WINDOW"]["slice"], ) else: url = self._get_url() screenshot = DashboardScreenshot( url, self._report_schedule.dashboard.digest, window_size=app.config["WEBDRIVER_WINDOW"]["dashboard"], thumb_size=app.config["WEBDRIVER_WINDOW"]["dashboard"], ) image_url = self._get_url(user_friendly=True) user = self._get_screenshot_user() image_data = screenshot.compute_and_cache( user=user, cache=thumbnail_cache, force=True, ) if not image_data: raise ReportScheduleScreenshotFailedError() return ScreenshotData(url=image_url, image=image_data)
def _get_screenshot(self) -> bytes: """ Get a chart or dashboard screenshot :raises: ReportScheduleScreenshotFailedError """ screenshot: Optional[BaseScreenshot] = None if self._report_schedule.chart: url = self._get_url(standalone="true") screenshot = ChartScreenshot( url, self._report_schedule.chart.digest, window_size=app.config["WEBDRIVER_WINDOW"]["slice"], thumb_size=app.config["WEBDRIVER_WINDOW"]["slice"], ) else: url = self._get_url() screenshot = DashboardScreenshot( url, self._report_schedule.dashboard.digest, window_size=app.config["WEBDRIVER_WINDOW"]["dashboard"], thumb_size=app.config["WEBDRIVER_WINDOW"]["dashboard"], ) user = self._get_screenshot_user() try: image_data = screenshot.get_screenshot(user=user) except SoftTimeLimitExceeded: raise ReportScheduleScreenshotTimeout() except Exception as ex: raise ReportScheduleScreenshotFailedError( f"Failed taking a screenshot {str(ex)}") if not image_data: raise ReportScheduleScreenshotFailedError() return image_data
def cache_chart_thumbnail( url: str, digest: str, force: bool = False, window_size: Optional[WindowSize] = None, thumb_size: Optional[WindowSize] = None, ) -> None: with app.app_context(): # type: ignore if not thumbnail_cache: logger.warning("No cache set, refusing to compute") return None logger.info("Caching chart: %s", url) screenshot = ChartScreenshot(url, digest) with session_scope(nullpool=True) as session: user = security_manager.get_user_by_username( current_app.config["THUMBNAIL_SELENIUM_USER"], session=session ) screenshot.compute_and_cache( user=user, cache=thumbnail_cache, force=force, window_size=window_size, thumb_size=thumb_size, ) return None
def cache_chart_thumbnail(chart_id: int, force: bool = False) -> None: with app.app_context(): # type: ignore if not thumbnail_cache: logger.warning("No cache set, refusing to compute") return None logging.info(f"Caching chart {chart_id}") screenshot = ChartScreenshot(model_id=chart_id) user = security_manager.find_user(current_app.config["THUMBNAIL_SELENIUM_USER"]) screenshot.compute_and_cache(user=user, cache=thumbnail_cache, force=force)
def deliver_alert(alert_id: int, recipients: Optional[str] = None) -> None: alert = db.session.query(Alert).get(alert_id) logging.info("Triggering alert: %s", alert) img_data = None images = {} recipients = recipients or alert.recipients if alert.slice: chart_url = get_url_path("Superset.slice", slice_id=alert.slice.id, standalone="true") screenshot = ChartScreenshot(chart_url, alert.slice.digest) image_url = _get_url_path( "Superset.slice", user_friendly=True, slice_id=alert.slice.id, standalone="true", ) standalone_index = image_url.find("/?standalone=true") if standalone_index != -1: image_url = image_url[:standalone_index] user = security_manager.find_user( current_app.config["THUMBNAIL_SELENIUM_USER"]) img_data = screenshot.compute_and_cache( user=user, cache=thumbnail_cache, force=True, ) else: # TODO: dashboard delivery! image_url = "https://media.giphy.com/media/dzaUX7CAG0Ihi/giphy.gif" # generate the email # TODO add sql query results to email subject = f"[Superset] Triggered alert: {alert.label}" deliver_as_group = False data = None if img_data: images = {"screenshot": img_data} body = render_template( "email/alert.txt", alert_url=_get_url_path("AlertModelView.show", user_friendly=True, pk=alert.id), label=alert.label, sql=alert.sql, image_url=image_url, ) _deliver_email(recipients, deliver_as_group, subject, body, data, images)
def _get_screenshots(self) -> List[bytes]: """ Get chart or dashboard screenshots :raises: ReportScheduleScreenshotFailedError """ url = self._get_url() user = self._get_user() if self._report_schedule.chart: screenshot: Union[ ChartScreenshot, DashboardScreenshot] = ChartScreenshot( url, self._report_schedule.chart.digest, window_size=app.config["WEBDRIVER_WINDOW"]["slice"], thumb_size=app.config["WEBDRIVER_WINDOW"]["slice"], ) else: screenshot = DashboardScreenshot( url, self._report_schedule.dashboard.digest, window_size=app.config["WEBDRIVER_WINDOW"]["dashboard"], thumb_size=app.config["WEBDRIVER_WINDOW"]["dashboard"], ) try: image = screenshot.get_screenshot(user=user) except SoftTimeLimitExceeded as ex: logger.warning("A timeout occurred while taking a screenshot.") raise ReportScheduleScreenshotTimeout() from ex except Exception as ex: raise ReportScheduleScreenshotFailedError( f"Failed taking a screenshot {str(ex)}") from ex if not image: raise ReportScheduleScreenshotFailedError() return [image]
def _get_slice_screenshot(slice_id: int, session: Session) -> ScreenshotData: slice_obj = session.query(Slice).get(slice_id) chart_url = get_url_path("Superset.slice", slice_id=slice_obj.id, standalone="true") screenshot = ChartScreenshot(chart_url, slice_obj.digest) image_url = _get_url_path( "Superset.slice", user_friendly=True, slice_id=slice_obj.id, ) user = security_manager.find_user(current_app.config["THUMBNAIL_SELENIUM_USER"]) image_data = screenshot.compute_and_cache( user=user, cache=thumbnail_cache, force=True, ) session.commit() return ScreenshotData(image_url, image_data)
def deliver_alert(alert: Alert, recipients: Optional[str] = None) -> None: logging.info("Triggering alert: %s", alert) img_data = None images = {} recipients = recipients or alert.recipients if alert.slice: chart_url = get_url_path("Superset.slice", slice_id=alert.slice.id, standalone="true") screenshot = ChartScreenshot(chart_url, alert.slice.digest) cache_key = screenshot.cache_key() image_url = get_url_path("ChartRestApi.screenshot", pk=alert.slice.id, digest=cache_key) user = security_manager.find_user( current_app.config["THUMBNAIL_SELENIUM_USER"]) img_data = screenshot.compute_and_cache( user=user, cache=thumbnail_cache, force=True, ) else: # TODO: dashboard delivery! image_url = "https://media.giphy.com/media/dzaUX7CAG0Ihi/giphy.gif" # generate the email subject = f"[Superset] Triggered alert: {alert.label}" deliver_as_group = False data = None if img_data: images = {"screenshot": img_data} body = __( textwrap.dedent("""\ <h2>Alert: %(label)s</h2> <img src="cid:screenshot" alt="%(label)s" /> """), label=alert.label, image_url=image_url, ) _deliver_email(recipients, deliver_as_group, subject, body, data, images)
def _get_screenshot(self, report_schedule: ReportSchedule) -> ScreenshotData: """ Get a chart or dashboard screenshot :raises: ReportScheduleScreenshotFailedError """ url = self._get_url(report_schedule) screenshot: Optional[BaseScreenshot] = None if report_schedule.chart: screenshot = ChartScreenshot(url, report_schedule.chart.digest) else: screenshot = DashboardScreenshot(url, report_schedule.dashboard.digest) image_url = self._get_url(report_schedule, user_friendly=True) user = security_manager.find_user(app.config["THUMBNAIL_SELENIUM_USER"]) image_data = screenshot.compute_and_cache( user=user, cache=thumbnail_cache, force=True, ) if not image_data: raise ReportScheduleScreenshotFailedError() return ScreenshotData(url=image_url, image=image_data)
def thumbnail(self, pk: int, digest: str, **kwargs: Dict[str, bool]) -> WerkzeugResponse: """Get Chart thumbnail --- get: description: Compute or get already computed chart thumbnail from cache. parameters: - in: path schema: type: integer name: pk - in: path schema: type: string name: sha responses: 200: description: Chart thumbnail image content: image/*: schema: type: string format: binary 302: description: Redirects to the current digest 400: $ref: '#/components/responses/400' 401: $ref: '#/components/responses/401' 404: $ref: '#/components/responses/404' 500: $ref: '#/components/responses/500' """ chart = self.datamodel.get(pk, self._base_filters) if not chart: return self.response_404() if kwargs["rison"].get("force", False): cache_chart_thumbnail.delay(chart.id, force=True) return self.response(202, message="OK Async") # fetch the chart screenshot using the current user and cache if set screenshot = ChartScreenshot(pk).get_from_cache(cache=thumbnail_cache) # If not screenshot then send request to compute thumb to celery if not screenshot: cache_chart_thumbnail.delay(chart.id, force=True) return self.response(202, message="OK Async") # If digests if chart.digest != digest: return redirect( url_for(f"{self.__class__.__name__}.thumbnail", pk=pk, digest=chart.digest)) return Response(FileWrapper(screenshot), mimetype="image/png", direct_passthrough=True)
def _get_screenshot(self) -> ScreenshotData: """ Get a chart or dashboard screenshot :raises: ReportScheduleScreenshotFailedError """ url = self._get_url() screenshot: Optional[BaseScreenshot] = None if self._report_schedule.chart: screenshot = ChartScreenshot(url, self._report_schedule.chart.digest) else: screenshot = DashboardScreenshot( url, self._report_schedule.dashboard.digest ) image_url = self._get_url(user_friendly=True) user = self._get_screenshot_user() image_data = screenshot.compute_and_cache( user=user, cache=thumbnail_cache, force=True, ) if not image_data: raise ReportScheduleScreenshotFailedError() return ScreenshotData(url=image_url, image=image_data)
def _get_screenshots(self) -> List[bytes]: """ Get chart or dashboard screenshots :raises: ReportScheduleScreenshotFailedError """ image_data = [] screenshots: List[BaseScreenshot] = [] if self._report_schedule.chart: url = self._get_url() logger.info("Screenshotting chart at %s", url) screenshots = [ ChartScreenshot( url, self._report_schedule.chart.digest, window_size=app.config["WEBDRIVER_WINDOW"]["slice"], thumb_size=app.config["WEBDRIVER_WINDOW"]["slice"], ) ] else: tabs: Optional[List[str]] = json.loads(self._report_schedule.extra).get( "dashboard_tab_ids", None ) dashboard_base_url = self._get_url() if tabs is None: urls = [dashboard_base_url] else: urls = [f"{dashboard_base_url}#{tab_id}" for tab_id in tabs] screenshots = [ DashboardScreenshot( url, self._report_schedule.dashboard.digest, window_size=app.config["WEBDRIVER_WINDOW"]["dashboard"], thumb_size=app.config["WEBDRIVER_WINDOW"]["dashboard"], ) for url in urls ] user = self._get_user() for screenshot in screenshots: try: image = screenshot.get_screenshot(user=user) except SoftTimeLimitExceeded as ex: logger.warning("A timeout occurred while taking a screenshot.") raise ReportScheduleScreenshotTimeout() from ex except Exception as ex: raise ReportScheduleScreenshotFailedError( f"Failed taking a screenshot {str(ex)}" ) from ex if image is not None: image_data.append(image) if not image_data: raise ReportScheduleScreenshotFailedError() return image_data
def test_get_cached_chart_screenshot(self): """ Thumbnails: Simple get cached chart screenshot """ chart = db.session.query(Slice).all()[0] # Cache a test "image" screenshot = ChartScreenshot(model_id=chart.id) thumbnail_cache.set(screenshot.cache_key, self.mock_image) self.login(username="******") uri = f"api/v1/chart/{chart.id}/thumbnail/{chart.digest}/" rv = self.client.get(uri) self.assertEqual(rv.status_code, 200) self.assertEqual(rv.data, self.mock_image)
def _get_screenshot(self) -> bytes: """ Get a chart or dashboard screenshot :raises: ReportScheduleScreenshotFailedError """ screenshot: Optional[BaseScreenshot] = None if self._report_schedule.chart: url = self._get_url() logger.info("Screenshotting chart at %s", url) screenshot = ChartScreenshot( url, self._report_schedule.chart.digest, window_size=app.config["WEBDRIVER_WINDOW"]["slice"], thumb_size=app.config["WEBDRIVER_WINDOW"]["slice"], ) else: url = self._get_url() logger.info("Screenshotting dashboard at %s", url) screenshot = DashboardScreenshot( url, self._report_schedule.dashboard.digest, window_size=app.config["WEBDRIVER_WINDOW"]["dashboard"], thumb_size=app.config["WEBDRIVER_WINDOW"]["dashboard"], ) user = self._get_user() try: image_data = screenshot.get_screenshot(user=user) except SoftTimeLimitExceeded as ex: logger.warning("A timeout occurred while taking a screenshot.") raise ReportScheduleScreenshotTimeout() from ex except Exception as ex: raise ReportScheduleScreenshotFailedError( f"Failed taking a screenshot {str(ex)}" ) from ex if not image_data: raise ReportScheduleScreenshotFailedError() return image_data
def test_get_cached_chart_wrong_digest(self): """ Thumbnails: Simple get chart with wrong digest """ chart = db.session.query(Slice).all()[0] chart_url = get_url_path("Superset.slice", slice_id=chart.id, standalone="true") # Cache a test "image" screenshot = ChartScreenshot(chart_url, chart.digest) thumbnail_cache.set(screenshot.cache_key, self.mock_image) self.login(username="******") uri = f"api/v1/chart/{chart.id}/thumbnail/1234/" rv = self.client.get(uri) self.assertEqual(rv.status_code, 302) self.assertRedirects(rv, f"api/v1/chart/{chart.id}/thumbnail/{chart.digest}/")
def cache_chart_thumbnail( url: str, digest: str, force: bool = False, window_size: Optional[WindowSize] = None, thumb_size: Optional[WindowSize] = None, ) -> None: with app.app_context(): # type: ignore if not thumbnail_cache: logger.warning("No cache set, refusing to compute") return None logging.info("Caching chart at {url}") screenshot = ChartScreenshot(url, digest) user = security_manager.find_user( current_app.config["THUMBNAIL_SELENIUM_USER"]) screenshot.compute_and_cache( user=user, cache=thumbnail_cache, force=force, window_size=window_size, thumb_size=thumb_size, ) return None
def screenshot(self, pk: int, digest: str) -> WerkzeugResponse: """Get Chart screenshot --- get: description: Get a computed screenshot from cache. parameters: - in: path schema: type: integer name: pk - in: path schema: type: string name: digest responses: 200: description: Chart thumbnail image content: image/*: schema: type: string format: binary 302: description: Redirects to the current digest 400: $ref: '#/components/responses/400' 401: $ref: '#/components/responses/401' 404: $ref: '#/components/responses/404' 500: $ref: '#/components/responses/500' """ chart = self.datamodel.get(pk, self._base_filters) # Making sure the chart still exists if not chart: return self.response_404() # TODO make sure the user has access to the chart # fetch the chart screenshot using the current user and cache if set img = ChartScreenshot.get_from_cache_key(thumbnail_cache, digest) if img: return Response(FileWrapper(img), mimetype="image/png", direct_passthrough=True) # TODO: return an empty image return self.response_404()
def cache_screenshot(self, pk: int, **kwargs: Dict[str, bool]) -> WerkzeugResponse: """ --- get: description: Compute and cache a screenshot. parameters: - in: path schema: type: integer name: pk - in: query name: q content: application/json: schema: $ref: '#/components/schemas/screenshot_query_schema' responses: 200: description: Chart async result content: application/json: schema: $ref: "#/components/schemas/ChartCacheScreenshotResponseSchema" 302: description: Redirects to the current digest 400: $ref: '#/components/responses/400' 401: $ref: '#/components/responses/401' 404: $ref: '#/components/responses/404' 500: $ref: '#/components/responses/500' """ rison_dict = kwargs["rison"] window_size = rison_dict.get("window_size") or (800, 600) # Don't shrink the image if thumb_size is not specified thumb_size = rison_dict.get("thumb_size") or window_size chart = self.datamodel.get(pk, self._base_filters) if not chart: return self.response_404() chart_url = get_url_path("Superset.slice", slice_id=chart.id, standalone="true") screenshot_obj = ChartScreenshot(chart_url, chart.digest) cache_key = screenshot_obj.cache_key(window_size, thumb_size) image_url = get_url_path("ChartRestApi.screenshot", pk=chart.id, digest=cache_key) def trigger_celery() -> WerkzeugResponse: logger.info("Triggering screenshot ASYNC") kwargs = { "url": chart_url, "digest": chart.digest, "force": True, "window_size": window_size, "thumb_size": thumb_size, } cache_chart_thumbnail.delay(**kwargs) return self.response( 202, cache_key=cache_key, chart_url=chart_url, image_url=image_url, ) return trigger_celery()