Easily Build and Export Multilingual Adaptive Cards Through Python
Project description
Python Adaptive Card Builder
Easily Build and Export Multilingual 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 or a Python Dictionary in a single method call
- Auto-translate all text elements in a card with a single method call
- Combine multiple individual cards through the + operator
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
await card.to_json()
Output when rendered in https://adaptivecards.io/visualizer/ :
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.
When using this visual alternative approach to building cards, we can use specific strings to execute logic.
- Strings containing
"<"
move us up/back a level in the tree - Strings containing
"^"
will move us back to the top of the tree
card = AdaptiveCard()
# Add a list of elements
card.add([
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"),
ActionOpenUrl(title="View Website", url="someurl.com"),
ActionShowCard(title="Click to Comment"),
InputText(ID="comment", placeholder="Type Here"),
ActionSubmit(title="Submit Comment")
])
await card.to_json()
Output when rendered in https://adaptivecards.io/visualizer/ :
Combining/Chaining Cards
We can also combine the contents of multiple cards through the +
operator:
def create_single_card(input_text_id: int):
card = AdaptiveCard()
card.add([
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"),
ActionOpenUrl(title="View Website", url="someurl.com"),
ActionShowCard(title="Click to Comment"),
InputText(ID=f"comment_{input_text_id}", placeholder="Type Here"),
ActionSubmit(title="Submit Comment")
])
return card
# Use above function to create cards
card1 = create_single_card(1)
card2 = create_single_card(2)
# Add the contents of card1 and card2
combined_card = card1 + card2
await combined_card.to_json()
Output when rendered in https://adaptivecards.io/visualizer/ :
To preserve intra-card ordering of elements, AdaptiveCardBuilder moves all actions in the outermost action container of each card into their bodies by placing them in ActionSets instead. Each constituent card's actions is therefore attached to the appropriate portion of the combined card.
The combine_adaptive_cards
function can also be used to combine a list of adaptive cards together, in a left-to-right fashion. The following code essentially produces the same result as the code above, except an arbitrary length list of cards can now be passed:
card1 = create_single_card(1)
card2 = create_single_card(2)
card3 = create_single_card(3)
# Add the contents of all above cards
combined_card = combine_adaptive_cards([card1, card2, card3])
await combined_card.to_json()
Translating Card Elements
Passing translator arguments to the to_json()
method will translate cards.
Using the example above, we can translate the created card in the same method call.
To view a list of supported languages and language codes, go to:
https://docs.microsoft.com/en-us/azure/cognitive-services/translator/language-support
# Translate all text in card to Malay
await card.to_json(translator_to_lang='ms', translator_key='<YOUR AZURE API KEY>')
If any translator_to_lang
argument is passed, translation will apply to all elements with translatable text attributes.
To specify that a given Adaptive element should not be translated, simply pass the keyworded argument dont_translate=True
during the construction of any element, and AdaptiveCardBuilder will leave this specific element untranslated.
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 = await card.to_json() # output to json
When rendered:
Each individual adaptive object (e.g. TextBlock, Column) is implemented as a class.
These are simply Python object representations of the standard Adaptive Card elements that take keyworded arguments as parameters.
View the Schema Explorer at https://adaptivecards.io/explorer/ to see which keyword arguments each Adaptive Object is allowed to take.
TextBlock(text="Header", weight="Bolder")
# Internal representation
>>> {
"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 adaptive object (e.g. Column, Container) 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 will add a given AdaptiveObject to the appropriate container. For instance, if an Action-type object is passed, such as a ActionSubmit
or ActionOpenUrl
, then this will be added to the parent object's action container.
If the parent object does not have the appropriate container for the element being added, then this will throw an AssertionError
and a corresponding suggestion.
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"))
await card.to_json()
Adding Actions
As previously mentioned, the AdaptiveCard's add()
method will automatically add action elements to the appropriate containers.
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, the next action element to be added will be sent there.
# Adding single url action
card.add(ActionOpenUrl(url="someurl.com", title="Open Me"))
# |--Card <- Pointer
# | |--Schema="XXX"
# | |--Version="1.0"
# | |--Body
# | |--TextBlock
# | |--TextBlock
# | |--TextBlock
# | |--ColumnSet
# | |--Column
# | |--TextBlock
# | |--Column
# | |--TextBlock
# | |--Actions
# | |--ActionOpenUrl <- added
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.9.tar.gz
.
File metadata
- Download URL: adaptivecardbuilder-0.0.9.tar.gz
- Upload date:
- Size: 15.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.2.0 pkginfo/1.5.0.1 requests/2.22.0 setuptools/49.2.0.post20200714 requests-toolbelt/0.9.1 tqdm/4.49.0 CPython/3.8.3
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 895625ed386f45ff68f8ebb904e0fb9602e205dd39e771259cc1877102518adb |
|
MD5 | 79a5f9bc8a050d5af0010d3a8b95b525 |
|
BLAKE2b-256 | 78025252918bf0464ca46e6e44f305e3da9ea2406c29fbcda95a8b1fb65ea3ce |
File details
Details for the file adaptivecardbuilder-0.0.9-py3-none-any.whl
.
File metadata
- Download URL: adaptivecardbuilder-0.0.9-py3-none-any.whl
- Upload date:
- Size: 14.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.2.0 pkginfo/1.5.0.1 requests/2.22.0 setuptools/49.2.0.post20200714 requests-toolbelt/0.9.1 tqdm/4.49.0 CPython/3.8.3
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 91a18cfb736348b79dcfca4dd607b897a9f38a00fa368977d89cc6083ce911cf |
|
MD5 | 16028ca3bcd8f9dca5fe91afe2e0b820 |
|
BLAKE2b-256 | 923e7c0584efb35899078fea910fcd22a15a91aa33945b876c55c5b694a87f93 |