Module presalytics

Presalytics Python Library

For more information, visit https://presalytics.io.

Overview

The Presalytics Python Library streamlines analytic operations across analysts, executives, consultants, and developers. These tools enable for a simplified workflow for analysts to rapidly generate client-ready presentation materials and web content that update in real-time and easily scale across your user-base.

Our objective when building this platform is make the analyst experience as simple as possible.
Set up time for new user should take less than half an hour, and all features are self-service and self-explanatory.

Of course, if you have any questions or need help to get going, you can contact us or quickly get the help you need on our slack channel (Join Here!).

To get going quickly, you can browse our Getting Started page and review some examples.

For more advanced users and developers, you can learn more about the API by reviewing the service structure to build a better understanding of the API and its security features.

Installation

The Presalytics Python Library is avialabe in python package index, and can be installed via pip:

pip install presalytics

Contributing

Presalytics.io is on Github. Bug reports and pull requests are strongly encouraged at the package repository. If you encounter any problems or have any suggestions for the API endpoints that this libary interacts with at https://api.presalytics.io, please open an issue in the API repository.

License

The Presalytics Python Library can be used in any of your applications and is covered by the MIT License. This library exchanges infromation with other web APIs that may be proprietary and carry their own licensing restriction. More more information on licensing, please contact inquires@presalytics.io.

Expand source code Browse git
"""
# Presalytics Python Library
For more information, visit https://presalytics.io.

# Overview

The Presalytics Python Library streamlines analytic operations across analysts, executives, consultants, 
and developers.  These tools enable for a simplified workflow for analysts to rapidly generate 
client-ready presentation materials and web content that update in real-time and easily scale across your user-base.  

Our objective when building this platform is make the analyst experience as simple as possible.  
Set up time for new user should take less than half an hour, and all features are self-service and self-explanatory.  

Of course, if you have any questions or need help to get going, you can [contact us](/contact-us) or 
quickly get the help you need on our [slack channel](https://presalytics.slack.com) 
([Join Here!](https://join.slack.com/t/presalytics/shared_invite/enQtODExMjc3MDE1Nzc5LWU0ZDlhZTgwZTM3MzQ4Yzc4Nzk4Zjc0NmQ3YjgzNTEwODdlYjM0ZjFkZWI4Y2ZhNzBmOTZhMzA2MzE3YjFiZTg)).

To get going quickly, you can browse our [Getting Started](https://presalytics.io/docs/getting-started/) page and review 
some [examples](https://presalytics.io/docs/examples).

For more advanced users and developers, you can learn more about the API by reviewing 
the [service structure](https://presalytics.io/docs/how-it-works) to build a better understanding 
of the API and its [security](https://presalytics.io/docs/develpers/security) features.

# Installation

The Presalytics Python Library is avialabe in python package index, and can be installed via pip:

~~~~python
pip install presalytics
~~~~

# Contributing

Presalytics.io is on [Github](https://github.com/presalytics).  Bug reports and pull requests are strongly encouraged at
the package [repository](https://github.com/presalytics/python-client). If you encounter any problems or have any suggestions for 
the API endpoints that this libary interacts with at https://api.presalytics.io, please open an issue in the 
[API repository](https://github.com/presalytics/Presalytics-API).

# License

The Presalytics Python Library can be used in any of your applications and is covered by the MIT License.  This library
exchanges infromation with other web APIs that may be proprietary and carry their own licensing restriction.  More more
information on licensing, please contact [inquires@presalytics.io](mailto:inquires@presalytics.io).

"""
import os
import environs
import logging
import pkg_resources
import presalytics.lib
import presalytics.lib.logger
import presalytics.lib.plugins
import presalytics.lib.plugins.base
import presalytics.lib.config_loader
import presalytics.story
import presalytics.story.components

env = environs.Env()
env.read_env()

# A comma-separated list of paths to search of config.py, plugin classes, and component classes
autodiscover_paths = env.list('AUTODISCOVER_PATHS', [])

CONFIG = presalytics.lib.config_loader.load_config(additional_paths=autodiscover_paths)
"""
Nested `dict` containing runtime configuration values for the Presalytics Python Library.
Typically, these are reuseable values stored in separate file that are loaded when the 
module in imported via the `import presalytics` command.  This top-level module then
runs the `presalytics.lib.config_loader.load_config` method into load values into the
the `CONFIG` global variable.  Modules through the package use the `CONFIG` variable to
simplify their API calls store conststants for use throughout the package.  See 
`presalytics.lib.config_loader.load_config` for ways to programmatically load the
`CONFIG`.

Configuration Values
----------

USE_LOGGER : bool, optional
    Toggles whether the presalytics verbose file logger should be used.  Helpful for 
    tracing exceptions while writing code.  Default is True.

LOG_LEVEL : str, optional
    Defaults to `DEBUG`

USERNAME : str, optional
    The user's Presalytics API email/username.  This is the email address that the user uses when logging in at 
    https://login.presalytics.io.  Will be passed to instances of the `presalytics.client.api.Client` object.

PASSWORD : str, optional
    The user's Presalytics API username.  Will be passed to instances of the 
    `presalytics.client.api.Client` object.  If running in an insecure or 
    multiuser environment, leave this blank and let the `presalytics.client.api.Client`
    object handle token acquisition via browser-based login.

DELEGATE_LOGIN: bool, optional
    Defaults to False.  Indicates whether the client would redirect to a browser to 
    acquire an API token. If `DELEGATE_LOGIN` is `True`, when the `presalytics.client.api.Client` does not have 
    access to a valid API token, the client will raise a `presalytics.lib.exceptions.InvalidTokenException`.
    The default operation will automatically open a new browser tab to acquire a new token 
    via website client from the presalytics.io login page.  Putting this setting to True is
    useful for server-side development.

CACHE_TOKENS: bool, optional
    Defaults to True.  Indicates whether the `presalytics.client.api.Client` should store
    tokens in the current working directory in a file call "token.json". This should be
    set to False for mutli-user environments.

CLIENT_ID : str, optional
    For developer use. Allows developers to implement a `client_credentials` OpenID
    Connect login.  Defaults to "python-client".  

CLIENT_SECRET : str, optional
    For developer use. Allows developers to implement a `client_credentials` OpenID
    Connect login.  Defaults to None.  

VERIFY_HTTPS : bool, optional
    For developer use.  Allows for unencrypted connections.  Defaults to True.  No 
    reason to turn this to False unless you're in a complex development scenario
    and you know what you're doing.

HOSTS : dict, optional
    For developer use.  Allows API class to target hosts other than api.presalytics.io

REDIRECT_URI : string, optional
    For developer use.  Useful if implementing authorization code flow for and OpenID Connect client.
    Redirect URIs must be approved by Presalytics API devops for use in client applications.

RESERVED_NAMES: list of str, optional
    A list of filenames for *.py files in the current workspace that should be ignored by the 
    registries. 

IGNORE_PATHS: list of str, optional
    A list of paths to not to include in registry autosdiscover

BROWSER_API_HOST: dict, optional
    If present, the root url for browser-based api calls.  Each service can have an independent browser host.
    Service keys include `OOXML_AUTOMATION`, `STORY`, `DOC_CONVERTER`, and `SITE`.  May be required when services are running on a cluster that
    deletegates certificate authentication to an external service or in debug/test environments.  
    See for more info: https://developer.mozilla.org/en-US/docs/Web/Security/Mixed_content

The object can also take on values for user-defined extensions, and please consult
the documentation for those package for those vairables definition.
"""



file_logger = CONFIG.get("USE_LOGGER", True)
log_level = CONFIG.get("LOG_LEVEL", logging.DEBUG)

presalytics.lib.logger.configure_logger(log_level=log_level, file_logger=file_logger)

registry_kwargs = {
    'show_errors': False,
    'autodiscover_paths': autodiscover_paths,
    'reserved_names': CONFIG.get("RESERVED_NAMES", []),
    'ignore_paths': CONFIG.get("IGNORE_PATHS", [])
}

PLUGINS = presalytics.lib.plugins.base.PluginRegistry(**registry_kwargs)
"""
Instance of `presalytics.lib.plugins.base.PluginRegistry`.  A container listing the
Presalytics Library Plugins available and loaded in this environment. This instance is used 
by `presalytics.story.components.Renderer` subclasses (e.g., `presalytics.story.revealer.Revealer`)
to write scripts and links into stories.
"""

COMPONENTS = presalytics.story.components.ComponentRegistry(**registry_kwargs)
"""
Instance of `presalytics.story.components.ComponentRegistry`.  Registry for Library components and 
component instances.  A container listing the Presalytics Library components and instances available 
and loaded in this environment. This instance is used by `presalytics.story.components.Renderer` subclasses 
(e.g., `presalytics.story.revealer.Revealer`) to convert widgets, pages, and themes into stories.
"""

try:
    __version__ = pkg_resources.require(presalytics.__name__)[0].version
except Exception:
    __version__ = "build"

from presalytics.client.api import Client
from presalytics.lib.plugins.base import PluginBase
from presalytics.lib.plugins.external import ApprovedExternalLinks, ApprovedExternalScripts
from presalytics.lib.plugins.jinja import JinjaPluginMakerMixin
from presalytics.lib.plugins.local import LocalStylesPlugin
from presalytics.lib.plugins.matplotlib import Mpld3Plugin
from presalytics.lib.plugins.ooxml import OoxmlTheme
from presalytics.lib.plugins.reveal import RevealConfigPlugin
from presalytics.lib.plugins.reveal_theme import RevealCustomTheme
from presalytics.lib.plugins.scss import ScssPlugin
from presalytics.lib.templates.base import (
    JinjaTemplateBuilder,
    BootstrapCustomTemplate
)
from presalytics.lib.widgets.matplotlib import MatplotlibFigure, MatplotlibResponsiveFigure
from presalytics.lib.widgets.d3 import (
    D3Widget
)
from presalytics.lib.widgets.ooxml import (
    OoxmlWidgetBase,
    OoxmlFileWidget, 
    OoxmlEndpointMap,
    ChartUpdaterWidget,
    TableUpdaterWidget
)
from presalytics.story.outline import StoryOutline
from presalytics.story.revealer import Revealer
from presalytics.story.components import WidgetBase, PageTemplateBase, Renderer, ThemeBase
from presalytics.lib.tools.ooxml_tools import (
    create_story_from_ooxml_file
)
from presalytics.lib.tools.story_tools import story_post_file_bytes
from presalytics.lib.tools.component_tools import (
    create_outline_from_page,
    create_outline_from_widget
)
from presalytics.lib.widgets.ooxml_editors import (
    OoxmlEditorWidget,
    XmlTransformBase,
    ChangeShapeColor,
    TextReplace,
    MultiXmlTransform
)


__all__ = [
    'CONFIG',
    'COMPONENTS',
    'PLUGINS',
    'Client',
    'StoryOutline',
    'Renderer',
    'Revealer',
    'MatplotlibFigure',
    'MatplotlibResponsiveFigure',
    'OoxmlFileWidget',
    'OoxmlEndpointMap',
    'OoxmlWidgetBase',
    'OoxmlEditorWidget',
    'D3Widget',
    'ChartUpdaterWidget',
    'TableUpdaterWidget',
    'XmlTransformBase',
    'ChangeShapeColor',
    'TextReplace',
    'MultiXmlTransform',
    'ApprovedExternalLinks',
    'ApprovedExternalScripts',
    'LocalStylesPlugin',
    'OoxmlTheme',
    'RevealConfigPlugin',
    'RevealCustomTheme',
    'JinjaTemplateBuilder',
    'WidgetBase',
    'PageTemplateBase',
    'ScssPlugin',
    'create_story_from_ooxml_file',
    'story_post_file_bytes',
    'create_outline_from_page',
    'create_outline_from_widget'
]

Sub-modules

presalytics.cli

Command Line Interface for Presalytics Python Library …

presalytics.client

This module conatains objects for interacting with the Presalytics API. It has three submodules: presalytics.client.presalytics_doc_converter, …

presalytics.lib

Contains configuration and library objects for users to build upon when create story objects

presalytics.story

Conains base objects for rendering, building, serializing, and view Story objects

Global variables

var CONFIG

Nested dict containing runtime configuration values for the Presalytics Python Library. Typically, these are reuseable values stored in separate file that are loaded when the module in imported via the import presalytics command. This top-level module then runs the load_config() method into load values into the the CONFIG global variable. Modules through the package use the CONFIG variable to simplify their API calls store conststants for use throughout the package. See load_config() for ways to programmatically load the CONFIG.

Configuration Values

USE_LOGGER : bool, optional
Toggles whether the presalytics verbose file logger should be used. Helpful for tracing exceptions while writing code. Default is True.
LOG_LEVEL : str, optional
Defaults to DEBUG
USERNAME : str, optional
The user's Presalytics API email/username. This is the email address that the user uses when logging in at https://login.presalytics.io. Will be passed to instances of the Client object.
PASSWORD : str, optional
The user's Presalytics API username. Will be passed to instances of the Client object. If running in an insecure or multiuser environment, leave this blank and let the Client object handle token acquisition via browser-based login.
DELEGATE_LOGIN : bool, optional
Defaults to False. Indicates whether the client would redirect to a browser to acquire an API token. If DELEGATE_LOGIN is True, when the Client does not have access to a valid API token, the client will raise a InvalidTokenException. The default operation will automatically open a new browser tab to acquire a new token via website client from the presalytics.io login page. Putting this setting to True is useful for server-side development.
CACHE_TOKENS : bool, optional
Defaults to True. Indicates whether the Client should store tokens in the current working directory in a file call "token.json". This should be set to False for mutli-user environments.
CLIENT_ID : str, optional
For developer use. Allows developers to implement a client_credentials OpenID Connect login. Defaults to "python-client".
CLIENT_SECRET : str, optional
For developer use. Allows developers to implement a client_credentials OpenID Connect login. Defaults to None.
VERIFY_HTTPS : bool, optional
For developer use. Allows for unencrypted connections. Defaults to True. No reason to turn this to False unless you're in a complex development scenario and you know what you're doing.
HOSTS : dict, optional
For developer use. Allows API class to target hosts other than api.presalytics.io
REDIRECT_URI : string, optional
For developer use. Useful if implementing authorization code flow for and OpenID Connect client. Redirect URIs must be approved by Presalytics API devops for use in client applications.
RESERVED_NAMES : list of str, optional
A list of filenames for *.py files in the current workspace that should be ignored by the registries.
IGNORE_PATHS : list of str, optional
A list of paths to not to include in registry autosdiscover
BROWSER_API_HOST : dict, optional
If present, the root url for browser-based api calls. Each service can have an independent browser host. Service keys include OOXML_AUTOMATION, STORY, DOC_CONVERTER, and SITE. May be required when services are running on a cluster that deletegates certificate authentication to an external service or in debug/test environments.
See for more info: https://developer.mozilla.org/en-US/docs/Web/Security/Mixed_content

The object can also take on values for user-defined extensions, and please consult the documentation for those package for those vairables definition.

var COMPONENTS

Instance of ComponentRegistry. Registry for Library components and component instances. A container listing the Presalytics Library components and instances available and loaded in this environment. This instance is used by Renderer subclasses (e.g., Revealer) to convert widgets, pages, and themes into stories.

var PLUGINS

Instance of PluginRegistry. A container listing the Presalytics Library Plugins available and loaded in this environment. This instance is used by Renderer subclasses (e.g., Revealer) to write scripts and links into stories.

Functions

def create_story_from_ooxml_file(filename, client_info={})

Utility Method for building stories into the Presalytics API directly from a Presentation or Spreadsheet file.

Parameters

filename : str
A string contain the local path to a presenation or spreadsheet object.
client_info : dict, optional
A dictionary containing arguments that will be unpacked and passed to a Client object on intialization. This dictionary can include the token, cache_tokens, delegate_login values. See Client for more information.

Returns:

A Story containing information about the Story object in the Presalytics API

Expand source code Browse git
def create_story_from_ooxml_file(filename: str, client_info={}) -> 'Story':
    """
    Utility Method for building stories into the Presalytics API directly from a Presentation or Spreadsheet file. 

    Parameters
    ----------
    filename : str
        A string contain the local path to a presenation or spreadsheet object.

    client_info : dict, optional
        A dictionary containing arguments that will be unpacked and passed to a `presalytics.client.api.Client` object on intialization. 
        This dictionary can include the `token`, `cache_tokens`, `delegate_login` values.  See `presalytics.client.api.Client` for more information.

    Returns:
    ----------
    A `presalytics.client.presalytics_story.models.story.Story` containing information about the Story object in the Presalytics API

    """

    story: 'Story'
    logger.info("Starting presalytics tool: create_story_from_ooxml_file")
    logger.info("Intializing presalytics client.")
    
    client = presalytics.Client(**client_info)
    logger.info("Sending file to presalytics server for document processing and base story creation")
    story = client.story.story_post_file(file=filename)
    logger.info("Creating local instances of file widgets")
    outline = presalytics.StoryOutline.load(story.outline)
    for i in range(0, len(outline.pages)):
        page = outline.pages[i]
        for j in range(0, len(page.widgets)):
            widget = page.widgets[j]
            logger.info('Creating OoxmlFileWidget with name "{}"'.format(widget.name))
            inst = presalytics.OoxmlFileWidget.deserialize(widget, **client_info)
            presalytics.COMPONENTS.register(inst)
            logger.info('Rewriting outline with widget: "{}"'.format(widget.name))
            outline.pages[i].widgets[j] = inst.serialize()
    story.outline = outline.dump()
    return story
def story_post_file_bytes(client, binary_obj, filename, mime_type=None)

Create a Presalytics API Story object from a file-like io.BytesIO object. Helpful for server-side interaction with the Presalytics Story API

Parameters

presalytics.client : Client
A client object for making api calls
binary_obj : io.BytesIO
A file-like object for storing file-data in memory. Often found in multipart messages uploaded from browsers.
filename : str
The filename of the object to be uploaded
mimetype : str, optional
If known, please add the mimetype of the file. Otherwise, this method will execute an additional API call ascertain the file's mimetype

Returns

A Story containing information about the Story object in the Presalytics API

Expand source code Browse git
def story_post_file_bytes(client: 'Client', 
                          binary_obj: 'BytesIO', 
                          filename: str,
                          mime_type: str = None):
    """
    Create a Presalytics API Story object from a file-like `io.BytesIO` object.  Helpful for server-side 
    interaction with the Presalytics Story API

    Parameters
    ----------
    client : presalytics.client.api.Client
        A client object for making api calls
    
    binary_obj : io.BytesIO
        A file-like object for storing file-data in memory.  Often found in multipart messages
        uploaded from browsers.
    
    filename : str
        The filename of the object to be uploaded

    mimetype : str, optional
        If known, please add the mimetype of the file.  Otherwise, this method will execute an 
        additional API call ascertain the file's mimetype

    Returns
    ----------
    A `presalytics.client.presalytics_story.models.story.Story` containing information about the Story object in the Presalytics API
    """
    if not mime_type:
        mime_type = presalytics.lib.tools.ooxml_tools.get_mime_type_from_filename(client, filename)
    _file = {'file': (filename, binary_obj, mime_type,)}
    headers = client.get_auth_header()
    headers.update(client.get_request_id_header())
    headers.update({
        'User-Agent': client.story.api_client.user_agent,
        'Accept': 'application/json'
    })
    endpoint = urllib.parse.urljoin(client.story.api_client.configuration.host, 'story/file')
    try:
        resp = requests.post(endpoint, headers=headers, files=_file)
    except Exception as ex:
        message = "An error occured in the presalytics API client"
        if locals().get("resp", None):
            code = resp.status_code
        else:
            code = 500
        raise presalytics.lib.exceptions.ApiError(message=message, status_code=code)
            
    data = resp.json()
    if resp.status_code > 299:
        logger.error(data['detail'])
        raise presalytics.lib.exceptions.ApiError(message=data["detail"], status_code=resp.status_code)
    else:
        try:
            story = client.story.api_client._ApiClient__deserialize(data, 'Story')
            return story
        except Exception as ex:
            logger.error("Story object could not be deserialized.")
            logger.exception(ex)
        return data
def create_outline_from_page(page, title=None, description=None)

Creates a StoryOutline from a subclass of PageTemplateBase. Useful for quick starts, demos, and rapidly sharing content without having to manaully build a StoryOutline.

Parameters

page : subclass of PageTemplateBase
The Page component that you want to build the story from
title : str, optional
The title of the StoryOutline. Defaults to the name of the page.
description : str, optional
The description of the story. Autopopulated if unassigned.
Expand source code Browse git
def create_outline_from_page(page: 'PageTemplateBase',
                             title: str = None,
                             description: str = None) -> 'StoryOutline':
    """
    Creates a `presalytics.story.outline.StoryOutline` from a subclass of 
    `presalytics.story.components.PageTemplateBase`.  Useful for quick starts, demos, and 
    rapidly sharing content without having to manaully build a `StoryOutline`.

    Parameters
    ----------

    page : subclass of presalytics.story.components.PageTemplateBase
        The Page component that you want to build the story from

    title : str, optional
        The title of the `presalytics.story.outline.StoryOutline`.  Defaults to
        the name of the page.

    description : str, optional
        The description of the story.  Autopopulated if unassigned.

    """
    info = presalytics.story.outline.Info(
        revision="0",
        date_created=datetime.datetime.now().astimezone(datetime.timezone.utc).isoformat(),
        date_modified=datetime.datetime.now().astimezone(datetime.timezone.utc).isoformat(),
        created_by=presalytics.CONFIG["USERNAME"],
        modified_by=presalytics.CONFIG["USERNAME"],
        revision_notes="Created by 'create_outline_from_page' method"
    )

    page_outline = page.serialize()

    if not description:
        description = "{0} created by 'create_outline_from_page' method".format(page_outline.name)

    if not title:
        title = page_outline.name

    outline = presalytics.story.outline.StoryOutline(
        info=info,
        pages=[page],
        description=description,
        title=title,
        themes=[]
    )

    return outline
def create_outline_from_widget(widget, page_name=None, title=None, description=None)

Creates a StoryOutline from a subclass of WidgetBase. Useful for quickstarts, demos, and rapidly sharing content without having to manaully build a StoryOutline.

Parameters

widget : subclass of WidgetBase
The widget component that you want to build the story from
page_name : str, optional
The name you want to give the Page in the outline. Defaults to the name of the supplied widget
title : str, optional
The title of the StoryOutline. Defaults to page_name.
description : str, optional
The description of the story. Autopopulated if unassigned.
Expand source code Browse git
def create_outline_from_widget(widget: 'WidgetBase',
                               page_name: str = None,
                               title: str = None,
                               description: str = None) -> 'StoryOutline':
    """
    Creates a `presalytics.story.outline.StoryOutline` from a subclass of 
    `presalytics.story.components.WidgetBase`.  Useful for quickstarts, demos, and 
    rapidly sharing content without having to manaully build a `StoryOutline`.

    Parameters
    ----------

    widget : subclass of presalytics.story.components.WidgetBase
        The widget component that you want to build the story from

    page_name : str, optional
        The name you want to give the `presalytics.story.outline.Page` in the outline.
        Defaults to the name of the supplied `widget`

    title : str, optional
        The title of the `presalytics.story.outline.StoryOutline`.  Defaults to
        `page_name`.

    description : str, optional
        The description of the story.  Autopopulated if unassigned.

    """
    info = presalytics.story.outline.Info(
        revision="0",
        date_created=datetime.datetime.now().astimezone(datetime.timezone.utc).isoformat(),
        date_modified=datetime.datetime.now().astimezone(datetime.timezone.utc).isoformat(),
        created_by=presalytics.CONFIG["USERNAME"],
        modified_by=presalytics.CONFIG["USERNAME"],
        revision_notes="Created by 'create_outline_from_widget' method"
    )

    outline_widget = widget.serialize()
    if not page_name:
        page_name = outline_widget.name

    page = presalytics.story.outline.Page(
        name=page_name,
        kind="widget-page",
        widgets=[outline_widget]
    )
    if not description:
        description = "{0} with {1} created by 'create_outline_from_widget' method".format(page_name, outline_widget.name)

    if not title:
        title = page_name

    outline = presalytics.story.outline.StoryOutline(
        info=info,
        pages=[page],
        description=description,
        title=title,
        themes=[]
    )

    return outline

Classes

class Client (username=None, password=None, delegate_login=False, token=None, cache_tokens=False, client_id=None, client_secret=None, **kwargs)

Class for interacting with Presalytics API endpoints

The Client class creates a simple interface for user to interactive with the Presalytics API and is the primary building block for user-built automation of stories, dashboards, and interactive presentations.

A client instance wraps python functions around Presalytics API endpoints and manages user authentication. On initialization, he client checks the status of a user authentication the expiry of their refresh an access tokens. When needed, the client will open a browser to prompt the user to login at the presalytics.io login page (or raise an InvalidTokenException when delegate_login is True).

After authenication, users can call the methods bound to the story, ooxml_automation, and doc_converter attributes to make calls in into the Presalytics API.

A note for server-side development:

The client class can automatically cache tokens in a file called "token.json", located in the python's current working directory. This is done so users running scripts accross multiple client instances do not have to acquire a new token every time an API call is made. If building a client to operate in a multi-user environment, this behavior should be turned off so that one user cannot not pull one another's tokens. To do this, ensure the following parameters are pass to the configuration either via initialization or in a CONFIG file:

cache_tokens = False,
delegate_login = True

When delegate login is True, the client assumes that the application creating instances of the client object will handle user authentication. The simplest way to do this is to pass a token to the client via the "token" keyword argument.

Parameters

username : str, optional
Defaults to None. The user's Presalytics API username. This keyword will take precedence over a passed to the client via CONFIG. The username must either be present in CONFIG or be passed in via keyword, otherwise the client will raise a MissingConfigException.
password : str, optional
Defaults to None. The user's Presalytics API password. This useful for quickly testing scripts, but in most scenario users should not be passing plaintext into the client via this keyword. In a secure, single-user environment, passwords are better placed in the CONFIG object for reuseability. A more secure is to leave passwords out of the configuration, keep delegate_login = False, and acquire tokens via the browser.
delegate_login : bool, optional
Defaults to False. Indicates whether the client would redirect to a browser to acquire an API token. If DELEGATE_LOGIN is True, when the Client does not have access to a valid API token, the client will raise a InvalidTokenException. The default operation will automatically open a new browser tab to acquire a new token via website client from the presalytics.io login page. Putting this setting to True is useful for server-side development.
token : dict, optional

Defaults to None. A dictionary contain information about tokens acquire from auth.presalytics.io. The dictionary must contain an access_token, a refresh_token, and entries contiaing information about token expiry.

Token expiry information can either passed in ISO 8601 formatted string with a UTC offset as dictionary keys access_token_expire_time and refresh_token_expire_time or an integer in seconds with the corresponding dictionary keysexpires_in and refresh_expires_in.

if the dict passed in via this keywork does is not have the correct entries, the client will raise an InvalidTokenException.

cache_tokens : bool, optional
Defaults to False. Toggles whether or the client should cache its acquired tokens in file called "token.json" in the current working directory. Minimizes the number of times a user is required to login. Set to False in multi-user environments.

Attributes

direct_grant : bool
Indicates whether an token will be acquire via the "direct_grant" OpenID Connect flow. Usually indicates whether the user has supplied a passwork to the client either through CONFIG ro during object initialization.
doc_converter : DefaultApi

Interface to the Presalytics API Doc Converter service. The object contains methods that enable the client to make api calls that return deserialized objects from the Presalytics API, simplying user and developer interaction with the Presaltytics API. API calls can be generated as follows:

client = presalytics.Client()
api_obj = client.doc_converter.{operation_id}(*args)

where {operation_id} is the operationId assocated with the endpoint specified the Doc Converter Service OpenAPI Contract , and *args are the corresponding arguments that are passed to the method. A complete list of the avialable methods is shown on the DefaultApi object.

Note: This attribute contains automatically generated methods via the OpenAPI generator. The DefaultApi has been passed an an api_client keyword argument with an instance of DocConverterApiClientWithAuth, which adds an authentication and request processing middleware layer to the default sub package built via code generatation.

ooxml_automation : DefaultApi

Interface to the Presalytics API Ooxml Automation service. The object contains methods that enable the client to make api calls that return deserialized objects from the Presalytics API, simplying user and developer interaction with the Presaltytics API. API calls can be generated as follows:

client = presalytics.Client()
api_obj = client.ooxml_automation.{operation_id}(*args)

where {operation_id} is the operationId assocated with the endpoint specified the Ooxml Automation Service OpenAPI Contract , and *args are the corresponding arguments that are passed to the method. A complete list of the avialable methods is shown on the DefaultApi object.

Note: This attribute contains automatically generated methods via the OpenAPI generator. The DefaultApi has been passed an an api_client keyword argument with an instance of OoxmlAutomationApiClientWithAuth, which adds an authentication and request processing middleware layer to the default sub package built via code generatation.

presalytics.story : DefaultApi

Interface to the Presalytics API Ooxml Automation service. The object contains methods that enable the client to make api calls that return deserialized objects from the Presalytics API, simplying user and developer interaction with the Presaltytics API. API calls can be generated as follows:

client = presalytics.Client()
api_obj = client.story.{operation_id}(*args)

where {operation_id} is the operationId assocated with the endpoint specified the Ooxml Automation Service OpenAPI Contract , and *args are the corresponding arguments that are passed to the method. A complete list of the avialable methods is shown on the DefaultApi object.

Note: This attribute contains automatically generated methods via the OpenAPI generator. The DefaultApi has been passed an an api_client keyword argument with an instance of StoryApiClientWithAuth, which adds an authentication and request processing middleware layer to the default sub package built via code generatation.

