Skip to content

Commit 327b452

Browse files
authored
Merge pull request #93 from sQu4rks/feature/adaptive_cards
Add Webex Teams Cards&Buttons as native python objects
2 parents 1bfcdae + 1c7f31d commit 327b452

File tree

16 files changed

+1125
-1
lines changed

16 files changed

+1125
-1
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ venv/
2121
dist/
2222
docs/_build/
2323
*.egg-info/
24+
.DS_Store

docs/images/cards_sample.png

42.9 KB
Loading

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ The User Guide
3333
user/intro
3434
user/quickstart
3535
user/api
36+
user/cards
3637

3738

3839
The Development Community

docs/user/api.rst

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,5 +281,93 @@ Warnings
281281
:show-inheritance:
282282
:members:
283283

284+
.. _CardsAPI:
285+
286+
Cards and Buttons
287+
=================
288+
289+
.. autoclass:: webexteamssdk.cards.card.AdaptiveCard()
290+
291+
Components
292+
----------
293+
294+
.. autoclass:: webexteamssdk.cards.components.Image()
295+
:members:
296+
297+
.. automethod:: __init__
298+
299+
.. autoclass:: webexteamssdk.cards.components.TextBlock()
300+
:members:
301+
302+
.. automethod:: __init__
303+
304+
.. autoclass:: webexteamssdk.cards.components.Column()
305+
306+
.. autoclass:: webexteamssdk.cards.components.Fact()
307+
308+
.. autoclass:: webexteamssdk.cards.components.Choice()
309+
310+
Options
311+
-------
312+
313+
.. autoclass:: webexteamssdk.cards.options.VerticalContentAlignment()
314+
315+
.. autoclass:: webexteamssdk.cards.options.Colors()
316+
317+
.. autoclass:: webexteamssdk.cards.options.HorizontalAlignment()
318+
319+
.. autoclass:: webexteamssdk.cards.options.FontSize()
320+
321+
.. autoclass:: webexteamssdk.cards.options.FontWeight()
322+
323+
.. autoclass:: webexteamssdk.cards.options.BlockElementHeight()
324+
325+
.. autoclass:: webexteamssdk.cards.options.Spacing()
326+
327+
.. autoclass:: webexteamssdk.cards.options.ImageSize()
328+
329+
.. autoclass:: webexteamssdk.cards.options.ImageStyle()
330+
331+
.. autoclass:: webexteamssdk.cards.options.ContainerStyle()
332+
333+
.. autoclass:: webexteamssdk.cards.options.TextInputStyle()
334+
335+
.. autoclass:: webexteamssdk.cards.options.ChoiceInputStyle()
336+
337+
338+
Container
339+
---------
340+
341+
.. autoclass:: webexteamssdk.cards.container.Container()
342+
343+
.. autoclass:: webexteamssdk.cards.container.ColumnSet()
344+
345+
.. autoclass:: webexteamssdk.cards.container.FactSet()
346+
347+
.. autoclass:: webexteamssdk.cards.container.ImageSet()
348+
349+
Inputs
350+
------
351+
352+
.. autoclass:: webexteamssdk.cards.inputs.Text()
353+
354+
.. autoclass:: webexteamssdk.cards.inputs.Number()
355+
356+
.. autoclass:: webexteamssdk.cards.inputs.Date()
357+
358+
.. autoclass:: webexteamssdk.cards.inputs.Time()
359+
360+
.. autoclass:: webexteamssdk.cards.inputs.Toggle()
361+
362+
.. autoclass:: webexteamssdk.cards.inputs.Choices()
363+
364+
Actions
365+
-------
366+
367+
.. autoclass:: webexteamssdk.cards.actions.OpenUrl
368+
369+
.. autoclass:: webexteamssdk.cards.actions.Submit
370+
371+
.. autoclass:: webexteamssdk.cards.actions.ShowCard
284372

285373
*Copyright (c) 2016-2019 Cisco and/or its affiliates.*

docs/user/cards.rst

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
.. _Cards:
2+
3+
=================
4+
Cards and Buttons
5+
=================
6+
7+
Webex Teams supports `AdaptiveCards <https://www.adaptivecards.io/>`_ to allow
8+
new levels of interactivity for bots and integrations. You can read more about
9+
how cards and buttons work `in the official guide <https://developer.webex.com/docs/api/guides/cards>`_.
10+
11+
In this guide I want to cover the abstraction build into the webexteamssdk that
12+
lets you author adaptive cards in pure python without having to touch the
13+
underlying json of a adaptive card.
14+
15+
Lets dive into a simple example that sends a card to a room
16+
17+
.. code-block:: python
18+
19+
from webexteamssdk import WebexTeamsAPI
20+
from webexteamssdk.cards.card import AdaptiveCard
21+
from webexteamssdk.cards.inputs import Text, Number
22+
from webexteamssdk.cards.components import TextBlock
23+
from webexteamssdk.cards.actions import Submit
24+
25+
greeting = TextBlock("Hey hello there! I am a adaptive card")
26+
first_name = Text('first_name', placeholder="First Name")
27+
age = Number('age', placeholder="Age")
28+
29+
submit = Submit(title="Send me!")
30+
31+
card = AdaptiveCard(body=[greeting, first_name, age], actions=[submit])
32+
33+
api = WebexTeamsAPI()
34+
api.messages.create(text="fallback", roomId="...", cards=card)
35+
36+
The message we send with this code then looks like this in our Webex Teams
37+
client:
38+
39+
.. image:: ../images/cards_sample.png

