Package: adaptivecardbuilder
Python Adaptive Card BuilderEasily Build and Export Multilingual Adaptive Cards Through PythonProgrammatically construct adaptive cards like Lego, without the learning curve of Adaptive Card 'Templating'Avoid the curly-braces jungle of traditional JSON editingBuild pythonically, but with minimal abstraction while preserving readabilityOutput built cards to JSON or a Python Dictionary in a single method callAuto-translate all text elements in a card with a single method callCombine multiple individual cards through the + operatorView this package on pypihttps://pypi.org/project/adaptivecardbuilder/Installation via pippipinstalladaptivecardbuilderLearn 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":fromadaptivecardbuilderimport*# initialize cardcard=AdaptiveCard()# Add a textblockcard.add(TextBlock(text="0.45 miles away",separator="true",spacing="large"))# add column setcard.add(ColumnSet())# First column contentscard.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 setcard.up_one_level()# Second column contentscard.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-linerawaitcard.to_json()Output when rendered inhttps://adaptivecards.io/visualizer/:A "Visual" AlternativeTheAdaptiveCardclass also supports a more visual approach to building cards by passing a list of elements to theadd()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 treeStrings containing"^"will move us back to the top of the treecard=AdaptiveCard()# Add a list of elementscard.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")])awaitcard.to_json()Output when rendered inhttps://adaptivecards.io/visualizer/:Combining/Chaining CardsWe can also combine the contents of multiple cards through the+operator:defcreate_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")])returncard# Use above function to create cardscard1=create_single_card(1)card2=create_single_card(2)# Add the contents of card1 and card2combined_card=card1+card2awaitcombined_card.to_json()Output when rendered inhttps://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.Thecombine_adaptive_cardsfunction 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 cardscombined_card=combine_adaptive_cards([card1,card2,card3])awaitcombined_card.to_json()Translating Card ElementsPassing translator arguments to theto_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 Malayawaitcard.to_json(translator_to_lang='ms',translator_key='<YOUR AZURE API KEY>')If anytranslator_to_langargument is passed, translation will apply to all elements with translatable text attributes.To specify that a given Adaptive elementshould notbe translated, simply pass the keyworded argumentdont_translate=Trueduring the construction of any element, and AdaptiveCardBuilder will leave this specific element untranslated.ConceptsTheAdaptiveCardclass centrally handles all construction & element-addition operations:fromadaptivecardbuilderimport*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# |   |--Actionscard_json=awaitcard.to_json()# output to jsonWhen 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 athttps://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 LogicCentral to theAdaptiveCardclass is an internal_pointerattribute. When we add an element to the card, the element is by defaultadded to the item containerof whichever object is being pointed at.Conceptually, an adaptive object (e.g. Column, Container) can have up to two kinds of containers (pythonlists):Itemcontainers (these hold non-interactive elements like TextBlocks, Images)Actioncontainers (these hold interactive actions like ActionShowUrl, ActionSubmit)For instance:AdaptiveCardobjects have bothitem(body=[]) andaction(actions=[]) containersColumnSetobjects have a singleitem(columns=[]) containerColumnobjects have a singleitem(items=[]) containerActionSetobjects have a singleaction(actions=[]) containerThecard.add()method will add a given AdaptiveObject to the appropriate container. For instance, if an Action-type object is passed, such as aActionSubmitorActionOpenUrl, then this will be added to the parent object'sactioncontainer.If the parent object does not have the appropriate container for the element being added, then this will throw anAssertionErrorand a corresponding suggestion.Recursing Into an Added ElementWhen adding elements that canthemselvescontain other elements(e.g. column sets and columns), the pointer will by defaultrecurse 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 adepth-firstapproach 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# |   |--Actionscard.add(TextBlock(text="Subheader"))card.add(TextBlock(text="*Quote*",isSubtle="true"))# |--Card               <- Pointer# |   |--Schema="XXX"# |   |--Version="1.0"# |   |--Body# |       |--TextBlock# |       |--TextBlock     <- added# |       |--TextBlock     <- added# |   |--Actionscard.add(ColumnSet())# |--Card# |   |--Schema="XXX"# |   |--Version="1.0"# |   |--Body# |       |--TextBlock# |       |--TextBlock# |       |--TextBlock# |       |--ColumnSet        <- Pointer <- added# |   |--Actionscard.add(Column(width=1))# |--Card# |   |--Schema="XXX"# |   |--Version="1.0"# |   |--Body# |       |--TextBlock# |       |--TextBlock# |       |--TextBlock# |       |--ColumnSet# |           |--Column         <- Pointer <- added# |   |--Actionscard.add(TextBlock(text="<Column 1 Contents>"))# |--Card# |   |--Schema="XXX"# |   |--Version="1.0"# |   |--Body# |       |--TextBlock# |       |--TextBlock# |       |--TextBlock# |       |--ColumnSet# |           |--Column         <- Pointer# |               |--TextBlock  <- added# |   |--ActionsRendered:Observe that when adding aTextBlockto aColumn's items, the pointer stays at theColumnlevel, rather than recursing into theTextBlock. Theadd()method will only recurse into the added element if it has anitemoractioncontainer within it.Because of the depth-first approach, we'll need toback ourselves outof a container once we are done adding elements to it.One easy method to doing so is by using theup_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# |   |--Actionscard.up_one_level()# |--Card# |   |--Schema="XXX"# |   |--Version="1.0"# |   |--Body# |       |--TextBlock# |       |--TextBlock# |       |--TextBlock# |       |--ColumnSet          <- Pointer# |           |--Column# |               |--TextBlock# |   |--Actionscard.add(Column(width=1))# |--Card# |   |--Schema="XXX"# |   |--Version="1.0"# |   |--Body# |       |--TextBlock# |       |--TextBlock# |       |--TextBlock# |       |--ColumnSet# |           |--Column# |               |--TextBlock# |           |--Column         <- Pointer <- added# |   |--Actionscard.add(TextBlock(text="Column 2 Contents"))# |--Card# |   |--Schema="XXX"# |   |--Version="1.0"# |   |--Body# |       |--TextBlock# |       |--TextBlock# |       |--TextBlock# |       |--ColumnSet# |           |--Column# |               |--TextBlock# |           |--Column         <- Pointer# |               |--TextBlock  <- added# |   |--ActionsRendered:We can also use thecard.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, usecard.load_level(checkpoint).# checkpoints examplecard=AdaptiveCard()card.add(Container())card.add(TextBlock(text="Text as the first item, at the container level"))# create checkpoint herecontainer_level=card.save_level()# add nested columnsets and columns for funforiinrange(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 levelcard.load_level(container_level)card.add(TextBlock(text="Text at the container level, below all the nested containers"))awaitcard.to_json()Adding ActionsAs previously mentioned, the AdaptiveCard'sadd()method will automatically add action elements to the appropriate containers.Let's first move our pointer back to the top level using theback_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# |   |--ActionsOur pointer is now pointing at the main Card object.Because it has anActionscontainer, the next action element to be added will be sent there.# Adding single url actioncard.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