client_id : str
The client_id that is used OpenID Connect login. Defaults to "python-client".
client_secret : str, optional
The client_secret used during OpenID Connect login. Useful confidential_client is True.
confidential_client : bool
Indicates whether a this client can obtain tokens from auth.presalytics.io without a user under OpenID Connect grant type "confidential_client". Requires a client_secret. Default is False.
oidc : presalytics.client.oidc.OidcClient
A middleware class to help acquire and validate tokens from login.presalytics.io.
token_util : presalytics.client.auth.TokenUtil
A handler for managing an caching tokens acquired from auth.presalytics.io.
site_host : str
The login site host for acquiring tokens. Set from CONFIG with keyword ["SITE"]["HOST"]. Defaults to https://presalytics.io.
redirect_uri : str
Useful if implementing authorization code flow for and OpenID Connect client. Redirect URIs must be approved by Presalytics API devops for use in client applications. Set from Set from CONFIG with keyword ["REDIRECT_URI"]. Defaults to https://presalytics.io/user/login-success.
login_sleep_interval : int
The duration (in seconds) between attempts to acquire a token after browser-based authentication. Defaults to 5 seconds.
login_timeout : int
Defaults to 60 seconds. The amount of time the client will attempt to acquire a token after the https://presalytics.io authenicates a user. Raises a LoginTimeout if the user has not authenticated by the time the interval has expired.
Expand source code Browse git
class Client(object):
    """ Class for interacting with Presalytics API endpoints

    The Client class creates a simple interface for user to interactive with the 
    Presalytics API and is the primary building block for user-built automation of stories, 
    dashboards, and interactive presentations.

    A client instance wraps python functions around Presalytics API endpoints and
    manages user authentication. On initialization, he client checks the status of 
    a user authentication the expiry of their refresh an access tokens.  When needed,
    the client will open a browser to prompt the user to login at the presalytics.io
    login page (or raise an `presalytics.lib.exceptions.InvalidTokenException` when 
    `delegate_login` is `True`).

    After authenication, users can call the methods bound to the story, ooxml_automation, 
    and doc_converter attributes to make calls in into the Presalytics API.

    *A note for server-side development*:

    The client class can automatically cache tokens in a file called 
    "token.json", located in the python's current working directory.  This is done so
    users running scripts accross multiple client instances do not have to acquire a new token 
    every time an API call is made. If building a client to operate in a multi-user environment, 
    this behavior should be turned off so that one user cannot not pull one another's tokens.
    To do this, ensure the following parameters are pass to the configuration either 
    via initialization or in a `presalytics.CONFIG` file: 
    
        cache_tokens = False,
        delegate_login = True
    
    When delegate login is True, the client assumes that the application creating 
    instances of the client object will handle user authentication.  The simplest way
    to do this is to pass a token to the client via the "token" keyword argument.

    Parameters
    ----------

    username : str, optional
        Defaults to None.  The user's Presalytics API username.  This keyword will take precedence over a passed to the client
        via `presalytics.CONFIG`.  The username must either be present in `presalytics.CONFIG` or be passed in 
        via keyword, otherwise the client will raise a `presalytics.lib.exceptions.MissingConfigException`.

    password : str, optional
        Defaults to None.  The user's Presalytics API password.  This useful for quickly testing scripts, but in most 
        scenario users should not be passing plaintext into the client via this keyword.  In a secure, single-user 
        environment, passwords are better placed in the `presalytics.CONFIG` object for reuseability.  A more secure
        is to leave passwords out of the configuration, keep `delegate_login` = `False`, and acquire tokens via the browser.
 
    delegate_login : bool, optional
        Defaults to False.  Indicates whether the client would redirect to a browser to 
        acquire an API token. If `DELEGATE_LOGIN` is `True`, when the `presalytics.client.api.Client` does not have 
        access to a valid API token, the client will raise a `presalytics.lib.exceptions.InvalidTokenException`.
        The default operation will automatically open a new browser tab to acquire a new token 
        via website client from the presalytics.io login page.  Putting this setting to True is
        useful for server-side development.

    token : dict, optional
        Defaults to None.  A dictionary contain information about tokens acquire from auth.presalytics.io.  The 
        dictionary must contain an `access_token`, a `refresh_token`, and entries contiaing information about token expiry.  

        Token expiry information can either passed in ISO 8601 formatted string with a UTC offset as dictionary keys
        `access_token_expire_time` and `refresh_token_expire_time` or an integer in seconds with the corresponding 
        dictionary keys`expires_in` and `refresh_expires_in`.
        
        if the `dict` passed in via this keywork does is not have the correct entries, the client will
        raise an `presalytics.lib.exceptions.InvalidTokenException`.

    cache_tokens : bool, optional
        Defaults to False.  Toggles whether or the client should cache its acquired tokens in file called "token.json"
        in the current working directory.  Minimizes the number of times a user is required to login.  Set to False
        in multi-user environments.

    Attributes
    ----------

    direct_grant : bool
        Indicates whether an token will be acquire via the "direct_grant" OpenID Connect flow.  Usually indicates
        whether the user has supplied a passwork to the client either through `presalytics.CONFIG` ro 
        during object initialization.

    doc_converter : presalytics.client.presalytics_doc_converter.api.default_api.DefaultApi
        Interface to the Presalytics API Doc Converter service.  The object contains methods that enable
        the client to make api calls that return deserialized objects from the Presalytics API,
        simplying user and developer interaction with the Presaltytics API.  API calls can be generated
        as follows:

            client = presalytics.Client()
            api_obj = client.doc_converter.{operation_id}(*args)

        where `{operation_id}` is the `operationId` assocated with the endpoint specified the [Doc Converter 
        Service OpenAPI Contract](https://presalytics.io/docs/api-specifications/doc-converter/) , and *args
        are the corresponding arguments that are passed to the method.  A complete list of the avialable
        methods is shown on the `presalytics.client.presalytics_doc_converter.api.default_api.DefaultApi` object.
        
        *Note*:
        This attribute contains automatically generated methods via 
        the [OpenAPI generator](https://github.com/OpenAPITools/openapi-generator).  The 
        `presalytics.client.presalytics_doc_converter.api.default_api.DefaultApi` has been passed an an `api_client`
        keyword argument with an instance of `presalytics.client.api.DocConverterApiClientWithAuth`, which adds 
        an authentication and request processing middleware layer to the default sub package 
        built via code generatation.

    ooxml_automation : presalytics.client.presalytics_ooxml_automation.api.default_api.DefaultApi
        Interface to the Presalytics API Ooxml Automation service.  The object contains methods that enable
        the client to make api calls that return deserialized objects from the Presalytics API,
        simplying user and developer interaction with the Presaltytics API.  API calls can be generated
        as follows:

            client = presalytics.Client()
            api_obj = client.ooxml_automation.{operation_id}(*args)

        where `{operation_id}` is the `operationId` assocated with the endpoint specified the [Ooxml Automation
        Service OpenAPI Contract](https://presalytics.io/docs/api-specifications/ooxml-automation/) , and *args
        are the corresponding arguments that are passed to the method.  A complete list of the avialable
        methods is shown on the `presalytics.client.presalytics_ooxml_automation.api.default_api.DefaultApi` object.
        
        *Note*:
        This attribute contains automatically generated methods via 
        the [OpenAPI generator](https://github.com/OpenAPITools/openapi-generator).  The 
        `presalytics.client.presalytics_ooxml_automation.api.default_api.DefaultApi` has been passed an an `api_client`
        keyword argument with an instance of `presalytics.client.api.OoxmlAutomationApiClientWithAuth`, which adds 
        an authentication and request processing middleware layer to the default sub package 
        built via code generatation.

    story : presalytics.client.presalytics_story.api.default_api.DefaultApi
        Interface to the Presalytics API Ooxml Automation service.  The object contains methods that enable
        the client to make api calls that return deserialized objects from the Presalytics API,
        simplying user and developer interaction with the Presaltytics API.  API calls can be generated
        as follows:

            client = presalytics.Client()
            api_obj = client.story.{operation_id}(*args)

        where `{operation_id}` is the `operationId` assocated with the endpoint specified the [Ooxml Automation
        Service OpenAPI Contract](https://presalytics.io/docs/api-specifications/story/) , and *args
        are the corresponding arguments that are passed to the method.  A complete list of the avialable
        methods is shown on the `presalytics.client.presalytics_story.api.default_api.DefaultApi` object.
        
        *Note*:
        This attribute contains automatically generated methods via 
        the [OpenAPI generator](https://github.com/OpenAPITools/openapi-generator).  The 
        `presalytics.client.presalytics_story.api.default_api.DefaultApi` has been passed an an `api_client`
        keyword argument with an instance of `presalytics.client.api.StoryApiClientWithAuth`, which adds 
        an authentication and request processing middleware layer to the default sub package 
        built via code generatation.

    client_id : str
        The client_id that is used OpenID Connect login.  Defaults to "python-client".  

    client_secret : str, optional
        The client_secret used during OpenID Connect login.  Useful `confidential_client` is True.

    confidential_client : bool
        Indicates whether a this client can obtain tokens from auth.presalytics.io without a user under
        OpenID Connect grant type "confidential_client".  Requires a `client_secret`.  Default is False. 

    oidc : `presalytics.client.oidc.OidcClient`
        A middleware class to help acquire and validate tokens from login.presalytics.io.

    token_util : `presalytics.client.auth.TokenUtil`
        A handler for managing an caching tokens acquired from auth.presalytics.io.

    site_host : str
        The login site host for acquiring tokens.  Set from `presalytics.CONFIG` with keyword `["SITE"]["HOST"]`.
        Defaults to https://presalytics.io.

    redirect_uri : str
        Useful if implementing authorization code flow for and OpenID Connect client.  Redirect URIs must 
        be approved by Presalytics API devops for use in client applications. Set from Set from 
        `presalytics.CONFIG` with keyword `["REDIRECT_URI"]`.  Defaults to https://presalytics.io/user/login-success. 

    login_sleep_interval : int
        The duration (in seconds) between attempts to acquire a token after browser-based authentication. Defaults
        to 5 seconds.        

    login_timeout : int
        Defaults to 60 seconds.  The amount of time the client will attempt to acquire a token after the 
        https://presalytics.io authenicates a user. Raises a `presalytics.lib.exceptions.LoginTimeout`
        if the user has not authenticated by the time the interval has expired.

    """
    def __init__(
            self,
            username=None,
            password=None,
            delegate_login=False,
            token=None,
            cache_tokens=False,
            client_id=None,
            client_secret=None,
            **kwargs):
    
        if username:
            self.username = username
        else:
            try:
                self.username = presalytics.CONFIG['USERNAME']
            except KeyError:
                if token:
                    self.username = None
                else:
                    if not client_secret:
                        raise presalytics.lib.exceptions.MissingConfigException("when not passing tokens directly, a clien must have either a client_secrect or a username")
        try:
            if password:
                self.password = password
            else:
                self.password = presalytics.CONFIG['PASSWORD']
            self.direct_grant = True
        except KeyError:
            self.password = None
            self.direct_grant = False
        try:
            if client_id:
                self.client_id = client_id
            else:
                self.client_id = presalytics.CONFIG['CLIENT_ID']
        except KeyError:
            self.client_id = cnst.DEFAULT_CLIENT_ID
        try:
            if client_secret:
                self.client_secret = client_secret
            else:
                self.client_secret = presalytics.CONFIG['CLIENT_SECRET']
            self.confidential_client = True
        except KeyError:
            self.client_secret = None
            self.confidential_client = False

        try:
            self.site_host = presalytics.CONFIG["HOSTS"]["SITE"]
        except KeyError:
            self.site_host = cnst.SITE_HOST


        try:
            self.redirect_uri = presalytics.CONFIG["REDIRECT_URI"]
        except KeyError:
            self.redirect_uri = cnst.REDIRECT_URI
        if delegate_login or presalytics.CONFIG.get("DELEGATE_LOGIN", False):
            self._delegate_login = True
        else:
            self._delegate_login = False
        self.oidc = presalytics.client.oidc.OidcClient(
            client_id=self.client_id,
            client_secret=self.client_secret
        )
        if presalytics.CONFIG.get("CACHE_TOKENS", None):
            cache_tokens = presalytics.CONFIG.get("CACHE_TOKENS")
        self.token_util = presalytics.client.auth.TokenUtil(token_cache=cache_tokens)
        if token:
            #  Assume if token is passed as string, then it's an access token
            if isinstance(token, str):
                self.token_util.token = {"access_token": token}
            
            # if token is a dictionary with an 'access_token_expire_time' key, it's previous been processed / deserialized
            elif token.get('access_token_expire_time', None):
                self.token_util.token = token

            # if token has an 'expires_in' key, if has not been deserialized
            elif token.get('expires_in', None):
                self.token_util.process_token(token)
            else:
                raise presalytics.lib.exceptions.InvalidTokenException(message="Unknown token format.")
            if self.token_util.token_cache:
                self.token_util._put_token_file()
        if not self._delegate_login:
            self.token_util.token = self.refresh_token()

        doc_converter_api_client = DocConverterApiClientWithAuth(self, **kwargs)
        self.doc_converter = presalytics.client.presalytics_doc_converter.DefaultApi(api_client=doc_converter_api_client)
        ooxml_automation_api_client = OoxmlAutomationApiClientWithAuth(self, **kwargs)
        self.ooxml_automation = presalytics.client.presalytics_ooxml_automation.DefaultApi(api_client=ooxml_automation_api_client)
        story_api_client = StoryApiClientWithAuth(self, **kwargs)
        self.story = presalytics.client.presalytics_story.DefaultApi(api_client=story_api_client)

    def login(self):
        """
        Triggers a an attempt to acquire an API token based on the the client configuration
        """
        if self.direct_grant:
            token = self.oidc.token(username=self.username, password=self.password)
        else:
            token = self.oidc.token(username=self.username)
        self.token_util.process_token(token)
        return self.token_util.token


    def refresh_token(self):
        """
        Obtains a new access token if the access token is expired. if refresh token is expired, 
        this method prompt user to re-authenticate when `delegate_login` is `False` or raise
        an `presalytics.lib.exceptions.InvalidTokenException` when `deletegate_login` is True.
        """
        if self.token_util.is_api_access_token_expired():
            if self.token_util.token.get('refresh_token', None) and self.client_secret:
                refresh_token = self.token_util.token["refresh_token"]
                token = self.oidc.refresh_token(refresh_token)
                self.token_util.process_token(token)
                logger.debug("Refresh token granted successfully.")
            else:
                if self.direct_grant:
                    token = self.oidc.token(username=self.username, password=self.password)
                elif self.confidential_client:
                    token = self.oidc.client_credentials_token()
                elif self._delegate_login:
                    raise presalytics.lib.exceptions.ApiError("Unauthorized. Token has expired", status_code=401)
                else:
                    token = self.oidc.token(username=self.username)
                self.token_util.process_token(token)
            if self.token_util.token_cache:
                self.token_util._put_token_file()
        return self.token_util.token

    def get_auth_header(self):
        """
        Creates a JWT Bearer Authorization token header

        Returns
        ----------
        A `dict` authorization crediential to be attached to an API request
        """
        self.refresh_token()
        auth_header = {
            "Authorization": "Bearer " + self.token_util.token["access_token"]
        }
        return auth_header

    def get_request_id_header(self):
        """
        Creates an 'X-Request-Id' token header for tracing requests through Presalytics API
        services.  If deployed alongside the [WSGI Microservice Middleware](https://github.com/presalytics/WSGI-Microservice-Middleware) 
        package, this method will pull the request id from the call stack.
        
        Returns
        ----------
        A `dict` header representation with an 'X-Request-Id' key to be attached to an API request
        """
        
        current_request_id = wsgi_microservice_middleware.current_request_id()
        if not current_request_id:
            current_request_id = str(uuid4())
        header = {
            "X-Request-Id": current_request_id
        }
        return header

    def download_file(self, story_id, ooxml_automation_id, download_folder=None, filename=None, **kwargs):
        """
        Downloads an updated Ooxml Automation file and places the file in a designated folder

        Parameters
        ---------
        story : str
            The id of the Presalytics Story API object that manages access to document
        
        ooxml_automation_id : str
            The id of the Presalytics API Ooxml Automation service object that you want to download
        
        download_folder : str, optional
            The filepath to the local directory that you want to download the file to. Defaults to the 
            current working directory.
        
        filename: str, optional
            The name of the downloaded file. Defaults to the original filename the the object was created.

        """
        response, status, headers = self.story.story_id_file_ooxmlautomationid_get_with_http_info(story_id, ooxml_automation_id, _preload_content=False)
        if download_folder is None:
            download_folder = os.getcwd()
        if filename is None:
            cd_header = headers.get('Content-Disposition')
            _, params = cgi.parse_header(cd_header)
            filename = params["filename"]
        filepath = os.path.join(download_folder, filename)
        with open(filepath, 'wb') as f:
            f.write(response.data)

    def get_client_info(self):
        """
        Convenience method returning information about this client to pass to downstream objects, e.g.,
        components and new client instances

        Returns
        ----------
        A dictionary containing instances values:
            - token: self.token_util.token
            - client_id: self.client_id
            - cache_tokens: self.token_util.token_cache
            - delegate_login: self.delegate login
        """
        return {
            "token": self.token_util.token,
            "client_id": self.client_id,
            "cache_tokens": self.token_util.token_cache,
            "delegate_login": self._delegate_login
        }

    STATUS_REPOLL_SECONDS = 2
    STATUS_REPOLL_MAX_CYCLES = 20

    def upload_file_and_await_outline(self, 
                                      file: typing.Union[FileStorage, str],
                                      include_relationships=True,
                                      status_repoll_seconds: int = None, 
                                      repoll_max_cycles: int = None):
        """ Useful for testing """
        if type(file) is str:
            content_type = mimetypes.guess_type(file, False)[0] # type: ignore
            with open(file, 'rb') as f:  # type: ignore
                stream = io.BytesIO(f.read())
            file = FileStorage(
                stream=stream,  # type: ignore
                filename=file,  # type: ignore
                content_type=content_type,
                content_length=stream.__sizeof__()             
            )
        if not status_repoll_seconds:
            status_repoll_seconds = self.STATUS_REPOLL_SECONDS
        if not repoll_max_cycles:
            repoll_max_cycles = self.STATUS_REPOLL_MAX_CYCLES
        story = self.story.story_post_file(file=file)
        self.await_outline(story.id)
        return self.story.story_id_get(story.id, include_outline=True, include_relationships=include_relationships)

    def await_outline(self,
                      story_id,
                      status_repoll_seconds: int = None, 
                      repoll_max_cycles: int = None):
        task_running = True
        repoll_cycle_count = 0
        if not status_repoll_seconds:
            status_repoll_seconds = self.STATUS_REPOLL_SECONDS
        if not repoll_max_cycles:
            repoll_max_cycles = self.STATUS_REPOLL_MAX_CYCLES
        while task_running:
            status, status_code, _ = self.story.story_id_status_get_with_http_info(story_id)
            if status_code == 204:
                task_running = False
            elif status_code == 200:
                if status.status == "SUCCESS":
                    task_running = False
                elif repoll_cycle_count >= repoll_max_cycles:
                    raise presalytics.lib.exceptions.ApiError(message="Error occured while uploading file", status_code=500)
                else:
                    logger.info("Story creation task still running.  Rechecking status in {0} seconds".format(status_repoll_seconds))
                    time.sleep(status_repoll_seconds)
                    repoll_cycle_count += 1
        return self.story.story_id_outline_get(story_id)

Class variables

var STATUS_REPOLL_SECONDS

int(x=0) -> integer int(x, base=10) -> integer

Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.int(). For floating point numbers, this truncates towards zero.

If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by '+' or '-' and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer literal.

>>> int('0b100', base=0)
4
var STATUS_REPOLL_MAX_CYCLES

int(x=0) -> integer int(x, base=10) -> integer

Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.int(). For floating point numbers, this truncates towards zero.

If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by '+' or '-' and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer literal.

>>> int('0b100', base=0)
4

Methods

def login(self)

Triggers a an attempt to acquire an API token based on the the client configuration

Expand source code Browse git
def login(self):
    """
    Triggers a an attempt to acquire an API token based on the the client configuration
    """
    if self.direct_grant:
        token = self.oidc.token(username=self.username, password=self.password)
    else:
        token = self.oidc.token(username=self.username)
    self.token_util.process_token(token)
    return self.token_util.token
def refresh_token(self)

Obtains a new access token if the access token is expired. if refresh token is expired, this method prompt user to re-authenticate when delegate_login is False or raise an InvalidTokenException when deletegate_login is True.

Expand source code Browse git
def refresh_token(self):
    """
    Obtains a new access token if the access token is expired. if refresh token is expired, 
    this method prompt user to re-authenticate when `delegate_login` is `False` or raise
    an `presalytics.lib.exceptions.InvalidTokenException` when `deletegate_login` is True.
    """
    if self.token_util.is_api_access_token_expired():
        if self.token_util.token.get('refresh_token', None) and self.client_secret:
            refresh_token = self.token_util.token["refresh_token"]
            token = self.oidc.refresh_token(refresh_token)
            self.token_util.process_token(token)
            logger.debug("Refresh token granted successfully.")
        else:
            if self.direct_grant:
                token = self.oidc.token(username=self.username, password=self.password)
            elif self.confidential_client:
                token = self.oidc.client_credentials_token()
            elif self._delegate_login:
                raise presalytics.lib.exceptions.ApiError("Unauthorized. Token has expired", status_code=401)
            else:
                token = self.oidc.token(username=self.username)
            self.token_util.process_token(token)
        if self.token_util.token_cache:
            self.token_util._put_token_file()
    return self.token_util.token
def get_auth_header(self)

Creates a JWT Bearer Authorization token header

Returns

A dict authorization crediential to be attached to an API request

Expand source code Browse git
def get_auth_header(self):
    """
    Creates a JWT Bearer Authorization token header

    Returns
    ----------
    A `dict` authorization crediential to be attached to an API request
    """
    self.refresh_token()
    auth_header = {
        "Authorization": "Bearer " + self.token_util.token["access_token"]
    }
    return auth_header
def get_request_id_header(self)

Creates an 'X-Request-Id' token header for tracing requests through Presalytics API services. If deployed alongside the WSGI Microservice Middleware package, this method will pull the request id from the call stack.

Returns

A dict header representation with an 'X-Request-Id' key to be attached to an API request

Expand source code Browse git
def get_request_id_header(self):
    """
    Creates an 'X-Request-Id' token header for tracing requests through Presalytics API
    services.  If deployed alongside the [WSGI Microservice Middleware](https://github.com/presalytics/WSGI-Microservice-Middleware) 
    package, this method will pull the request id from the call stack.
    
    Returns
    ----------
    A `dict` header representation with an 'X-Request-Id' key to be attached to an API request
    """
    
    current_request_id = wsgi_microservice_middleware.current_request_id()
    if not current_request_id:
        current_request_id = str(uuid4())
    header = {
        "X-Request-Id": current_request_id
    }
    return header
def download_file(self, story_id, ooxml_automation_id, download_folder=None, filename=None, **kwargs)

Downloads an updated Ooxml Automation file and places the file in a designated folder

Parameters

presalytics.story : str
The id of the Presalytics Story API object that manages access to document
ooxml_automation_id : str
The id of the Presalytics API Ooxml Automation service object that you want to download
download_folder : str, optional
The filepath to the local directory that you want to download the file to. Defaults to the current working directory.
filename : str, optional
The name of the downloaded file. Defaults to the original filename the the object was created.
Expand source code Browse git
def download_file(self, story_id, ooxml_automation_id, download_folder=None, filename=None, **kwargs):
    """
    Downloads an updated Ooxml Automation file and places the file in a designated folder

    Parameters
    ---------
    story : str
        The id of the Presalytics Story API object that manages access to document
    
    ooxml_automation_id : str
        The id of the Presalytics API Ooxml Automation service object that you want to download
    
    download_folder : str, optional
        The filepath to the local directory that you want to download the file to. Defaults to the 
        current working directory.
    
    filename: str, optional
        The name of the downloaded file. Defaults to the original filename the the object was created.

    """
    response, status, headers = self.story.story_id_file_ooxmlautomationid_get_with_http_info(story_id, ooxml_automation_id, _preload_content=False)
    if download_folder is None:
        download_folder = os.getcwd()
    if filename is None:
        cd_header = headers.get('Content-Disposition')
        _, params = cgi.parse_header(cd_header)
        filename = params["filename"]
    filepath = os.path.join(download_folder, filename)
    with open(filepath, 'wb') as f:
        f.write(response.data)
def get_client_info(self)

Convenience method returning information about this client to pass to downstream objects, e.g., components and new client instances

Returns

A dictionary containing instances values:
  • token: self.token_util.token
  • client_id: self.client_id
  • cache_tokens: self.token_util.token_cache
  • delegate_login: self.delegate login
Expand source code Browse git
def get_client_info(self):
    """
    Convenience method returning information about this client to pass to downstream objects, e.g.,
    components and new client instances

    Returns
    ----------
    A dictionary containing instances values:
        - token: self.token_util.token
        - client_id: self.client_id
        - cache_tokens: self.token_util.token_cache
        - delegate_login: self.delegate login
    """
    return {
        "token": self.token_util.token,
        "client_id": self.client_id,
        "cache_tokens": self.token_util.token_cache,
        "delegate_login": self._delegate_login
    }
def upload_file_and_await_outline(self, file, include_relationships=True, status_repoll_seconds=None, repoll_max_cycles=None)

Useful for testing

Expand source code Browse git
def upload_file_and_await_outline(self, 
                                  file: typing.Union[FileStorage, str],
                                  include_relationships=True,
                                  status_repoll_seconds: int = None, 
                                  repoll_max_cycles: int = None):
    """ Useful for testing """
    if type(file) is str:
        content_type = mimetypes.guess_type(file, False)[0] # type: ignore
        with open(file, 'rb') as f:  # type: ignore
            stream = io.BytesIO(f.read())
        file = FileStorage(
            stream=stream,  # type: ignore
            filename=file,  # type: ignore
            content_type=content_type,
            content_length=stream.__sizeof__()             
        )
    if not status_repoll_seconds:
        status_repoll_seconds = self.STATUS_REPOLL_SECONDS
    if not repoll_max_cycles:
        repoll_max_cycles = self.STATUS_REPOLL_MAX_CYCLES
    story = self.story.story_post_file(file=file)
    self.await_outline(story.id)
    return self.story.story_id_get(story.id, include_outline=True, include_relationships=include_relationships)
def await_outline(self, story_id, status_repoll_seconds=None, repoll_max_cycles=None)
Expand source code Browse git
def await_outline(self,
                  story_id,
                  status_repoll_seconds: int = None, 
                  repoll_max_cycles: int = None):
    task_running = True
    repoll_cycle_count = 0
    if not status_repoll_seconds:
        status_repoll_seconds = self.STATUS_REPOLL_SECONDS
    if not repoll_max_cycles:
        repoll_max_cycles = self.STATUS_REPOLL_MAX_CYCLES
    while task_running:
        status, status_code, _ = self.story.story_id_status_get_with_http_info(story_id)
        if status_code == 204:
            task_running = False
        elif status_code == 200:
            if status.status == "SUCCESS":
                task_running = False
            elif repoll_cycle_count >= repoll_max_cycles:
                raise presalytics.lib.exceptions.ApiError(message="Error occured while uploading file", status_code=500)
            else:
                logger.info("Story creation task still running.  Rechecking status in {0} seconds".format(status_repoll_seconds))
                time.sleep(status_repoll_seconds)
                repoll_cycle_count += 1
    return self.story.story_id_outline_get(story_id)
class StoryOutline (info, pages, description, title, themes, plugins=None, story_id='empty', **kwargs)

A StoryOutline contains instructions for a Renderer (e.g., Revealer) to render an story into html.

A story outline's info, pages, widgets, and themes are intended to be easily editable by both human users and machines via json serialization and deserialization, and the ecosystem of tools that can be used to edit json objects. Once a valid StoryOutline is built, it contains sufficient instructions for this library find the required components in the user's workspace and render theme.

StoryOutlines are stored in the Presalytics API Story Service. The story service manages version history and user permissions for StoryOutlines. For more information about how outlines are used, please see the How It Works section of the website.

Attributes

outline_version : str
the version of that StoryOuline schema.
info : Info
Metadata about this StoryOutline
pages : list of Page
The pages that will be rendered in this story
themes : list of Theme
The themes that will underlie each Page in the StoryOutline
title : str, optional
A title for the story
description : str, optional
A description of the story
story_id : str, optional
The Presalytics API Story Id. Automatically added once the story outline has been pushed to the server.
Expand source code Browse git
class StoryOutline(OutlineBase):
    """
    A StoryOutline contains instructions for a `presalytics.story.components.Renderer` 
    (e.g., `presalytics.story.revealer.Revealer`) to render an story into html.

    A story outline's `info`, `pages`, `widgets`, and `themes` are intended to be easily editable
    by both human users and machines via json serialization and deserialization, and the ecosystem
    of tools that can be used to edit json objects. Once a valid `StoryOutline` is
    built, it contains sufficient instructions for this library find the required components in the user's
    workspace and render theme.

    StoryOutlines are stored in the [Presalytics API Story Service](https://presalytics.io/docs/api-specifications/story/).
    The story service manages version history and user permissions for StoryOutlines.  For more information
    about how outlines are used, please see the [How It Works](https://presalytics.io/docs/how-it-works/)
    section of the website.

    Attributes
    ----------

    outline_version : str
        the version of that StoryOuline schema.
    
    info : presalytics.story.outline.Info
        Metadata about this StoryOutline
    
    pages : list of presalytics.story.outline.Page
        The pages that will be rendered in this story
    
    themes : list of presalytics.story.outline.Theme
        The themes that will underlie each `presalytics.story.outline.Page` in the StoryOutline
    
    title : str, optional
        A title for the story

    description: str, optional
        A description of the story
    
    story_id : str, optional
        The Presalytics API Story Id.  Automatically added once the story outline has been pushed to
        the server.

    """
    outline_version: str
    info: Info
    pages: typing.List[Page]
    description: str
    title: str
    themes: typing.List[Theme]
    story_id: str

    __required__ = [
        'outline_version',
        'info',
        'pages',
        'title',
    ]

    def __init__(self, info, pages, description, title, themes, plugins=None, story_id="empty", **kwargs):
        super(StoryOutline, self).__init__(**kwargs)
        self.outline_version = get_current_spec_version()
        self.info = Info.deserialize(info)
        self.pages = [Page.deserialize(x) for x in pages]
        if description:
            self.description = description
        else:
            self.description = ""
        if title:
            self.title = title
        else:
            self.title = ""
        if themes:
            self.themes = [Theme.deserialize(x) for x in themes]
        else:
            self.themes = []
        if plugins:
            self.plugins = [Plugin.deserialize(x) for x in plugins]
        else:
            self.plugins = []
        self.story_id = story_id
        if not kwargs.get("validate", True):
            self.validate()

        def validate(self):
            super(StoryOutline, self).validate()
            jsonschema.validate(instance=self.to_dict(), schema=load_latest_schema())

Ancestors

Inherited members

class Renderer (story_outline, **kwargs)

Base class for objects that convert StoryOutline objects into html and rendering them over the web

With this class, users can push changes to their StoryOutline to the Presalytics API and web clients. Renderer class contains a couplemethods for syncing changes from component instances in the CONFIG to the Presalytics API Story service.

  • The view method allows users programattically view their stories at https://presalytics.io after changes are made

  • The manage method takes users to to the story management interface, where users can share their work with other users, continue making edits or change story properties.

Parameters

story_outline : StoryOutline
The presalytics StoryOutline to be rendered and presented

Attributes

