Easily Build and Export Adaptive Cards Through Python
Project description
Python Adaptive Card Builder (PREVIEW)
Easily Build and Export Adaptive Cards Through Python
- Programmatically construct adaptive cards like Lego, without the learning curve of Adaptive Card 'Templating'
- Avoid the curly-braces jungle of traditional JSON editing
- Build pythonically, but with minimal abstraction while preserving readability
- Output built cards to JSON in a single method call
- Send output to any channel with Adaptive Card support to be rendered.
View this package on pypi
https://pypi.org/project/adaptivecardbuilder/
Installation via pip
pip install adaptivecardbuilder
Learn about Adaptive Cards:
- Home Page: https://adaptivecards.io/
- Adaptive Card Designer: https://adaptivecards.io/designer/
- Schema Explorer: https://adaptivecards.io/explorer/
- Documentation: https://docs.microsoft.com/en-us/adaptive-cards/
Adaptive Card Builder "Hello World":
from adaptivecardbuilder import *
# initialize card
card = AdaptiveCard()
# Add a textblock
card.add(TextBlock(text="0.45 miles away", separator="true", spacing="large"))
# add column set
card.add(ColumnSet())
# First column contents
card.add(Column(width=2))
card.add(TextBlock(text="BANK OF LINGFIELD BRANCH"))
card.add(TextBlock(text="NE Branch", size="ExtraLarge", weight="Bolder"))
card.add(TextBlock(text="4.2 stars", isSubtle=True, spacing="None"))
card.add(TextBlock(text=f"Some review text for illustration", size="Small"))
# Back up to column set
card.up_one_level()
# Second column contents
card.add(Column(width=1))
card.add(Image(url="https://s17026.pcdn.co/wp-content/uploads/sites/9/2018/08/Business-bank-account-e1534519443766.jpeg"))
# Serialize to a json payload with a one-liner
card.to_json()
Output when rendered:
A "Visual" Alternative
The AdaptiveCard
class also supports a more visual approach to building cards by passing a list of elements to the add()
method instead.
This allows us to freely indent our code within the method call and better illustrate card structure:
card = AdaptiveCard()
# Add a list of elements
card.add([
"items -----------------",
TextBlock("Top Level"),
ColumnSet(),
Column(),
TextBlock("Column 1 Top Item"),
TextBlock("Column 1 Second Item"),
"<",
Column(),
TextBlock("Column 2 Top Item"),
TextBlock("Column 2 Second Item"),
"<",
"<",
TextBlock("Lowest Level"),
"actions -----------------",
ActionOpenUrl(title="View Website", url="someurl.com"),
ActionShowCard(title="Click to Comment"),
"item after this",
InputText(ID="comment", placeholder="Type Here"),
"action again",
ActionSubmit(title="Submit Comment")
])
card.to_json()
- Strings containing
"<"
move us up/back a level in the tree - Strings containing
"^"
will move us back to the top of the tree - Strings containing
"item"
tellAdaptiveCard
to expect item elements below - Strings containing
"action"
tellAdaptiveCard
to expect action elements below
Concepts
The AdaptiveCard
class centrally handles all construction & element-addition operations:
from adaptivecardbuilder import *
card = AdaptiveCard() # initialize
# Structure:
# |--Card
# | |--Schema="XXX"
# | |--Version="1.0"
# | |--Body=[]
# | |--Actions=[]
card.add(TextBlock(text="Header", weight="Bolder"))
card.add(TextBlock(text="Subheader"))
card.add(TextBlock(text="*Quote*", isSubtle="true"))
# |--Card
# | |--Schema="XXX"
# | |--Version="1.0"
# | |--Body
# | |--TextBlock
# | |--TextBlock
# | |--TextBlock
# | |--Actions
card_json = card.to_json() # output to json
When rendered:
Each individual element is implemented as a class.
These are simply Python object representations of the standard Adaptive Card elements that take keyworded arguments as parameters like so:
element = TextBlock(text="Header", weight="Bolder")
print(element)
>>> {
"type": "TextBlock",
"text": "Header",
"weight": "Bolder"
}
Pointer Logic
Central to the AdaptiveCard
class is an internal pointer
attribute. When we add an element to the card, the element is by default added to the item container of whichever object is being pointed at.
Conceptually, an object can have up to two kinds of containers (python list
s):
- Item containers (these hold non-interactive elements like TextBlocks, Images)
- Action containers (these hold interactive actions like ActionShowUrl, ActionSubmit)
For instance:
AdaptiveCard
objects have both item (body=[]
) and action (actions=[]
) containersColumnSet
objects have a single item (columns=[]
) containerColumn
objects have a single item (items=[]
) containerActionSet
objects have a single action (actions=[]
) container
The card.add()
method by default adds any new elements to the item container of an object being pointed at. However, we can add to the actions container by setting is_action=True
. We'll come back to examples of adding actions later.
Recursing Into an Added Element
When adding elements that can themselves contain other elements (e.g. column sets and columns), the pointer will by default recurse into the added element, so that any elements added thereafter will go straight into the added element's container (making our code less verbose).
This is essentially a depth-first approach to building cards:
card = AdaptiveCard()
# |--Card <- Pointer
# | |--Schema="XXX"
# | |--Version="1.0"
# | |--Body=[]
# | |--Actions=[]
card.add(TextBlock(text="Header", weight="Bolder"))
# |--Card <- Pointer
# | |--Schema="XXX"
# | |--Version="1.0"
# | |--Body
# | |--TextBlock <- added
# | |--Actions
card.add(TextBlock(text="Subheader"))
card.add(TextBlock(text="*Quote*", isSubtle="true"))
# |--Card <- Pointer
# | |--Schema="XXX"
# | |--Version="1.0"
# | |--Body
# | |--TextBlock
# | |--TextBlock <- added
# | |--TextBlock <- added
# | |--Actions
card.add(ColumnSet())
# |--Card
# | |--Schema="XXX"
# | |--Version="1.0"
# | |--Body
# | |--TextBlock
# | |--TextBlock
# | |--TextBlock
# | |--ColumnSet <- Pointer <- added
# | |--Actions
card.add(Column(width=1))
# |--Card
# | |--Schema="XXX"
# | |--Version="1.0"
# | |--Body
# | |--TextBlock
# | |--TextBlock
# | |--TextBlock
# | |--ColumnSet
# | |--Column <- Pointer <- added
# | |--Actions
card.add(TextBlock(text="<Column 1 Contents>"))
# |--Card
# | |--Schema="XXX"
# | |--Version="1.0"
# | |--Body
# | |--TextBlock
# | |--TextBlock
# | |--TextBlock
# | |--ColumnSet
# | |--Column <- Pointer
# | |--TextBlock <- added
# | |--Actions
Rendered:
Observe that when adding a TextBlock
to a Column
's items, the pointer stays at the Column
level, rather than recursing into the TextBlock
. The add()
method will only recurse into the added element if it has an item or action container within it.
Because of the depth-first approach, we'll need to back ourselves out of a container once we are done adding elements to it.
One easy method to doing so is by using the up_one_level()
method, can be called multiple times and just moves the pointer one step up the element tree.
card = AdaptiveCard()
card.add(TextBlock(text="Header", weight="Bolder"))
card.add(TextBlock(text="Subheader"))
card.add(TextBlock(text="*Quote*", isSubtle="true"))
card.add(ColumnSet())
card.add(Column(width=1))
card.add(TextBlock(text="<Column 1 Contents>"))
# |--Card
# | |--Schema="XXX"
# | |--Version="1.0"
# | |--Body
# | |--TextBlock
# | |--TextBlock
# | |--TextBlock
# | |--ColumnSet
# | |--Column <- Pointer
# | |--TextBlock <- added
# | |--Actions
card.up_one_level()
# |--Card
# | |--Schema="XXX"
# | |--Version="1.0"
# | |--Body
# | |--TextBlock
# | |--TextBlock
# | |--TextBlock
# | |--ColumnSet <- Pointer
# | |--Column
# | |--TextBlock
# | |--Actions
card.add(Column(width=1))
# |--Card
# | |--Schema="XXX"
# | |--Version="1.0"
# | |--Body
# | |--TextBlock
# | |--TextBlock
# | |--TextBlock
# | |--ColumnSet
# | |--Column
# | |--TextBlock
# | |--Column <- Pointer <- added
# | |--Actions
card.add(TextBlock(text="Column 2 Contents"))
# |--Card
# | |--Schema="XXX"
# | |--Version="1.0"
# | |--Body
# | |--TextBlock
# | |--TextBlock
# | |--TextBlock
# | |--ColumnSet
# | |--Column
# | |--TextBlock
# | |--Column <- Pointer
# | |--TextBlock <- added
# | |--Actions
Rendered:
We can also use the card.save_level()
method to create a "checkpoint" at any level if we intend to back ourselves out to the level we are currently at in our code block. To "reload" to that checkpoint, use card.load_level(checkpoint)
.
# checkpoints example
card = AdaptiveCard()
card.add(Container())
card.add(TextBlock(text="Text as the first item, at the container level"))
# create checkpoint here
container_level = card.save_level()
# add nested columnsets and columns for fun
for i in range(1, 6):
card.add(ColumnSet())
card.add(Column(style="emphasis"))
card.add(TextBlock(text=f"Nested Column {i}"))
# our pointer continues to move downwards into the nested structure
# reset pointer back to container level
card.load_level(container_level)
card.add(TextBlock(text="Text at the container level, below all the nested containers"))
card.to_json()
Adding Actions
The add()
method has an optional is_action
parameter - if we set this to True
when adding an element to the object being pointed at, then the element is instead added to that object's actions container (if it has one).
Let's first move our pointer back to the top level using the back_to_top()
method:
card.back_to_top() # back to top of tree
# |--Card <- Pointer
# | |--Schema="XXX"
# | |--Version="1.0"
# | |--Body
# | |--TextBlock
# | |--TextBlock
# | |--TextBlock
# | |--ColumnSet
# | |--Column
# | |--TextBlock
# | |--Column
# | |--TextBlock
# | |--Actions
Our pointer is now pointing at the main Card object.
Because it has an Actions container, we can add actions to it by setting is_action=True
within the add()
method:
# Adding single url action
card.add(ActionOpenUrl(url="someurl.com", title="Open Me"), is_action=True)
# |--Card <- Pointer
# | |--Schema="XXX"
# | |--Version="1.0"
# | |--Body
# | |--TextBlock
# | |--TextBlock
# | |--TextBlock
# | |--ColumnSet
# | |--Column
# | |--TextBlock
# | |--Column
# | |--TextBlock
# | |--Actions
# | |--ActionOpenUrl <- added
Worked Example 1 - Creating a simple table with row-specific action buttons
Intended Output
Table with actionable buttons unique to each row:
- Each row is represented as a
ColumnSet
- Each column in the row is a
Column
object- Values in each column are
TextBlock
objects
- Values in each column are
- Each column in the row is a
Full Code
Here's our full Adaptive Card Builder-related code for creating the table from our input data:
card = AdaptiveCard()
# Add our first "row" for table headers
card.add(ColumnSet())
# Add headers as columns + bold textblocks
for header in headers:
card.add(Column())
card.add(TextBlock(text=header, horizontalAlignment="center", weight="Bolder"))
card.up_one_level() # Back to ColumnSet level
# Back to Card's main body
card.back_to_top()
# Adding our transactions
for transaction in table:
card.add(ColumnSet())
for element in transaction:
card.add(Column())
card.add(TextBlock(text=element, horizontalAlignment="center"))
card.up_one_level() # move pointer back to ColumnSet level
# Before moving to the next row, add a "Flag" button
card.add(Column())
card.add(ActionSet())
flag_url = "https://pngimage.net/wp-content/uploads/2018/06/red-flag-png-5.png"
transaction_id = transaction[0]
data = {"ID": transaction_id} # data to submit to our hosting interface
card.add(ActionSubmit(iconUrl=flag_url, data=data), is_action=True)
card.back_to_top() # Go back to the top level, ready to add our next row
Step-by-Step Walkthrough
Raw Data Format
Picture this kind of HTTP response from a SQL database query regarding transactions data:
sql_output = {
"Table1": [
{
"ID": "TRN-349824",
"Amount": "$400.50",
"Receiver": "Walmart",
"Date": "29-05-2020"
},
{
"ID": "TRN-334244",
"Amount": "$50.35",
"Receiver": "Delta Airlines",
"Date": "01-06-2020"
},
{
"ID": "TRN-503134",
"Amount": "$60.50",
"Receiver": "Smoothie King",
"Date": "03-06-2020"
}
]
}
Formatting data
Let's convert this JSON into a list of lists which makes it a bit more workable
# One-off helper function
def to_tabular(json_list):
first_element = json_list[0]
headers = list(first_element.keys())
result_table = []
for item in json_list:
item_values = list(item.values())
result_table.append(item_values)
return (headers, result_table)
(headers, table) = to_tabular(sql_output['Table1'])
headers.append('Suspicious') # Add 'suspicious' column
print(headers)
print(table)
>>>['ID', 'Amount', 'Receiver', 'Date', 'Suspicious']
>>>[['TRN-349824', '$400.50', 'Walmart', '29-05-2020'],
['TRN-334244', '$50.35', 'Delta Airlines', '01-06-2020'],
['TRN-503134', '$60.50', 'Smoothie King', '03-06-2020']]
Initialize our Adaptive Card and add headers
card = AdaptiveCard()
# Add our first "row" for table headers
card.add(ColumnSet())
# Add headers as columns + bold textblocks
for header in headers:
card.add(Column())
card.add(TextBlock(text=header, horizontalAlignment="center", weight="Bolder"))
card.up_one_level() # Back to ColumnSet level
Here's what we have so far:
ID | Amount | Reciever | Date | Suspicious |
---|
Let's now add the transactions, line by line
...
# Back to Card's main body
card.back_to_top()
# Adding our transactions
for transaction in table:
card.add(ColumnSet())
for element in transaction:
card.add(Column())
card.add(TextBlock(text=element, horizontalAlignment="center"))
card.up_one_level() # move pointer back to ColumnSet level
Here's what we have after the inner loop is complete (first row):
ID | Amount | Reciever | Date | Suspicious |
---|---|---|---|---|
TRN-349824 | $400.50 | Walmart | 29-05-2020 |
Now let's add the button as the last column entry
...
for element in transaction:
card.add(Column())
card.add(TextBlock(text=element, horizontalAlignment="center"))
card.up_one_level() # move pointer back to ColumnSet level
# Before moving to the next row, add a "Flag" button
card.add(Column())
card.add(ActionSet())
flag_url = "https://pngimage.net/wp-content/uploads/2018/06/red-flag-png-5.png"
transaction_id = transaction[0]
data = {"ID": transaction_id} # data to submit to our hosting interface
card.add(ActionSubmit(iconUrl=flag_url, data=data), is_action=True)
card.back_to_top() # Go back to the top level, ready to add our next row
ID | Amount | Reciever | Date | Suspicious |
---|---|---|---|---|
TRN-349824 | $400.50 | Walmart | 29-05-2020 | [FLAG-BUTTON] |
When the outer loop is complete, we have a fully populated table complete with buttons on each row that a user can click on to report suspicious transactions. Each button is specific to the row, and will submit the transaction ID of that row (or whatever data we like) to whichever interface is hosting the Adaptive Card.
Worked Example 2: Banks and Appointments
List of nearest banks and their free appointment slots
Example of the finished product:
Here's an example of the data behind this:
# branch names
branches = ["NE Branch", "SE Branch", "SW Branch", "NW Branch"]
# distances in miles
distances = {
"NE Branch": 4.5,
"SE Branch": 5.0,
"SW Branch": 6.5,
"NW Branch": 7.0
}
# appointment slots per bank (start time, end time)
appointments = {
"NE Branch": [("08:00", "09:00"), ("09:15", "10:30")],
"SE Branch": [("09:00", "09:30"), ("13:15", "14:15"), ("15:00", "17:00")],
"SW Branch": [("11:00", "13:30")],
"NW Branch": [("08:15", "08:45"), ("13:15", "14:15"), ("15:00", "17:00"), ("17:00", "18:00")]
}
Adaptive Card Builder allows us to break up the construction of this into more manageable programmatic blocks.
We'll utilize loops to construct a card for each bank, complete with its appointment info and a link to view the banks location on a map.
Let's first add our bank info and an image onto the card:
# initialize our card
card = AdaptiveCard()
# loop over branches - each one will have a mini-card to itself
for branch in branches:
card.add(TextBlock(text=f"{distances[branch]} miles away", separator="true", spacing="large"))
card.add(ColumnSet())
# First column - bank info
card.add(Column(width=2))
card.add(TextBlock(text="BANK OF LINGFIELD BRANCH"))
card.add(TextBlock(text=branch, size="ExtraLarge", weight="Bolder", spacing="None"))
card.add(TextBlock(text="5 stars", isSubtle=True, spacing="None"))
card.add(TextBlock(text="Bank Review"*10, size="Small", wrap="true"))
card.up_one_level() # Back up to column set
# Second column - image
card.add(Column(width=1))
img = "https://s17026.pcdn.co/wp-content/uploads/sites/9/2018/08/Business-bank-account-e1534519443766.jpeg"
card.add(Image(url=img))
card.up_one_level() #Back up to column set
card.up_one_level() #Back up to Container
Now to add our interactive elements:
- The "View on Map" button
- Expandible "View Appointments" card
# add action set to contain our interactive elements
card.add(ActionSet())
# First add our "View on Map" button
card.add(ActionOpenUrl(url="map_url.com", title="View on Map"), is_action=True)
# create expandible card to show all our bank-specific appointment items
card.add(ActionShowCard(title="View Appointments"), is_action=True)
# Save a checkpoint at this level to come back to later
action_showcard_level = card.save_level()
# now loops over appointment items and add them
for (start_time, end_time) in appointments[branch]:
card.add(ColumnSet())
# Add our slots, start, end times
row_items = ["Slot", start_time, end_time]
for item in row_items:
card.add(Column(style="emphasis", verticalContentAlignment="Center"))
card.add(TextBlock(text=item, horizontalAlignment="Center"))
card.up_one_level() # Back to column set level
# Add the "Book This!" button, in the final column
card.add(Column())
card.add(ActionSet())
card.add(ActionSubmit(title="Book this!", data={"Appt": f"({start_time}, {end_time})"}), is_action=True)
card.load_level(action_showcard_level) # back to showcard's body
# Go back to the main body of the card, ready for next branch
card.back_to_top()
Finally, output the result to a JSON:
card.to_json()
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file adaptivecardbuilder-0.0.7.tar.gz
.
File metadata
- Download URL: adaptivecardbuilder-0.0.7.tar.gz
- Upload date:
- Size: 18.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.2.0 pkginfo/1.5.0.1 requests/2.24.0 setuptools/46.4.0.post20200518 requests-toolbelt/0.9.1 tqdm/4.47.0 CPython/3.8.3
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 92964b488ce6075056195ba56e54ba7334e7311a784fbf4021ac9f24f2ca3a86 |
|
MD5 | 51c745de7ae3bcb1772389d4ea9e9d11 |
|
BLAKE2b-256 | ed2683b76a2977acc997d9e500acf0bc9aaa8e4fa4f0ea2922d091c4b40230e9 |
File details
Details for the file adaptivecardbuilder-0.0.7-py3-none-any.whl
.
File metadata
- Download URL: adaptivecardbuilder-0.0.7-py3-none-any.whl
- Upload date:
- Size: 12.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.2.0 pkginfo/1.5.0.1 requests/2.24.0 setuptools/46.4.0.post20200518 requests-toolbelt/0.9.1 tqdm/4.47.0 CPython/3.8.3
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 1e3127eff20670b2b3ba2ab5d9a57c62a9ac29ad6d32a716de13a90f3910ead1 |
|
MD5 | 41a2d03adb92a7b7bfce1633c833cce3 |
|
BLAKE2b-256 | 0feb736a4e12731e1649b208578fe124720ed1403c888d37795d743e8e69850e |