From 4f3c798bfc9e3e3d49589265f70b12f1f9b4ce22 Mon Sep 17 00:00:00 2001 From: "Neidinger, Marcel" Date: Sat, 26 Oct 2019 17:10:31 +0200 Subject: [PATCH 01/12] working version w/ all features from v1.1 --- webexteamssdk/cards/__init__.py | 0 webexteamssdk/cards/abstract_components.py | 62 +++++++++ webexteamssdk/cards/actions.py | 36 +++++ webexteamssdk/cards/card.py | 22 ++++ webexteamssdk/cards/components.py | 146 +++++++++++++++++++++ webexteamssdk/cards/container.py | 83 ++++++++++++ webexteamssdk/cards/inputs.py | 146 +++++++++++++++++++++ webexteamssdk/cards/options.py | 50 +++++++ webexteamssdk/cards/utils.py | 3 + 9 files changed, 548 insertions(+) create mode 100644 webexteamssdk/cards/__init__.py create mode 100644 webexteamssdk/cards/abstract_components.py create mode 100644 webexteamssdk/cards/actions.py create mode 100644 webexteamssdk/cards/card.py create mode 100644 webexteamssdk/cards/components.py create mode 100644 webexteamssdk/cards/container.py create mode 100644 webexteamssdk/cards/inputs.py create mode 100644 webexteamssdk/cards/options.py create mode 100644 webexteamssdk/cards/utils.py 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..3ce88f1 --- /dev/null +++ b/webexteamssdk/cards/abstract_components.py @@ -0,0 +1,62 @@ +from abc import ABC, abstractmethod +import json + +class Serializable: + """Parent class to + + """ + def __init__(self, serializable_properties, simple_properties): + self.serializable_properties = serializable_properties + self.simple_properties = simple_properties + + def to_json(self, pretty=False): + 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. + + 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 + +class Component: + def __init__(self, component_type): + self.component_type = component_type + + def get_type(self): + return self.component_type diff --git a/webexteamssdk/cards/actions.py b/webexteamssdk/cards/actions.py new file mode 100644 index 0000000..2ac5c54 --- /dev/null +++ b/webexteamssdk/cards/actions.py @@ -0,0 +1,36 @@ +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..945a734 --- /dev/null +++ b/webexteamssdk/cards/card.py @@ -0,0 +1,22 @@ +from .abstract_components import Serializable, Component + +class AdaptiveCard(Serializable): + def __init__(self, body=None, + actions=None, + selectAction=None, + style=None, + fallbackText=None, + lang=None): + super().__init__(serializable_properties=['body', 'actions', 'selectAction', 'style'], + simple_properties=['version', 'fallbackText', 'lang', 'schema', 'type']) + + # 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.style = style + self.fallbackText = fallbackText + self.lang = lang + self.schema = "http://adaptivecards.io/schemas/adaptive-card.json" diff --git a/webexteamssdk/cards/components.py b/webexteamssdk/cards/components.py new file mode 100644 index 0000000..4e16d46 --- /dev/null +++ b/webexteamssdk/cards/components.py @@ -0,0 +1,146 @@ +from .abstract_components import Serializable + +class MediaSource(Serializable): + def __init__(self, + mimeType, + url): + self.mimeType = mimeType + self.url = url + + super().__init__(serializable_properties=[], + simple_properties=['mimeType', 'url']) + +class Media(Serializable): + def __init__(self, + sources, + poster=None, + altText=None, + height=None, + separator=None, + spacing=None, + id=None): + 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): + def __init__(self, + url, + altText=None, + backgroundColor=None, + height=None, + horizontalAlignment=None, + selectAction=None, + size=None, + style=None, + width=None, + seperator=None, + spacing=None, + id=None): + + 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.seperator = seperator + 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..2f8eb1e --- /dev/null +++ b/webexteamssdk/cards/container.py @@ -0,0 +1,83 @@ +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..81180af --- /dev/null +++ b/webexteamssdk/cards/inputs.py @@ -0,0 +1,146 @@ +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..789945f --- /dev/null +++ b/webexteamssdk/cards/options.py @@ -0,0 +1,50 @@ +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 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 diff --git a/webexteamssdk/cards/utils.py b/webexteamssdk/cards/utils.py new file mode 100644 index 0000000..0bb07dd --- /dev/null +++ b/webexteamssdk/cards/utils.py @@ -0,0 +1,3 @@ +def set_if_not_none(property_name, property, export): + if property is not None: + export[property_name] = property.to_dict() From 59ddc6e87a16a53848885d93943b3e786283455e Mon Sep 17 00:00:00 2001 From: "Neidinger, Marcel" Date: Sat, 26 Oct 2019 17:32:23 +0200 Subject: [PATCH 02/12] added docstrings to abstract components and card --- webexteamssdk/cards/abstract_components.py | 42 +++++++++++++++++----- webexteamssdk/cards/card.py | 31 ++++++++++++++-- 2 files changed, 62 insertions(+), 11 deletions(-) diff --git a/webexteamssdk/cards/abstract_components.py b/webexteamssdk/cards/abstract_components.py index 3ce88f1..7bc6c02 100644 --- a/webexteamssdk/cards/abstract_components.py +++ b/webexteamssdk/cards/abstract_components.py @@ -2,14 +2,43 @@ import json class Serializable: - """Parent class to + """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) @@ -26,6 +55,10 @@ def to_dict(self): (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. """ @@ -53,10 +86,3 @@ def to_dict(self): export[cp] = l return export - -class Component: - def __init__(self, component_type): - self.component_type = component_type - - def get_type(self): - return self.component_type diff --git a/webexteamssdk/cards/card.py b/webexteamssdk/cards/card.py index 945a734..d695397 100644 --- a/webexteamssdk/cards/card.py +++ b/webexteamssdk/cards/card.py @@ -1,14 +1,39 @@ -from .abstract_components import Serializable, Component +from .abstract_components import Serializable 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, style=None, fallbackText=None, lang=None): - super().__init__(serializable_properties=['body', 'actions', 'selectAction', 'style'], - simple_properties=['version', 'fallbackText', 'lang', 'schema', 'type']) + """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 + + """ + super().__init__(serializable_properties=[ + 'body', 'actions', 'selectAction', 'style' + ], + simple_properties=[ + 'version', 'fallbackText', 'lang', 'schema', 'type' + ]) # Set properties self.type = "AdaptiveCard" From db2ab7cfd32063a1074fd160410a9b3895197f99 Mon Sep 17 00:00:00 2001 From: "Neidinger, Marcel" Date: Sat, 26 Oct 2019 17:44:57 +0200 Subject: [PATCH 03/12] Added missing options --- webexteamssdk/cards/options.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/webexteamssdk/cards/options.py b/webexteamssdk/cards/options.py index 789945f..49ff3b0 100644 --- a/webexteamssdk/cards/options.py +++ b/webexteamssdk/cards/options.py @@ -10,6 +10,11 @@ def __str__(self): def __repr__(self): return self.to_value() +class VerticalContentAlignment: + TOP = 1 + CENTER = 2 + BOTTOM = 3 + class Colors(AbstractOption): DEFAULT = 1 DARK = 2 @@ -48,3 +53,28 @@ class Spacing(AbstractOption): 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 From fe0b769daa35376196fa4d2509087b9ea25b25ed Mon Sep 17 00:00:00 2001 From: "Neidinger, Marcel" Date: Sat, 26 Oct 2019 17:47:16 +0200 Subject: [PATCH 04/12] Added copyright --- webexteamssdk/cards/abstract_components.py | 25 +++++++++++++++++++++- webexteamssdk/cards/actions.py | 24 +++++++++++++++++++++ webexteamssdk/cards/card.py | 24 +++++++++++++++++++++ webexteamssdk/cards/components.py | 24 +++++++++++++++++++++ webexteamssdk/cards/container.py | 24 +++++++++++++++++++++ webexteamssdk/cards/inputs.py | 24 +++++++++++++++++++++ webexteamssdk/cards/options.py | 24 +++++++++++++++++++++ webexteamssdk/cards/utils.py | 24 +++++++++++++++++++++ 8 files changed, 192 insertions(+), 1 deletion(-) diff --git a/webexteamssdk/cards/abstract_components.py b/webexteamssdk/cards/abstract_components.py index 7bc6c02..90906c5 100644 --- a/webexteamssdk/cards/abstract_components.py +++ b/webexteamssdk/cards/abstract_components.py @@ -1,4 +1,27 @@ -from abc import ABC, abstractmethod +# -*- 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: diff --git a/webexteamssdk/cards/actions.py b/webexteamssdk/cards/actions.py index 2ac5c54..7ef4e35 100644 --- a/webexteamssdk/cards/actions.py +++ b/webexteamssdk/cards/actions.py @@ -1,3 +1,27 @@ +# -*- 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): diff --git a/webexteamssdk/cards/card.py b/webexteamssdk/cards/card.py index d695397..e255d3a 100644 --- a/webexteamssdk/cards/card.py +++ b/webexteamssdk/cards/card.py @@ -1,3 +1,27 @@ +# -*- 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 AdaptiveCard(Serializable): diff --git a/webexteamssdk/cards/components.py b/webexteamssdk/cards/components.py index 4e16d46..5b3e638 100644 --- a/webexteamssdk/cards/components.py +++ b/webexteamssdk/cards/components.py @@ -1,3 +1,27 @@ +# -*- 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 MediaSource(Serializable): diff --git a/webexteamssdk/cards/container.py b/webexteamssdk/cards/container.py index 2f8eb1e..b0f7cc0 100644 --- a/webexteamssdk/cards/container.py +++ b/webexteamssdk/cards/container.py @@ -1,3 +1,27 @@ +# -*- 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): diff --git a/webexteamssdk/cards/inputs.py b/webexteamssdk/cards/inputs.py index 81180af..f8060e3 100644 --- a/webexteamssdk/cards/inputs.py +++ b/webexteamssdk/cards/inputs.py @@ -1,3 +1,27 @@ +# -*- 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): diff --git a/webexteamssdk/cards/options.py b/webexteamssdk/cards/options.py index 49ff3b0..be6af3a 100644 --- a/webexteamssdk/cards/options.py +++ b/webexteamssdk/cards/options.py @@ -1,3 +1,27 @@ +# -*- 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): diff --git a/webexteamssdk/cards/utils.py b/webexteamssdk/cards/utils.py index 0bb07dd..ab5f54f 100644 --- a/webexteamssdk/cards/utils.py +++ b/webexteamssdk/cards/utils.py @@ -1,3 +1,27 @@ +# -*- 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() From 7cfc5bb4c3bbeae7542eecedf49c714a7e83a3b8 Mon Sep 17 00:00:00 2001 From: "Neidinger, Marcel" Date: Sat, 26 Oct 2019 18:09:02 +0200 Subject: [PATCH 05/12] Added type checks to cards.py --- webexteamssdk/cards/card.py | 23 ++++++++----- webexteamssdk/cards/utils.py | 64 ++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 8 deletions(-) diff --git a/webexteamssdk/cards/card.py b/webexteamssdk/cards/card.py index e255d3a..2d024b3 100644 --- a/webexteamssdk/cards/card.py +++ b/webexteamssdk/cards/card.py @@ -23,6 +23,9 @@ """ 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. @@ -34,7 +37,6 @@ class AdaptiveCard(Serializable): def __init__(self, body=None, actions=None, selectAction=None, - style=None, fallbackText=None, lang=None): """Creates a new adaptive card object. @@ -52,12 +54,11 @@ def __init__(self, body=None, used for localization of date/time functions """ - super().__init__(serializable_properties=[ - 'body', 'actions', 'selectAction', 'style' - ], - simple_properties=[ - 'version', 'fallbackText', 'lang', 'schema', 'type' - ]) + # 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" @@ -65,7 +66,13 @@ def __init__(self, body=None, self.body = body self.actions = actions self.selectAction = selectAction - self.style = style self.fallbackText = fallbackText self.lang = lang self.schema = "http://adaptivecards.io/schemas/adaptive-card.json" + + super().__init__(serializable_properties=[ + 'body', 'actions', 'selectAction' + ], + simple_properties=[ + 'version', 'fallbackText', 'lang', 'schema', 'type' + ]) diff --git a/webexteamssdk/cards/utils.py b/webexteamssdk/cards/utils.py index ab5f54f..4e8cfab 100644 --- a/webexteamssdk/cards/utils.py +++ b/webexteamssdk/cards/utils.py @@ -25,3 +25,67 @@ 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) From 9981afa18d78a40d30922d8a1fffc118c103a0c8 Mon Sep 17 00:00:00 2001 From: "Neidinger, Marcel" Date: Sat, 26 Oct 2019 18:16:48 +0200 Subject: [PATCH 06/12] added docs and type check to MediaSource component --- webexteamssdk/cards/components.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/webexteamssdk/cards/components.py b/webexteamssdk/cards/components.py index 5b3e638..138d2d8 100644 --- a/webexteamssdk/cards/components.py +++ b/webexteamssdk/cards/components.py @@ -23,11 +23,23 @@ """ from .abstract_components import Serializable +from .utils import check_type class MediaSource(Serializable): + """Defines the source of a Media element.""" def __init__(self, mimeType, url): + """Create a new MediaSource + + 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 From c05e19a92137cb13b5ea75c5a6f9342f0bf90189 Mon Sep 17 00:00:00 2001 From: "Neidinger, Marcel" Date: Sat, 26 Oct 2019 18:26:21 +0200 Subject: [PATCH 07/12] added docs and type check for Media --- webexteamssdk/cards/components.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/webexteamssdk/cards/components.py b/webexteamssdk/cards/components.py index 138d2d8..d897fc4 100644 --- a/webexteamssdk/cards/components.py +++ b/webexteamssdk/cards/components.py @@ -24,13 +24,14 @@ from .abstract_components import Serializable from .utils import check_type +from .options import BlockElementHeight, Spacing class MediaSource(Serializable): """Defines the source of a Media element.""" def __init__(self, mimeType, url): - """Create a new MediaSource + """Create a new MediaSource component. Args: mimeType(str): Mime type of the associated media(i.e. 'video/mp4') @@ -47,6 +48,7 @@ def __init__(self, simple_properties=['mimeType', 'url']) class Media(Serializable): + """Displays a media player for audio or video content""" def __init__(self, sources, poster=None, @@ -55,6 +57,26 @@ def __init__(self, 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 From 574ae5bb01aa94d880a703e6f977877603762a35 Mon Sep 17 00:00:00 2001 From: "Neidinger, Marcel" Date: Sat, 26 Oct 2019 18:35:18 +0200 Subject: [PATCH 08/12] fixed schema $ error --- webexteamssdk/cards/card.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/webexteamssdk/cards/card.py b/webexteamssdk/cards/card.py index 2d024b3..512e42b 100644 --- a/webexteamssdk/cards/card.py +++ b/webexteamssdk/cards/card.py @@ -68,11 +68,18 @@ def __init__(self, body=None, self.selectAction = selectAction self.fallbackText = fallbackText self.lang = lang - self.schema = "http://adaptivecards.io/schemas/adaptive-card.json" super().__init__(serializable_properties=[ 'body', 'actions', 'selectAction' ], simple_properties=[ - 'version', 'fallbackText', 'lang', 'schema', 'type' + '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 From 432169ace7e6099481d9f9c466b23562891f0c6d Mon Sep 17 00:00:00 2001 From: "Neidinger, Marcel" Date: Sat, 26 Oct 2019 21:05:49 +0200 Subject: [PATCH 09/12] Added ability to send "pythonic" cards to message create method You can now send a card like this: ``` from webexteamssdk import WebexTeamsAPI from webexteamssdk.cards.card import AdaptiveCard from webexteamssdk.cards.components import TextBlock from webexteamssdk.cards.options import Colors block = TextBlock("Hey hello there! I am a adaptive card", color=Colors.GOOD) card = AdaptiveCard(body=[block]) api = WebexTeamsAPI() api.messages.create(text="fallback", roomId="...", cards=card) ``` --- webexteamssdk/api/messages.py | 19 +++++++++++++++++-- webexteamssdk/utils.py | 16 ++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/webexteamssdk/api/messages.py b/webexteamssdk/api/messages.py index 025dfae..2809110 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' @@ -135,7 +136,7 @@ def list(self, roomId, mentionedPeople=None, before=None, yield self._object_factory(OBJECT_TYPE, item) def create(self, roomId=None, toPersonId=None, toPersonEmail=None, - text=None, markdown=None, files=None, **request_parameters): + text=None, markdown=None, files=None, cards=None, **request_parameters): """Post a message, and optionally a attachment, to a room. The files parameter is a list, which accepts multiple values to allow @@ -154,6 +155,8 @@ def create(self, roomId=None, toPersonId=None, toPersonEmail=None, markdown(basestring): The message, in markdown format. files(`list`): A list of public URL(s) or local path(s) to files to be posted into the room. Only one file is allowed per message. + cards(`list`): A list of adaptive cards objects that will be send + with this message. **request_parameters: Additional request parameters (provides support for parameters that may be added in the future). @@ -174,6 +177,7 @@ def create(self, roomId=None, toPersonId=None, toPersonEmail=None, check_type(text, basestring) check_type(markdown, basestring) check_type(files, list) + check_type(cards, (list, AdaptiveCard)) if files: if len(files) != 1: raise ValueError("The length of the `files` list is greater " @@ -194,6 +198,17 @@ def create(self, roomId=None, toPersonId=None, toPersonEmail=None, files=files, ) + # 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/utils.py b/webexteamssdk/utils.py index 00c4658..8df4c08 100644 --- a/webexteamssdk/utils.py +++ b/webexteamssdk/utils.py @@ -254,6 +254,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.""" From 53022397e781799b00394007ecaa40346da4b4da Mon Sep 17 00:00:00 2001 From: "Neidinger, Marcel" Date: Sat, 26 Oct 2019 22:09:35 +0200 Subject: [PATCH 10/12] Added all classes to autodoc --- docs/images/cards_sample.png | Bin 0 -> 43949 bytes docs/index.rst | 1 + docs/user/api.rst | 88 ++++++++++++++++++++++++++++++ docs/user/cards.rst | 39 +++++++++++++ webexteamssdk/cards/components.py | 37 ++++++++++++- 5 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 docs/images/cards_sample.png create mode 100644 docs/user/cards.rst diff --git a/docs/images/cards_sample.png b/docs/images/cards_sample.png new file mode 100644 index 0000000000000000000000000000000000000000..66eb05bab0fd65558d215c6fd283ae4efca808da GIT binary patch literal 43949 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 m%Q_fgxnne9Ms~zJ_|NdxA+IOF`}a)-An`_ 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/cards/components.py b/webexteamssdk/cards/components.py index d897fc4..2910de9 100644 --- a/webexteamssdk/cards/components.py +++ b/webexteamssdk/cards/components.py @@ -25,6 +25,7 @@ from .abstract_components import Serializable from .utils import check_type from .options import BlockElementHeight, Spacing +from .actions import OpenUrl, ShowCard, Submit class MediaSource(Serializable): """Defines the source of a Media element.""" @@ -76,7 +77,7 @@ def __init__(self, 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 @@ -92,6 +93,8 @@ def __init__(self, 'separator', 'spacing', 'id' ]) class Image(Serializable): + """Displays a image object""" + def __init__(self, url, altText=None, @@ -105,6 +108,38 @@ def __init__(self, seperator=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_style(width, str, False, True) + check_style(separator, bool, False, True) + check_style(spacing, Spacing, False, True) + check_style(id, str, False, True) self.type = "Image" self.url = url From f643af0f9bd12269e7ec7fee32f0ec2f3a552973 Mon Sep 17 00:00:00 2001 From: "Neidinger, Marcel" Date: Sat, 26 Oct 2019 22:10:19 +0200 Subject: [PATCH 11/12] added .DS_Store to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) 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 From c1271e74c9492c9148e833b5c046bb8110d6b525 Mon Sep 17 00:00:00 2001 From: "Neidinger, Marcel" Date: Mon, 28 Oct 2019 09:00:45 +0100 Subject: [PATCH 12/12] Fixed typos in image type check --- webexteamssdk/cards/components.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/webexteamssdk/cards/components.py b/webexteamssdk/cards/components.py index 2910de9..669ae84 100644 --- a/webexteamssdk/cards/components.py +++ b/webexteamssdk/cards/components.py @@ -24,7 +24,7 @@ from .abstract_components import Serializable from .utils import check_type -from .options import BlockElementHeight, Spacing +from .options import BlockElementHeight, Spacing, ImageSize, ImageStyle from .actions import OpenUrl, ShowCard, Submit class MediaSource(Serializable): @@ -105,7 +105,7 @@ def __init__(self, size=None, style=None, width=None, - seperator=None, + separator=None, spacing=None, id=None): """Create a new image component @@ -136,10 +136,10 @@ def __init__(self, check_type(selectAction, (OpenUrl, Submit), False, True) check_type(size, ImageSize, False, True) check_type(style, ImageStyle, False, True) - check_style(width, str, False, True) - check_style(separator, bool, False, True) - check_style(spacing, Spacing, False, True) - check_style(id, str, 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 @@ -151,7 +151,7 @@ def __init__(self, self.size = size self.style = style self.width = width - self.seperator = seperator + self.separator = separator self.spacing = spacing self.id = id