plugins : list of dict
Plugin data that transform to html <script> and <link> tags through the rendering process
site_host : str
The host of the website. Defaults to https://presalytics.io.
view_url : str, optional
The url to view the story. Unavailable if story outline has not been pushed to the Presalytics API story service
manage_url : str, optional
The url to view the story. Unavailable if story outline has not been pushed to the Presalytics API story service
Expand source code Browse git
class Renderer(ComponentBase):
    """
    Base class for objects that convert `presalytics.story.outline.StoryOutline` 
    objects into html and rendering them over the web

    With this class, users can push changes to their `presalytics.story.outline.StoryOutline`
    to the Presalytics API and web clients.  Renderer class contains a couplemethods for 
    syncing changes from component instances in the `presalytics.CONFIG` to the Presalytics API 
    Story service.

    * The `view` method allows users programattically view their stories at https://presalytics.io 
    after changes are made

    * The `manage` method takes users to to the story management interface, where users can share their 
    work with other users, continue making edits or change story properties.


    Parameters
    ----------
    story_outline : presalytics.story.outline.StoryOutline
        The presalytics StoryOutline to be rendered and presented
    
    Attributes
    -----------
    plugins : list of dict
        Plugin data that transform to html `<script>` and `<link>` tags through
        the rendering process
    
    site_host : str
        The host of the website.  Defaults to https://presalytics.io.

    view_url : str, optional
         The url to view the story.  Unavailable if story outline has not been pushed to
         the Presalytics API story service

    manage_url : str, optional
         The url to view the story.  Unavailable if story outline has not been pushed to
         the Presalytics API story service
    """
    story_outline: 'StoryOutline'
    plugins: typing.List[typing.Dict]
    view_url: typing.Optional[str]
    manage_url: typing.Optional[str]

    __component_type__ = 'renderer'
    
    def __init__(self, story_outline : 'StoryOutline', **kwargs):
        super(Renderer, self).__init__(**kwargs)
        self.story_outline = story_outline
        try:
            self.site_host = presalytics.CONFIG["HOSTS"]["SITE"]
        except (KeyError, AttributeError):
            self.site_host = presalytics.lib.constants.SITE_HOST
        try:
            story_id = self.story_outline.story_id
            view_endpoint = presalytics.lib.constants.STORY_VIEW_URL.format(story_id)
            self.view_url = urllib.parse.urljoin(self.site_host, view_endpoint)
            manage_endpoint = presalytics.lib.constants.STORY_MANAGE_URL.format(story_id)
            self.manage_url = urllib.parse.urljoin(self.site_host, manage_endpoint)
        except (KeyError, AttributeError):
            self.view_url = None
            self.manage_url = None

    def render(self, **kwargs):
        """
        Renders server-side (i.e., opens the story on https://presalytics.io)
        """
        return self.view()

            
    
    def strip_unauthorized_scripts(self, body):
        """
        Finds and removes unauthorized scripts from that the html document.  For security reasons,
        content in `<script>` tags that has not been vetted by presalytics.io devops 

        If you would like to get a tag included in the base library, raise an issue on 
        [Github](https://github.com/presalytics/python-client/issues/new).  We'd love to hear from you and learn 
        about your use case, and will respond promptly to help.
        """
        allowed_scripts = presalytics.lib.plugins.external.ApprovedExternalScripts().attr_dict.flatten().values()
        script_elements = body.findall(".//script")
        for ele in script_elements:
            remove_ele = True
            script_id = ele.get("id")
            if script_id in self.story_outline.allowed_ids:
                remove_ele = False
            link = ele.get("src")
            if link in allowed_scripts:
                remove_ele = False
            if remove_ele:
                ele.getparent().remove(ele)
        return body
        
    @classmethod
    def deserialize(cls, component: 'StoryOutline', **kwargs):
        """
        Initializes the class from a `presalytics.story.outline.StoryOutline`.  See __init___.
        """
        return cls(component, **kwargs)

    def serialize(self) -> 'StoryOutline':
        """
        Updates the story_outline

        Returns
        -----------
        A refreshed `presalytics.story.outline.StoryOutline`
        """
        self.update_outline_from_instances()
        return self.story_outline

    def update_outline_from_instances(self, sub_dict: typing.Dict = None):
        """
        If a component instance for the widget is available in `presalytics.COMPONENTS`, 
        this method find the instance and regenerates the component data 
        so the latest data is available during the renering process.
        """
        if not sub_dict:
            sub_dict = self.story_outline.to_dict()
        if sub_dict:
            for key, val in sub_dict.items():
                if key in ["widgets", "themes", "pages"]:
                    if isinstance(val, list):
                        for list_item in val:
                            if isinstance(list_item, dict):
                                if "kind" in list_item:
                                    class_key = key.rstrip("s") + "." + list_item["kind"]
                                    klass = presalytics.COMPONENTS.get(class_key)
                                    if klass:
                                        if "name" in list_item:
                                            instance_key = class_key + "." + list_item["name"]
                                            inst = presalytics.COMPONENTS.get_instance(instance_key)
                                            if inst:
                                                self._set_outline_data_from_instance(inst)
                if isinstance(val, dict):
                    if len(val.keys()) > 0:
                        self.update_outline_from_instances(val)
                if isinstance(val, list):
                    for list_item in val:
                        if isinstance(list_item, dict):
                            self.update_outline_from_instances(list_item)

    def get_component_implicit_plugins(self, sub_dict: typing.Dict = None):
        """
        Retrieves plugin data from plugins attached to `presalytics.story.components`
        classes referenced in the `presalytics.story.outline.StoryOutline`
        """
        if not sub_dict:
            sub_dict = self.story_outline.to_dict()
        if sub_dict:
            for key, val in sub_dict.items():
                if key in ["widgets", "themes", "pages"]:
                    if isinstance(val, list):
                        for list_item in val:
                            if isinstance(list_item, dict):
                                if "kind" in list_item:
                                    class_key = key.rstrip("s") + "." + list_item["kind"]
                                    klass = presalytics.COMPONENTS.get(class_key)
                                    if klass:
                                        if len(klass.__plugins__) > 0:
                                            self.plugins.extend(klass.__plugins__)                           
                if isinstance(val, dict):
                    if len(val.keys()) > 0:
                        self.get_component_implicit_plugins(val)
                if isinstance(val, list):
                    for list_item in val:
                        if isinstance(list_item, dict):
                            self.get_component_implicit_plugins(list_item)

    def _set_outline_data_from_instance(self, inst):
        
        if inst.__component_type__ == 'widget':
            self._set_widget_outline_data(inst)
        if inst.__component_type__ == 'page':
            self._set_page_outline_data(inst)
        if inst.__component_type__ == 'theme':
            self._set_theme_outline_data(inst)

    def _set_theme_outline_data(self, inst: 'ThemeBase'):
        theme_index = None
        for t in range(0, len(self.story_outline.themes)):
            if inst.name == self.story_outline.themes[t].name:
                theme_index = t
            if theme_index:
                break
        theme_outline = inst.serialize()
        if theme_index:
            self.story_outline.themes[theme_index] = theme_outline    

    def _set_page_outline_data(self, inst: 'PageTemplateBase'):
        page_index = None
        for p in range(0, len(self.story_outline.pages)):
            if inst.name == self.story_outline.pages[p].name:
                page_index = p
            if page_index:
                break
        page_outline = inst.serialize()
        if page_index:
            self.story_outline.pages[page_index] = page_outline

    def _set_widget_outline_data(self, inst: 'WidgetBase'):
        widget_index: typing.Optional[int]
        page_index: typing.Optional[int]
        widget_index = None
        page_index = None
        for p in range(0, len(self.story_outline.pages)):
            for w in range(0, len(self.story_outline.pages[p].widgets)):
                widget = self.story_outline.pages[p].widgets[w]
                if widget.name == inst.name:
                    page_index = p
                    widget_index = w
                if page_index:
                    break
            if page_index:
                break
        w_outline = inst.serialize()
        if isinstance(page_index, int) and isinstance(widget_index, int): #  Causes 'unsupported target for assingment error`
            self.story_outline.pages[page_index].widgets[widget_index] = w_outline #type: ignore

    def update_story(self):
        """
        Updates the StoryOutline and pushes those updates to the Presalytics API Story service
        """
        self.update_outline_from_instances()
        client = presalytics.client.api.Client(**self.client_info)
        story = client.story.story_id_get(self.story_outline.story_id)
        story.outline = self.story_outline.dump()
        client.story.story_id_put(story.id, story)
    
    def view(self, update=False):
        """
        Updates a story and opens it on the presalytics.io website

        Parameters
        ----------
        update : bool
            Defaults to True.  Indicates whether the StoryOutline should be updated
            prior to opening in the web browser
        """
        if not self.view_url:
            message = "The outline has not been pushed to the Presalytics API yet, and therefore cannot be viewed via preslaytics.io"
            raise presalytics.lib.exceptions.InvalidConfigurationError(message=message)
        if update:
            self.update_story()
        webbrowser.open_new_tab(self.view_url)


    def manage(self, update=False):
        """
        Updates a story and opens the management page on the presalytics.io website

        Parameters
        ----------
        update : bool
            Defaults to True.  Indicates whether the StoryOutline should be updated
            prior to opening in the web browser
        """
        if not self.manage_url:
            message = "The outline has not been pushed to the Presalytics API yet, and therefore cannot be viewed via preslaytics.io"
            raise presalytics.lib.exceptions.InvalidConfigurationError(message=message)
            
        if update:
            self.update_story()
        webbrowser.open_new_tab(self.manage_url)

Ancestors

Subclasses

Static methods

def deserialize(component, **kwargs)

Initializes the class from a StoryOutline. See __init___.

Expand source code Browse git
@classmethod
def deserialize(cls, component: 'StoryOutline', **kwargs):
    """
    Initializes the class from a `presalytics.story.outline.StoryOutline`.  See __init___.
    """
    return cls(component, **kwargs)

Methods

def render(self, **kwargs)

Renders server-side (i.e., opens the story on https://presalytics.io)

Expand source code Browse git
def render(self, **kwargs):
    """
    Renders server-side (i.e., opens the story on https://presalytics.io)
    """
    return self.view()
def strip_unauthorized_scripts(self, body)

Finds and removes unauthorized scripts from that the html document. For security reasons, content in <script> tags that has not been vetted by presalytics.io devops

If you would like to get a tag included in the base library, raise an issue on Github. We'd love to hear from you and learn about your use case, and will respond promptly to help.

Expand source code Browse git
def strip_unauthorized_scripts(self, body):
    """
    Finds and removes unauthorized scripts from that the html document.  For security reasons,
    content in `<script>` tags that has not been vetted by presalytics.io devops 

    If you would like to get a tag included in the base library, raise an issue on 
    [Github](https://github.com/presalytics/python-client/issues/new).  We'd love to hear from you and learn 
    about your use case, and will respond promptly to help.
    """
    allowed_scripts = presalytics.lib.plugins.external.ApprovedExternalScripts().attr_dict.flatten().values()
    script_elements = body.findall(".//script")
    for ele in script_elements:
        remove_ele = True
        script_id = ele.get("id")
        if script_id in self.story_outline.allowed_ids:
            remove_ele = False
        link = ele.get("src")
        if link in allowed_scripts:
            remove_ele = False
        if remove_ele:
            ele.getparent().remove(ele)
    return body
def serialize(self)

Updates the story_outline

Returns

A refreshed StoryOutline

Expand source code Browse git
def serialize(self) -> 'StoryOutline':
    """
    Updates the story_outline

    Returns
    -----------
    A refreshed `presalytics.story.outline.StoryOutline`
    """
    self.update_outline_from_instances()
    return self.story_outline
def update_outline_from_instances(self, sub_dict=None)

If a component instance for the widget is available in COMPONENTS, this method find the instance and regenerates the component data so the latest data is available during the renering process.

Expand source code Browse git
def update_outline_from_instances(self, sub_dict: typing.Dict = None):
    """
    If a component instance for the widget is available in `presalytics.COMPONENTS`, 
    this method find the instance and regenerates the component data 
    so the latest data is available during the renering process.
    """
    if not sub_dict:
        sub_dict = self.story_outline.to_dict()
    if sub_dict:
        for key, val in sub_dict.items():
            if key in ["widgets", "themes", "pages"]:
                if isinstance(val, list):
                    for list_item in val:
                        if isinstance(list_item, dict):
                            if "kind" in list_item:
                                class_key = key.rstrip("s") + "." + list_item["kind"]
                                klass = presalytics.COMPONENTS.get(class_key)
                                if klass:
                                    if "name" in list_item:
                                        instance_key = class_key + "." + list_item["name"]
                                        inst = presalytics.COMPONENTS.get_instance(instance_key)
                                        if inst:
                                            self._set_outline_data_from_instance(inst)
            if isinstance(val, dict):
                if len(val.keys()) > 0:
                    self.update_outline_from_instances(val)
            if isinstance(val, list):
                for list_item in val:
                    if isinstance(list_item, dict):
                        self.update_outline_from_instances(list_item)
def get_component_implicit_plugins(self, sub_dict=None)

Retrieves plugin data from plugins attached to presalytics.story.components classes referenced in the StoryOutline

Expand source code Browse git
def get_component_implicit_plugins(self, sub_dict: typing.Dict = None):
    """
    Retrieves plugin data from plugins attached to `presalytics.story.components`
    classes referenced in the `presalytics.story.outline.StoryOutline`
    """
    if not sub_dict:
        sub_dict = self.story_outline.to_dict()
    if sub_dict:
        for key, val in sub_dict.items():
            if key in ["widgets", "themes", "pages"]:
                if isinstance(val, list):
                    for list_item in val:
                        if isinstance(list_item, dict):
                            if "kind" in list_item:
                                class_key = key.rstrip("s") + "." + list_item["kind"]
                                klass = presalytics.COMPONENTS.get(class_key)
                                if klass:
                                    if len(klass.__plugins__) > 0:
                                        self.plugins.extend(klass.__plugins__)                           
            if isinstance(val, dict):
                if len(val.keys()) > 0:
                    self.get_component_implicit_plugins(val)
            if isinstance(val, list):
                for list_item in val:
                    if isinstance(list_item, dict):
                        self.get_component_implicit_plugins(list_item)
def update_story(self)

Updates the StoryOutline and pushes those updates to the Presalytics API Story service

Expand source code Browse git
def update_story(self):
    """
    Updates the StoryOutline and pushes those updates to the Presalytics API Story service
    """
    self.update_outline_from_instances()
    client = presalytics.client.api.Client(**self.client_info)
    story = client.story.story_id_get(self.story_outline.story_id)
    story.outline = self.story_outline.dump()
    client.story.story_id_put(story.id, story)
def view(self, update=False)

Updates a story and opens it on the presalytics.io website

Parameters

update : bool
Defaults to True. Indicates whether the StoryOutline should be updated prior to opening in the web browser
Expand source code Browse git
def view(self, update=False):
    """
    Updates a story and opens it on the presalytics.io website

    Parameters
    ----------
    update : bool
        Defaults to True.  Indicates whether the StoryOutline should be updated
        prior to opening in the web browser
    """
    if not self.view_url:
        message = "The outline has not been pushed to the Presalytics API yet, and therefore cannot be viewed via preslaytics.io"
        raise presalytics.lib.exceptions.InvalidConfigurationError(message=message)
    if update:
        self.update_story()
    webbrowser.open_new_tab(self.view_url)
def manage(self, update=False)

Updates a story and opens the management page on the presalytics.io website

Parameters

update : bool
Defaults to True. Indicates whether the StoryOutline should be updated prior to opening in the web browser
Expand source code Browse git
def manage(self, update=False):
    """
    Updates a story and opens the management page on the presalytics.io website

    Parameters
    ----------
    update : bool
        Defaults to True.  Indicates whether the StoryOutline should be updated
        prior to opening in the web browser
    """
    if not self.manage_url:
        message = "The outline has not been pushed to the Presalytics API yet, and therefore cannot be viewed via preslaytics.io"
        raise presalytics.lib.exceptions.InvalidConfigurationError(message=message)
        
    if update:
        self.update_story()
    webbrowser.open_new_tab(self.manage_url)

Inherited members

class Revealer (story_outline, pages=None, **kwargs)

Renders StoryOutline objects to reveal.js presentations

Parameters

story_outline : StoryOutline
The presalytics StoryOutline to be rendered and presented

Attributes

base : lxml.etree.Element
An etree element containing the base html for each slide
plugins : list of dict
Plugin data that transform to html <script> and <link> tags through the rendering process
plugin_mgr : PluginManager
Sorts, validates, and renders plugins
Expand source code Browse git
class Revealer(presalytics.story.components.Renderer):
    """
    Renders `presalytics.story.outline.StoryOutline` objects to 
    [reveal.js](https://github.com/hakimel/reveal.js/) presentations

    Parameters
    ----------
    story_outline : presalytics.story.outline.StoryOutline
        The presalytics StoryOutline to be rendered and presented
    
    Attributes
    -----------
    base : lxml.etree.Element
        An etree element containing the base html for each slide

    plugins : list of dict
        Plugin data that transform to html `<script>` and `<link>` tags through
        the rendering process

    plugin_mgr : presalytics.lib.plugins.base.PluginManager
        Sorts, validates, and renders plugins

    """
    base: lxml.etree.Element
    reveal_params: typing.Dict[str, typing.Any]

    __component_kind__ = 'revealer'

    def __init__(
            self,
            story_outline: 'StoryOutline',
            pages: typing.List[int] = None,
            **kwargs):
        super(Revealer, self).__init__(story_outline, **kwargs)
        logger.info("Initializing story render for {}".format(story_outline.title))
        self.story_outline.validate()
        if isinstance(pages, int):
            pages = [pages]
        elif not pages:
            pages = [p for p in range(0, len(self.story_outline.pages))]
        elif not isinstance(pages, list) or not isinstance(pages[0], int):
            raise presalytics.lib.exceptions.InvalidArgumentException(message='"pages" must be a list of integers')
        if len([p for p in pages if p >= len(self.story_outline.pages)]) > 0:
            raise presalytics.lib.exceptions.InvalidArgumentException(message='"pages" can only contain integers lower than the number of pages in the story')
        self.pages_to_render = pages
        self.base = self._make_base()
        logger.info("Loading plugins")
        reveal_params = {}
        for key, val in kwargs.items():
            if key in presalytics.lib.plugins.reveal.RevealConfigPlugin.default_config.keys():
                reveal_params.update({key: val})
        if len(self.pages_to_render) == 1:
            reveal_params.update({'controls': False})  # hide controls on single page story
        reveal_plugin_config = {
            'kind': 'script',
            'name': 'reveal',
            'config': {"reveal_params": reveal_params} if len(reveal_params.keys()) > 0 else {}
        }
        overrides_config = {
            'kind': 'style',
            'name': 'local',
            'config': {
                "css_file_id": "reveal_overrides"
            }
        }
        self.update_outline_from_instances()
        self.plugins = [reveal_plugin_config, overrides_config]
        self.get_component_implicit_plugins()
        outline_plugins = presalytics.lib.plugins.base.PluginManager.get_plugins_from_nested_dict(source_dict=self.story_outline.to_dict())
        self.plugins.extend(outline_plugins)
        self.plugin_mgr = presalytics.lib.plugins.base.PluginManager(self.plugins)
        logger.info("Revealer initilized.")

    def _make_base(self):
        base = lxml.etree.Element("div", attrib={
            "class": "reveal",
        })
        lxml.etree.SubElement(base, "div", attrib={"class": "slides"})

        try:
            story_id = self.story_outline.story_id
            base.attrib['data-story-id'] = story_id
        except Exception:
            logger.info("Revealer could not extract story_id from outline.")
        return base
    
    def get_meta_tags(self, body=tuple()):
        """
        Security Note: If supplying a body, ensure that its already been stripped of unauthorized scripts. 
        """

        scripts = body.findall(".//script")

        srcs = []
        for script in scripts:
            src = script.get("src")
            if src:
                root = urllib.parse.urlparse(src).netloc
                if root:
                    srcs.append(root)
        allowed = ' '.join(set(srcs))

        tags = [
            '<meta charset="utf-8">',
            '<meta http-equiv="X-UA-Compatible" content="IE=edge">',
            '<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">',
        #    """<meta http-equiv="Content-Security-Policy" content="default-src 'self' https://*.presalytics.io; script-src 'self' https://*.presalytics.io {0};">""".format(allowed)
        ]
        return tags

    def package_as_standalone(self):
        """
        Render the story outline as a html document with only the 
        reveal.js presentation as conent

        Returns
        ----------
        A `str` containing a complete html document with the presentation
        """
        pres = self.render()
        body = E.BODY()
        body.append(pres)
        body = self.strip_unauthorized_scripts(body)
        for scripts in self.plugin_mgr.get_scripts():
            lxml_scripts = lxml.html.fragments_fromstring(scripts)
            for item in lxml_scripts:
                body.append(item)
        head = E.HEAD()
        for meta in self.get_meta_tags(body):
            lxml_meta = lxml.html.fragment_fromstring(meta)
            head.append(lxml_meta)
        for link in self.plugin_mgr.get_styles():
            lxml_links = lxml.html.fragments_fromstring(link)
            for item in lxml_links:
                head.append(item)
        head = self.strip_unauthorized_scripts(head)
        html = E.HTML(
            head,
            body
        )
        return lxml.html.tostring(html, pretty_print=True)

    # def render_plugin(self, plugin):
    #     plugin_type = plugin['type']
    #     plugin_config = plugin['config']
    #     plugin_class = self.plugin_loader.load(plugin_type)
    #     script_string = plugin_class().to_string(plugin_config)
    #     return lxml.html.fragment_fromstring(script_string)

    def update_info(self):
        """
        Updates story metadata
        """
        info = self.story_outline.info
        info.date_modified = datetime.datetime.utcnow()

    def render(self):
        """
        Creates a reveal.js presenation html fragement

        Returns:
        ---------
        A `str` html fragment containing a reveal.js presentation
        """
        reveal_base = self.base
        for p in range(0, len(self.story_outline.pages)):
            if p in self.pages_to_render:
                page = self.story_outline.pages[p]
                slides_container = reveal_base[0]
                slide = lxml.etree.SubElement(slides_container, "section")
                page_html = self.render_page(page)
                slide_fragment = lxml.html.fragment_fromstring(page_html)
                slide.append(slide_fragment)
        return reveal_base

    def render_page(self, page: 'Page') -> str:
        """
        Creates a reveal.js slide

        Returns
        ----------
        A `str` html framgment of the page
        """
        class_key = "page." + page.kind
        key = class_key + "." + page.name
        try:
            if presalytics.COMPONENTS.get_instance(key):
                page_instance = presalytics.COMPONENTS.get_instance(key)
            else:
                klass = presalytics.COMPONENTS.get(class_key)
                deserialize_method = getattr(klass, "deserialize", None)
                if callable(deserialize_method):
                    page_instance = deserialize_method(page, client_info=self.client_info)
                else:
                    message = 'Page component instance or class (kind) "{0}" unavailable in component registry'.format(key)
                    raise presalytics.lib.exceptions.MissingConfigException(message)
            page_html = page_instance.render()
        except Exception as ex:
            logger.exception(ex)
            t, v, tb = sys.exc_info()
            if not presalytics.CONFIG.get("DEBUG", False):
                page_html = presalytics.lib.exceptions.RenderExceptionHandler(ex, "page", traceback=tb).render_exception()
            else:
                six.reraise(t, v, tb)
        return page_html

    def present(self, files_path=None, debug=True, port=8082, host='127.0.0.1'):
        """
        Creates and opens the rendered story in the browser.  Story files are served by 
        a local flask server.  Not for production use.  Press Ctrl + C to close the server.

        Parameters
        ----------
        files_path : str
            filepath to a local folder that will work as root folder for a local flask
            server.  Defaults to the user's temporary files directory

        debug : str
            Defaults to True.  Indicates whether the flask server should be started
            in debug mode.
        
        port : str
            The network port to serve the story onto.  Defautls to 8082.
        
        host : str
            The host to for the local server.  Typically either localhost or the default gateway.
            Defaults to 127.0.0.1 (localhost).
        
        """
        logger.info("Building story rendering at http://{0}:{1}".format(host, port))
        if not files_path:
            files_path = tempfile.gettempdir()
        logger.info("Buidling standalone package for local rendering.")
        html = self.package_as_standalone().decode('utf-8')
        id = presalytics.story.util.to_title_case(self.story_outline.title)
        if id == '':
            id = 'blank'
        server = presalytics.story.server.LocalServer(host=host, debug=debug, port=port, root_path=files_path)
        pkg_templates_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "lib", "templates")
        shutil.copy(os.path.join(pkg_templates_dir, "favicon.ico"), os.path.join(files_path, "presalytics", "static"))
        html_file = os.path.join(files_path, "presalytics", "templates", id + '.html')
        with open(html_file, 'w') as file:
            file.write(html)
        address = "http://{}:{}/story/{}".format(host, port, id)
        logger.info("Opening browser tab...")
        presalytics.story.server.Browser(address).start()
        server.run()

Ancestors

Methods

def get_meta_tags(self, body=())

Security Note: If supplying a body, ensure that its already been stripped of unauthorized scripts.

Expand source code Browse git
def get_meta_tags(self, body=tuple()):
    """
    Security Note: If supplying a body, ensure that its already been stripped of unauthorized scripts. 
    """

    scripts = body.findall(".//script")

    srcs = []
    for script in scripts:
        src = script.get("src")
        if src:
            root = urllib.parse.urlparse(src).netloc
            if root:
                srcs.append(root)
    allowed = ' '.join(set(srcs))

    tags = [
        '<meta charset="utf-8">',
        '<meta http-equiv="X-UA-Compatible" content="IE=edge">',
        '<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">',
    #    """<meta http-equiv="Content-Security-Policy" content="default-src 'self' https://*.presalytics.io; script-src 'self' https://*.presalytics.io {0};">""".format(allowed)
    ]
    return tags
def package_as_standalone(self)

Render the story outline as a html document with only the reveal.js presentation as conent

Returns

A str containing a complete html document with the presentation

Expand source code Browse git
def package_as_standalone(self):
    """
    Render the story outline as a html document with only the 
    reveal.js presentation as conent

    Returns
    ----------
    A `str` containing a complete html document with the presentation
    """
    pres = self.render()
    body = E.BODY()
    body.append(pres)
    body = self.strip_unauthorized_scripts(body)
    for scripts in self.plugin_mgr.get_scripts():
        lxml_scripts = lxml.html.fragments_fromstring(scripts)
        for item in lxml_scripts:
            body.append(item)
    head = E.HEAD()
    for meta in self.get_meta_tags(body):
        lxml_meta = lxml.html.fragment_fromstring(meta)
        head.append(lxml_meta)
    for link in self.plugin_mgr.get_styles():
        lxml_links = lxml.html.fragments_fromstring(link)
        for item in lxml_links:
            head.append(item)
    head = self.strip_unauthorized_scripts(head)
    html = E.HTML(
        head,
        body
    )
    return lxml.html.tostring(html, pretty_print=True)
def update_info(self)

Updates story metadata

Expand source code Browse git
def update_info(self):
    """
    Updates story metadata
    """
    info = self.story_outline.info
    info.date_modified = datetime.datetime.utcnow()
def render(self)

Creates a reveal.js presenation html fragement

Returns:

A str html fragment containing a reveal.js presentation

Expand source code Browse git
def render(self):
    """
    Creates a reveal.js presenation html fragement

    Returns:
    ---------
    A `str` html fragment containing a reveal.js presentation
    """
    reveal_base = self.base
    for p in range(0, len(self.story_outline.pages)):
        if p in self.pages_to_render:
            page = self.story_outline.pages[p]
            slides_container = reveal_base[0]
            slide = lxml.etree.SubElement(slides_container, "section")
            page_html = self.render_page(page)
            slide_fragment = lxml.html.fragment_fromstring(page_html)
            slide.append(slide_fragment)
    return reveal_base
def render_page(self, page)

Creates a reveal.js slide

Returns

A str html framgment of the page

Expand source code Browse git
def render_page(self, page: 'Page') -> str:
    """
    Creates a reveal.js slide

    Returns
    ----------
    A `str` html framgment of the page
    """
    class_key = "page." + page.kind
    key = class_key + "." + page.name
    try:
        if presalytics.COMPONENTS.get_instance(key):
            page_instance = presalytics.COMPONENTS.get_instance(key)
        else:
            klass = presalytics.COMPONENTS.get(class_key)
            deserialize_method = getattr(klass, "deserialize", None)
            if callable(deserialize_method):
                page_instance = deserialize_method(page, client_info=self.client_info)
            else:
                message = 'Page component instance or class (kind) "{0}" unavailable in component registry'.format(key)
                raise presalytics.lib.exceptions.MissingConfigException(message)
        page_html = page_instance.render()
    except Exception as ex:
        logger.exception(ex)
        t, v, tb = sys.exc_info()
        if not presalytics.CONFIG.get("DEBUG", False):
            page_html = presalytics.lib.exceptions.RenderExceptionHandler(ex, "page", traceback=tb).render_exception()
        else:
            six.reraise(t, v, tb)
    return page_html
def present(self, files_path=None, debug=True, port=8082, host='127.0.0.1')

Creates and opens the rendered story in the browser. Story files are served by a local flask server. Not for production use. Press Ctrl + C to close the server.

Parameters

files_path : str
filepath to a local folder that will work as root folder for a local flask server. Defaults to the user's temporary files directory
debug : str
Defaults to True. Indicates whether the flask server should be started in debug mode.
port : str
The network port to serve the story onto. Defautls to 8082.
host : str
The host to for the local server. Typically either localhost or the default gateway. Defaults to 127.0.0.1 (localhost).
Expand source code Browse git
def present(self, files_path=None, debug=True, port=8082, host='127.0.0.1'):
    """
    Creates and opens the rendered story in the browser.  Story files are served by 
    a local flask server.  Not for production use.  Press Ctrl + C to close the server.

    Parameters
    ----------
    files_path : str
        filepath to a local folder that will work as root folder for a local flask
        server.  Defaults to the user's temporary files directory

    debug : str
        Defaults to True.  Indicates whether the flask server should be started
        in debug mode.
    
    port : str
        The network port to serve the story onto.  Defautls to 8082.
    
    host : str
        The host to for the local server.  Typically either localhost or the default gateway.
        Defaults to 127.0.0.1 (localhost).
    
    """
    logger.info("Building story rendering at http://{0}:{1}".format(host, port))
    if not files_path:
        files_path = tempfile.gettempdir()
    logger.info("Buidling standalone package for local rendering.")
    html = self.package_as_standalone().decode('utf-8')
    id = presalytics.story.util.to_title_case(self.story_outline.title)
    if id == '':
        id = 'blank'
    server = presalytics.story.server.LocalServer(host=host, debug=debug, port=port, root_path=files_path)
    pkg_templates_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "lib", "templates")
    shutil.copy(os.path.join(pkg_templates_dir, "favicon.ico"), os.path.join(files_path, "presalytics", "static"))
    html_file = os.path.join(files_path, "presalytics", "templates", id + '.html')
    with open(html_file, 'w') as file:
        file.write(html)
    address = "http://{}:{}/story/{}".format(host, port, id)
    logger.info("Opening browser tab...")
    presalytics.story.server.Browser(address).start()
    server.run()

Inherited members

