diff --git a/.gitignore b/.gitignore index cf72631..62453c5 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ venv/ dist/ docs/_build/ *.egg-info/ +.DS_Store diff --git a/docs/images/cards_sample.png b/docs/images/cards_sample.png new file mode 100644 index 0000000..66eb05b Binary files /dev/null and b/docs/images/cards_sample.png differ diff --git a/docs/index.rst b/docs/index.rst index e10232e..517a02a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -33,6 +33,7 @@ The User Guide user/intro user/quickstart user/api + user/cards The Development Community diff --git a/docs/user/api.rst b/docs/user/api.rst index 44c45f1..ff4a7e8 100644 --- a/docs/user/api.rst +++ b/docs/user/api.rst @@ -281,5 +281,93 @@ Warnings :show-inheritance: :members: +.. _CardsAPI: + +Cards and Buttons +================= + +.. autoclass:: webexteamssdk.cards.card.AdaptiveCard() + +Components +---------- + +.. autoclass:: webexteamssdk.cards.components.Image() + :members: + + .. automethod:: __init__ + +.. autoclass:: webexteamssdk.cards.components.TextBlock() + :members: + + .. automethod:: __init__ + +.. autoclass:: webexteamssdk.cards.components.Column() + +.. autoclass:: webexteamssdk.cards.components.Fact() + +.. autoclass:: webexteamssdk.cards.components.Choice() + +Options +------- + +.. autoclass:: webexteamssdk.cards.options.VerticalContentAlignment() + +.. autoclass:: webexteamssdk.cards.options.Colors() + +.. autoclass:: webexteamssdk.cards.options.HorizontalAlignment() + +.. autoclass:: webexteamssdk.cards.options.FontSize() + +.. autoclass:: webexteamssdk.cards.options.FontWeight() + +.. autoclass:: webexteamssdk.cards.options.BlockElementHeight() + +.. autoclass:: webexteamssdk.cards.options.Spacing() + +.. autoclass:: webexteamssdk.cards.options.ImageSize() + +.. autoclass:: webexteamssdk.cards.options.ImageStyle() + +.. autoclass:: webexteamssdk.cards.options.ContainerStyle() + +.. autoclass:: webexteamssdk.cards.options.TextInputStyle() + +.. autoclass:: webexteamssdk.cards.options.ChoiceInputStyle() + + +Container +--------- + +.. autoclass:: webexteamssdk.cards.container.Container() + +.. autoclass:: webexteamssdk.cards.container.ColumnSet() + +.. autoclass:: webexteamssdk.cards.container.FactSet() + +.. autoclass:: webexteamssdk.cards.container.ImageSet() + +Inputs +------ + +.. autoclass:: webexteamssdk.cards.inputs.Text() + +.. autoclass:: webexteamssdk.cards.inputs.Number() + +.. autoclass:: webexteamssdk.cards.inputs.Date() + +.. autoclass:: webexteamssdk.cards.inputs.Time() + +.. autoclass:: webexteamssdk.cards.inputs.Toggle() + +.. autoclass:: webexteamssdk.cards.inputs.Choices() + +Actions +------- + +.. autoclass:: webexteamssdk.cards.actions.OpenUrl + +.. autoclass:: webexteamssdk.cards.actions.Submit + +.. autoclass:: webexteamssdk.cards.actions.ShowCard *Copyright (c) 2016-2019 Cisco and/or its affiliates.* diff --git a/docs/user/cards.rst b/docs/user/cards.rst new file mode 100644 index 0000000..9c7a369 --- /dev/null +++ b/docs/user/cards.rst @@ -0,0 +1,39 @@ +.. _Cards: + +================= +Cards and Buttons +================= + +Webex Teams supports `AdaptiveCards `_ to allow +new levels of interactivity for bots and integrations. You can read more about +how cards and buttons work `in the official guide `_. + +In this guide I want to cover the abstraction build into the webexteamssdk that +lets you author adaptive cards in pure python without having to touch the +underlying json of a adaptive card. + +Lets dive into a simple example that sends a card to a room + +.. code-block:: python + + from webexteamssdk import WebexTeamsAPI + from webexteamssdk.cards.card import AdaptiveCard + from webexteamssdk.cards.inputs import Text, Number + from webexteamssdk.cards.components import TextBlock + from webexteamssdk.cards.actions import Submit + + greeting = TextBlock("Hey hello there! I am a adaptive card") + first_name = Text('first_name', placeholder="First Name") + age = Number('age', placeholder="Age") + + submit = Submit(title="Send me!") + + card = AdaptiveCard(body=[greeting, first_name, age], actions=[submit]) + + api = WebexTeamsAPI() + api.messages.create(text="fallback", roomId="...", cards=card) + +The message we send with this code then looks like this in our Webex Teams +client: + +.. image:: ../images/cards_sample.png diff --git a/webexteamssdk/api/messages.py b/webexteamssdk/api/messages.py index 37c6a7a..49eeac2 100644 --- a/webexteamssdk/api/messages.py +++ b/webexteamssdk/api/messages.py @@ -39,8 +39,9 @@ from ..restsession import RestSession from ..utils import ( check_type, dict_from_items_with_values, is_local_file, is_web_url, - open_local_file, + open_local_file, make_card_attachment ) +from ..cards.card import AdaptiveCard API_ENDPOINT = 'messages' @@ -208,6 +209,17 @@ def create(self, roomId=None, toPersonId=None, toPersonEmail=None, attachments=attachments, ) + # Add cards + if cards is not None: + cards_list = [] + + if isinstance(cards, list): + cards_list = [make_card_attachment(c) for c in cards] + else: + cards_list = [make_card_attachment(cards)] + + post_data['attachments'] = cards_list + # API request if not files or is_web_url(files[0]): # Standard JSON post diff --git a/webexteamssdk/cards/__init__.py b/webexteamssdk/cards/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/webexteamssdk/cards/abstract_components.py b/webexteamssdk/cards/abstract_components.py new file mode 100644 index 0000000..90906c5 --- /dev/null +++ b/webexteamssdk/cards/abstract_components.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- +"""Webex Teams Access-Tokens API wrapper. + +Copyright (c) 2016-2019 Cisco and/or its affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import json + +class Serializable: + """Parent class for all components of adaptive cards. + + Each component should inherit from this class and then specify, from + its properties, which fall into the following two categories: + + * Simple properties are text properties like "type" or "id" + * Serializable properties are properties that can themselfes be serilized. + This includes lists of items (i.e. the 'body' field of the adaptive card) or + single objects that also inherit from Serializable + """ + def __init__(self, serializable_properties, simple_properties): + """Creates a serializable object. + + See class docstring for an explanation what the different types of + properties are. + + Args: + serializable_properties(list): List of all serializable properties + simple_properties(list): List of all simple properties. + """ + self.serializable_properties = serializable_properties + self.simple_properties = simple_properties + + def to_json(self, pretty=False): + """Create json from a serializable component + + This function is used to render the json from a component. While all + components do support this operation it is mainly used on the + AdaptiveCard to generate the json for the attachment. + + Args: + pretty(boolean): If true, the returned json will be sorted by keys + and indented with 4 spaces to make it more human-readable + + Returns: + A Json representation of this component + """ + ret = None + if pretty: + ret = json.dumps(self.to_dict(), indent=4, sort_keys=True) + else: + ret = json.dumps(self.to_dict()) + + return ret + + def to_dict(self): + """Export a dictionary representation of this card/component by + parsing all simple and serializable properties. + + A simple_component is a single-text property of the exported card + (i.e. {'version': "1.2"}) while a serializable property is another + subcomponent that also implements a to_dict() method. + + The to_dict() method is used to recursively create a dict representation + of the adaptive card. This dictionary representation can then be + converted into json for usage with the API. + + Returns: + dict: Dictionary representation of this component. + """ + export = {} + + # Export simple properties (i.e. properties that are only single text) + for sp in self.simple_properties: + o = getattr(self, sp, None) + + if o is not None: + export[sp] = str(o) + + # Export all complex properties by calling its respective serialization + for cp in self.serializable_properties: + o = getattr(self, cp, None) + + if o is not None: + # Check if it is a list or a single component + l = [] + if isinstance(o, list): + for i in o: + l.append(i.to_dict()) + else: + l.append(o.to_dict()) + export[cp] = l + + return export diff --git a/webexteamssdk/cards/actions.py b/webexteamssdk/cards/actions.py new file mode 100644 index 0000000..7ef4e35 --- /dev/null +++ b/webexteamssdk/cards/actions.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +"""Webex Teams Access-Tokens API wrapper. + +Copyright (c) 2016-2019 Cisco and/or its affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +from .abstract_components import Serializable + +class OpenUrl(Serializable): + def __init__(self, url, title=None, + iconURL=None): + self.type = "Action.OpenUrl" + self.title = title + self.iconURL = iconURL + + super().__init__(serializable_properties=[], + simple_properties=['type', 'title', 'iconURL']) + +class Submit(Serializable): + def __init__(self, data=None, + title=None, + iconURL=None, + ): + self.type = "Action.Submit" + self.data = data + self.title = title + self.iconURL = iconURL + + super().__init__(serializable_properties=['data'], + simple_properties=['title', 'iconURL', 'type']) + +class ShowCard(Serializable): + def __init__(self, card=None, + title=None, + iconURL=None): + self.type = "Action.ShowCard" + self.card = card + self.title = title + self.iconURL = iconURL + + super().__init__(serializable_properties=['card'], + simple_properties=['title', 'type', 'iconURL']) diff --git a/webexteamssdk/cards/card.py b/webexteamssdk/cards/card.py new file mode 100644 index 0000000..512e42b --- /dev/null +++ b/webexteamssdk/cards/card.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +"""Webex Teams Access-Tokens API wrapper. + +Copyright (c) 2016-2019 Cisco and/or its affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +from .abstract_components import Serializable +from .actions import OpenUrl, ShowCard, Submit + +from .utils import check_type + +class AdaptiveCard(Serializable): + """AdaptiveCard class that represents a adaptive card python object. + + Note: + Webex Teams currently supports version 1.1 of adaptive cards and thus + only features from that release are supported in this abstraction. + """ + def __init__(self, body=None, + actions=None, + selectAction=None, + fallbackText=None, + lang=None): + """Creates a new adaptive card object. + + Args: + body(list): The list of components and containers making up the + body of this adaptive card. + actions(list): The list of actions this adaptive card should contain + selectAction(action): The action that should be invoked when this + adaptive card is selected. Can be any action other then + 'ShowCard' + fallbackText(str): The text that should be displayed on clients that + can't render adaptive cards + lang(str): The 2-letter ISO-639-1 language used in the card. This is + used for localization of date/time functions + + """ + # Check types + check_type(actions, (ShowCard, Submit, OpenUrl), True, True) + check_type(selectAction, (Submit, OpenUrl), False, True) + check_type(fallbackText, str, False, True) + check_type(lang, str, False, True) + + # Set properties + self.type = "AdaptiveCard" + self.version = "1.1" # This is the version currently supported in Teams + self.body = body + self.actions = actions + self.selectAction = selectAction + self.fallbackText = fallbackText + self.lang = lang + + super().__init__(serializable_properties=[ + 'body', 'actions', 'selectAction' + ], + simple_properties=[ + 'version', 'fallbackText', 'lang', 'type' + ]) + def to_dict(self): + # We need to overwrite the to_dict method to add the $schema + # property that can't be specified the normal way due to the + # $ in the beginning + ret = super().to_dict() + ret["$schema"] = "http://adaptivecards.io/schemas/adaptive-card.json" + + return ret diff --git a/webexteamssdk/cards/components.py b/webexteamssdk/cards/components.py new file mode 100644 index 0000000..669ae84 --- /dev/null +++ b/webexteamssdk/cards/components.py @@ -0,0 +1,239 @@ +# -*- coding: utf-8 -*- +"""Webex Teams Access-Tokens API wrapper. + +Copyright (c) 2016-2019 Cisco and/or its affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +from .abstract_components import Serializable +from .utils import check_type +from .options import BlockElementHeight, Spacing, ImageSize, ImageStyle +from .actions import OpenUrl, ShowCard, Submit + +class MediaSource(Serializable): + """Defines the source of a Media element.""" + def __init__(self, + mimeType, + url): + """Create a new MediaSource component. + + Args: + mimeType(str): Mime type of the associated media(i.e. 'video/mp4') + url(str): URL of the media. + """ + # Check types + check_type(mimeType, str, False, False) + check_type(url, str, False, False) + + self.mimeType = mimeType + self.url = url + + super().__init__(serializable_properties=[], + simple_properties=['mimeType', 'url']) + +class Media(Serializable): + """Displays a media player for audio or video content""" + def __init__(self, + sources, + poster=None, + altText=None, + height=None, + separator=None, + spacing=None, + id=None): + """Create a new Media component. + + Args: + sources(list): A list of media sources to be played + poster(str): The url to the image that is displayed before playing + altText(str): Alternative text for this component + height(BlockElementHeight): The height of this block element + separator(bool): Draw a separating line when set to true + spacing(Spacing): Specify the spacing of this component + id(str): The id of this component + """ + # Check types + check_type(sources, MediaSource, True, False) + check_type(poster, str, False, True) + check_type(altText, str, False, True) + check_type(height, BlockElementHeight, False, True) + check_type(separator, bool, False, True) + check_type(spacing, Spacing, False, True) + check_type(id, str, False, True) + + self.type = "Media" + self.sources = sources #Needs to be a list of media sources + self.poster = poster + self.altText = altText + self.height = height + self.separator = separator + self.spacing = spacing + self.id = id + + super().__init__(serializable_properties=['sources'], + simple_properties=[ + 'type', 'poster', 'altText', 'height', + 'separator', 'spacing', 'id' + ]) +class Image(Serializable): + """Displays a image object""" + + def __init__(self, + url, + altText=None, + backgroundColor=None, + height=None, + horizontalAlignment=None, + selectAction=None, + size=None, + style=None, + width=None, + separator=None, + spacing=None, + id=None): + """Create a new image component + + Args: + url(str): The URL to the image + altText(str): Alternative text describing the image + backgroundColor(str): Background color for transparent images. + height(str, BlockElementHeight): Height of the image either as a + pixel value(i.e. '50px') or as an instance of BlockElementHeight + horizontalAlignmnet(HorizontalAlignment): Controls how the component + is positioned within its parent. + selectAction(OpenUrl, Submit): Option that is caried out when the + card is selected. + size(ImageSize): Controls the approximate size of the image. + style(ImageStyle): The display style of this image. + width(str): Width of the image as a pixel value (i.e. '50px') + separator(bool): Draw a separating line when set to true + spacing(Spacing): Specify the spacing of this component + id(str): The id of this component + + """ + check_type(url, str, False, False) + check_type(altText, str, False, True) + check_type(backgroundColor, str, False, True) + check_type(height, (str, BlockElementHeight), False, True) + check_type(horizontalAlignment, horizontalAlignment, False, True) + check_type(selectAction, (OpenUrl, Submit), False, True) + check_type(size, ImageSize, False, True) + check_type(style, ImageStyle, False, True) + check_type(width, str, False, True) + check_type(separator, bool, False, True) + check_type(spacing, Spacing, False, True) + check_type(id, str, False, True) + + self.type = "Image" + self.url = url + self.altText = altText + self.backgroundColor = backgroundColor + self.height = height + self.horizontalAlignment = horizontalAlignment + self.selectAction = selectAction + self.size = size + self.style = style + self.width = width + self.separator = separator + self.spacing = spacing + self.id = id + + super().__init__(serializable_properties=[], + simple_properties=[ + 'type', 'url', 'altText', 'backgroundColor', + 'height', 'horizontalAlignment', 'selectAction', + 'size', 'style', 'width', 'separator', 'spacing', + 'id' + ]) +class TextBlock(Serializable): + def __init__(self, + text, + color=None, + horizontalAlignment=None, + isSubtle=None, + maxLines=None, + size=None, + weight=None, + wrap=None, + separator=None, + spacing=None, + id=None): + + + #ToDo(mneiding): Type check + self.type = "TextBlock" + self.text = text + self.color = color + self.horizontalAlignment = horizontalAlignment + self.isSubtle = isSubtle + self.maxLines = maxLines + self.size = size + self.weight = weight + self.wrap = wrap + self.separator = separator + self.spacing = spacing + self.id = id + + super().__init__(serializable_properties=[], + simple_properties=[ + 'type', 'text', 'color', 'horizontalAlignment', + 'isSubtle', 'maxLines', 'size', 'weight', 'wrap', + 'spacing', 'id', 'separator' + ]) +class Column(Serializable): + def __init__(self, items=None, + separator=None, + spacing=None, + selectAction=None, + style=None, + verticalContentAlignment=None, + width=None, + id=None): + self.type = "Column" + self.items = items + self.separator = separator + self.spacing = spacing + self.selectAction = selectAction + self.style = style + self.verticalContentAlignment = verticalContentAlignment + self.width = width + self.id = id + + super().__init__(serializable_properties=['items'], + simple_properties=[ + 'type', 'separator', 'spacing', 'selectAction', + 'style', 'verticalContentAlignment', 'width', 'id' + ]) + +class Fact(Serializable): + def __init__(self, title, value): + self.title = title + self.value = value + + super().__init__(serializable_properties=[], + simple_properties=['title', 'value']) + +class Choice(Serializable): + def __init__(self, title, value): + self.title = title + self.value = value + + super().__init__(serializable_properties=[], + simple_properties=['title', 'value']) diff --git a/webexteamssdk/cards/container.py b/webexteamssdk/cards/container.py new file mode 100644 index 0000000..b0f7cc0 --- /dev/null +++ b/webexteamssdk/cards/container.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +"""Webex Teams Access-Tokens API wrapper. + +Copyright (c) 2016-2019 Cisco and/or its affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +from .abstract_components import Serializable + +class Container(Serializable): + def __init__(self, items, selectAction=None, + style=None, + verticalContentAlignment=None, + height=None, + separator=None, + spacing=None, + id=None): + self.type = "Container" + self.items = items + self.selectAction = selectAction + self.style = style + self.verticalContentAlignment = verticalContentAlignment + self.height = height + self.separator = separator + self.spacing = spacing + self.id = id + + super().__init__(serializable_properties=['items'], + simple_properties=[ + 'selectAction', 'style', 'verticalContentAlignment', + 'height', 'separator', 'spacing', 'id', 'type' + ]) + +class ColumnSet(Serializable): + def __init__(self, columns=None, + selectAction=None, + height=None, + separator=None, + spacing=None, + id=None): + self.type = "ColumnSet" + self.columns = columns + self.selectAction = selectAction + self.height = height + self.separator = separator + self.spacing = spacing + self.id = id + + super().__init__(serializable_properties=['columns'], + simple_properties=[ + 'selectAction', 'height', 'separator', 'spacing', + 'id', 'type' + ]) + +class FactSet(Serializable): + def __init__(self, facts, height=None, + separator=None, + spacing=None, + id=None): + self.type = "FactSet" + self.facts = facts + self.height = height + self.separator = separator + self.spacing = spacing + self.id = id + + super().__init__(serializable_properties=['facts'], + simple_properties=[ + 'type', 'height', 'separator', 'id', 'spacing' + ]) + +class ImageSet(Serializable): + def __init__(self, images, imageSize=None, + height=None, + separator=None, + spacing=None, + id=None): + self.type = "ImageSet" + self.images = images + self.imageSize = imageSize + self.height = height + self.separator = separator + self.spacing = spacing + self.id = id + + super().__init__(serializable_properties=['images'], + simple_properties=[ + 'imageSize', 'height', 'separator', 'spacing', 'id', + 'type' + ]) diff --git a/webexteamssdk/cards/inputs.py b/webexteamssdk/cards/inputs.py new file mode 100644 index 0000000..f8060e3 --- /dev/null +++ b/webexteamssdk/cards/inputs.py @@ -0,0 +1,170 @@ +# -*- coding: utf-8 -*- +"""Webex Teams Access-Tokens API wrapper. + +Copyright (c) 2016-2019 Cisco and/or its affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +from .abstract_components import Serializable + +class Text(Serializable): + def __init__(self, id, isMultiline=None, + maxLength=None, + placeholder=None, + style=None, + value=None, + height=None, + separator=None, + spacing=None): + + self.type = "Input.Text" + self.id = id + self.isMultiline = isMultiline + self.maxLength = maxLength + self.placeholder = placeholder + self.style = style + self.value = value + self.height = height + self.separator = separator + self.spacing = spacing + + super().__init__(serializable_properties=[], + simple_properties=[ + 'id', 'type', 'isMultiline', 'maxLength', + 'placeholder', 'style', 'value', 'height', + 'separator', 'spacing' + ]) + +class Number(Serializable): + def __init__(self, id, max=None, + min=None, + placeholder=None, + value=None, + height=None, + separator=None, + spacing=None): + self.type = "Input.Number" + self.id = id + self.max = max + self.min = min + self.placeholder = placeholder + self.value = value + self.height = height + self.separator = separator + self.spacing = spacing + + super().__init__(serializable_properties=[], + simple_properties=[ + 'type', 'id', 'max', 'min', 'placeholder', 'value', + 'height', 'separator', 'spacing' + ]) + +class Date(Serializable): + def __init__(self, id, max=None, + min=None, + placeholder=None, + value=None, + height=None, + separator=None, + spacing=None): + self.type = "Input.Date" + self.id = id + self.max = max + self.min = min + self.placeholder = placeholder + self.value = value + self.height = height + self.separator = separator + self.spacing = spacing + + super().__init__(serializable_properties=[], + simple_properties=[ + 'type', 'id', 'max', 'min', 'placeholder', 'value', + 'height', 'separator', 'spacing' + ]) +class Time(Serializable): + def __init__(self, id, max=None, + min=None, + placeholder=None, + value=None, + height=None, + separator=None, + spacing=None): + self.id = id + self.type = "Input.Time" + self.max = max + self.min = min + self.placeholder = placeholder + self.value = value + self.height = height + self.separator = separator + self.spacing = spacing + + super().__init__(serializable_properties=[], + simple_properties=[ + 'id', 'type', 'max', 'min', 'placeholder', 'value', + 'height', 'separator', 'spacing' + ]) + +class Toggle(Serializable): + def __init__(self, title, id, value=None, + valueOff=None, + valueOn=None, + height=None, + separator=None, + spacing=None): + self.title = title + self.type = "Input.Toggle" + self.id = id + self.value = value + self.valueOff = valueOff + self.valueOn = valueOn + self.height = height + self.separator = separator + self.spacing = spacing + + super().__init__(serializable_properties=[], + simple_properties=[ + 'type', 'id', 'title', 'value', 'valueOff', + 'valueOn', 'height', 'separator', 'spacing' + ]) + +class Choices(Serializable): + def __init__(self, choices, id, isMultiSelect=None, + style=None, + value=None, + height=None, + separator=None, + spacing=None): + self.choices = choices + self.type = "Input.ChoiceSet" + self.id = id + self.isMultiSelect = isMultiSelect + self.style = style + self.value = value + self.height = height + self.separator = separator + self.spacing = spacing + + super().__init__(serializable_properties=['choices'], + simple_properties=[ + 'id', 'type', 'isMultiSelect', 'style', 'value', + 'height', 'separator', 'spacing' + ]) diff --git a/webexteamssdk/cards/options.py b/webexteamssdk/cards/options.py new file mode 100644 index 0000000..be6af3a --- /dev/null +++ b/webexteamssdk/cards/options.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +"""Webex Teams Access-Tokens API wrapper. + +Copyright (c) 2016-2019 Cisco and/or its affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +from enum import Enum + +class AbstractOption(Enum): + def to_value(self): + return str(self.name).lower() + + def __str__(self): + return self.to_value() + + def __repr__(self): + return self.to_value() + +class VerticalContentAlignment: + TOP = 1 + CENTER = 2 + BOTTOM = 3 + +class Colors(AbstractOption): + DEFAULT = 1 + DARK = 2 + LIGHT = 3 + ACCENT = 4 + GOOD = 5 + WARNING = 6 + ATTENTION = 7 + +class HorizontalAlignment(AbstractOption): + LEFT = 1 + CENTER = 2 + RIGHT = 3 + +class FontSize(AbstractOption): + DEFAULT = 1 + SMALL = 2 + MEDIUM = 3 + LARGE = 4 + EXTRALARGE = 5 + +class FontWeight(AbstractOption): + DEFAULT = 1 + LIGHTER = 2 + BOLDER = 3 + +class BlockElementHeight(AbstractOption): + AUTO = 1 + STRETCH = 2 + +class Spacing(AbstractOption): + DEFAULT = 1 + NONE = 2 + SMALL = 3 + MEDIUM = 4 + LARGE = 5 + EXTRALARGE = 6 + PADDING = 7 + +class ImageSize(AbstractOption): + AUTO = 1 + STRETCH = 2 + SMALL = 3 + MEDIUM = 4 + LARGE = 5 + +class ImageStyle(AbstractOption): + DEFAULT = 1 + PERSON = 2 + +class ContainerStyle(AbstractOption): + DEFAULT = 1 + EMPHASIS = 2 + +class TextInputStyle(AbstractOption): + TEXT = 1 + TEL = 2 + URL = 3 + EMAIL = 4 + +class ChoiceInputStyle(AbstractOption): + COMPACT = 1 + EXPANDED = 2 diff --git a/webexteamssdk/cards/utils.py b/webexteamssdk/cards/utils.py new file mode 100644 index 0000000..4e8cfab --- /dev/null +++ b/webexteamssdk/cards/utils.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +"""Webex Teams Access-Tokens API wrapper. + +Copyright (c) 2016-2019 Cisco and/or its affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +def set_if_not_none(property_name, property, export): + if property is not None: + export[property_name] = property.to_dict() + +def check_type(obj, acceptable_types, is_list=False, may_be_none=False): + """Object is an instance of one of the acceptable types or None. + + Args: + obj: The object to be inspected. + acceptable_types: A type or tuple of acceptable types. + is_list(bool): Whether or not we expect a list of objects of acceptable + type + may_be_none(bool): Whether or not the object may be None. + + Raises: + TypeError: If the object is None and may_be_none=False, or if the + object is not an instance of one of the acceptable types. + + """ + error_message = None + if not isinstance(acceptable_types, tuple): + acceptable_types = (acceptable_types,) + + if may_be_none and obj is None: + pass + elif is_list: + # Check that all objects in that list are of the required type + if not isinstance(obj, list): + error_message = ( + "We were expecting to receive a list of one of the following " + "types: {types}{none}; but instead we received {o} which is a " + "{o_type}.".format( + types=", ".join([repr(t.__name__) for t in acceptable_types]), + none="or 'None'" if may_be_none else "", + o=obj, + o_type=repr(type(obj).__name__) + ) + ) + else: + for o in obj: + if not isinstance(o, acceptable_types): + error_message = ( + "We were expecting to receive an instance of one of the following " + "types: {types}{none}; but instead we received {o} which is a " + "{o_type}.".format( + types=", ".join([repr(t.__name__) for t in acceptable_types]), + none="or 'None'" if may_be_none else "", + o=o, + o_type=repr(type(o).__name__) + ) + ) + elif isinstance(obj, acceptable_types): + pass + else: + # Object is something else. + error_message = ( + "We were expecting to receive an instance of one of the following " + "types: {types}{none}; but instead we received {o} which is a " + "{o_type}.".format( + types=", ".join([repr(t.__name__) for t in acceptable_types]), + none="or 'None'" if may_be_none else "", + o=obj, + o_type=repr(type(obj).__name__) + ) + ) + if error_message is not None: + raise TypeError(error_message) diff --git a/webexteamssdk/utils.py b/webexteamssdk/utils.py index 04773ee..809ebb4 100644 --- a/webexteamssdk/utils.py +++ b/webexteamssdk/utils.py @@ -257,6 +257,22 @@ def json_dict(json_data): "received: {!r}".format(json_data) ) +def make_card_attachment(card): + """Given a card, makes a card attachment by attaching the correct + content type and content. + + Args: + card(AdaptiveCard): Adaptive Card object that should be attached + + Returns: + A Python dictionary containing the card attachment dictionary + """ + ret = { + "contentType": "application/vnd.microsoft.card.adaptive", + "content": card.to_dict() + } + + return ret class ZuluTimeZone(tzinfo): """Zulu Time Zone."""