webexteamssdk/api/messages.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@
3939
from ..restsession import RestSession
4040
from ..utils import (
4141
check_type, dict_from_items_with_values, is_local_file, is_web_url,
42-
open_local_file,
42+
open_local_file, make_card_attachment
4343
)
44+
from ..cards.card import AdaptiveCard
4445

4546

4647
API_ENDPOINT = 'messages'
@@ -208,6 +209,17 @@ def create(self, roomId=None, toPersonId=None, toPersonEmail=None,
208209
attachments=attachments,
209210
)
210211

212+
# Add cards
213+
if cards is not None:
214+
cards_list = []
215+
216+
if isinstance(cards, list):
217+
cards_list = [make_card_attachment(c) for c in cards]
218+
else:
219+
cards_list = [make_card_attachment(cards)]
220+
221+
post_data['attachments'] = cards_list
222+
211223
# API request
212224
if not files or is_web_url(files[0]):
213225
# Standard JSON post

webexteamssdk/cards/__init__.py

Whitespace-only changes.
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# -*- coding: utf-8 -*-
2+
"""Webex Teams Access-Tokens API wrapper.
3+
4+
Copyright (c) 2016-2019 Cisco and/or its affiliates.
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in all
14+
copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
SOFTWARE.
23+
"""
24+
25+
import json
26+
27+
class Serializable:
28+
"""Parent class for all components of adaptive cards.
29+
30+
Each component should inherit from this class and then specify, from
31+
its properties, which fall into the following two categories:
32+
33+
* Simple properties are text properties like "type" or "id"
34+
* Serializable properties are properties that can themselfes be serilized.
35+
This includes lists of items (i.e. the 'body' field of the adaptive card) or
36+
single objects that also inherit from Serializable
37+
"""
38+
def __init__(self, serializable_properties, simple_properties):
39+
"""Creates a serializable object.
40+
41+
See class docstring for an explanation what the different types of
42+
properties are.
43+
44+
Args:
45+
serializable_properties(list): List of all serializable properties
46+
simple_properties(list): List of all simple properties.
47+
"""
48+
self.serializable_properties = serializable_properties
49+
self.simple_properties = simple_properties
50+
51+
def to_json(self, pretty=False):
52+
"""Create json from a serializable component
53+
54+
This function is used to render the json from a component. While all
55+
components do support this operation it is mainly used on the
56+
AdaptiveCard to generate the json for the attachment.
57+
58+
Args:
59+
pretty(boolean): If true, the returned json will be sorted by keys
60+
and indented with 4 spaces to make it more human-readable
61+
62+
Returns:
63+
A Json representation of this component
64+
"""
65+
ret = None
66+
if pretty:
67+
ret = json.dumps(self.to_dict(), indent=4, sort_keys=True)
68+
else:
69+
ret = json.dumps(self.to_dict())
70+
71+
return ret
72+
73+
def to_dict(self):
74+
"""Export a dictionary representation of this card/component by
75+
parsing all simple and serializable properties.
76+
77+
A simple_component is a single-text property of the exported card
78+
(i.e. {'version': "1.2"}) while a serializable property is another
79+
subcomponent that also implements a to_dict() method.
80+
81+
The to_dict() method is used to recursively create a dict representation
82+
of the adaptive card. This dictionary representation can then be
83+
converted into json for usage with the API.
84+
85+
Returns:
86+
dict: Dictionary representation of this component.
87+
"""
88+
export = {}
89+
90+
# Export simple properties (i.e. properties that are only single text)
91+
for sp in self.simple_properties:
92+
o = getattr(self, sp, None)
93+
94+
if o is not None:
95+
export[sp] = str(o)
96+
97+
# Export all complex properties by calling its respective serialization
98+
for cp in self.serializable_properties:
99+
o = getattr(self, cp, None)
100+
101+
if o is not None:
102+
# Check if it is a list or a single component
103+
l = []
104+
if isinstance(o, list):
105+
for i in o:
106+
l.append(i.to_dict())
107+
else:
108+
l.append(o.to_dict())
109+
export[cp] = l
110+
111+
return export

webexteamssdk/cards/actions.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# -*- coding: utf-8 -*-
2+
"""Webex Teams Access-Tokens API wrapper.
3+
4+
Copyright (c) 2016-2019 Cisco and/or its affiliates.
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in all
14+
copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
SOFTWARE.
23+
"""
24+
25+
from .abstract_components import Serializable
26+
27+
class OpenUrl(Serializable):
28+
def __init__(self, url, title=None,
29+
iconURL=None):
30+
self.type = "Action.OpenUrl"
31+
self.title = title
32+
self.iconURL = iconURL
33+
34+
super().__init__(serializable_properties=[],
35+
simple_properties=['type', 'title', 'iconURL'])
36+
37+
class Submit(Serializable):
38+
def __init__(self, data=None,
39+
title=None,
40+
iconURL=None,
41+
):
42+
self.type = "Action.Submit"
43+
self.data = data
44+
self.title = title
45+
self.iconURL = iconURL
46+
47+
super().__init__(serializable_properties=['data'],
48+
simple_properties=['title', 'iconURL', 'type'])
49+
50+
class ShowCard(Serializable):
51+
def __init__(self, card=None,
52+
title=None,
53+
iconURL=None):
54+
self.type = "Action.ShowCard"
55+
self.card = card
56+
self.title = title
57+
self.iconURL = iconURL
58+
59+
super().__init__(serializable_properties=['card'],
60+
simple_properties=['title', 'type', 'iconURL'])

0 commit comments

Comments
 (0)