class MatplotlibFigure (figure, name, *args, **kwargs)

A Widget for rendering matplotlib.pyplot.Figure instances in stories

This class acts as wrapper class for matplotlib figures, allowing their packaging into Widget objects and serialization to json. At render-time, the figure is converted to a d3.js object via the mpld3 package.

Parameters

figure : matplotlib.pyplot.Figure
the figure to create the widget from
name : str
the name of widget. Must be unique within COMPONENTS

Attributes

figure_dict : dict
A dict containing json-serializable data for reconstituting the matplotlib.pyplot.Figure object.
figure_id : str
A unique identifier used to render the that figure into a d3.js object
Expand source code Browse git
class MatplotlibFigure(presalytics.story.components.WidgetBase):
    """
    A `Widget` for rendering `matplotlib.pyplot.Figure` instances in stories

    This class acts as wrapper class for matplotlib figures, allowing their packaging into
    `presalytics.story.outline.Widget` objects and serialization to json.  At render-time, 
    the figure is converted to a d3.js object via the [mpld3](https://mpld3.github.io/)
    package.

    Parameters
    ----------

    figure : `matplotlib.pyplot.Figure`
        the figure to create the widget from

    name : str
        the name of widget.  Must be unique within `presalytics.COMPONENTS`

    Attributes
    ----------
    figure_dict : dict
        A `dict` containing json-serializable data for reconstituting the
        `matplotlib.pyplot.Figure` object.

    figure_id : str
        A unique identifier used to render the that figure into a d3.js object
    """
    __component_kind__ = 'matplotlib-figure'
    additional_properties: typing.Dict

    def __init__(self, figure: 'Figure', name: str, *args, **kwargs):
        super().__init__(self, *args, **kwargs)
        self.fig = figure
        self.name = name
        self.figure_dict = None
        if self.fig:
            self.figure_dict = mpld3.fig_to_dict(figure)
        else:
            figure_dict = kwargs.pop("figure_dict", None)
            if figure_dict:
                self.figure_dict = figure_dict
        if self.figure_dict is None:
            message = "MatplotlibFigure requires a figure_dict attribute.  Please supply either a valid figure_dict or matplotlib.pyplot.Figure object to __init__"
            raise presalytics.lib.exceptions.ValidationError(message)
        self.figure_id = self.figure_dict["id"]
        self.additional_properties = {}
        for key, val in kwargs.items():
            self.additional_properties[key] = val
        self.outline_widget = self.serialize()

    def to_html(self):
        return '<div id="{0}" class="mpld3"></div>'.format(self.figure_id)

    @classmethod
    def deserialize(cls, outline, **kwargs):
        figure_dict = outline.data.get("figure_dict")
        kwargs.update(**outline.additional_properties)
        return cls(None, outline.name, figure_dict=figure_dict, **outline.additional_properties)

    def serialize(self, **kwargs):
        data = {
            'figure_dict': self.figure_dict,
            'id': self.figure_id
        }
        plugin_obj = presalytics.story.outline.Plugin(
            name='mpld3',
            kind='script',
            config=data
        )
        return presalytics.story.outline.Widget(
            name=self.name,
            kind=self.__component_kind__,
            plugins=[plugin_obj.to_dict()],
            data=data,
            additional_properties=self.additional_properties
        )

Ancestors

Subclasses

Inherited members

class MatplotlibResponsiveFigure (figure, name, story_id='empty', *args, **kwargs)

A Widget for rendering matplotlib.pyplot.Figure instances in stories

This class acts as wrapper class for matplotlib figures, allowing their packaging into Widget objects and serialization to json. At render-time, the figure is converted to a d3.js object via the mpld3 package.

This class also depends on the static plugin

Parameters

figure : matplotlib.pyplot.Figure
the figure to create the widget from
name : str
the name of widget. Must be unique within COMPONENTS
story_id : str, optional
The story of thet

Attributes

figure_dict : dict
A dict containing json-serializable data for reconstituting the matplotlib.pyplot.Figure object.
figure_id : str
A unique identifier used to render the that figure into a d3.js object
Expand source code Browse git
class MatplotlibResponsiveFigure(MatplotlibFigure):
    """
    A `Widget` for rendering `matplotlib.pyplot.Figure` instances in stories

    This class acts as wrapper class for matplotlib figures, allowing their packaging into
    `presalytics.story.outline.Widget` objects and serialization to json.  At render-time, 
    the figure is converted to a d3.js object via the [mpld3](https://mpld3.github.io/)
    package.

    This class also depends on the static plugin

    Parameters
    ----------

    figure : `matplotlib.pyplot.Figure`
        the figure to create the widget from

    name : str
        the name of widget.  Must be unique within `presalytics.COMPONENTS`

    story_id : str, optional
        The story of thet

    Attributes
    ----------
    figure_dict : dict
        A `dict` containing json-serializable data for reconstituting the
        `matplotlib.pyplot.Figure` object.

    figure_id : str
        A unique identifier used to render the that figure into a d3.js object
    """

    additional_properties: typing.Dict
    __component_kind__ = 'matplotlib-responsive'

    __plugins__ = [
        {
            'name': 'external_scripts',
            'kind': 'script',
            'config': {
                'approved_scripts_key': 'jquery'
            }
        },
        {
            'name': 'external_scripts',
            'kind': 'script',
            'config': {
                'approved_scripts_key': 'mpl-responsive'
            }
        },
        {
            'name': 'external_links',
            'kind': 'style',
            'config': {
                'approved_styles_key': 'preloaders'
            }
        }
    ] #type: ignore

    def __init__(self, figure: 'Figure', name: str, story_id: str = "empty", *args, **kwargs):
        self.story_id = story_id
        super(MatplotlibResponsiveFigure, self).__init__(figure, name, *args, **kwargs)
        self.story_host = self.get_client(delegate_login=True).story.api_client.configuration.host

        
    def to_html(self):
        if not self.story_id:
            message = "This object requires a valid story_id to render."
            raise presalytics.lib.exceptions.MissingConfigException(message=message)
        html = self.create_container()
        return html


    def create_container(self, **kwargs):
        """
        Wraps the Matplotlib Figure in a SVG endpoint load via `<iframe>` that
        will be rendered inside of a story and rescaled to give repsonsive effect
        """
        params = {
            "site_host": self.get_client(delegate_login=True).site_host,
            "figure_id": self.figure_id,
            "story_id": self.story_id
        }
        source_url = "{site_host}/story/matplotlib-figure/{story_id}/{figure_id}".format(**params)
        empty_parent_div = lxml.html.Element("div", {
            'class': 'empty-parent bg-light matplotlib-responsive-container',
            'style': 'height: 100%; width: 100%, display: block; text-align: left;'
        })
        frame = lxml.html.Element("iframe", {
            'src': source_url,
            'frameborder': "0",
            'scrolling': "no",
            'class': 'matplotlib-responsive-frame',
            'style': 'max-height: none; max-width: none;'
        })
        empty_parent_div.append(frame)
        return lxml.html.tostring(empty_parent_div).decode('utf-8')


    @classmethod
    def deserialize(cls, outline, **kwargs):
        figure_dict = outline.data.get("figure_dict")
        story_id = outline.data.get("story_id", None)
        kwargs.update(**outline.additional_properties)
        return cls(None, 
                   outline.name,
                   story_id,
                   figure_dict=figure_dict,
                   **kwargs)

    def serialize(self, **kwargs):
        data = {
            'figure_dict': self.figure_dict,
            'id': self.figure_id,
            'story_id': self.story_id
        }
        return presalytics.story.outline.Widget(
            name=self.name,
            kind=self.__component_kind__,
            data=data,
            additional_properties=self.additional_properties
        )

    def standalone_html(self) -> str:
        SIMPLE_HTML = jinja2.Template("""<!DOCTYPE html>
        <html>
            <head>
                <meta charset="UTF-8">
                <style>
                {{ extra_css }}
                </style>
            </head>
            <body>
                <script type="text/javascript" src="{{ d3_url }}"></script>
                <script type="text/javascript" src="{{ mpld3_url }}"></script>
                <div id="{{ figid }}"></div>
                <script type="text/javascript">

                    mpld3.draw_figure("{{ figid }}", {{ figure_json }});
            
                </script>
            </body>
        </html>""")
        figure_json = json.dumps(self.figure_dict, cls=presalytics.lib.plugins.matplotlib.Mpld3NumpyToJson)
        context = {
            "d3_url": presalytics.lib.plugins.external.ApprovedExternalScripts().attr_dict.flatten().get('d3v3'),
            "mpld3_url": presalytics.lib.plugins.external.ApprovedExternalScripts().attr_dict.flatten().get('mpld3'),
            "figid": self.figure_id,
            "figure_json": figure_json
        }
        return SIMPLE_HTML.render(**context)

Ancestors

Methods

def create_container(self, **kwargs)

Wraps the Matplotlib Figure in a SVG endpoint load via <iframe> that will be rendered inside of a story and rescaled to give repsonsive effect

Expand source code Browse git
def create_container(self, **kwargs):
    """
    Wraps the Matplotlib Figure in a SVG endpoint load via `<iframe>` that
    will be rendered inside of a story and rescaled to give repsonsive effect
    """
    params = {
        "site_host": self.get_client(delegate_login=True).site_host,
        "figure_id": self.figure_id,
        "story_id": self.story_id
    }
    source_url = "{site_host}/story/matplotlib-figure/{story_id}/{figure_id}".format(**params)
    empty_parent_div = lxml.html.Element("div", {
        'class': 'empty-parent bg-light matplotlib-responsive-container',
        'style': 'height: 100%; width: 100%, display: block; text-align: left;'
    })
    frame = lxml.html.Element("iframe", {
        'src': source_url,
        'frameborder': "0",
        'scrolling': "no",
        'class': 'matplotlib-responsive-frame',
        'style': 'max-height: none; max-width: none;'
    })
    empty_parent_div.append(frame)
    return lxml.html.tostring(empty_parent_div).decode('utf-8')
def standalone_html(self)
Expand source code Browse git
def standalone_html(self) -> str:
    SIMPLE_HTML = jinja2.Template("""<!DOCTYPE html>
    <html>
        <head>
            <meta charset="UTF-8">
            <style>
            {{ extra_css }}
            </style>
        </head>
        <body>
            <script type="text/javascript" src="{{ d3_url }}"></script>
            <script type="text/javascript" src="{{ mpld3_url }}"></script>
            <div id="{{ figid }}"></div>
            <script type="text/javascript">

                mpld3.draw_figure("{{ figid }}", {{ figure_json }});
        
            </script>
        </body>
    </html>""")
    figure_json = json.dumps(self.figure_dict, cls=presalytics.lib.plugins.matplotlib.Mpld3NumpyToJson)
    context = {
        "d3_url": presalytics.lib.plugins.external.ApprovedExternalScripts().attr_dict.flatten().get('d3v3'),
        "mpld3_url": presalytics.lib.plugins.external.ApprovedExternalScripts().attr_dict.flatten().get('mpld3'),
        "figid": self.figure_id,
        "figure_json": figure_json
    }
    return SIMPLE_HTML.render(**context)

Inherited members

class OoxmlFileWidget (filename, name=None, story_id=None, object_ooxml_id=None, endpoint_map=None, object_name=None, previous_ooxml_version={}, file_last_modified=None, document_ooxml_id=None, **kwargs)

Builds a widget from a Presentation or Spreadsheet document

This class interacts with the Presalytics API to extract SVG objects from Presentation and spreadsheet documents, identify them, and render them into a story. The file is uploaded to Presalytics API Ooxml Automation service, which then processes the file and scans for objects in the file's object tree (As seen in the 'Selection Pane' in PowerPoint) for objects matching the 'object_name'. When rendered, this widget retrieves an SVG of the identified object for rendering within the story.

Please note that the Presalytics API Ooxml Automation object will be created overwritten each time this widget is initialized, and replaced within the corresponding StoryOutline. For in-place editing of widgets Ooxml Automation objects that are already bound to the Story, please see OoxmlEditorWidget

Parameters

filename : str
The local filepath a presentation or spreadsheet file containing the object to be rendered
name : str, optional
The widget name. If not provided, attribute will be set as the object_name or filename
story_id : str, optional
The the id of the story in the Presalytics API Story service. If not provided, a new story will be created. Do not supply if this object has not yet been created.
object_ooxml_id : str, optional
The identifier of the Ooxml Automation service object bound the Story. Do not supply if this object has not yet been created.
endpoint_map : OoxmlEndpointMap, optional
Reference to the Presalytics API Ooxml Automation service endpoint and object type that for the object of interest
object_name : str, optional
The name of the object in the file's object tree the will be rendered
previous_ooxml_version : str, optional
The id Ooxml Automation service document object that was previously used to occupy this widget in the StoryOutline
file_last_modified : str, optional
The "last modified" date for the file at the filename path. Used to ascertain whether file has been updated since the last time the widget was initialized, and correspondingly, whether the widget should be updated in the StoryOutline
document_ooxml_id : str, optional
The identifier for the parent "Document" object in the Ooxml Automation service for the object idenitifiable by a combinatation of object_ooxml_id and endpoint_map.
Expand source code Browse git
class OoxmlFileWidget(OoxmlWidgetBase):
    """
    Builds a `widget` from a Presentation or Spreadsheet document

    This class interacts with the Presalytics API to extract SVG objects from
    Presentation and spreadsheet documents, identify them, and render them 
    into a story. The file is uploaded to Presalytics API Ooxml Automation service,
    which then processes the file and scans for objects in the file's object tree 
    (As seen in the 'Selection Pane' in PowerPoint) for objects matching the 'object_name'.
    When rendered, this widget retrieves an SVG of the identified object for rendering within 
    the story. 

    Please note that the Presalytics API Ooxml Automation object will be created overwritten 
    each time this widget is initialized, and replaced within the corresponding 
    `presalytics.story.outline.StoryOutline`.  For in-place editing of widgets Ooxml Automation objects 
    that are already bound to the `Story`, please see `presalytics.lib.widgets.ooxml_editors.OoxmlEditorWidget`

    Parameters
    ----------
    filename : str
        The local filepath a presentation or spreadsheet file containing
        the object to be rendered

    name : str, optional
        The widget name.  If not provided, attribute will be set as the `object_name` 
        or `filename`

    story_id : str, optional
        The the id of the story in the Presalytics API Story service.  If not provided, 
        a new story will be created.  Do not supply if this object has not yet been created. 
    
    object_ooxml_id : str, optional
        The identifier of the Ooxml Automation service object bound the Story. Do not supply if this 
        object has not yet been created.

    endpoint_map : presalytics.lib.widgets.ooxml.OoxmlEndpointMap, optional
        Reference to the Presalytics API Ooxml Automation service endpoint and object type
        that for the object of interest

    object_name : str, optional
        The name of the object in the file's object tree the will be rendered

    previous_ooxml_version : str, optional
        The id Ooxml Automation service document object that was previously used to 
        occupy this widget in the `presalytics.story.outline.StoryOutline`

    file_last_modified : str, optional
        The "last modified" date for the file at the `filename` path.  Used to ascertain
        whether file has been updated since the last time the widget was initialized, and
        correspondingly, whether the widget should be updated in the `presalytics.story.outline.StoryOutline`

    document_ooxml_id : str, optional
        The identifier for the parent "Document" object in the Ooxml Automation service for the object
        idenitifiable by a combinatation of `object_ooxml_id` and `endpoint_map`.
    
    """
    object_name: typing.Optional[str]
    ooxml_id: str
    file_last_modified: datetime.datetime
    previous_ooxml_version: typing.Dict[str, str]

    __component_kind__ = 'ooxml-file-object'

    def __init__(self,
                 filename,
                 name=None,
                 story_id=None,
                 object_ooxml_id=None,
                 endpoint_map=None,
                 object_name=None,
                 previous_ooxml_version={},
                 file_last_modified=None,
                 document_ooxml_id=None,
                 **kwargs):
        if object_name:
            self.object_name = object_name
        else:
            self.object_name = None
        if not name:
            if self.object_name:
                name = self.object_name
            else:
                name = filename
        super(OoxmlFileWidget, self).__init__(name, story_id, object_ooxml_id, endpoint_map, **kwargs)
        self.filename = os.path.basename(filename)
        if not self.endpoint_map:
            if filename.split(".")[-1] in ["pptx", "ppt"]:
                self.endpoint_map = OoxmlEndpointMap(OoxmlEndpointMap.SLIDE)
        self.previous_ooxml_version = previous_ooxml_version
        if file_last_modified:
            self.file_last_modified = file_last_modified.replace(tzinfo=datetime.timezone.utc)
        else:
            self.file_last_modified = datetime.datetime.utcnow()
        self.document_ooxml_id = document_ooxml_id
        self.update()
        self.svg_html = self.create_container(**kwargs)

    def update(self):
        """
        If the file is available locally, this renders that updated file and pushes
        the updated rendering data to the server
        """
        story: 'ApiStory'
        page: 'Page'
        widget: 'Widget'

        search_paths = list(set(presalytics.autodiscover_paths))
        if os.getcwd() not in search_paths:
            search_paths.append(os.getcwd())
        for path in search_paths:
            fpath = os.path.join(path, self.filename)
            if os.path.exists(fpath):
                # update only the file has been modified sine last time
                this_file_last_modified = datetime.datetime.utcfromtimestamp(os.path.getmtime(fpath)).astimezone(tz=datetime.timezone.utc)
                if self.file_last_modified is None or self.file_last_modified <= this_file_last_modified:
                    client = self.get_client()
                    story, status, headers = client.story.story_id_file_post_with_http_info(self.story_id, file=fpath, replace_existing=True, obsolete_id=self.document_ooxml_id)
                    if status >= 299:
                        raise presalytics.lib.exceptions.ApiError()
                    self.previous_ooxml_version = {
                        "document_ooxml_id": self.document_ooxml_id,
                        "object_ooxml_id": self.object_ooxml_id
                    }
                    new_outline = presalytics.story.outline.StoryOutline.load(story.outline)
                    for page in new_outline.pages:
                        found = False
                        for widget in page.widgets:
                            if widget.name == self.object_name:
                                self.document_ooxml_id = widget.data["document_ooxml_id"]
                                found = True
                                break
                        if found:
                            break
                    if not found:
                        message = "Unable to find widget object name {0} in new story outline.  Has this widget been deleted?".format(self.object_name)
                        raise presalytics.lib.exceptions.ValidationError(message)

                    # Get object tree, compare to object name
                    child_tree = client.ooxml_automation.documents_childobjects_get_id(self.document_ooxml_id)
                    target_dto = None

                    try:
                        target_dto = next(x for x in child_tree if x.entity_name == self.object_name)
                    except StopIteration:
                        pass
                    # if name not in object or _object_name is none, get first item of type in end_point id
                    if not target_dto:
                        try:
                            target_dto = next(x for x in child_tree if x.object_type.split(".")[1] == self.endpoint_map.endpoint_id)
                        except StopIteration:
                            message = "Child tree of document {0} does not have a child object of type {1} or name {2}.".format(self.ooxml_id, self.endpoint_map.endpoint_id, self.object_name)
                            raise presalytics.lib.exceptions.InvalidConfigurationError(message)
                    # set widget parameters for recreation server-side (without file)
                    self.object_ooxml_id = target_dto.entity_id
                    self.file_last_modified = presalytics.lib.util.roundup_date_modified(this_file_last_modified)


    @classmethod
    def deserialize(cls, component, **kwargs):
        init_args = {
            "filename": component.data["filename"],
            "endpoint_map": OoxmlEndpointMap(component.data["endpoint_id"]),
            "object_name": component.data["object_name"],
            "name": component.name,
        }
        if "document_ooxml_id" in component.data:
            init_args.update(
                {
                    "document_ooxml_id": component.data["document_ooxml_id"]
                }
            )
        if "object_ooxml_id" in component.data:
            init_args.update(
                {
                    "object_ooxml_id": component.data["object_ooxml_id"]
                }
            )
        if "file_last_modified" in component.data:
            init_args.update(
                {
                    "file_last_modified": dateutil.parser.parse(component.data["file_last_modified"]).replace(tzinfo=datetime.timezone.utc)
                }
            )
        if "previous_ooxml_version" in component.data:
            init_args.update(
                {
                    "previous_ooxml_version": component.data["previous_ooxml_version"]
                }
            )
        if "story_id" in component.data:
            init_args.update(
                {
                    "story_id": component.data["story_id"]
                }
            )
        if len(kwargs.keys()) > 0:
            init_args.update(kwargs)
        return cls(**init_args)

    def serialize(self):
        self.update()
        data = {
            "filename": self.filename,
            "object_name": self.object_name,
            "endpoint_id": self.endpoint_map.endpoint_id,
            "document_ooxml_id": self.document_ooxml_id,
            "object_ooxml_id": self.object_ooxml_id,
            "story_id": self.story_id
        }
        if self.file_last_modified:
            data.update(
                {
                    "file_last_modified": self.file_last_modified.isoformat()
                }
            )
        if self.previous_ooxml_version:
            data.update(
                {
                    "previous_ooxml_version": self.previous_ooxml_version
                }
            )
        widget = presalytics.story.outline.Widget(
            name=self.name,
            kind=self.__component_kind__,
            data=data,
            plugins=None
        )
        return widget

Ancestors

Methods

def update(self)

If the file is available locally, this renders that updated file and pushes the updated rendering data to the server

Expand source code Browse git
def update(self):
    """
    If the file is available locally, this renders that updated file and pushes
    the updated rendering data to the server
    """
    story: 'ApiStory'
    page: 'Page'
    widget: 'Widget'

    search_paths = list(set(presalytics.autodiscover_paths))
    if os.getcwd() not in search_paths:
        search_paths.append(os.getcwd())
    for path in search_paths:
        fpath = os.path.join(path, self.filename)
        if os.path.exists(fpath):
            # update only the file has been modified sine last time
            this_file_last_modified = datetime.datetime.utcfromtimestamp(os.path.getmtime(fpath)).astimezone(tz=datetime.timezone.utc)
            if self.file_last_modified is None or self.file_last_modified <= this_file_last_modified:
                client = self.get_client()
                story, status, headers = client.story.story_id_file_post_with_http_info(self.story_id, file=fpath, replace_existing=True, obsolete_id=self.document_ooxml_id)
                if status >= 299:
                    raise presalytics.lib.exceptions.ApiError()
                self.previous_ooxml_version = {
                    "document_ooxml_id": self.document_ooxml_id,
                    "object_ooxml_id": self.object_ooxml_id
                }
                new_outline = presalytics.story.outline.StoryOutline.load(story.outline)
                for page in new_outline.pages:
                    found = False
                    for widget in page.widgets:
                        if widget.name == self.object_name:
                            self.document_ooxml_id = widget.data["document_ooxml_id"]
                            found = True
                            break
                    if found:
                        break
                if not found:
                    message = "Unable to find widget object name {0} in new story outline.  Has this widget been deleted?".format(self.object_name)
                    raise presalytics.lib.exceptions.ValidationError(message)

                # Get object tree, compare to object name
                child_tree = client.ooxml_automation.documents_childobjects_get_id(self.document_ooxml_id)
                target_dto = None

                try:
                    target_dto = next(x for x in child_tree if x.entity_name == self.object_name)
                except StopIteration:
                    pass
                # if name not in object or _object_name is none, get first item of type in end_point id
                if not target_dto:
                    try:
                        target_dto = next(x for x in child_tree if x.object_type.split(".")[1] == self.endpoint_map.endpoint_id)
                    except StopIteration:
                        message = "Child tree of document {0} does not have a child object of type {1} or name {2}.".format(self.ooxml_id, self.endpoint_map.endpoint_id, self.object_name)
                        raise presalytics.lib.exceptions.InvalidConfigurationError(message)
                # set widget parameters for recreation server-side (without file)
                self.object_ooxml_id = target_dto.entity_id
                self.file_last_modified = presalytics.lib.util.roundup_date_modified(this_file_last_modified)

Inherited members

class OoxmlEndpointMap (endpoint_id, baseurl=None)

Mapping class that bridges Presalytics API Ooxml Automation service endpoints and component class that consume those endpoints (typically subclasses of OoxmlWidgetBase)

The classmethods on this class are conveninece methods to help users quickly inform their widget which endpoint their of a Ooxml Document their targets.

Instance methods on this class are used by widget to generate urls and lookup against object tree for target objects.

Parameters

endpoint_id : str
A unique string for identifying the object_type related to the enpoint
baseurl : str
For developer use. Allows this to generate urls for non-standard instances of the Ooxml Automation service. Defaults to https://api.presalytics.io/ooxml-automation/

Attributes

root_url : str
the home url for the class instance. Typically <http://api.presalytics.io/ooxml-automation/{object_type}>
OBJECT_TYPE_MAP : str
A mapping table for object_types and object tree lookup keys
Expand source code Browse git
class OoxmlEndpointMap(object):
    """
    Mapping class that bridges Presalytics API Ooxml Automation service endpoints
    and component class that consume those endpoints (typically subclasses of 
    `presalytics.lib.widgets.ooxml.OoxmlWidgetBase`)

    The classmethods on this class are conveninece methods to help users 
    quickly inform their widget which endpoint their of a Ooxml Document their
    targets.

    Instance methods on this class are used by widget to generate urls and 
    lookup against object tree for target objects.

    Parameters
    ----------
    endpoint_id : str
        A unique string for identifying the object_type related to the enpoint
    
    baseurl : str
        For developer use. Allows this to generate urls for non-standard instances
        of the Ooxml Automation service.  Defaults to https://api.presalytics.io/ooxml-automation/

    Attributes
    ----------
    root_url : str
        the home url for the class instance.  Typically `http://api.presalytics.io/ooxml-automation/{object_type}`
    
    OBJECT_TYPE_MAP : str
        A mapping table for object_types and object tree lookup keys
    """
    _BASE_URL = "https://api.presalytics.io/ooxml-automation"
    _CHART = "Charts"
    _CONNECTION_SHAPE = "ConnectionShapes"
    _DOCUMENT = "Documents"
    _GROUP = "Groups"
    _IMAGE = "Images"
    _SHAPE = "Shapes"
    _SHAPETREE = "ShapeTrees"
    _SLIDE = "Slides"
    _TABLE = "Tables"
    _THEME = "Themes"

    def __init__(self, endpoint_id, baseurl: str = None):
        if endpoint_id not in OoxmlEndpointMap.__dict__.values():
            raise presalytics.lib.exceptions.ValidationError("{0} is not a valid endpoint ID".format(endpoint_id))
        self.endpoint_id = endpoint_id
        if not baseurl:
            self.baseurl = OoxmlEndpointMap._BASE_URL
            custom_hosts = presalytics.CONFIG.get("HOSTS", None)
            if custom_hosts:
                ooxml_host = custom_hosts.get("OOXML_AUTOMATION", None)
                if ooxml_host:
                    self.baseurl = ooxml_host
                
        else:
            self.baseurl = baseurl
        self.root_url = posixpath.join(self.baseurl, self.endpoint_id)
        self.OBJECT_TYPE_MAP = self._build_object_type_map()
    


    def _build_object_type_map(self):
        return {
            "Chart": [
                OoxmlEndpointMap._CHART,
            ],
            "Slide": [
                OoxmlEndpointMap._GROUP,
                OoxmlEndpointMap._SHAPE,
                OoxmlEndpointMap._SHAPETREE, 
                OoxmlEndpointMap._CONNECTION_SHAPE,
                OoxmlEndpointMap._SLIDE
            ],
            "Table": [
                OoxmlEndpointMap._TABLE
            ],
            "Theme": [
                OoxmlEndpointMap._THEME
            ],
            "Shared": [
                OoxmlEndpointMap._IMAGE
            ],
            "EMPTY": [
                OoxmlEndpointMap._DOCUMENT
            ]
        }
    
    def get_object_type(self):
        """
        Returns the Ooxml Automation service object type for this endpoint
        """
        for key, val in self.OBJECT_TYPE_MAP.items():
            for test_ep in val:
                if test_ep == self.endpoint_id:
                    if key == "EMPTY":
                        return self.endpoint_id
                    else:
                        return "{0}.{1}".format(key, self.endpoint_id)
        message = "Invalid EndpointMap configuration: {0} is not in OBJECT_TYPE_MAP".format(self.endpoint_id)
        raise presalytics.lib.exceptions.ValidationError(message)

    def get_id_url(self, id):
        """
        Returns a url to pull metadata from this Ooxml Automation service endpoint
        """
        return posixpath.join(self.root_url, id)

    def get_svg_url(self, id):
        """
        Returns a url to download an svg from this Ooxml Automation service endpoint
        """
        return posixpath.join(self.root_url, "Svg", id)

    def get_xml_url(self, id):
        """
        Returns a url to pull the Open Office Xml from this Ooxml Automation service endpoint
        """
        return posixpath.join(self.root_url, "OpenOfficeXml", id)

    @classmethod
    def connection_shape(cls, baseurl=None):
        """
        Factory method to create an `presalytics.lib.widgets.ooxml.OoxmlEndpointMap` instance
        targeting ConnectionShape objects
        """
        return cls(OoxmlEndpointMap._CONNECTION_SHAPE, baseurl)

    @classmethod
    def chart(cls, baseurl=None):
        """
        Factory method to create an `presalytics.lib.widgets.ooxml.OoxmlEndpointMap` instance
        targeting Chart objects
        """
        return cls(OoxmlEndpointMap._CHART, baseurl)

    @classmethod
    def document(cls, baseurl=None):
        """
        Factory method to create an `presalytics.lib.widgets.ooxml.OoxmlEndpointMap` instance
        targeting Document objects
        """
        return cls(OoxmlEndpointMap._DOCUMENT, baseurl)
    
    @classmethod
    def group(cls, baseurl=None):
        """
        Factory method to create an `presalytics.lib.widgets.ooxml.OoxmlEndpointMap` instance
        targeting Group objects
        """
        return cls(OoxmlEndpointMap._GROUP, baseurl)

    @classmethod
    def image(cls, baseurl=None):
        """
        Factory method to create an `presalytics.lib.widgets.ooxml.OoxmlEndpointMap` instance
        targeting Image objects
        """
        return cls(OoxmlEndpointMap._IMAGE, baseurl)

    @classmethod
    def shape(cls, baseurl=None):
        """
        Factory method to create an `presalytics.lib.widgets.ooxml.OoxmlEndpointMap` instance
        targeting Shape objects
        """
        return cls(OoxmlEndpointMap._SHAPE, baseurl)

    @classmethod
    def shapetree(cls, baseurl=None):
        """
        Factory method to create an `presalytics.lib.widgets.ooxml.OoxmlEndpointMap` instance
        targeting ShapeTree objects
        """
        return cls(OoxmlEndpointMap._SHAPETREE, baseurl)

    @classmethod
    def slide(cls, baseurl=None):
        """
        Factory method to create an `presalytics.lib.widgets.ooxml.OoxmlEndpointMap` instance
        targeting Slide objects
        """
        return cls(OoxmlEndpointMap._SLIDE, baseurl)

    @classmethod
    def table(cls, baseurl=None):
        """
        Factory method to create an `presalytics.lib.widgets.ooxml.OoxmlEndpointMap` instance
        targeting Table objects
        """
        return cls(OoxmlEndpointMap._TABLE, baseurl)

    @classmethod
    def theme(cls, baseurl=None):
        """
        Factory method to create an `presalytics.lib.widgets.ooxml.OoxmlEndpointMap` instance
        targeting Theme objects
        """
        return cls(OoxmlEndpointMap._THEME, baseurl)

Static methods

def connection_shape(baseurl=None)

Factory method to create an OoxmlEndpointMap instance targeting ConnectionShape objects

Expand source code Browse git
@classmethod
def connection_shape(cls, baseurl=None):
    """
    Factory method to create an `presalytics.lib.widgets.ooxml.OoxmlEndpointMap` instance
    targeting ConnectionShape objects
    """
    return cls(OoxmlEndpointMap._CONNECTION_SHAPE, baseurl)
def chart(baseurl=None)

Factory method to create an OoxmlEndpointMap instance targeting Chart objects

Expand source code Browse git
@classmethod
def chart(cls, baseurl=None):
    """
    Factory method to create an `presalytics.lib.widgets.ooxml.OoxmlEndpointMap` instance
    targeting Chart objects
    """
    return cls(OoxmlEndpointMap._CHART, baseurl)
def document(baseurl=None)

Factory method to create an OoxmlEndpointMap instance targeting Document objects

Expand source code Browse git
@classmethod
def document(cls, baseurl=None):
    """
    Factory method to create an `presalytics.lib.widgets.ooxml.OoxmlEndpointMap` instance
    targeting Document objects
    """
    return cls(OoxmlEndpointMap._DOCUMENT, baseurl)
def group(baseurl=None)

Factory method to create an OoxmlEndpointMap instance targeting Group objects

Expand source code Browse git
@classmethod
def group(cls, baseurl=None):
    """
    Factory method to create an `presalytics.lib.widgets.ooxml.OoxmlEndpointMap` instance
    targeting Group objects
    """
    return cls(OoxmlEndpointMap._GROUP, baseurl)
def image(baseurl=None)

Factory method to create an OoxmlEndpointMap instance targeting Image objects

Expand source code Browse git
@classmethod
def image(cls, baseurl=None):
    """
    Factory method to create an `presalytics.lib.widgets.ooxml.OoxmlEndpointMap` instance
    targeting Image objects
    """
    return cls(OoxmlEndpointMap._IMAGE, baseurl)
def shape(baseurl=None)

Factory method to create an OoxmlEndpointMap instance targeting Shape objects

Expand source code Browse git
@classmethod
def shape(cls, baseurl=None):
    """
    Factory method to create an `presalytics.lib.widgets.ooxml.OoxmlEndpointMap` instance
    targeting Shape objects
    """
    return cls(OoxmlEndpointMap._SHAPE, baseurl)
def shapetree(baseurl=None)

Factory method to create an OoxmlEndpointMap instance targeting ShapeTree objects

Expand source code Browse git
@classmethod
def shapetree(cls, baseurl=None):
    """
    Factory method to create an `presalytics.lib.widgets.ooxml.OoxmlEndpointMap` instance
    targeting ShapeTree objects
    """
    return cls(OoxmlEndpointMap._SHAPETREE, baseurl)
def slide(baseurl=None)

Factory method to create an OoxmlEndpointMap instance targeting Slide objects

Expand source code Browse git
@classmethod
def slide(cls, baseurl=None):
    """
    Factory method to create an `presalytics.lib.widgets.ooxml.OoxmlEndpointMap` instance
    targeting Slide objects
    """
    return cls(OoxmlEndpointMap._SLIDE, baseurl)
def table(baseurl=None)

Factory method to create an OoxmlEndpointMap instance targeting Table objects

Expand source code Browse git
@classmethod
def table(cls, baseurl=None):
    """
    Factory method to create an `presalytics.lib.widgets.ooxml.OoxmlEndpointMap` instance
    targeting Table objects
    """
    return cls(OoxmlEndpointMap._TABLE, baseurl)
def theme(baseurl=None)

Factory method to create an OoxmlEndpointMap instance targeting Theme objects

Expand source code Browse git
@classmethod
def theme(cls, baseurl=None):
    """
    Factory method to create an `presalytics.lib.widgets.ooxml.OoxmlEndpointMap` instance
    targeting Theme objects
    """
    return cls(OoxmlEndpointMap._THEME, baseurl)

Methods

def get_object_type(self)

Returns the Ooxml Automation service object type for this endpoint

Expand source code Browse git
def get_object_type(self):
    """
    Returns the Ooxml Automation service object type for this endpoint
    """
    for key, val in self.OBJECT_TYPE_MAP.items():
        for test_ep in val:
            if test_ep == self.endpoint_id:
                if key == "EMPTY":
                    return self.endpoint_id
                else:
                    return "{0}.{1}".format(key, self.endpoint_id)
    message = "Invalid EndpointMap configuration: {0} is not in OBJECT_TYPE_MAP".format(self.endpoint_id)
    raise presalytics.lib.exceptions.ValidationError(message)
def get_id_url(self, id)

Returns a url to pull metadata from this Ooxml Automation service endpoint

Expand source code Browse git
def get_id_url(self, id):
    """
    Returns a url to pull metadata from this Ooxml Automation service endpoint
    """
    return posixpath.join(self.root_url, id)
def get_svg_url(self, id)

Returns a url to download an svg from this Ooxml Automation service endpoint

Expand source code Browse git
def get_svg_url(self, id):
    """
    Returns a url to download an svg from this Ooxml Automation service endpoint
    """
    return posixpath.join(self.root_url, "Svg", id)
def get_xml_url(self, id)

Returns a url to pull the Open Office Xml from this Ooxml Automation service endpoint

Expand source code Browse git
def get_xml_url(self, id):
    """
    Returns a url to pull the Open Office Xml from this Ooxml Automation service endpoint
    """
    return posixpath.join(self.root_url, "OpenOfficeXml", id)
class OoxmlWidgetBase (name, story_id=None, object_ooxml_id=None, endpoint_map=None, **kwargs)

Base class for creating widgets from objects at endpoints in the Presalytics API Ooxml Automation service.

Parameters

name : str, optional
The widget name. If not provided, will be the object_name or filename
story_id : str, optional
The the id of the story in the Presalytics API Story service. If not provided, a new story will be created. Do not supply if this object has not yet been created.
object_ooxml_id : str, optional
The identifier of the Ooxml Automation service object bound the Story. Do not supply if this object has not yet been created.
endpoint_map : OoxmlEndpointMap, optional
Reference to the Presalytics API Ooxml Automation service endpoint and object type that for the object of interest

Attributes

svg_html : str
An html fragment containing the svg data
Expand source code Browse git
class OoxmlWidgetBase(presalytics.story.components.WidgetBase):
    """
    Base class for creating widgets from objects at endpoints in the 
    Presalytics API Ooxml Automation service.

    Parameters
    ----------
    name : str, optional
        The widget name.  If not provided, will be the `object_name` or `filename`

    story_id : str, optional
        The the id of the story in the Presalytics API Story service.  If not provided, 
        a new story will be created.  Do not supply if this object has not yet been created. 
    
    object_ooxml_id : str, optional
        The identifier of the Ooxml Automation service object bound the Story. Do not supply if this 
        object has not yet been created.

    endpoint_map : presalytics.lib.widgets.ooxml.OoxmlEndpointMap, optional
        Reference to the Presalytics API Ooxml Automation service endpoint and object type
        that for the object of interest

    Attributes
    ----------
    svg_html : str
        An html fragment containing the svg data
    """
    endpoint_map: OoxmlEndpointMap
    data: typing.Dict

    __component_kind__ = "ooxml-base-widget"

    __plugins__ = [
        {
            'name': 'external_scripts',
            'kind': 'script',
            'config': {
                'approved_scripts_key': 'jquery'
            }
        },
        {
            'name': 'external_scripts',
            'kind': 'script',
            'config': {
                'approved_scripts_key': 'ooxml'
            }
        },
        {
            'name': 'external_links',
            'kind': 'style',
            'config': {
                'approved_styles_key': 'preloaders'
            }
        }
    ]

    def __init__(self, 
                 name,
                 story_id=None,
                 object_ooxml_id=None,
                 endpoint_map=None,
                 **kwargs):
        super(OoxmlWidgetBase, self).__init__(name, **kwargs)
        if object_ooxml_id:
            self.object_ooxml_id = object_ooxml_id
        if story_id:
            self.story_id = story_id
        if endpoint_map:
            self.endpoint_map = endpoint_map
        self.svg_html = None

    def create_container(self, **kwargs):
        """
        Wraps the Presalytics API Ooxml Automation service SVG endpoint in an `<iframe>` that
        will be rendered inside of a story
        """
        svg_container_div = lxml.html.Element("div", {
            'class': 'svg-container',
            'data-object-type': self.endpoint_map.endpoint_id,
            'data-object-id': self.object_ooxml_id
        })
        preloader_container_div = lxml.html.Element( "div", {"class":"preloader-container"})
        preloader_row_div = lxml.etree.SubElement(preloader_container_div, "div", attrib={"class":"preloader-row"})
        preloader_file = os.path.join(os.path.dirname(__file__), "img", "preloader.svg")
        svg = lxml.html.parse(preloader_file)
        preloader_row_div.append(svg.getroot())
        empty_parent_div = lxml.html.Element("div", {
            'class': 'empty-parent bg-light'
        })
        empty_parent_div.extend([svg_container_div, preloader_container_div])
        return lxml.html.tostring(empty_parent_div)


    def to_html(self, **kwargs):
        """
        Returns an html string that will render the object at the endpoint
        """
        html = self.create_container()
        try:
            html = html.decode('utf-8')
        except Exception:
            pass
        self.svg_html = html
        return self.svg_html

    def get_svg(self, id, timeout_iterator=0) -> str:
        """
        Get an svg-formatted version of the object from the Ooxml Automation service
        """
        svg_url = self.endpoint_map.get_svg_url(id)
        client = self.get_client()
        headers = client.get_auth_header()
        headers.update(client.get_request_id_header())
        response = requests.get(svg_url, headers=headers)
        svg_data = response.text
        if response.status_code != 200:
            raise presalytics.lib.exceptions.ApiError(message=response.text, status_code=response.status_code)
        if response.text.startswith("Temp data"):
            if timeout_iterator > 5:
                raise presalytics.lib.exceptions.ApiError(message="Unable to download svg.  Please check for upstream processing errors.")
            else:
                time.sleep(2)
                svg_data = self.get_svg(id, timeout_iterator)
        return svg_data

    def get_svg_file(self, filename=None):
        """
        Writes an svg representation of the object to current working directory.  Filname is optional.
        """
        if not filename:
            filename = self.endpoint_map.get_object_type() + "-" + self.object_ooxml_id + ".pptx"
        with open(filename, 'w') as f:
            f.write(self.get_svg(self.object_ooxml_id))

    def serialize(self):
        data = {
            "story_id": self.story_id,
            "object_id": self.object_ooxml_id,
            "endpoint_id": self.endpoint_map.endpoint_id
        }
        widget = presalytics.story.outline.Widget(
            name=self.name,
            kind=self.__component_kind__,
            data=data,
            plugins=None
        )
        return widget

    @classmethod
    def deserialize(cls, component, **kwargs):
        return cls(
            component.name, 
            component.data["story_id"], 
            component.data["object_id"],
            OoxmlEndpointMap(component.data["endpoint_id"]),
            **kwargs
        )

Ancestors

Subclasses

Methods

def create_container(self, **kwargs)

Wraps the Presalytics API Ooxml Automation service SVG endpoint in an <iframe> that will be rendered inside of a story

Expand source code Browse git
def create_container(self, **kwargs):
    """
    Wraps the Presalytics API Ooxml Automation service SVG endpoint in an `<iframe>` that
    will be rendered inside of a story
    """
    svg_container_div = lxml.html.Element("div", {
        'class': 'svg-container',
        'data-object-type': self.endpoint_map.endpoint_id,
        'data-object-id': self.object_ooxml_id
    })
    preloader_container_div = lxml.html.Element( "div", {"class":"preloader-container"})
    preloader_row_div = lxml.etree.SubElement(preloader_container_div, "div", attrib={"class":"preloader-row"})
    preloader_file = os.path.join(os.path.dirname(__file__), "img", "preloader.svg")
    svg = lxml.html.parse(preloader_file)
    preloader_row_div.append(svg.getroot())
    empty_parent_div = lxml.html.Element("div", {
        'class': 'empty-parent bg-light'
    })
    empty_parent_div.extend([svg_container_div, preloader_container_div])
    return lxml.html.tostring(empty_parent_div)
def to_html(self, **kwargs)

Returns an html string that will render the object at the endpoint

Expand source code Browse git
def to_html(self, **kwargs):
    """
    Returns an html string that will render the object at the endpoint
    """
    html = self.create_container()
    try:
        html = html.decode('utf-8')
    except Exception:
        pass
    self.svg_html = html
    return self.svg_html
def get_svg(self, id, timeout_iterator=0)

Get an svg-formatted version of the object from the Ooxml Automation service

Expand source code Browse git
def get_svg(self, id, timeout_iterator=0) -> str:
    """
    Get an svg-formatted version of the object from the Ooxml Automation service
    """
    svg_url = self.endpoint_map.get_svg_url(id)
    client = self.get_client()
    headers = client.get_auth_header()
    headers.update(client.get_request_id_header())
    response = requests.get(svg_url, headers=headers)
    svg_data = response.text
    if response.status_code != 200:
        raise presalytics.lib.exceptions.ApiError(message=response.text, status_code=response.status_code)
    if response.text.startswith("Temp data"):
        if timeout_iterator > 5:
            raise presalytics.lib.exceptions.ApiError(message="Unable to download svg.  Please check for upstream processing errors.")
        else:
            time.sleep(2)
            svg_data = self.get_svg(id, timeout_iterator)
    return svg_data
def get_svg_file(self, filename=None)

Writes an svg representation of the object to current working directory. Filname is optional.

Expand source code Browse git
def get_svg_file(self, filename=None):
    """
    Writes an svg representation of the object to current working directory.  Filname is optional.
    """
    if not filename:
        filename = self.endpoint_map.get_object_type() + "-" + self.object_ooxml_id + ".pptx"
    with open(filename, 'w') as f:
        f.write(self.get_svg(self.object_ooxml_id))

Inherited members

class OoxmlEditorWidget (name, story_id, object_ooxml_id, endpoint_map, transform_class, transform_params={}, **kwargs)

Edits a widget from a Presentation or Spreadsheet document and renders the edited widget.

This class interacts with the Presalytics API to extract SVG objects from Presentation and spreadsheet documents, from Presaltytics Ooxml Automation service objects that have already been loaded into the API. This class requires that users supply transform_function by subclassing XmlTransformBase, and an optional set of parameters to act as variables in the transform function.

Parameters

filename : str
The local filepath a presentation or spreadsheet file containing the object to be rendered
name : str
The widget name. If not provided, attribute will be set as the object_name or filename
story_id : str
The the id of the story in the Presalytics API Story service. If not provided, a new story will be created. Do not supply if this object has not yet been created.
object_ooxml_id : str
The identifier of the Ooxml Automation service object bound the Story. Do not supply if this object has not yet been created.
endpoint_map : OoxmlEndpointMap
Reference to the Presalytics API Ooxml Automation service endpoint and object type that for the object of interest
tranform_class : subclass of XmlTransformBase
A class containing a transform_function method that transforms Open Office Xml via an lxml.etree.Element instance
transform_params : dict, optional
A dictionary of parameters that will be passed to the transform_class's transform_function as variables to modify the underlying OpenOfficeXml
Expand source code Browse git
class OoxmlEditorWidget(presalytics.lib.widgets.ooxml.OoxmlWidgetBase):
    """
    Edits a `widget` from a Presentation or Spreadsheet document and renders 
    the edited widget.

    This class interacts with the Presalytics API to extract SVG objects from
    Presentation and spreadsheet documents, from Presaltytics Ooxml Automation
    service objects that have already been loaded into the API.  This class requires
    that users supply `transform_function` by subclassing 
    `presalytics.lib.widgets.ooxml_editors.XmlTransformBase`, and an optional set of 
    parameters to act as variables in the transform function.

    Parameters
    ----------
    filename : str
        The local filepath a presentation or spreadsheet file containing
        the object to be rendered

    name : str
        The widget name.  If not provided, attribute will be set as the `object_name` 
        or `filename`

    story_id : str
        The the id of the story in the Presalytics API Story service.  If not provided, 
        a new story will be created.  Do not supply if this object has not yet been created. 
    
    object_ooxml_id : str
        The identifier of the Ooxml Automation service object bound the Story. Do not supply if this 
        object has not yet been created.

    endpoint_map : presalytics.lib.widgets.ooxml.OoxmlEndpointMap
        Reference to the Presalytics API Ooxml Automation service endpoint and object type
        that for the object of interest

    tranform_class : subclass of presalytics.lib.widgets.ooxml_editors.XmlTransformBase
        A class containing a `transform_function` method that transforms Open Office Xml via
        an `lxml.etree.Element` instance

    transform_params : dict, optional
        A dictionary of parameters that will be passed to the `transform_class`'s `transform_function`
        as variables to modify the underlying OpenOfficeXml 
    
    """
    transform: XmlTransformBase

    __component_kind__ = 'ooxml-xml-editor'

    def __init__(self,
                 name: str,
                 story_id,
                 object_ooxml_id,
                 endpoint_map,
                 transform_class,
                 transform_params={},
                 **kwargs):
        super(OoxmlEditorWidget, self).__init__(name, story_id=story_id, object_ooxml_id=object_ooxml_id, endpoint_map=endpoint_map, **kwargs)
        self.name = name
        self.transform = transform_class(transform_params)
        self.update()
        self.svg_html = self.create_container(**self.client_info)
        self.outline_widget = self.serialize()

    def update_xml(self, xml_str) -> str:
        """
        Runs the `transform_function` Open Office Xml data downloaded via the Presalytics API
        """
        xml = lxml.etree.fromstring(xml_str)
        new_xml = self.transform.execute(xml)
        new_xml_str = lxml.etree.tostring(new_xml)
        return new_xml_str

    def update(self):
        """
        Update the widget, include changes to the Xml
        """
        client = self.get_client()
        headers = client.get_auth_header()
        headers.update(client.get_request_id_header())
        if self.transform:
            xml_url = self.endpoint_map.get_xml_url(self.object_ooxml_id)
            xml_response = requests.get(xml_url + "?updated=false", headers=headers)
            if xml_response.status_code != 200:
                raise presalytics.lib.exceptions.ApiError(message=xml_response.text)
            dto = xml_response.json()
            new_xml = self.update_xml(dto["openOfficeXml"])
            dto["openOfficeXml"] = new_xml.decode('utf-8')
            xml_update_response = requests.put(xml_url, json=dto, headers=headers)
            if xml_response.status_code != 200:
                raise presalytics.lib.exceptions.ApiError(message=xml_update_response.content)

    @classmethod
    def deserialize(cls, component, **kwargs):
        endpoint_map = presalytics.lib.widgets.ooxml.OoxmlEndpointMap(component.data["endpoint_id"])
        class_key = "XmlTransform." + component.data.get("transform_class", "")
        transform_class = get_transform_registry().get(class_key)
        transform_params = component.data.get("transform_params", {})
        return cls(component.name,
                   component.data["story_id"],
                   component.data["object_ooxml_id"],
                   endpoint_map,
                   transform_class=transform_class,
                   transform_params=transform_params,
                   **kwargs)

    def serialize(self, **kwargs):
        data = {
            "story_id": self.story_id,
            "object_ooxml_id": self.object_ooxml_id,
            "endpoint_id": self.endpoint_map.endpoint_id,
            "transform_class": self.transform.__xml_transform_name__,
            "transform_params": self.transform.function_params
        }
        widget = presalytics.story.outline.Widget(
            name=self.name,
            data=data,
            kind=self.__component_kind__
        )
        return widget

Ancestors

Methods

def update_xml(self, xml_str)

Runs the transform_function Open Office Xml data downloaded via the Presalytics API

Expand source code Browse git
def update_xml(self, xml_str) -> str:
    """
    Runs the `transform_function` Open Office Xml data downloaded via the Presalytics API
    """
    xml = lxml.etree.fromstring(xml_str)
    new_xml = self.transform.execute(xml)
    new_xml_str = lxml.etree.tostring(new_xml)
    return new_xml_str
def update(self)

Update the widget, include changes to the Xml

Expand source code Browse git
def update(self):
    """
    Update the widget, include changes to the Xml
    """
    client = self.get_client()
    headers = client.get_auth_header()
    headers.update(client.get_request_id_header())
    if self.transform:
        xml_url = self.endpoint_map.get_xml_url(self.object_ooxml_id)
        xml_response = requests.get(xml_url + "?updated=false", headers=headers)
        if xml_response.status_code != 200:
            raise presalytics.lib.exceptions.ApiError(message=xml_response.text)
        dto = xml_response.json()
        new_xml = self.update_xml(dto["openOfficeXml"])
        dto["openOfficeXml"] = new_xml.decode('utf-8')
        xml_update_response = requests.put(xml_url, json=dto, headers=headers)
        if xml_response.status_code != 200:
            raise presalytics.lib.exceptions.ApiError(message=xml_update_response.content)

Inherited members

class D3Widget (name, d3_data, id=None, story_id=None, script64=None, script_filename=None, css64=None, css_filename=None, html64=None, html_filename=None, *args, **kwargs)

A Widget for rendering user-defined d3.js scripts

This class allows users to load d3.js objects into widgets in order to create custom and interactive widgets. User can define d3 scripts in a separate file and load them via script_filename parameter.

Parameters

name : str
the name of widget. Must be unique within COMPONENTS
d3_data : dict
Data that will be loaded into the script when run in the browser. Avaiable in the script as the data object.
id : str, optional
A unique identifier the widget. Automatically generated when the widget is created or updated.
story_id : str, optional
The story_id of the parent story
script64 : str. optional
A base64-encoded string of the script's text. Used for server-to-server transport over https
script_filename : str, optional
Required when updating the script text. Read into the script64 parameter via the read_file method
html64 : str. optional
A base64-encoded string of the html fragments's text. Used for server-to-server transport over https
html_filename : str, optional
The name of file in the local directory with an html framement that should be rendered within the body (inside element <div id="{{id}}" class="d3-container"></div>) of the iframe containing the d3 script
css64 : str. optional
A base64-encoded string of the css styles to apply to the d3 document. Used for server-to-server transport over https.
css_filename : str, optional

A css file containing styles that will be applied to d3

Note: Styles html {width: 100%; height:100%;} body {width: 100%; height: 100%; margin: 0px;} are applied by default if not css is provided

Script Local Variables:

The following vairables are avialable to users when writing scripts:

data: javascript object The data loaded into the script via d3_data parameter

container: html element The first div in the body

d3: javascript object The root d3 object for selecting, creating and editing elements on the DOM

Security Note:

Scripts loaded via this widget are Sandboxed. These script can only interact with dom elements defined in the widget script loaded via the script_filename parameter. Fetch and xhr actions are also disabled via a restrictive Content Security Policy.

Expand source code Browse git
class D3Widget(presalytics.story.components.WidgetBase):
    """
    A `Widget` for rendering user-defined d3.js scripts

    This class allows users to load [d3.js](https://d3js.org/) objects
    into widgets in order to create custom and interactive widgets.
    User can define d3 scripts in a separate file and load them via `script_filename`
    parameter.

    Parameters
    ----------

    name : str
        the name of widget.  Must be unique within `presalytics.COMPONENTS`

    d3_data: dict
        Data that will be loaded into the script when run in the browser. Avaiable
        in the script as the `data` object.
    
    id: str, optional
        A unique identifier the widget.  Automatically generated when the
        widget is created or updated.

    story_id : str, optional
        The story_id of the parent story

    script64 : str. optional
        A base64-encoded string of the script's text.  Used for server-to-server
        transport over https

    script_filename: str, optional
        Required when updating the script text.  Read into the `script64` parameter
        via the `read_file` method

    html64 : str. optional
        A base64-encoded string of the html fragments's text.  Used for server-to-server
        transport over https

    html_filename: str, optional
        The name of file in the local directory with an html framement that should be rendered within the
        body (inside element `<div id="{{id}}" class="d3-container"></div>`) of the iframe containing the 
        d3 script

    css64 : str. optional
        A base64-encoded string of the css styles to apply to the d3 document.  Used for server-to-server
        transport over https.
    
    css_filename: str, optional
        A css file containing styles that will be applied to d3

        Note: Styles `html {width: 100%; height:100%;} body {width: 100%; height: 100%; margin: 0px;}`
        are applied by default if not css is provided


    


    Script Local Variables:
    ----------

    The following vairables are avialable to users when writing scripts:

    data: `javascript object`
        The data loaded into the script via `d3_data` parameter

    container: `html element`
        The first div in the body
    
    d3: `javascript object`
        The root d3 object for selecting, creating and editing elements on the DOM
        

    Security Note:
    ----------

    Scripts loaded via this widget are *Sandboxed*.  These script can only interact with
    dom elements defined in the widget script loaded via the `script_filename` parameter.
    Fetch and xhr actions are also disabled via a restrictive Content Security Policy.


    """
    __component_kind__ = 'd3'
    __plugins__ = [
        {
            'name': 'external_scripts',
            'kind': 'script',
            'config': {
                'approved_scripts_key': 'd3'
            }
        }
    ]

    def __init__(self, 
                 name: str, 
                 d3_data: typing.Dict,
                 id: str = None, 
                 story_id: str = None,
                 script64: str = None, 
                 script_filename: str = None, 
                 css64: str = None,
                 css_filename: str = None, 
                 html64: str = None,
                 html_filename: str = None,
                 *args, **kwargs):
        if not id:
            id = 'd3-' + str(uuid.uuid4())
        self.id = id
        self.d3_data = d3_data
        self.story_id = story_id
        super(D3Widget, self).__init__(name, *args, **kwargs)
        self.script_filename = script_filename
        self.script64 = self.read_file(script_filename)
        if not self.script64:
            self.script64 = script64
        if not self.script64:
            raise presalytics.lib.exceptions.InvalidConfigurationError("D3 Widget must be supplied either a script64 or script_filename keyword argument.")
        self.html64 = self.read_file(html_filename)
        self.html64 = self.html64 if self.html64 else html64
        self.css64 = self.read_file(css_filename)
        self.css64 = self.css64 if self.css64 else css64
        self.html_filename = html_filename
        self.css_filename = css_filename

    def read_file(self, filename) -> typing.Optional[str]:
        """
        Find a file named `filename` and return its base64-ecoded content
        """
        data64 = None
        if filename:
            search_paths = list(set(presalytics.autodiscover_paths))
            if os.getcwd() not in search_paths:
                search_paths.append(os.getcwd())
            for path in search_paths:
                fpath = os.path.join(path, filename)
                if os.path.exists(fpath):
                    with open(fpath, 'rb') as f:
                        data = f.read()
                        data64 = base64.b64encode(data).decode('utf-8')  #type: ignore
                    break
            if not data64:
                logger.debug("File {0} could not be found".format(filename))
        return data64 

    def to_html(self, data=None, **kwargs) -> str:
        """
        Renders the sandboxed iframe with will contain the d3 script widget
        """
        if not self.story_id:
            message = "This object requires a valid story_id to render."
            raise presalytics.lib.exceptions.MissingConfigException(message=message)
        html = self.create_container()
        return html

    def create_container(self, **kwargs):
        """
        Wraps the D3 objects in an endpoint at the story API load via a sandboxed `<iframe>` that
        will be rendered
        """
        params = {
            "story_host": self.get_client(delegate_login=True).story.api_client.external_root_url,
            "id": self.id,
            "story_id": self.story_id,
        }
        source_url = "{story_host}/{story_id}/d3/{id}".format(**params)
        empty_parent_div = lxml.html.Element("div", {
            'class': 'empty-parent bg-light',
            'style': 'height: 100%; width: 100%, display: block; text-align: left;'
        })
        frame = lxml.html.Element("iframe", {
            'src': source_url,
            'frameborder': "0",
            'scrolling': "auto",
            'class': 'd3-responsive-frame',
            'style': 'max-height: none; max-width: none; height:100%; width: 100%;',
            'sandbox': 'allow-forms allow-scripts'
        })
        empty_parent_div.append(frame)
        return lxml.html.tostring(empty_parent_div).decode('utf-8')

    @classmethod
    def deserialize(cls, outline, **kwargs):
        d3_data = outline.data.get("d3_data")
        story_id = outline.data.get("story_id", None)
        id = outline.data.get('id', None)
        data = outline.data.get('d3_data', None)
        script_filename = outline.data.get('script_filename', None)
        script64 = outline.data.get('script64', None)
        html_filename = outline.data.get('html_filename', None)
        html64 = outline.data.get('html64', None)
        css_filename = outline.data.get('css_filename', None)
        css64 = outline.data.get('css64', None)
        return cls(outline.name,
                   d3_data,
                   id=id,
                   story_id=story_id,
                   script64=script64,
                   script_filename=script_filename,
                   html64=html64,
                   html_filename=html_filename,
                   css64=css64,
                   css_filename=css_filename,
                   **kwargs)

    def serialize(self, **kwargs):
        data = {
            'd3_data': self.d3_data,
            'id': self.id,
            'story_id': self.story_id,
            'script_filename': self.script_filename,
            'script64': self.script64,
            'css64': self.css64,
            'html64': self.html64,
            'html_filename': self.html_filename,
            'css_filename': self.css_filename
        }
        return presalytics.story.outline.Widget(
            name=self.name,
            kind=self.__component_kind__,
            data=data,
        )

    DEFAULT_CSS = """
    html {
        height: 100%;
        width: 100%;
    }
    body {
        height: 100%;
        width: 100%;
        margin: 0px;
    }
    """


    def standalone_html(self) -> str:
        """
        Returns string with an html document containing that d3 widget

        Loaded via the Story API d3 endpoint
        """
        SIMPLE_HTML = jinja2.Template("""<!DOCTYPE html>
        <html>
            <head>
                <meta charset="UTF-8">
                <style>
                {{ css }}
                </style>
            </head>
            <body>
                <script type="text/javascript" src="{{ d3_url }}"></script>
                <div id="{{ id }}" class="d3-container">{{ html_fragment }}</div>
                <script type="text/javascript">

                    var id = '{{id}}';

                    var data = JSON.parse('{{data|safe}}');

                    var container = document.getElementById(id);

                    {{script|safe}}
            
                </script>
            </body>
        </html>""")
        data = json.dumps(self.d3_data)  # dont use hyphens in data keys
        script = base64.b64decode(self.script64).decode('utf-8')  #type: ignore  #Required
        extra_css = base64.b64decode(self.css64).decode('utf-8') if self.css64 else D3Widget.DEFAULT_CSS  #type: ignore  
        html_fragment = base64.b64decode(self.html64).decode('utf-8') if self.html64 else None  #type: ignore   # disable nested iframes
        context = {
            "id": self.id,
            "d3_url": presalytics.lib.plugins.external.ApprovedExternalScripts().attr_dict.flatten().get('d3'),
            "data": data,
            "script": script,
            "css": extra_css,
            "html_fragment": html_fragment
        }
        return SIMPLE_HTML.render(**context)

Ancestors

Class variables

var DEFAULT_CSS

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

Methods

def read_file(self, filename)

Find a file named filename and return its base64-ecoded content

Expand source code Browse git
def read_file(self, filename) -> typing.Optional[str]:
    """
    Find a file named `filename` and return its base64-ecoded content
    """
    data64 = None
    if filename:
        search_paths = list(set(presalytics.autodiscover_paths))
        if os.getcwd() not in search_paths:
            search_paths.append(os.getcwd())
        for path in search_paths:
            fpath = os.path.join(path, filename)
            if os.path.exists(fpath):
                with open(fpath, 'rb') as f:
                    data = f.read()
                    data64 = base64.b64encode(data).decode('utf-8')  #type: ignore
                break
        if not data64:
            logger.debug("File {0} could not be found".format(filename))
    return data64 
def to_html(self, data=None, **kwargs)

Renders the sandboxed iframe with will contain the d3 script widget

Expand source code Browse git
def to_html(self, data=None, **kwargs) -> str:
    """
    Renders the sandboxed iframe with will contain the d3 script widget
    """
    if not self.story_id:
        message = "This object requires a valid story_id to render."
        raise presalytics.lib.exceptions.MissingConfigException(message=message)
    html = self.create_container()
    return html
def create_container(self, **kwargs)

Wraps the D3 objects in an endpoint at the story API load via a sandboxed <iframe> that will be rendered

Expand source code Browse git
def create_container(self, **kwargs):
    """
    Wraps the D3 objects in an endpoint at the story API load via a sandboxed `<iframe>` that
    will be rendered
    """
    params = {
        "story_host": self.get_client(delegate_login=True).story.api_client.external_root_url,
        "id": self.id,
        "story_id": self.story_id,
    }
    source_url = "{story_host}/{story_id}/d3/{id}".format(**params)
    empty_parent_div = lxml.html.Element("div", {
        'class': 'empty-parent bg-light',
        'style': 'height: 100%; width: 100%, display: block; text-align: left;'
    })
    frame = lxml.html.Element("iframe", {
        'src': source_url,
        'frameborder': "0",
        'scrolling': "auto",
        'class': 'd3-responsive-frame',
        'style': 'max-height: none; max-width: none; height:100%; width: 100%;',
        'sandbox': 'allow-forms allow-scripts'
    })
    empty_parent_div.append(frame)
    return lxml.html.tostring(empty_parent_div).decode('utf-8')
def standalone_html(self)

Returns string with an html document containing that d3 widget

Loaded via the Story API d3 endpoint

Expand source code Browse git
def standalone_html(self) -> str:
    """
    Returns string with an html document containing that d3 widget

    Loaded via the Story API d3 endpoint
    """
    SIMPLE_HTML = jinja2.Template("""<!DOCTYPE html>
    <html>
        <head>
            <meta charset="UTF-8">
            <style>
            {{ css }}
            </style>
        </head>
        <body>
            <script type="text/javascript" src="{{ d3_url }}"></script>
            <div id="{{ id }}" class="d3-container">{{ html_fragment }}</div>
            <script type="text/javascript">

                var id = '{{id}}';

                var data = JSON.parse('{{data|safe}}');

                var container = document.getElementById(id);

                {{script|safe}}
        
            </script>
        </body>
    </html>""")
    data = json.dumps(self.d3_data)  # dont use hyphens in data keys
    script = base64.b64decode(self.script64).decode('utf-8')  #type: ignore  #Required
    extra_css = base64.b64decode(self.css64).decode('utf-8') if self.css64 else D3Widget.DEFAULT_CSS  #type: ignore  
    html_fragment = base64.b64decode(self.html64).decode('utf-8') if self.html64 else None  #type: ignore   # disable nested iframes
    context = {
        "id": self.id,
        "d3_url": presalytics.lib.plugins.external.ApprovedExternalScripts().attr_dict.flatten().get('d3'),
        "data": data,
        "script": script,
        "css": extra_css,
        "html_fragment": html_fragment
    }
    return SIMPLE_HTML.render(**context)

Inherited members

class ChartUpdaterWidget (name, story_id, chart_id, dto=None, data_table=None, **kwargs)

Updates a Chart in the Ooxml Automation service API at the the endpoint '/Chart/ChartUpdate/'

This class simplifies chart updates, for charts residing in the Ooxml Automation Service, allowing updates to ooxml object data and its underlying xml either via a list of lists or the ChartChartDataDTO object.

Parameters

name : str
A name for the widget.
story_id : str
The the id of the story in the Presalytics API Story service.
chart_id : str
The identifier of the Ooxml Automation Chart service object.
dto : ChartChartDataDTO, optional
A an instance of the data transfer object model. The class of this object is defined by the _get_dto_class() method. Represents the current state of the data of the data in the service.
data_table : list of lists, optional
A representation of a data table that will be incorporates into a dto object's property defined by the subclass' _get_dto_table_name method.
Expand source code Browse git
class ChartUpdaterWidget(UpdaterWidgetBase):
    """
    Updates a Chart in the Ooxml Automation service API at the the endpoint '/Chart/ChartUpdate/'

    This class simplifies chart updates, for charts residing in the Ooxml Automation Service, 
    allowing updates to ooxml object data and its underlying xml either via a list of lists or
    the `presalytics.client.presalytics_ooxml_automation.models.chart_chart_data_dto.ChartChartDataDTO`
    object.  

    Parameters
    ----------
    name : str
        A name for the widget.

    story_id : str
        The the id of the story in the Presalytics API Story service. 
    
    chart_id : str
        The identifier of the Ooxml Automation Chart service object. 

    dto: presalytics.client.presalytics_ooxml_automation.models.chart_chart_data_dto.ChartChartDataDTO, optional
        A an instance of the data transfer object model. The class of this object is defined by the
        `_get_dto_class()` method.  Represents the current state of the data of the data in the service.

    data_table: list of lists, optional
        A representation of a data table that will be incorporates into a dto object's property defined by
        the subclass' `_get_dto_table_name` method.

    """
    __component_kind__ = "chart-updater"
    
    def __init__(self, 
                name,
                story_id: str,
                chart_id: str,
                dto: 'ChartChartDataDTO' = None, 
                data_table: typing.Sequence[typing.Sequence] = None,
                **kwargs):
        super().__init__(name, story_id, chart_id, OoxmlEndpointMap.chart(), dto=dto, data_table=data_table, **kwargs)
        self.chart_id = chart_id


    def _get_dto_class(self):
        return presalytics.client.presalytics_ooxml_automation.models.chart_chart_data_dto.ChartChartDataDTO

    def _get_endpoint_path(self):
        return "ChartUpdate"

    def _get_dto_table_name(self):
        return "data_points"

    def get_dataframe(self) -> pandas.DataFrame:
        """
        Returns a panda datagrame of the 
        """
        data: collections.OrderedDict
        
        if not self.dto:
            self.dto = self.get_dto()
        data = collections.OrderedDict()
        for i in range(0, len(self.dto.series_names)):
            data.update({
                self.dto.series_names[i]: pandas.Series(self.dto.data_points[i], self.dto.category_names)
            })
        return pandas.DataFrame(data)
    
    def put_dataframe(self, df: pandas.DataFrame):
        data_dict = df.to_dict('split')
        data_points = list(map(list, zip(*data_dict['data'])))
        dto = self._get_dto_class()(chart_id=self.chart_id, 
                                 series_names=data_dict["columns"], 
                                 category_names=data_dict["index"],
                                 data_points=data_points)
        self._put_dto(dto)

Ancestors

Methods

def get_dataframe(self)

Returns a panda datagrame of the

Expand source code Browse git
def get_dataframe(self) -> pandas.DataFrame:
    """
    Returns a panda datagrame of the 
    """
    data: collections.OrderedDict
    
    if not self.dto:
        self.dto = self.get_dto()
    data = collections.OrderedDict()
    for i in range(0, len(self.dto.series_names)):
        data.update({
            self.dto.series_names[i]: pandas.Series(self.dto.data_points[i], self.dto.category_names)
        })
    return pandas.DataFrame(data)
def put_dataframe(self, df)
Expand source code Browse git
def put_dataframe(self, df: pandas.DataFrame):
    data_dict = df.to_dict('split')
    data_points = list(map(list, zip(*data_dict['data'])))
    dto = self._get_dto_class()(chart_id=self.chart_id, 
                             series_names=data_dict["columns"], 
                             category_names=data_dict["index"],
                             data_points=data_points)
    self._put_dto(dto)

Inherited members

class TableUpdaterWidget (name, story_id, table_id, dto=None, data_table=None, **kwargs)

Updates a Table in the Ooxml Automation service API at the the endpoint '/Table/TableUpdate/'

This class simplifies table updates, for tables residing in the Ooxml Automation Service, allowing updates to ooxml object data and its underlying xml either via a list of lists or the TableTableDataDTO object.

Parameters

name : str
A name for the widget.
story_id : str
The the id of the story in the Presalytics API Story service.
table_id : str
The identifier of the Ooxml Automation Table service object.
dto : TableTableDataDTO, optional
A an instance of the data transfer object model. The class of this object is defined by the _get_dto_class() method. Represents the current state of the data of the data in the service.
data_table : list of lists, optional
A representation of a data table that will be incorporates into a dto object's property defined by the subclass' _get_dto_table_name method.
Expand source code Browse git
class TableUpdaterWidget(UpdaterWidgetBase):
    """
    Updates a Table in the Ooxml Automation service API at the the endpoint '/Table/TableUpdate/'

    This class simplifies table updates, for tables residing in the Ooxml Automation Service, 
    allowing updates to ooxml object data and its underlying xml either via a list of lists or
    the `presalytics.client.presalytics_ooxml_automation.models.table_table_data_dto.TableTableDataDTO`
    object.  

    Parameters
    ----------
    name : str
        A name for the widget.

    story_id : str
        The the id of the story in the Presalytics API Story service. 
    
    table_id : str
        The identifier of the Ooxml Automation Table service object. 

    dto: presalytics.client.presalytics_ooxml_automation.models.table_table_data_dto.TableTableDataDTO, optional
        A an instance of the data transfer object model. The class of this object is defined by the
        `_get_dto_class()` method.  Represents the current state of the data of the data in the service.

    data_table: list of lists, optional
        A representation of a data table that will be incorporates into a dto object's property defined by
        the subclass' `_get_dto_table_name` method.

    """
    __component_kind__ = "table-updater"
    
    def __init__(self, 
                name,
                story_id: str,
                table_id: str,
                dto: 'TableTableDataDTO' = None, 
                data_table: typing.Sequence[typing.Sequence] = None,
                **kwargs):
        super().__init__(name, story_id, table_id, OoxmlEndpointMap.table(), dto=dto, data_table=data_table, **kwargs)
        self.table_id = table_id


    def _get_dto_class(self):
        return presalytics.client.presalytics_ooxml_automation.models.table_table_data_dto.TableTableDataDTO

    def _get_endpoint_path(self):
        return "TableUpdate"

    def _get_dto_table_name(self):
        return "table_data"

Ancestors

Inherited members

class XmlTransformBase (function_params, *args, **kwargs)

Base class for writing Open Office Xml tranform functions to be implemented by OoxmlEditorWidget

*For more information about Open Office Xml Schema that underlies .pptx and .xlsx files, see http://officeopenxml.com/

*For more information on how to lxml to write transforms, consult https://lxml.de/

Parameters

function_params : dict
A dictionary containing variables that will be used when the transform_function is executed
Expand source code Browse git
class XmlTransformBase(abc.ABC):
    """
    Base class for writing Open Office Xml tranform functions to be implemented
    by `presalytics.lib.widgets.ooxml_editors.OoxmlEditorWidget`
    
    *For more information about Open Office Xml Schema that underlies 
    .pptx and .xlsx files, see http://officeopenxml.com/

    *For more information on how to lxml to write transforms, consult https://lxml.de/

    Parameters
    ----------
    function_params : dict
        A dictionary containing variables that will be used when the `transform_function`
        is executed
    """
    __xml_transform_kind__ = 'XmlTransform'

    def __init__(self, function_params: typing.Dict, *args, **kwargs):
        self.function_params = function_params

    @abc.abstractmethod
    def transform_function(self, lxml_element: lxml.etree.Element, params: typing.Dict) -> lxml.etree.Element:
        """
        Modifies Open Office Xml via transforming `lxml.etree.Element` using a params `dict` containing
        variables. Must be overridden in subclasses.
        """
        pass
    

    def execute(self, lxml_element: lxml.etree.Element):
        """
        Called by widget classes (e.g., `presalytics.lib.widgets.ooxml_editors.OoxmlEditorWidget`) to 
        perform the updates prescribed in the `transform_function`
        """
        return self.transform_function(lxml_element, self.function_params)

Ancestors

  • abc.ABC

Subclasses

Methods

def transform_function(self, lxml_element, params)

Modifies Open Office Xml via transforming lxml.etree.Element using a params dict containing variables. Must be overridden in subclasses.

Expand source code Browse git
@abc.abstractmethod
def transform_function(self, lxml_element: lxml.etree.Element, params: typing.Dict) -> lxml.etree.Element:
    """
    Modifies Open Office Xml via transforming `lxml.etree.Element` using a params `dict` containing
    variables. Must be overridden in subclasses.
    """
    pass
def execute(self, lxml_element)

Called by widget classes (e.g., OoxmlEditorWidget) to perform the updates prescribed in the transform_function

Expand source code Browse git
def execute(self, lxml_element: lxml.etree.Element):
    """
    Called by widget classes (e.g., `presalytics.lib.widgets.ooxml_editors.OoxmlEditorWidget`) to 
    perform the updates prescribed in the `transform_function`
    """
    return self.transform_function(lxml_element, self.function_params)
class ChangeShapeColor (function_params, *args, **kwargs)

Changes the color of a set of Open Office Xml Shapes

Function Parameters Dictionary

hex_color : str
The six-digit hexadecimal-format color string (e.g., ffa500 for orange). See https://www.color-hex.com/ for an example color calculator
object_name : str, optional
The object tree name of the target shape. If not supplied, all descendent shapes will have their color changed.
Expand source code Browse git
class ChangeShapeColor(XmlTransformBase):
    """
    Changes the color of a set of [Open Office Xml Shapes](http://officeopenxml.com/drwShape.php)
    
    Function Parameters Dictionary
    ----------
    hex_color : str
        The six-digit hexadecimal-format color string (e.g., ffa500 for orange).  See
        https://www.color-hex.com/ for an example color calculator

    object_name : str, optional
        The object tree name of the target shape.  If not supplied, all descendent shapes will
        have their color changed.      
    """
    __xml_transform_name__ = "ChangeShapeColor"

    @staticmethod
    def replace_color_on_target_shape(shape_xml: lxml.etree.Element, new_color: str) -> lxml.etree.Element:
        """
        Changes the color of an [Open Office Xml Shape](http://officeopenxml.com/drwShape.php)
        """
        new_fill = lxml.etree.Element("solidFill")
        lxml.etree.SubElement(new_fill, "srbgClr", {"val": new_color})
        fill_tags = [
            'noFill',
            'blipFill',
            'gradFill',
            'pattFill',
            'solidFill'
        ]
        props = shape_xml.find('.//{*}spPr')
        for child in props.getchildren():
            if any(re.search('{*}' + r, child.tag) for r in fill_tags):
                current_nsmap = child.nsmap
                current_index = props.index(child)
                current_prefix = child.prefix
                current_ns = current_nsmap.get(current_prefix, None)
                new_fill = lxml.etree.Element(lxml.etree.QName(current_ns, "solidFill"), nsmap=current_nsmap)
                lxml.etree.SubElement(new_fill, lxml.etree.QName(current_ns, "srgbClr"), {"val": new_color}, nsmap=current_nsmap)
                child.getparent().remove(child)
                props.insert(current_index, new_fill)
        return shape_xml

    def transform_function(self, lxml_element, params):
        """
        Changes the color of an [Open Office Xml Shape](http://officeopenxml.com/drwShape.php)

        Parameters
        -----------
        lxml_element : lxml.etree.Element
            An element containing as least one an `<sp>` element

        params : dict
            See the `Function Parameters Dictionary` for required entries
        """
        
        if re.match('{.*}sp', lxml_element.tag):
            shapes = [lxml_element]
        else:
            shapes = lxml_element.findall('.//{*}sp')
        object_name = params.get("object_name", None)
        color = params.get("hex_color").lstrip("#").upper()
        if object_name:
            for shape in shapes:
                is_target = False
                nvprops = shape.find('.//{*}nvSpPr')
                if len(nvprops) > 0:
                    name = nvprops.find('.//*[@name]').get("name")
                    if name == object_name:
                        is_target = True
                if is_target:
                    shape = ChangeShapeColor.replace_color_on_target_shape(shape, color)
        else:
            for shape in shapes:
                shape = ChangeShapeColor.replace_color_on_target_shape(shape, color)
        return lxml_element

Ancestors

Static methods

def replace_color_on_target_shape(shape_xml, new_color)

Changes the color of an Open Office Xml Shape

Expand source code Browse git
@staticmethod
def replace_color_on_target_shape(shape_xml: lxml.etree.Element, new_color: str) -> lxml.etree.Element:
    """
    Changes the color of an [Open Office Xml Shape](http://officeopenxml.com/drwShape.php)
    """
    new_fill = lxml.etree.Element("solidFill")
    lxml.etree.SubElement(new_fill, "srbgClr", {"val": new_color})
    fill_tags = [
        'noFill',
        'blipFill',
        'gradFill',
        'pattFill',
        'solidFill'
    ]
    props = shape_xml.find('.//{*}spPr')
    for child in props.getchildren():
        if any(re.search('{*}' + r, child.tag) for r in fill_tags):
            current_nsmap = child.nsmap
            current_index = props.index(child)
            current_prefix = child.prefix
            current_ns = current_nsmap.get(current_prefix, None)
            new_fill = lxml.etree.Element(lxml.etree.QName(current_ns, "solidFill"), nsmap=current_nsmap)
            lxml.etree.SubElement(new_fill, lxml.etree.QName(current_ns, "srgbClr"), {"val": new_color}, nsmap=current_nsmap)
            child.getparent().remove(child)
            props.insert(current_index, new_fill)
    return shape_xml

Methods

def transform_function(self, lxml_element, params)

Changes the color of an Open Office Xml Shape

Parameters

lxml_element : lxml.etree.Element
An element containing as least one an <sp> element
params : dict
See the Function Parameters Dictionary for required entries
Expand source code Browse git
def transform_function(self, lxml_element, params):
    """
    Changes the color of an [Open Office Xml Shape](http://officeopenxml.com/drwShape.php)

    Parameters
    -----------
    lxml_element : lxml.etree.Element
        An element containing as least one an `<sp>` element

    params : dict
        See the `Function Parameters Dictionary` for required entries
    """
    
    if re.match('{.*}sp', lxml_element.tag):
        shapes = [lxml_element]
    else:
        shapes = lxml_element.findall('.//{*}sp')
    object_name = params.get("object_name", None)
    color = params.get("hex_color").lstrip("#").upper()
    if object_name:
        for shape in shapes:
            is_target = False
            nvprops = shape.find('.//{*}nvSpPr')
            if len(nvprops) > 0:
                name = nvprops.find('.//*[@name]').get("name")
                if name == object_name:
                    is_target = True
            if is_target:
                shape = ChangeShapeColor.replace_color_on_target_shape(shape, color)
    else:
        for shape in shapes:
            shape = ChangeShapeColor.replace_color_on_target_shape(shape, color)
    return lxml_element

Inherited members

class TextReplace (function_params, *args, **kwargs)

Replaces text in a template built into an Office Office Xml object that has been uploaded to the Presalytics Ooxml Automation service. Text to be should be in the format of a "template tag", which is a string enclosed in handlebars as such: '{{template_tag}}'

Function Parameters Dictionary

A dictionary that maps template tags to the new strings that will replace the tags in the rendered widget. The the dictionary keys should not be enclosed in handlebars.

Expand source code Browse git
class TextReplace(XmlTransformBase):
    """
    Replaces text in a template built into an Office Office Xml object that has 
    been uploaded to the Presalytics Ooxml Automation service.  Text to be should
    be in the format of a "template tag", which is a string enclosed in handlebars
    as such: '{{template_tag}}'

    Function Parameters Dictionary
    ----------

    A dictionary that maps template tags to the new strings that will replace the
    tags in the rendered widget.  The the dictionary keys should not be enclosed in handlebars.
    """
    __xml_transform_name__ = "TextReplace"

    class TextElementInfo(object):
        """
        Holds metadata for a list of `<t>` tags from an Office Open Xml document. 
        """
        def __init__(self, element: lxml.etree.Element, start_position: int):
            self.element = element
            self.text = "" if not element.text else element.text
            self.length = len(self.text)
            self.start_position = start_position
            self.end_position = self.start_position + self.length - 1

    class TextList(object):
        """
        Class for managing a list of `TextElementInfo` objects
        """
        _list: typing.List['TextReplace.TextElementInfo']

        def __init__(self):
            self._list = []

        def append(self, text_element_info):
            if isinstance(text_element_info, globals()['TextReplace'].TextElementInfo):
                self._list.append(text_element_info)
            else:
                raise presalytics.lib.exceptions.InvalidArgumentException(message="Argument must be an instance of class 'TextElementInfo'")
        
        def set_text(self, index, new_string):
            self._list[index].text = new_string
            self._list[index].element.text = new_string

        def get_text(self, index):
            return self._list[index].text

        def get_plaintext_string(self):
            ret = ""
            for item in self._list:
                ret += item.text
            return ret

        def get_position(self, match_strings):
            for potential_match in match_strings:
                i = self.get_plaintext_string().find(potential_match) 
                if i >= 0:
                    break
            return i, potential_match

        def get_list_index_of_position(self, position: int):
            for item in self._list:
                if item.start_position <= position and item.end_position >= position:
                    return self.get_index(item)
            raise presalytics.lib.exceptions.InvalidArgumentException(message="Position {} out of range".format(position))
        
        def get_index(self, text_element_info: 'TextReplace.TextElementInfo'):
            for i in range(len(self._list)):
                if self._list[i] == text_element_info:
                    return i
            raise presalytics.lib.exceptions.InvalidArgumentException(message="Supplied argument on in self._list")
        
        def set_text_to_empty_string(self, index):
            self._list[index].text = ""
            self._list[index].element.text = ""

        def set_start_to_new_value(self, index, new_value):
            start_text, remainder = self._list[index].text.split("{{", 1)
            end = ""
            if "}}" in remainder:
                end = remainder.split("}}", 1)[1]
            new_text = "{0}{1}{2}".format(start_text, new_value, end)
            self._list[index].text = new_text
            self._list[index].element.text = new_text

        def truncate_end(self, index):
            end_text = self._list[index].text.split("}}", 1)[-1]
            self._list[index].text = end_text
            self._list[index].element.text = end_text

        def plaintext_string_list(self):
            return [x.text for x in self._list]

        def reset(self):
            for i in range(0, len(self._list)):
                start_position = 0 if i == 0 else self._list[i-1].end_position + 1
                self._list[i] = TextReplace.TextElementInfo(self._list[i].element, start_position)


            
    def replace_handlebars(self, info_list, params):
        """
        Method that finds template tags and replaces them 

        TODO: Upgrade to Liquid Syntax (or similar)
        """
        for key, val in params.items():
            match_keys = ["{{" + key + "}}", "{{ " + key + " }}"]
            match_start_position, match_key =  info_list.get_position(match_keys)
            if match_start_position > -1:
                match_end_position = match_start_position + len(match_key) - 1
                match_start_index = info_list.get_list_index_of_position(match_start_position)
                match_end_index = info_list.get_list_index_of_position(match_end_position)
                if match_start_index < match_end_index:
                    info_list.truncate_end(match_end_index)
                for i in range(match_start_index + 1, match_end_index):
                    info_list.set_text_to_empty_string(i)
                info_list.set_start_to_new_value(match_start_index, val)
                info_list.reset()
                self.replace_handlebars(info_list, params)


    def transform_function(self, lxml_element, params):
        """
        Replaces template tags located {{inside_handlebars}} that match keys in the
        `params` dict with values from the `params` dict. 

        This method searches for match for plain text strings, so that if template
        tags are split across xml `<t>` elements, they are still identified and replaced. 

        Parameters
        -----------
        lxml_element : lxml.etree.Element
            An element containing as least one an `<t>` element

        params : dict
            A dictionary that maps template tags (no handlebars) to their respective
            replacement values.
        """
        text_list = lxml_element.findall('.//{*}t')
        info_list = TextReplace.TextList()
        position = 0
        for tag in text_list:
            info = self.TextElementInfo(tag, position)
            position = info.end_position + 1
            info_list.append(info)
        self.replace_handlebars(info_list, params)
        return lxml_element

Ancestors

Class variables

var TextElementInfo

Holds metadata for a list of <t> tags from an Office Open Xml document.

Expand source code Browse git
class TextElementInfo(object):
    """
    Holds metadata for a list of `<t>` tags from an Office Open Xml document. 
    """
    def __init__(self, element: lxml.etree.Element, start_position: int):
        self.element = element
        self.text = "" if not element.text else element.text
        self.length = len(self.text)
        self.start_position = start_position
        self.end_position = self.start_position + self.length - 1
var TextList

Class for managing a list of TextElementInfo objects

Expand source code Browse git
class TextList(object):
    """
    Class for managing a list of `TextElementInfo` objects
    """
    _list: typing.List['TextReplace.TextElementInfo']

    def __init__(self):
        self._list = []

    def append(self, text_element_info):
        if isinstance(text_element_info, globals()['TextReplace'].TextElementInfo):
            self._list.append(text_element_info)
        else:
            raise presalytics.lib.exceptions.InvalidArgumentException(message="Argument must be an instance of class 'TextElementInfo'")
    
    def set_text(self, index, new_string):
        self._list[index].text = new_string
        self._list[index].element.text = new_string

    def get_text(self, index):
        return self._list[index].text

    def get_plaintext_string(self):
        ret = ""
        for item in self._list:
            ret += item.text
        return ret

    def get_position(self, match_strings):
        for potential_match in match_strings:
            i = self.get_plaintext_string().find(potential_match) 
            if i >= 0:
                break
        return i, potential_match

    def get_list_index_of_position(self, position: int):
        for item in self._list:
            if item.start_position <= position and item.end_position >= position:
                return self.get_index(item)
        raise presalytics.lib.exceptions.InvalidArgumentException(message="Position {} out of range".format(position))
    
    def get_index(self, text_element_info: 'TextReplace.TextElementInfo'):
        for i in range(len(self._list)):
            if self._list[i] == text_element_info:
                return i
        raise presalytics.lib.exceptions.InvalidArgumentException(message="Supplied argument on in self._list")
    
    def set_text_to_empty_string(self, index):
        self._list[index].text = ""
        self._list[index].element.text = ""

    def set_start_to_new_value(self, index, new_value):
        start_text, remainder = self._list[index].text.split("{{", 1)
        end = ""
        if "}}" in remainder:
            end = remainder.split("}}", 1)[1]
        new_text = "{0}{1}{2}".format(start_text, new_value, end)
        self._list[index].text = new_text
        self._list[index].element.text = new_text

    def truncate_end(self, index):
        end_text = self._list[index].text.split("}}", 1)[-1]
        self._list[index].text = end_text
        self._list[index].element.text = end_text

    def plaintext_string_list(self):
        return [x.text for x in self._list]

    def reset(self):
        for i in range(0, len(self._list)):
            start_position = 0 if i == 0 else self._list[i-1].end_position + 1
            self._list[i] = TextReplace.TextElementInfo(self._list[i].element, start_position)

Methods

def replace_handlebars(self, info_list, params)

Method that finds template tags and replaces them

TODO: Upgrade to Liquid Syntax (or similar)

Expand source code Browse git
def replace_handlebars(self, info_list, params):
    """
    Method that finds template tags and replaces them 

    TODO: Upgrade to Liquid Syntax (or similar)
    """
    for key, val in params.items():
        match_keys = ["{{" + key + "}}", "{{ " + key + " }}"]
        match_start_position, match_key =  info_list.get_position(match_keys)
        if match_start_position > -1:
            match_end_position = match_start_position + len(match_key) - 1
            match_start_index = info_list.get_list_index_of_position(match_start_position)
            match_end_index = info_list.get_list_index_of_position(match_end_position)
            if match_start_index < match_end_index:
                info_list.truncate_end(match_end_index)
            for i in range(match_start_index + 1, match_end_index):
                info_list.set_text_to_empty_string(i)
            info_list.set_start_to_new_value(match_start_index, val)
            info_list.reset()
            self.replace_handlebars(info_list, params)
def transform_function(self, lxml_element, params)

Replaces template tags located {{inside_handlebars}} that match keys in the params dict with values from the params dict.

This method searches for match for plain text strings, so that if template tags are split across xml <t> elements, they are still identified and replaced.

Parameters

lxml_element : lxml.etree.Element
An element containing as least one an <t> element
params : dict
A dictionary that maps template tags (no handlebars) to their respective replacement values.
Expand source code Browse git
def transform_function(self, lxml_element, params):
    """
    Replaces template tags located {{inside_handlebars}} that match keys in the
    `params` dict with values from the `params` dict. 

    This method searches for match for plain text strings, so that if template
    tags are split across xml `<t>` elements, they are still identified and replaced. 

    Parameters
    -----------
    lxml_element : lxml.etree.Element
        An element containing as least one an `<t>` element

    params : dict
        A dictionary that maps template tags (no handlebars) to their respective
        replacement values.
    """
    text_list = lxml_element.findall('.//{*}t')
    info_list = TextReplace.TextList()
    position = 0
    for tag in text_list:
        info = self.TextElementInfo(tag, position)
        position = info.end_position + 1
        info_list.append(info)
    self.replace_handlebars(info_list, params)
    return lxml_element

Inherited members

class MultiXmlTransform (transforms, fail_quietly=True, **kwargs)

This class allow users to run mutiple transforms on multiple targets in a single widget. MultiXmlTransform wraps multiple subclasses of XmlTransformBase, creates instances of them, and feeds them into an OoxmlEditorWidget instance. To do this, the XmlTransformBase subclasses must be loaded into the XML_TRANSFORM_REGISTRY when called.

The function parameters for the

Parameters

fail_quietly : bool, optional
Defaults to false. Indicates whether an exception should be raised when a subclass specified in the function parameters cannot not be found in the XML_TRANSFORM_REGISTRY instance.

Function Parameters Dictionary

transforms_list : list of dict

A list of dictionaries, with each item in the list consiting of a dictionary of two entries.
The entries are as follows:

  • name: [str] The name of the the subclass of XmlTransformBase that will be that will be initialized

  • function_params: [dict] The params that will be the loaded into the instance's transform_function

Expand source code Browse git
class MultiXmlTransform(XmlTransformBase):
    """
    This class allow users to run mutiple transforms on multiple targets in a single widget.  `MultiXmlTransform`
    wraps multiple subclasses of `presalytics.lib.widgets.ooxml_editors.XmlTransformBase`, creates instances of
    them, and feeds them into an `presalytics.lib.widgets.ooxml_editors.OoxmlEditorWidget` instance.  To do this,
    the `presalytics.lib.widgets.ooxml_editors.XmlTransformBase` subclasses must be loaded into the 
    `presalytics.lib.widgets.ooxml_editors.XML_TRANSFORM_REGISTRY` when called.

    The function parameters for the 

    Parameters
    ----------
    fail_quietly: bool, optional
        Defaults to false.  Indicates whether an exception should be raised when a subclass specified
        in the function parameters cannot not be found in the `presalytics.lib.widgets.ooxml_editors.XML_TRANSFORM_REGISTRY`
        instance. 
        

    Function Parameters Dictionary
    ----------
    transforms_list: list of dict
        A list of dictionaries, with each item in the list consiting of a dictionary of two entries.  
        The entries are as follows:
         
         * name: [str]
            The name of the the subclass of `presalytics.lib.widgets.ooxml_editors.XmlTransformBase` that
            will be that will be initialized
         
         * function_params: [dict]
            The `params` that will be the loaded into the instance's `transform_function` 
    """
    transform_instances: typing.List[XmlTransformBase]

    __xml_transform_name__ = "MultiXmlTransform"

    def __init__(self, transforms: typing.Dict[str, typing.List[typing.Dict[str, typing.Any]]], fail_quietly=True, **kwargs):
        super(MultiXmlTransform, self).__init__(transforms, **kwargs)
        transforms_list = transforms.get("transforms_list", None)
        self.fail_quietly = fail_quietly
        self.transform_instances = []
        self.transform_registry = get_transform_registry()
        for _transform in transforms_list: #type: ignore
            key = "XmlTransform." + _transform["name"]
            transform_class = self.transform_registry.get(key)
            if not transform_class:
                message = "Could not find XmlTransform with name '{}'".format(_transform["name"])
                if self.fail_quietly:
                    logging.info(message)
                else:
                    raise self.transform_registry.raise_error(message) #noqa
            else:
                inst = transform_class(_transform["function_params"])
                self.transform_instances.append(inst)

    def transform_function(self, lxml_element, params):
        for inst in self.transform_instances:
            lxml_element = inst.execute(lxml_element)
        return lxml_element    

Ancestors

Inherited members

StylePlugin subclass for converting a Plugin config into an html <link> fragment.

Attributes

attr_dict : AttrDict
Performs nested lookups on the STYLES_MAP
Expand source code Browse git
class ApprovedExternalLinks(presalytics.lib.plugins.base.StylePlugin):
    """
    `presalytics.lib.plugins.base.StylePlugin` subclass for converting a `presalytics.story.outline.Plugin` 
    config into an html `<link>` fragment.

    Attributes
    ----------
    attr_dict: presalytics.lib.plugins.external.AttrDict
        Performs nested lookups on the `STYLES_MAP`
    """
    __plugin_name__ = 'external_links'

    STYLES_MAP = {
        'reveal': {
            'base': '{0}/static/vendor/reveal/reveal.css'.format(site_host),
            'themes': {
                'beige': '{0}/static/vendor/reveal/theme/beige.css'.format(site_host),
                'black': '{0}/static/vendor/reveal/theme/black.css'.format(site_host),
                'blood': '{0}/static/vendor/reveal/theme/blood.css'.format(site_host),
                'league': '{0}/static/vendor/reveal/theme/league.css'.format(site_host),
                'moon': '{0}/static/vendor/reveal/theme/moon.css'.format(site_host),
                'night': '{0}/static/vendor/reveal/theme/night.css'.format(site_host),
                'serif': '{0}/static/vendor/reveal/theme/serif.css'.format(site_host),
                'simple': '{0}/static/vendor/reveal/theme/simple.css'.format(site_host),
                'sky': '{0}/static/vendor/reveal/theme/sky.min.css'.format(site_host),
                'solarized': '{0}/static/vendor/reveal/theme/solarized.css'.format(site_host),
                'white': '{0}/static/vendor/reveal/theme/white.css'.format(site_host)
            },
            'print': 'https://cdnjs.cloudflare.com/ajax/libs/reveal.js/3.6.0/css/print/pdf.min.css',
            'customizations': '{0}/static/css/reveal-customizations.css'.format(site_host),
            'toolbar': '{0}/static/css/toolbar.css'.format(site_host),
        },
        'preloaders' : '{0}/static/css/preloaders.css'.format(site_host),
        'bootstrap4': "https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css",
        'font-awesome': '{0}/static/vendor/fontawesome-free/css/all.min.css'.format(site_host),
    }
    
    """
    Static nested dictionary containing links to external stylesheets that will be rendered alongside this plugin
    """

    def __init__(self, **kwargs):
        super(ApprovedExternalLinks, self).__init__(**kwargs)
        self.attr_dict = AttrDict(self.STYLES_MAP)


    def to_style(self, config, **kwargs):
        """
        Converts a dot-notation key for nested dictionaries (e.g., 'reveal.base') into a
        string contain an html fragement with `<link>` tag.  The dot-notation key 
        is pulled from the 'approved_styles_key' of the 'config' attrubte of subclass 
        of a `presalytics.story.outline.Plugin` object.
        """
        key = config['approved_styles_key']
        link = self.attr_dict.flatten()[key]
        if link is None:
            message = "Key {0} does not reference a link in the APPROVED_STYLES dictionary".format(key)
            raise presalytics.lib.exceptions.MissingConfigException(message)
        return '<link rel="stylesheet" href="{0}"/>'.format(link)

Ancestors

Class variables

var STYLES_MAP

Static nested dictionary containing links to external stylesheets that will be rendered alongside this plugin

Methods

def to_style(self, config, **kwargs)

Converts a dot-notation key for nested dictionaries (e.g., 'reveal.base') into a string contain an html fragement with <link> tag. The dot-notation key is pulled from the 'approved_styles_key' of the 'config' attrubte of subclass of a Plugin object.

Expand source code Browse git
def to_style(self, config, **kwargs):
    """
    Converts a dot-notation key for nested dictionaries (e.g., 'reveal.base') into a
    string contain an html fragement with `<link>` tag.  The dot-notation key 
    is pulled from the 'approved_styles_key' of the 'config' attrubte of subclass 
    of a `presalytics.story.outline.Plugin` object.
    """
    key = config['approved_styles_key']
    link = self.attr_dict.flatten()[key]
    if link is None:
        message = "Key {0} does not reference a link in the APPROVED_STYLES dictionary".format(key)
        raise presalytics.lib.exceptions.MissingConfigException(message)
    return '<link rel="stylesheet" href="{0}"/>'.format(link)

Inherited members

class ApprovedExternalScripts (**kwargs)

ScriptPlugin subclass for converting a Plugin config into an html <script> fragment.

Attributes

attr_dict : AttrDict
Performs nested lookups on the SCRIPT_MAP
Expand source code Browse git
class ApprovedExternalScripts(presalytics.lib.plugins.base.ScriptPlugin):
    """
    `presalytics.lib.plugins.base.ScriptPlugin` subclass for converting a `presalytics.story.outline.Plugin` 
    config into an html `<script>` fragment.

    Attributes
    ----------
    attr_dict: presalytics.lib.plugins.external.AttrDict
        Performs nested lookups on the `SCRIPT_MAP`
    """
    __plugin_name__ = 'external_scripts'

    SCRIPT_MAP = {
        'd3': 'https://d3js.org/d3.v5.min.js',
        'd3v3': 'https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js',
        'reveal': {
            'base': '{0}/static/vendor/reveal/reveal.js'.format(site_host),
            # 'markdown': 'https://cdnjs.cloudflare.com/ajax/libs/reveal.js/3.6.0/plugin/markdown/markdown.min.js',
            # 'highlight': 'https://cdnjs.cloudflare.com/ajax/libs/reveal.js/3.6.0/plugin/highlight/highlight.min.js',
            # 'math': 'https://cdnjs.cloudflare.com/ajax/libs/reveal.js/3.6.0/plugin/math/math.min.js',
            # 'zoom': 'https://cdnjs.cloudflare.com/ajax/libs/reveal.js/3.6.0/plugin/zoom-js/zoom.min.js',
            # 'notes': 'https://cdnjs.cloudflare.com/ajax/libs/reveal.js/3.6.0/plugin/notes/notes.min.js',
            # 'print': 'https://cdnjs.cloudflare.com/ajax/libs/reveal.js/3.6.0/plugin/print-pdf/print-pdf.min.js',
            'customizations': '{0}/static/js/revealcustomizations.js'.format(site_host),
            'toolbar': '{0}/static/js/toolbar.js'.format(site_host),
            'screenfull': '{0}/static/vendor/screenfull/screenfull.min.js'.format(site_host),

        },
        'mpld3': '{0}/static/mpld3/mpld3.min.js'.format(site_host),
        'ooxml': '{0}/static/ooxml/ooxml.js'.format(site_host),
        'mpl-responsive': '{0}/static/js/mpl-responsive.js'.format(site_host),
        'jquery': 'https://code.jquery.com/jquery-3.4.1.min.js',
        'popper': 'https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js',
        'bootstrap4': 'https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js'
    }
    """
    Static nested dictionary containing links to external scripts that will be rendered alongside this plugin
    """

    def __init__(self, **kwargs):
        super(ApprovedExternalScripts, self).__init__(**kwargs)
        self.attr_dict = AttrDict(self.SCRIPT_MAP)


    def to_script(self, config, **kwargs):
        """
        Converts a dot-notation key for nested dictionaries (e.g., 'reveal.base') into a
        string containing an html fragement with `<script>` tags.  The dot-notation key 
        is pulled from the 'approved_styles_key' of the 'config' attrubte of subclass 
        of a `presalytics.story.outline.Plugin` object.
        """
        key = config['approved_scripts_key']
        link = self.attr_dict.flatten()[key]
        if link is None:
            message = "Key {0} does not reference a link in the APPROVED_SCRIPTS dictionary".format(key)
            raise presalytics.lib.exceptions.MissingConfigException(message)
        return '<script type="text/javascript" src="{0}"></script>'.format(link)

Ancestors

Class variables

var SCRIPT_MAP

Static nested dictionary containing links to external scripts that will be rendered alongside this plugin

Methods

def to_script(self, config, **kwargs)

Converts a dot-notation key for nested dictionaries (e.g., 'reveal.base') into a string containing an html fragement with <script> tags. The dot-notation key is pulled from the 'approved_styles_key' of the 'config' attrubte of subclass of a Plugin object.

Expand source code Browse git
def to_script(self, config, **kwargs):
    """
    Converts a dot-notation key for nested dictionaries (e.g., 'reveal.base') into a
    string containing an html fragement with `<script>` tags.  The dot-notation key 
    is pulled from the 'approved_styles_key' of the 'config' attrubte of subclass 
    of a `presalytics.story.outline.Plugin` object.
    """
    key = config['approved_scripts_key']
    link = self.attr_dict.flatten()[key]
    if link is None:
        message = "Key {0} does not reference a link in the APPROVED_SCRIPTS dictionary".format(key)
        raise presalytics.lib.exceptions.MissingConfigException(message)
    return '<script type="text/javascript" src="{0}"></script>'.format(link)

Inherited members

class LocalStylesPlugin (**kwargs)

Plugin incorporate styles from a css stylesheet in a local filepath

Expand source code Browse git
class LocalStylesPlugin(presalytics.lib.plugins.base.StylePlugin):
    """
    Plugin incorporate styles from a css stylesheet in a local filepath
    """
    __plugin_name__ = 'local'

    LOCAL_STYLES_MAP = {
        "single_item_grid": os.path.join(css_path, "single-item-grid.css"),
        "flex_row": os.path.join(css_path, "flex-row.css"),
        "light_grey": os.path.join(css_path, "light-grey.css"),
        "responsive_title": os.path.join(css_path, "responsive-title.css"),
        "reveal_overrides": os.path.join(css_path, "reveal-overrides-base.css")
    }
    """
    Dictionary containing a map configuration keys to css files that are 
    member of the Presalytics Python Library manifest
    """

    def to_style(self, config, **kwargs):
        """
        Renders a css stylesheet as an inline style tag in a story

        Parameters
        ----------
        config : dict
            A dictionary containing either a "css_file_path" key, which points
            to a local file path, or a "css_file_id" refers to a css file included
            in the Presalytics Python Library's manifest 
        """
        id = config.get("css_file_id", None)
        if not id:
            file_path = config.get("css_file_path", None)
            if not file_path:
                message = 'Plugin requires "css_file_id" or "css_file_path" in its configuration dictionary'
                raise presalytics.lib.exceptions.MissingConfigException(message)
            if not os.path.exists(file_path):
                message = 'Path {0} in plugin configuration does not exist'.format(file_path)
                raise presalytics.lib.exceptions.MissingConfigException(message)
        else:
            file_path = LocalStylesPlugin.LOCAL_STYLES_MAP[id]
        with open(file_path, 'r') as f:
            style_data = f.read()
        tag = "<style>{0}</style>".format(style_data)
        return tag

Ancestors

Class variables

var LOCAL_STYLES_MAP

Dictionary containing a map configuration keys to css files that are member of the Presalytics Python Library manifest

Methods

def to_style(self, config, **kwargs)

Renders a css stylesheet as an inline style tag in a story

Parameters

config : dict
A dictionary containing either a "css_file_path" key, which points to a local file path, or a "css_file_id" refers to a css file included in the Presalytics Python Library's manifest
Expand source code Browse git
def to_style(self, config, **kwargs):
    """
    Renders a css stylesheet as an inline style tag in a story

    Parameters
    ----------
    config : dict
        A dictionary containing either a "css_file_path" key, which points
        to a local file path, or a "css_file_id" refers to a css file included
        in the Presalytics Python Library's manifest 
    """
    id = config.get("css_file_id", None)
    if not id:
        file_path = config.get("css_file_path", None)
        if not file_path:
            message = 'Plugin requires "css_file_id" or "css_file_path" in its configuration dictionary'
            raise presalytics.lib.exceptions.MissingConfigException(message)
        if not os.path.exists(file_path):
            message = 'Path {0} in plugin configuration does not exist'.format(file_path)
            raise presalytics.lib.exceptions.MissingConfigException(message)
    else:
        file_path = LocalStylesPlugin.LOCAL_STYLES_MAP[id]
    with open(file_path, 'r') as f:
        style_data = f.read()
    tag = "<style>{0}</style>".format(style_data)
    return tag

Inherited members

class OoxmlTheme (**kwargs)

This class takes theme data from an Presalytics API Ooxml Automation service Theme object and feeds those values into the RevealCustomTheme

Expand source code Browse git
class OoxmlTheme(presalytics.lib.plugins.reveal_theme.RevealCustomTheme):
    """
    This class takes theme data from an Presalytics API Ooxml Automation service Theme object
    and feeds those values into the `presalytics.lib.plugins.reveal_theme.RevealCustomTheme`
    """
    __plugin_name__ = 'ooxml-theme'

    def to_style(self, config, **kwargs):
        base_path = os.path.join(os.path.dirname(__file__), 'scss')
        if os.name == "nt":
            import_path = posixpath.join(*base_path.split('\\'))
        else:
            import_path = base_path
        scss_file = os.path.join(base_path, 'overrides.tmpl')
        config.update({'path': import_path})
        with open(scss_file, 'r') as file:
            scss_template_string = file.read()
        config = self.config_to_camelCase(config)
        scss_string = scss_template_string.format(**config)
        new_css = sass.compile(string=scss_string)
        style_string = "<style>\n{0}\n</style>".format(new_css)
        font_names = [config["headingFont"], config["bodyFont"]]
        links = self.get_fonts(font_names)
        return links + style_string
    
    def config_to_camelCase(self, config: typing.Dict) -> typing.Dict:
        new_dict = dict()
        for key, val in config.items():
            new_dict.update({
                presalytics.story.util.to_camel_case(key): val
            })
        return new_dict

Ancestors

Methods

def config_to_camelCase(self, config)
Expand source code Browse git
def config_to_camelCase(self, config: typing.Dict) -> typing.Dict:
    new_dict = dict()
    for key, val in config.items():
        new_dict.update({
            presalytics.story.util.to_camel_case(key): val
        })
    return new_dict

Inherited members

class RevealConfigPlugin (**kwargs)

Allows users configure reveal.js initialization settings for when their story is rendered via Revealer

For more information regarding reveal.js' initialization settings, please refer to https://github.com/hakimel/reveal.js/#configuration

Expand source code Browse git
class RevealConfigPlugin(ext.ScriptPlugin, jinja.JinjaPluginMakerMixin):
    """
    Allows users configure reveal.js initialization settings for when their 
    story is rendered via `presalytics.story.revealer.Revealer`

    For more information regarding reveal.js' initialization settings, please
    refer to https://github.com/hakimel/reveal.js/#configuration        
    """
    __plugin_name__ = 'reveal'

    __dependencies__ = [
        {
            'kind': 'script',
            'name': 'external_scripts',
            'config': {
                'approved_scripts_key': 'reveal.base'
            }
        },
        {
            'kind': 'style',
            'name': 'external_links',
            'config': {
                'approved_styles_key': 'reveal.base'
            }
        },
        {
            'kind': 'script',
            'name': 'external_scripts',
            'config': {
                'approved_scripts_key': 'reveal.screenfull'
            }
        },
        {
            'kind': 'style',
            'name': 'external_links',
            'config': {
                'approved_styles_key': 'reveal.toolbar'
            }
        },
        {
            'kind': 'style',
            'name': 'external_links',
            'config': {
                'approved_styles_key': 'font-awesome'
            }
        },
        {
            'kind': 'script',
            'name': 'external_scripts',
            'config': {
                'approved_scripts_key': 'reveal.toolbar'
            }
        },
        {
            'kind': 'style',
            'name': 'external_links',
            'config': {
                'approved_styles_key': 'reveal.themes.white'
            }
        },
        {
            'kind': 'style',
            'name': 'external_links',
            'config': {
                'approved_styles_key': 'reveal.customizations'
            }
        },
        {
            'kind': 'script',
            'name': 'external_scripts',
            'config': {
                'approved_scripts_key': 'reveal.customizations'
            }
        },
        
        
    ]

    def to_script(self, config, **kwargs):
        reveal_config = self.default_config
        if config.get("reveal_params", None):
            reveal_config.update(config.pop("reveal_params"))
        config.update({'reveal_config': reveal_config})
        return self.render(config)

    template = """
    <script type="text/javascript"> 
        window.addEventListener('toolbar-initialized', function(e) {
            var config = {{ reveal_config|tojson(indent=4) }};
            config.plugins = [window.presalyticsToolbar];
            Reveal.initialize(config);
        });
    </script>
    """
    """
    The `<script>` fragment that get populated with a initialization data 
    via the `presalytics.lib.plugins.jinja.JinjaPluginMakerMixin`
    """
    
    default_config = {
        'controls': True,  # Display presentation control arrows
        # Help the user learn the controls by providing hints, for example by
        # bouncing the down arrow when they first encounter a vertical slide
        'controlsTutorial': True,

        # Determines where controls appear, "edges" or "bottom-right" #
        'controlsLayout': 'bottom-right',

        # Visibility rule for backwards navigation arrows; "faded", "hidden"
        # or "visible"
        'controlsBackArrows': 'faded',

        # Display a presentation progress bar
        'progress': True,

        # Display the page number of the current slide
        'slideNumber': False,

        # Add the current slide number to the URL hash so that reloading the
        #page/copying the URL will return you to the same slide
        'hash': False,

        # Push each slide change to the browser history. Implies `hash: true`
        'history': False,

        # Enable keyboard shortcuts for navigation 
        'keyboard': True,

        # Enable the slide overview mode
        'overview': True,

        # Vertical centering of slides
        'center': True,

        # Enables touch navigation on devices with touch input
        'touch': True,

        # Loop the presentation
        'loop': True,

        # Change the presentation direction to be RTL
        'rtl': False,

        # See https://github.com/hakimel/reveal.js/#navigation-mode
        'navigationMode': 'default',

        # Randomizes the order of slides each time the presentation loads
        'shuffle': False,

        # Turns fragments on and off globally
        'fragments': True,

        # Flags whether to include the current fragment in the URL,
        # so that reloading brings you to the same fragment position #}
        'fragmentInURL': False,

        # Flags if the presentation is running in an embedded mode,
        # i.e. contained within a limited portion of the screen #}
        'embedded': False,

        # Flags if we should show a help overlay when the questionmark
        # key is pressed #}
        'help': True,

        # Flags if speaker notes should be visible to all viewers
        'showNotes': False,

        # Global override for autoplaying embedded media (video/audio/iframe)
        # - null: Media will only autoplay if data-autoplay is present
        # - true: All media will autoplay, regardless of individual setting
        # - false: No media will autoplay, regardless of individual setting
        'autoPlayMedia': 'null',

        # Global override for preloading lazy-loaded iframes
        # - null: Iframes with data-src AND data-preload will be loaded when within
        #   the viewDistance, iframes with only data-src will be loaded when visible
        # - true: All iframes with data-src will be loaded when within the viewDistance
        # - false: All iframes with data-src will be loaded only when visible #}
        'preloadIframes': 'null',

        # Number of milliseconds between automatically proceeding to the
        # next slide, disabled when set to 0, this value can be overwritten
        # by using a data-autoslide attribute on your slides #}
        'autoSlide': 0,

        # Stop auto-sliding after user input
        'autoSlideStoppable': True,

        # Use this method for navigation when auto-sliding
        'autoSlideMethod': 'Reveal.navigateNext',

        # Specify the average time in seconds that you think you will spend
        # presenting each slide. This is used to show a pacing timer in the
        # speaker view 
        'defaultTiming': 120,

        # Enable slide navigation via mouse wheel
        'mouseWheel': False,

        # Hide cursor if inactive
        'hideInactiveCursor': True,

        # Time before the cursor is hidden (in ms)
        'hideCursorTime': 5000,

        # Hides the address bar on mobile devices
        'hideAddressBar': True,

        # Opens links in an iframe preview overlay
        #  Add `data-preview-link` and `data-preview-link="false"` to customise each link
        # individually
        'previewLinks': False,

        # Transition style
        'transition': 'slide', # none/fade/slide/convex/concave/zoom

        # Transition speed
        'transitionSpeed': 'default', # default/fast/slow

        # Transition style for full page slide backgrounds
        'backgroundTransition': 'fade', # none/fade/slide/convex/concave/zoom

        # Number of slides away from the current that are visible
        'viewDistance': 3,

        # Parallax background image
        'parallaxBackgroundImage': '', # e.g. "'https://s3.amazonaws.com/hakim-static/reveal-js/reveal-parallax-1.jpg'"

        # Parallax background size
        'parallaxBackgroundSize': '', # CSS syntax, e.g. "2100px 900px"

        # Number of pixels to move the parallax background per slide
        # - Calculated automatically unless specified
        # - Set to 0 to disable movement along an axis 
        'parallaxBackgroundHorizontal': 'null',
        'parallaxBackgroundVertical': 'null',

        # The display mode that will be used to show slides
        'display': 'block',

        # Allow for responsive presenation formats
        'width': "100%",
        'height': "100%",
        'margin': 0,
        'minScale': 1,
        'maxScale': 1,

        # Presalytics toolbar
        'showToolbar': True,

        'toolbar': {
            # Specifies where the toolbar will be shown: 'top' or 'bottom'
            'position': 'bottom',

            # Add button to toggle fullscreen mode for the presentation
            'fullscreen': True,

            # Add button to toggle the overview mode on and off
            'overview': True,

            # Add button to pause (hide) the presentation display
            'pause': True,

            # Add button to show the speaker notes
            'notes': False,

            # Add button to show the help overlay
            'help': False,

            # If true, the reveal.js-menu will be moved into the toolbar.
            # Set to false to leave the menu on its own.
            'captureMenu': True,

            # If true, the playback control will be moved into the toolbar.
            # This is only relevant if the presentation is configured to autoSlide.
            # Set to false to leave the menu on its own.
            'capturePlaybackControl': True,

            # By default the menu will load it's own font-awesome library
            # icons. If your presentation needs to load a different
            # font-awesome library the 'loadIcons' option can be set to false
            # and the menu will not attempt to load the font-awesome library.
            'loadIcons': True,

            # Instructs the toolbar to load the Presaytics chat interface
            'chat': True,

            # Shows the make pdf button
            'pdf': True,
        }
    }
    """
    A dictionary containing default values for rendering presalytics stories
    """

Ancestors

Class variables

var template

The <script> fragment that get populated with a initialization data via the JinjaPluginMakerMixin

var default_config

A dictionary containing default values for rendering presalytics stories

Inherited members

class RevealCustomTheme (**kwargs)

Plugin to for customizing reveal.js settings for a given story

This plugin's configuration dictionary keys and that load them in as variable to a _variables.scss file and compiles the scss using the sass python package.

See reveal.js' Creating a Theme page for more information regarding configuration values.

Expand source code Browse git
class RevealCustomTheme(presalytics.lib.plugins.base.StylePlugin):
    """
    Plugin to for customizing reveal.js settings for a given story

    This plugin's configuration dictionary keys and that load them in
    as variable to a _variables.scss file and compiles the scss using the
    sass python package. 

    See reveal.js' [Creating a Theme](https://github.com/hakimel/reveal.js/blob/8a54118f43b91030f3965088d5e1c1c7598a5cd3/css/theme/README.md)
    page for more information regarding configuration values.
    """
    __plugin_name__ = 'reveal_custom_theme'
    defaults: typing.Dict[str, str]

    fonts_base_url = 'https://fonts.googleapis.com/css?family={0}'
    """
    Google fonts base url for loading fonts via a `<link>` tag 
    """

    def to_style(self, config, **kwargs):
        """
        Returns compiled scss and links to download fonts
        """
        scss_variables = self.defaults
        scss_variables.update(config)
        scss_string = self.get_base_scss(scss_variables)
        new_css = sass.compile(string=scss_string)
        style_string = "<style>\n{0}\n</style>".format(new_css)
        links = self.get_fonts(config["fonts"])
        return links + style_string

    def get_fonts(self, fonts: typing.List[str]):
        """
        Creates `<link>` tags from font names
        """
        links = ""
        for font in fonts:
            link = self.get_font_link(font)
            if link:
                links = links + link + "\n"
        return links

    def get_font_link(self, font_name) -> typing.Optional[str]:
        """
        Tests whether a given font is available for download form google fonts.  Returns
        the link tag if available
        """
        test_url = self.fonts_base_url.format(font_name)
        r = requests.get(test_url)
        if r.status_code == 200:
            return '<link href="{0}" rel="stylesheet">'.format(test_url)
        else:
            return None

    def get_base_scss(self, scss_variables):
        """
        loads the reveal.js scss files into string to be compiled by the sass module
        """
        scss_folder = os.path.join(os.path.dirname(__file__), "scss")
        mixins_file = os.path.join(scss_folder, "reveal-mixins.scss")
        settings_file = os.path.join(scss_folder, "reveal-settings.scss")
        theme_file = os.path.join(scss_folder, "reveal-theme.scss")
        overrides_file = os.path.join(scss_folder, "overrides.scss.tmpl")

        with open(mixins_file, 'r') as m:
            mixins = m.read()

        with open(settings_file, 'r') as s:
            settings = s.read()

        with open(theme_file, 'r') as t:
            theme = t.read()

        with open(overrides_file, 'r') as o:
            overrides_template = o.read()

        overrides = overrides_template.format(**scss_variables)

        scss_string = "{0}\n{1}\n{2}\n{3}".format(mixins, settings, overrides, theme)
        return scss_string

    # Defaults to white theme parameters
    defaults = {
        # Background of the presentation
        "background_color": "#2b2b2b",

        # Primary/body text
        "main_font": 'Lato',
        "main_font_size": "inherit",
        "main_color": "#eee",

        # Vertical spacing between blocks of text
        "block_margin": "20px",

        # Headings
        "heading_margin": "0 0 $blockMargin 0",
        "heading_font": 'Lato',
        "heading_color": "#eee",
        "heading_line_height": "1.2",
        "heading_letter_spacing": "normal",
        "heading_text_transform": "none",
        "heading_text_shadow": "none",
        "heading_font_weight": "normal",

        "heading_one_text_shadow": "$headingTextShadow",
        "heading_one_size": "3.77em",
        "heading_two_size": "2.11em",
        "heading_three_size": "1.55em",
        "heading_four_size": "1.00em",

        "code_font": "monospace",

        # Links and actions
        "link_color": "#13DAEC",
        "linkColorHover": "lighten( $linkColor, 20% )",

        # Text selection
        "selection_background_color": "#FF5E99",
        "selection_color": "#fff"
    }
    """
    Default reveal.js theme configuration for presalytics stories
    """

Ancestors

Subclasses

Class variables

var fonts_base_url

Google fonts base url for loading fonts via a <link> tag

var defaults

Default reveal.js theme configuration for presalytics stories

Methods

def to_style(self, config, **kwargs)

Returns compiled scss and links to download fonts

Expand source code Browse git
def to_style(self, config, **kwargs):
    """
    Returns compiled scss and links to download fonts
    """
    scss_variables = self.defaults
    scss_variables.update(config)
    scss_string = self.get_base_scss(scss_variables)
    new_css = sass.compile(string=scss_string)
    style_string = "<style>\n{0}\n</style>".format(new_css)
    links = self.get_fonts(config["fonts"])
    return links + style_string
def get_fonts(self, fonts)

Creates <link> tags from font names

Expand source code Browse git
def get_fonts(self, fonts: typing.List[str]):
    """
    Creates `<link>` tags from font names
    """
    links = ""
    for font in fonts:
        link = self.get_font_link(font)
        if link:
            links = links + link + "\n"
    return links

Tests whether a given font is available for download form google fonts. Returns the link tag if available

Expand source code Browse git
def get_font_link(self, font_name) -> typing.Optional[str]:
    """
    Tests whether a given font is available for download form google fonts.  Returns
    the link tag if available
    """
    test_url = self.fonts_base_url.format(font_name)
    r = requests.get(test_url)
    if r.status_code == 200:
        return '<link href="{0}" rel="stylesheet">'.format(test_url)
    else:
        return None
def get_base_scss(self, scss_variables)

loads the reveal.js scss files into string to be compiled by the sass module

Expand source code Browse git
def get_base_scss(self, scss_variables):
    """
    loads the reveal.js scss files into string to be compiled by the sass module
    """
    scss_folder = os.path.join(os.path.dirname(__file__), "scss")
    mixins_file = os.path.join(scss_folder, "reveal-mixins.scss")
    settings_file = os.path.join(scss_folder, "reveal-settings.scss")
    theme_file = os.path.join(scss_folder, "reveal-theme.scss")
    overrides_file = os.path.join(scss_folder, "overrides.scss.tmpl")

    with open(mixins_file, 'r') as m:
        mixins = m.read()

    with open(settings_file, 'r') as s:
        settings = s.read()

    with open(theme_file, 'r') as t:
        theme = t.read()

    with open(overrides_file, 'r') as o:
        overrides_template = o.read()

    overrides = overrides_template.format(**scss_variables)

    scss_string = "{0}\n{1}\n{2}\n{3}".format(mixins, settings, overrides, theme)
    return scss_string

Inherited members

class JinjaTemplateBuilder (page, **kwargs)

Base class for building objects that render html from Page objects that implementing the Jinja2 python library.

Instances of this class will look for templates located at file location identified by the __template_file__ attribute. If not template is found, the content in the template_string attribute will be used. Templates should call each widget's to_html() method in a placeholder in order to generate valid html for the page.

About building templates: Templates are passed a widgets attribute and a widget_index integer (initialized at 0) as part of the context during rendering,. To render multiple widgets on a page, the following pattern can be used inside of templates to increment through the widgets as the jinja2 rendering engine moves through the template:

{{ widgets[widget_index.next()].to_html() }}  // renders widget and increments widget_index

Please also note that if <script> tags are included in the template, they will be stripped out downstream by a Renderer for security reasons. Scripts included in templates will not make it to the browser.

Parameters

page : Page
The page to be rendered

Attributes

__template_file__ : str
The filename to an html file containing a fragment that will be rendered into a page by a Renderer
__template_paths__ : list of str
user-defined filepaths to directories where Jinja2 should look for the __template_file__
__css__ : list of str
Each str in this list is a key that maps to an entry in the LocalStylesPlugin.LOCAL_STYLES_MAP. Ids matched here load the css files as a dependent plugin.
template_paths : list of str
The folders Jinja2 will look in. Includes that html directory adjacent to this __file__, appending with the files in the __template_paths__
widgets : list of subclass instances of WidgetBase
Widget to be rendered into the placeholders in the template identified by __template_file__
Expand source code Browse git
class JinjaTemplateBuilder(presalytics.story.components.PageTemplateBase):
    """
    Base class for building objects that render html from `presalytics.story.outline.Page` 
    objects that implementing the [Jinja2](https://jinja.palletsprojects.com/) 
    python library.

    Instances of this class will look for templates located at file location identified by the
    `__template_file__` attribute.  If not template is found, the content in the `template_string`
    attribute will be used. Templates should call each widget's `to_html()` method in a placeholder
    in order to generate valid html for the page.      

    *About building templates*: Templates are passed a `widgets` attribute and a `widget_index` 
    integer (initialized at 0) as part of the context during rendering,. To render 
    multiple widgets on a page, the following pattern can be used inside of templates to 
    increment through the widgets as the jinja2 rendering engine moves through the template:
        
        {{ widgets[widget_index.next()].to_html() }}  // renders widget and increments widget_index

    Please also note that if `<script>` tags are included in the template, they will be 
    stripped out downstream by a `presalytics.story.components.Renderer` for security
    reasons.  Scripts included in templates will not make it to the browser.

    Parameters
    ----------
    page : presalytics.story.outline.Page
        The page to be rendered

    Attributes
    ----------
    __template_file__ : str
        The filename to an html file containing a fragment that will be rendered into 
        a page by a `presalytics.story.components.Renderer`
    
    __template_paths__ : list of str
        user-defined filepaths to directories where Jinja2 should look for the `__template_file__`

    __css__ : list of str
        Each str in this list is a key that maps to an entry in the
        `presalytics.lib.plugins.local.LocalStylesPlugin.LOCAL_STYLES_MAP`. Ids matched here 
        load the css files as a dependent plugin.

    template_paths : list of str
        The folders Jinja2 will look in. Includes that html directory adjacent to this `__file__`, 
        appending with the files in the `__template_paths__`

    widgets : list of subclass instances of presalytics.story.components.WidgetBase
        Widget to be rendered into the placeholders in the template identified by `__template_file__`
    
    """
    __css__: typing.Sequence[str]
    __template_file__: str
    __template_paths__: typing.List[str]
    template_paths: typing.List[str]

    __template_paths__ = []
    __css__ = []

    class WidgetIndexer(object):
        """
        A counter class for 

        call `widgetindex.next()` in html templates to move this the widget list
        """
        def __init__(self):
            self._val = 0
        
        def next(self):
            """
            Returns the current widget index and increments the value for the next call 
            """
            cur = self._val
            self._val += 1
            return cur

    def __init__(self, page: 'Page', **kwargs) -> None:
        super().__init__(page, **kwargs)
        pkg_templates = os.path.join(os.path.dirname(__file__), "html")
        self.template_paths = [pkg_templates, os.getcwd()]
        if len(self.__template_paths__) > 0:
            self.template_paths[0:0] = self.__template_paths__
        self.is_template_local = self.check_for_file()
        if self.is_template_local:
            self.template_string = self.read_template_string()
        elif self.outline_page.additional_properties.get("template_string", None):
            self.template_string = self.outline_page.additional_properties.get("template_string")
        else:
            self.template_string = None
        if len(kwargs.keys()) > 0:
            self.outline_page.additional_properties.update(kwargs)

    @classmethod
    def deserialize(cls, component, **kwargs):
        return cls(component, **kwargs)

    @util.classproperty
    def __plugins__(cls):
        plugin_list = []
        for id in cls.__css__:
            new_item = {
                'kind': 'style',
                'name': 'local',
                'config': {
                    'css_file_id': id
                }
            }
            plugin_list.append(new_item)
        return plugin_list

    def serialize(self):
        updated_plugins = []
        for plugin_data in self.__plugins__:
            updated_plugins.append(presalytics.story.outline.Plugin(**plugin_data))
        self.outline_page.plugins = updated_plugins
        if self.template_string:
            self.outline_page.additional_properties["template_string"] = self.template_string
        return self.outline_page

    def get_template_name(self):
        """
        Requires subclasses have either a `__template_file__` class property, or override this method
        """
        if self.__template_file__:
            return self.__template_file__
        else:
            raise NotImplementedError
    
    def render(self, **kwargs):
        """
        Renders the widgets to html
        """
        if self.is_template_local:
            return self.render_from_file(**kwargs)
        elif self.template_string:
            return self.render_from_backup_string(**kwargs)
        else:
            raise presalytics.lib.exceptions.MissingConfigException("Missing __template_file__: {}".format(self.get_template_name()))

    def _make_context(self):
        context = {
            "widgets": self.widgets,
            "widget_index": self.WidgetIndexer()
        }
        if self.outline_page.additional_properties:
            context.update(self.outline_page.additional_properties)
            context.pop("template_string", None)
        return context

    def render_from_file(self, **kwargs) -> str:
        """
        Returns rendered html with widgets rendered into template placeholders
        """
        template = self.load_jinja_template()
        context = self._make_context()
        return template.render(**context)

    def render_from_backup_string(self, **kwargs):
        """
        Returns rendered html with widgets rendered into template placeholders
        """
        context = self._make_context()
        options = {
            "loader": jinja2.BaseLoader()
        }
        if context.get("jinja_options", None):
            options.update(context.pop("jinja_options"))
        env = jinja2.Environment(**options)
        env.filters['htmlize'] = htmlize
        template = env.from_string(self.template_string)
        return template.render(**context)

    def read_template_string(self) -> typing.Optional[str]:
        """
        Finds the template file and reads it into a string
        """
        for _dir in self.template_paths:
            fpath = os.path.join(_dir, self.get_template_name())
            if os.path.exists(fpath):
                with open(fpath, 'r') as f:
                    template_string = f.read()
                return template_string
        return None



    def load_jinja_template(self) -> jinja2.Template:
        """
        Uses the fileloader to load a local template file into the jinja2 environment
        """
        loader = jinja2.FileSystemLoader(self.template_paths)
        env = jinja2.Environment(loader=loader)
        env.filters['htmlize'] = htmlize
        return env.get_template(self.get_template_name())

    def check_for_file(self):
        """
        Checks whether `__template_file__` exists locally
        """
        for _dir in self.template_paths:
            fpath = os.path.join(_dir, self.get_template_name())
            if os.path.exists(fpath):
                return True
        return False

Ancestors

Subclasses

Class variables

var WidgetIndexer

A counter class for

call widgetindex.next() in html templates to move this the widget list

Expand source code Browse git
class WidgetIndexer(object):
    """
    A counter class for 

    call `widgetindex.next()` in html templates to move this the widget list
    """
    def __init__(self):
        self._val = 0
    
    def next(self):
        """
        Returns the current widget index and increments the value for the next call 
        """
        cur = self._val
        self._val += 1
        return cur

Methods

def get_template_name(self)

Requires subclasses have either a __template_file__ class property, or override this method

Expand source code Browse git
def get_template_name(self):
    """
    Requires subclasses have either a `__template_file__` class property, or override this method
    """
    if self.__template_file__:
        return self.__template_file__
    else:
        raise NotImplementedError
def render(self, **kwargs)

Renders the widgets to html

Expand source code Browse git
def render(self, **kwargs):
    """
    Renders the widgets to html
    """
    if self.is_template_local:
        return self.render_from_file(**kwargs)
    elif self.template_string:
        return self.render_from_backup_string(**kwargs)
    else:
        raise presalytics.lib.exceptions.MissingConfigException("Missing __template_file__: {}".format(self.get_template_name()))
def render_from_file(self, **kwargs)

Returns rendered html with widgets rendered into template placeholders

Expand source code Browse git
def render_from_file(self, **kwargs) -> str:
    """
    Returns rendered html with widgets rendered into template placeholders
    """
    template = self.load_jinja_template()
    context = self._make_context()
    return template.render(**context)
def render_from_backup_string(self, **kwargs)

Returns rendered html with widgets rendered into template placeholders

Expand source code Browse git
def render_from_backup_string(self, **kwargs):
    """
    Returns rendered html with widgets rendered into template placeholders
    """
    context = self._make_context()
    options = {
        "loader": jinja2.BaseLoader()
    }
    if context.get("jinja_options", None):
        options.update(context.pop("jinja_options"))
    env = jinja2.Environment(**options)
    env.filters['htmlize'] = htmlize
    template = env.from_string(self.template_string)
    return template.render(**context)
def read_template_string(self)

Finds the template file and reads it into a string

Expand source code Browse git
def read_template_string(self) -> typing.Optional[str]:
    """
    Finds the template file and reads it into a string
    """
    for _dir in self.template_paths:
        fpath = os.path.join(_dir, self.get_template_name())
        if os.path.exists(fpath):
            with open(fpath, 'r') as f:
                template_string = f.read()
            return template_string
    return None
def load_jinja_template(self)

Uses the fileloader to load a local template file into the jinja2 environment

Expand source code Browse git
def load_jinja_template(self) -> jinja2.Template:
    """
    Uses the fileloader to load a local template file into the jinja2 environment
    """
    loader = jinja2.FileSystemLoader(self.template_paths)
    env = jinja2.Environment(loader=loader)
    env.filters['htmlize'] = htmlize
    return env.get_template(self.get_template_name())
def check_for_file(self)

Checks whether __template_file__ exists locally

Expand source code Browse git
def check_for_file(self):
    """
    Checks whether `__template_file__` exists locally
    """
    for _dir in self.template_paths:
        fpath = os.path.join(_dir, self.get_template_name())
        if os.path.exists(fpath):
            return True
    return False

Inherited members

class WidgetBase (name, *args, **kwargs)

Inherit from this base class to create widget components that can be rendered to html via the Renderer class. This component also needs to build a method that allows the widget to be serialzed into a Widget object.

Parameters

widget : Widget
A Widget object use for initialized the component class.

Attributes

outline_widget : Widget
A Widget object
Expand source code Browse git
class WidgetBase(ComponentBase):
    """
    Inherit from this base class to create widget components that can be rendered to html via the
    `presalytics.story.components.Renderer` class.  This component also needs to build a method
    that allows the widget to be serialzed into a `presalytics.story.outline.Widget` object.

    Parameters
    ----------
    widget: Widget
        A `presalytics.story.outline.Widget` object use for initialized the component class.

    Attributes
    ----------
    outline_widget: Widget
        A `presalytics.story.outline.Widget` object
    """
    outline_widget: typing.Optional['Widget']

    __component_type__ = 'widget'

    def __init__(self, name, *args, **kwargs) -> None:
        super(WidgetBase, self).__init__(*args, **kwargs)
        self.name = name
        self.outline_widget = None

    def render(self, component, **kwargs):
        self.to_html(component, **kwargs)

    @abc.abstractmethod
    def to_html(self, data: typing.Dict = None, **kwargs) -> str:
        """
        Returns valid html that renders the widget in a browser.

        Parameters
        ----------
        data: dict
            The data parameter is a dictionary should contain the minimum amount of that is required to
            successfully render the object.  As the widget is update, data control how the display of
            information changes.

        **kwargs:
            Optional keyword arguments can be used in subclass to modify the behavior of the `to_html` function.
            these keyword arguments should be invariant through successive updates to the chart.  For example,
            keyword arguments could control the styling of the widget, which should not change as the data in
            the object (e.g., a chart) is updated.  Keyword arguments are loaded via `additional_properties`
            parameter in in the `presalytics.story.outline.Widget` object.

        Returns
        ----------
        A str of containing an html fragment that will be loaded into a template in successive operations
        """
        raise NotImplementedError

    @abc.abstractmethod
    def serialize(self, **kwargs) -> 'Widget':
        """
        Creates `presalytics.story.outline.Widget` object from instance data. This widget should
        have the correct `name`, `data` and `additional_properties` so the same widget can be reconstituted
        via the to_html method, given the same set of data.

        Typically, this method will call an update method that run a local script with updates this
        Widget's data Dictionary prior being loading into the Widget outline object for serialization.

        Parameters
        ----------
        **kwargs:
            Optional keyword arguments can be used in subclass to modify the behavior of the to_html function.
            these keyword arguments should be be invariant through successive updates to the chart. Overrides
            for this widgets default additional_properties should be loaded via these keyword arguments.

        """
        raise NotImplementedError

    @classmethod
    def deserialize(cls, component: 'Widget', **kwargs) -> 'WidgetBase':
        """
        Creates an instance of the widget from the data object in the `presalytics.story.outline.Widget`
        object. This method exists to ensure widgets can be portable across environments.  To clarify,
        widgets built on the client-side via the `__init__` method can be reconstructed server-side via
        the deserialize method.  This allows decoupling of the widget generation/updating of data and
        the rendering of the widget in a UI.  Renderers (e.g., `presalytics.story.revealer.Revealer` object)
        need not know about how the data get updated, but can update the graphic with data generated by
        the widget when the serialize method is called.

        Parameters
        ----------

        widget: Widget
            A `presalytics.story.outline.Widget` object

        Returns
        ----------
        `presalytics.story.components.WidgetBase` subclass instance
        """
        raise NotImplementedError

Ancestors

Subclasses

Static methods

def deserialize(component, **kwargs)

Creates an instance of the widget from the data object in the Widget object. This method exists to ensure widgets can be portable across environments. To clarify, widgets built on the client-side via the __init__ method can be reconstructed server-side via the deserialize method. This allows decoupling of the widget generation/updating of data and the rendering of the widget in a UI. Renderers (e.g., Revealer object) need not know about how the data get updated, but can update the graphic with data generated by the widget when the serialize method is called.

Parameters

widget : Widget
A Widget object

Returns

WidgetBase subclass instance

Expand source code Browse git
@classmethod
def deserialize(cls, component: 'Widget', **kwargs) -> 'WidgetBase':
    """
    Creates an instance of the widget from the data object in the `presalytics.story.outline.Widget`
    object. This method exists to ensure widgets can be portable across environments.  To clarify,
    widgets built on the client-side via the `__init__` method can be reconstructed server-side via
    the deserialize method.  This allows decoupling of the widget generation/updating of data and
    the rendering of the widget in a UI.  Renderers (e.g., `presalytics.story.revealer.Revealer` object)
    need not know about how the data get updated, but can update the graphic with data generated by
    the widget when the serialize method is called.

    Parameters
    ----------

    widget: Widget
        A `presalytics.story.outline.Widget` object

    Returns
    ----------
    `presalytics.story.components.WidgetBase` subclass instance
    """
    raise NotImplementedError

Methods

def to_html(self, data=None, **kwargs)

Returns valid html that renders the widget in a browser.

Parameters

data : dict
The data parameter is a dictionary should contain the minimum amount of that is required to successfully render the object. As the widget is update, data control how the display of information changes.

**kwargs: Optional keyword arguments can be used in subclass to modify the behavior of the to_html function. these keyword arguments should be invariant through successive updates to the chart. For example, keyword arguments could control the styling of the widget, which should not change as the data in the object (e.g., a chart) is updated. Keyword arguments are loaded via additional_properties parameter in in the Widget object.

Returns

A str of containing an html fragment that will be loaded into a template in successive operations
 
Expand source code Browse git
@abc.abstractmethod
def to_html(self, data: typing.Dict = None, **kwargs) -> str:
    """
    Returns valid html that renders the widget in a browser.

    Parameters
    ----------
    data: dict
        The data parameter is a dictionary should contain the minimum amount of that is required to
        successfully render the object.  As the widget is update, data control how the display of
        information changes.

    **kwargs:
        Optional keyword arguments can be used in subclass to modify the behavior of the `to_html` function.
        these keyword arguments should be invariant through successive updates to the chart.  For example,
        keyword arguments could control the styling of the widget, which should not change as the data in
        the object (e.g., a chart) is updated.  Keyword arguments are loaded via `additional_properties`
        parameter in in the `presalytics.story.outline.Widget` object.

    Returns
    ----------
    A str of containing an html fragment that will be loaded into a template in successive operations
    """
    raise NotImplementedError
def serialize(self, **kwargs)

Creates Widget object from instance data. This widget should have the correct name, data and additional_properties so the same widget can be reconstituted via the to_html method, given the same set of data.

Typically, this method will call an update method that run a local script with updates this Widget's data Dictionary prior being loading into the Widget outline object for serialization.

Parameters

**kwargs: Optional keyword arguments can be used in subclass to modify the behavior of the to_html function. these keyword arguments should be be invariant through successive updates to the chart. Overrides for this widgets default additional_properties should be loaded via these keyword arguments.

Expand source code Browse git
@abc.abstractmethod
def serialize(self, **kwargs) -> 'Widget':
    """
    Creates `presalytics.story.outline.Widget` object from instance data. This widget should
    have the correct `name`, `data` and `additional_properties` so the same widget can be reconstituted
    via the to_html method, given the same set of data.

    Typically, this method will call an update method that run a local script with updates this
    Widget's data Dictionary prior being loading into the Widget outline object for serialization.

    Parameters
    ----------
    **kwargs:
        Optional keyword arguments can be used in subclass to modify the behavior of the to_html function.
        these keyword arguments should be be invariant through successive updates to the chart. Overrides
        for this widgets default additional_properties should be loaded via these keyword arguments.

    """
    raise NotImplementedError

Inherited members

class PageTemplateBase (page, **kwargs)

Inherit from this base class to render templates to html via the Revealer class.

Parameters

page : Page
A presalytics.story.outline.Page object for instalizing the class

Attributes

outline_page : Page
The page data
widgets : list of subclasses of WidgetBase
A list widget that will be loaded into templates and rendered via placeholders. These widgets must have a "to_html(self, data, **kwargs)" method.
Expand source code Browse git
class PageTemplateBase(ComponentBase):
    """
    Inherit from this base class to render templates to html via the
    `presalytics.story.revealer.Revealer` class.

    Parameters
    ----------
    page: Page
        A presalytics.story.outline.Page object for instalizing the class

    Attributes
    ----------
    outline_page: presalytics.story.outline.Page
        The page data

    widgets: list of subclasses of presalytics.story.components.WidgetBase
        A list widget that will be loaded into templates and rendered via placeholders.
        These widgets must have a "to_html(self, data, **kwargs)" method.

    """

    outline_page: 'Page'
    widgets: typing.List['WidgetBase']
    plugins: typing.List[typing.Dict]

    __component_type__ = 'page'

    def __init__(self, page: 'Page', **kwargs) -> None:
        super(PageTemplateBase, self).__init__(**kwargs)
        self.outline_page = page
        self.widgets = self.get_page_widgets(self.outline_page)
        

    @abc.abstractmethod
    def render(self, **kwargs) -> str:
        """
        Returns valid html that renders the template in a broswer with data loaded from widgets.
        """
        raise NotImplementedError

    def load_widget(self, widget: 'Widget'):
        """
        Converts a presalytics.story.outline.Widget object to a subclass of WidgetComponentBase
        via the `presalytics.COMPONENTS` registry.
        """
        class_key = "widget." + widget.kind
        key = class_key + "." + widget.name
        try:
            if presalytics.COMPONENTS.get_instance(key):
                widget_instance = presalytics.COMPONENTS.get_instance(key)
            else:
                klass = presalytics.COMPONENTS.get(class_key)
                deserialize_method = getattr(klass, "deserialize", None)
                if callable(deserialize_method):
                    widget_instance = deserialize_method(widget, client_info=self.client_info)
                else:
                    message = "Widget component instance or class (kind) {0} unavailable in component registry".format(key)
                    raise presalytics.lib.exceptions.MissingConfigException(message)
        except Exception as ex:
            logger.exception(ex)
            if not presalytics.CONFIG.get("DEBUG", False):
                widget_instance = presalytics.lib.exceptions.RenderExceptionHandler(ex)
            else:
                t, v, tb = sys.exc_info()
                six.reraise(t, v, tb)
        return widget_instance

    def get_page_widgets(self, page: 'Page'):
        """
        Converts the widgets within a `presalytics.story.outline.Page` object to a list
        of widgets subclassed from `presalytics.story.components.WidgetBase`
        """
        widget_instances = []
        for widget_outline in page.widgets:
            next_widget = self.load_widget(widget_outline)
            widget_instances.append(next_widget)
        return widget_instances
    
    def serialize(self):
        return self.outline_page.dump()

    @classmethod
    def deseriailize(cls, component, **kwargs):
        return cls(page=component)

Ancestors

Subclasses

Static methods

def deseriailize(component, **kwargs)
Expand source code Browse git
@classmethod
def deseriailize(cls, component, **kwargs):
    return cls(page=component)

Methods

def render(self, **kwargs)

Returns valid html that renders the template in a broswer with data loaded from widgets.

Expand source code Browse git
@abc.abstractmethod
def render(self, **kwargs) -> str:
    """
    Returns valid html that renders the template in a broswer with data loaded from widgets.
    """
    raise NotImplementedError
def load_widget(self, widget)

Converts a presalytics.story.outline.Widget object to a subclass of WidgetComponentBase via the COMPONENTS registry.

Expand source code Browse git
def load_widget(self, widget: 'Widget'):
    """
    Converts a presalytics.story.outline.Widget object to a subclass of WidgetComponentBase
    via the `presalytics.COMPONENTS` registry.
    """
    class_key = "widget." + widget.kind
    key = class_key + "." + widget.name
    try:
        if presalytics.COMPONENTS.get_instance(key):
            widget_instance = presalytics.COMPONENTS.get_instance(key)
        else:
            klass = presalytics.COMPONENTS.get(class_key)
            deserialize_method = getattr(klass, "deserialize", None)
            if callable(deserialize_method):
                widget_instance = deserialize_method(widget, client_info=self.client_info)
            else:
                message = "Widget component instance or class (kind) {0} unavailable in component registry".format(key)
                raise presalytics.lib.exceptions.MissingConfigException(message)
    except Exception as ex:
        logger.exception(ex)
        if not presalytics.CONFIG.get("DEBUG", False):
            widget_instance = presalytics.lib.exceptions.RenderExceptionHandler(ex)
        else:
            t, v, tb = sys.exc_info()
            six.reraise(t, v, tb)
    return widget_instance
def get_page_widgets(self, page)

Converts the widgets within a Page object to a list of widgets subclassed from WidgetBase

Expand source code Browse git
def get_page_widgets(self, page: 'Page'):
    """
    Converts the widgets within a `presalytics.story.outline.Page` object to a list
    of widgets subclassed from `presalytics.story.components.WidgetBase`
    """
    widget_instances = []
    for widget_outline in page.widgets:
        next_widget = self.load_widget(widget_outline)
        widget_instances.append(next_widget)
    return widget_instances

Inherited members

class ScssPlugin (**kwargs)

Compiles scss from files and a dict of variables and loads them into a script tag

*Note: Any <script> tags nested into the resultant style tag will be removed at render-time by a Renderer. These script tags will not make it to the browser.

Configuration Values

filenames : list of str
A list of scss filenames that will get compiled to css if all files are present
rendered_css : str, optional
If the files are not present, this string will be placed in the resultant style tag
variables : dict, optional
Scss variables to include. Dictionary keys should be prefixed with a $ to indicate that they are scss variable names.
Expand source code Browse git
class ScssPlugin(presalytics.lib.plugins.base.StylePlugin):
    """
    Compiles scss from files and a `dict` of variables and loads
    them into a script tag

    *Note: Any `<script>` tags nested into the resultant style tag will
    be removed at render-time by a `presalytics.story.components.Renderer`.
    These script tags will not make it to the browser.

    Configuration Values
    ----------
    filenames : list of str
        A list of scss filenames that will get compiled to css if
        all files are present

    rendered_css : str, optional
        If the files are not present, this string will be placed in the
        resultant style tag

    variables : dict, optional
        Scss variables to include. Dictionary keys should be prefixed with a
        `$` to indicate that they are scss variable names.  
    """
    __plugin_name__ = "scss_files"

    def __init__(self, **kwargs):
        super(ScssPlugin, self).__init__(**kwargs)

    def _files_are_local(self, filenames):
        for fname in filenames:
            if not os.path.exists(fname):
                return False
        return True

    def _create_variables_string(self, variables):
        _vars = ""
        for key, val in variables.items():
            _vars = _vars + key + ": " + val + "; "
        return _vars

    def _load_files_to_string(self, filenames):
        scss_string = ""
        for fname in filenames:
            with open(fname, 'r') as f:
                addl_scss = f.read()
            scss_string = scss_string + addl_scss
        return scss_string

    def make_css(self, filenames: typing.List[str], variables: typing.Dict[str, str]):
        """
        Compiles the scss in filenames to an html fragment using variables 
        
        Returns
        ----------
        A `<style>` tag html fragment in a string
        """
        _scss = self._create_variables_string(variables) + self._load_files_to_string(filenames)
        return sass.compile(string=_scss)

    def to_style(self, config, **kwargs):
        """
        Renders a config containing `Configuration Values` to a style tag
        """
        filenames = config.get("filenames", None)
        if not filenames:
            raise presalytics.lib.exceptions.MissingConfigException("Filenames are required in the config.")
        variables = config.get("variables", {})
        if self._files_are_local(filenames):
            rendered_css = self.make_css(filenames, variables)
        elif config.get("rendered_css", None):
            rendered_css = config.get("rendered_css")
        else:
            raise presalytics.lib.exceptions.MissingConfigException("A file referenced by this plugin could not be found.")
        return "<style>{0}</style>".format(rendered_css)


    @classmethod
    def configure(cls, filenames: typing.List[str], variables: typing.Dict = {}, old_css: str = None) -> typing.Dict:
        """
        Call this method when serializing a subclass of `presalytics.story.components.ComponentBase` 
        to generate a configuration for the plugin with a valid `rendered_css` entry
        """
        inst = cls()
        if not filenames:
            raise presalytics.lib.exceptions.MissingConfigException("Filenames are required in the config.")
        if inst._files_are_local(filenames):
            rendered_css = inst.make_css(filenames, variables)
        elif old_css:
            rendered_css = old_css
        else:
            raise presalytics.lib.exceptions.MissingConfigException("A file referenced by this plugin could not be found.")
        ret = {
            "name": inst.__plugin_name__,
            "kind": inst.__plugin_kind__,
            "config": {
                "filenames": filenames,
                "variables": variables,
                "rendered_css": rendered_css
            }
        }
        return ret

Ancestors

Static methods

def configure(filenames, variables={}, old_css=None)

Call this method when serializing a subclass of ComponentBase to generate a configuration for the plugin with a valid rendered_css entry

Expand source code Browse git
@classmethod
def configure(cls, filenames: typing.List[str], variables: typing.Dict = {}, old_css: str = None) -> typing.Dict:
    """
    Call this method when serializing a subclass of `presalytics.story.components.ComponentBase` 
    to generate a configuration for the plugin with a valid `rendered_css` entry
    """
    inst = cls()
    if not filenames:
        raise presalytics.lib.exceptions.MissingConfigException("Filenames are required in the config.")
    if inst._files_are_local(filenames):
        rendered_css = inst.make_css(filenames, variables)
    elif old_css:
        rendered_css = old_css
    else:
        raise presalytics.lib.exceptions.MissingConfigException("A file referenced by this plugin could not be found.")
    ret = {
        "name": inst.__plugin_name__,
        "kind": inst.__plugin_kind__,
        "config": {
            "filenames": filenames,
            "variables": variables,
            "rendered_css": rendered_css
        }
    }
    return ret

Methods

def make_css(self, filenames, variables)

Compiles the scss in filenames to an html fragment using variables

Returns

A <style> tag html fragment in a string

Expand source code Browse git
def make_css(self, filenames: typing.List[str], variables: typing.Dict[str, str]):
    """
    Compiles the scss in filenames to an html fragment using variables 
    
    Returns
    ----------
    A `<style>` tag html fragment in a string
    """
    _scss = self._create_variables_string(variables) + self._load_files_to_string(filenames)
    return sass.compile(string=_scss)
def to_style(self, config, **kwargs)

Renders a config containing Configuration Values to a style tag

Expand source code Browse git
def to_style(self, config, **kwargs):
    """
    Renders a config containing `Configuration Values` to a style tag
    """
    filenames = config.get("filenames", None)
    if not filenames:
        raise presalytics.lib.exceptions.MissingConfigException("Filenames are required in the config.")
    variables = config.get("variables", {})
    if self._files_are_local(filenames):
        rendered_css = self.make_css(filenames, variables)
    elif config.get("rendered_css", None):
        rendered_css = config.get("rendered_css")
    else:
        raise presalytics.lib.exceptions.MissingConfigException("A file referenced by this plugin could not be found.")
    return "<style>{0}</style>".format(rendered_css)

Inherited members