diff --git a/docs/user/cards.rst b/docs/user/cards.rst index 9a7352f..bc48550 100644 --- a/docs/user/cards.rst +++ b/docs/user/cards.rst @@ -8,7 +8,7 @@ Webex supports `AdaptiveCards `_ 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 built into the webexpythonsdk that +In this guide we want to cover the abstraction built into the webexpythonsdk that lets you author adaptive cards in pure python without having to touch the underlying JSON of an adaptive card. @@ -22,7 +22,7 @@ Lets dive into a simple example that sends a card to a room from webexpythonsdk import WebexAPI from webexpythonsdk.models.cards.card import AdaptiveCard from webexpythonsdk.models.cards.inputs import Text, Number - from webexpythonsdk.models.cards.components import TextBlock + from webexpythonsdk.models.cards.card_elements import TextBlock from webexpythonsdk.models.cards.actions import Submit greeting = TextBlock("Hey hello there! I am a adaptive card") diff --git a/docs/user/migrate.rst b/docs/user/migrate.rst index e8963e3..d4c3b61 100644 --- a/docs/user/migrate.rst +++ b/docs/user/migrate.rst @@ -8,7 +8,7 @@ Migration This *should* 🤞 be easy! -``webexpythonsdk`` is designed to be a drop-in replacement for the ``webexteamssdk`` package. The SDK interface and data objects are largely unchanged with only a few minor name changes. +The transition from `webexteamssdk` to `webexpythonsdk` is not entirely a "drop-in replacement" due to substantial changes in class structures and functionalities. This guide aims to clarify these changes and offer solutions to ease the migration process. Major changes that you should be aware of: @@ -17,7 +17,6 @@ Major changes that you should be aware of: * The primary API object has changed from ``WebexTeamsAPI`` to ``WebexAPI`` - --------------- Migration Guide --------------- @@ -39,7 +38,9 @@ The following table summarizes the name changes that need to be made to migrate *Note:* The old ``WEBEX_TEAMS_ACCESS_TOKEN`` environment variable should continue to work with the new package; however, you will receive a deprecation warning. It is recommended to update the environment variable name to ``WEBEX_ACCESS_TOKEN``. -**Doing a quick search-and-replace in your codebase should be all you need to do to migrate.** + + +**Doing a quick search-and-replace in your codebase will help when migrating.** Detailed Steps -------------- @@ -64,6 +65,80 @@ Detailed Steps **Primary API Object:** Replace all instances of ``WebexTeamsAPI`` with ``WebexAPI``. +Key Changes For Adaptive Cards +------------------------------ + +Module and Class Changes +~~~~~~~~~~~~~~~~~~~~~~~~ + +The following table outlines the changes in module and class names: + +.. list-table:: + :widths: 25 25 50 + :header-rows: 1 + + * - Old Module/Class + - New Module/Class + - Example Usage + * - `webexteamssdk.models.cards.components.TextBlock` + - `webexpythonsdk.models.cards.card_elements.TextBlock` + - `TextBlock(color=Colors.light)` + * - `webexteamssdk.models.cards.container.ColumnSet` + - `webexpythonsdk.models.cards.containers.ColumnSet` + - `ColumnSet(columns=[Column()])` + * - `webexteamssdk.models.cards.components.Image` + - `webexpythonsdk.models.cards.card_elements.Image` + - `Image(url="https://example.com/image.jpg")` + * - `webexteamssdk.models.cards.components.Choice` + - `webexpythonsdk.models.cards.inputs.Choice` + - `Choice(title="Option", value="option")` + * - `webexteamssdk.models.cards.options.BlockElementHeight` + - `webexpythonsdk.models.cards.options.BlockElementHeight` + - `BlockElementHeight(height="stretch")` + * - New Imports + - `webexpythonsdk.models.cards.actions.OpenUrl`, `Submit`, `ShowCard` + - `OpenUrl(url="https://example.com")` + * - New Imports + - `webexpythonsdk.models.cards.types.BackgroundImage` + - `BackgroundImage(url="https://example.com/image.jpg")` + +Enums and Case Sensitivity +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Attributes now require specific enums for values, which are case-sensitive. For example: + +- **Previous**: `TextBlock.color = "Light"` +- **New**: `TextBlock.color = Colors.light` + +Refer to the `Adaptive Cards TextBlock documentation `_ for valid enum values. + +Compatibility Solutions +----------------------- + +Wrapper Classes +~~~~~~~~~~~~~~~ + +To facilitate backward compatibility, consider using the following wrapper classes: + +.. code-block:: python + + # Example wrapper for components.py + from webexpythonsdk.models.cards.card_elements import TextBlock, Image + from webexpythonsdk.models.cards.containers import Column, Fact + + # Example wrapper for container.py + from webexpythonsdk.models.cards.containers import Container, ColumnSet, FactSet + +Module Flag for Compatibility +----------------------------- + +A module flag can be introduced to bypass the `validate_input` function where backward compatibility is needed. Ensure this flag is set before executing legacy code. + +.. code-block:: python + + # Example usage + webexpythonsdk.enable_backward_compatibility(True) + ---------------- For Contributors ---------------- @@ -95,6 +170,7 @@ Project changes that you should be aware of: +-------------------------------------+-------------------------------+ + *Copyright (c) 2016-2024 Cisco and/or its affiliates.* diff --git a/src/webexpythonsdk/models/cards/__init__.py b/src/webexpythonsdk/models/cards/__init__.py index 3136650..d99fc42 100644 --- a/src/webexpythonsdk/models/cards/__init__.py +++ b/src/webexpythonsdk/models/cards/__init__.py @@ -22,11 +22,9 @@ """ from webexpythonsdk.models.cards.adaptive_card_component import ( - AdaptiveCardComponent -) -from webexpythonsdk.models.cards.cards import ( - AdaptiveCard + AdaptiveCardComponent, ) +from webexpythonsdk.models.cards.cards import AdaptiveCard from webexpythonsdk.models.cards.card_elements import ( TextBlock, Image, diff --git a/src/webexpythonsdk/models/cards/actions.py b/src/webexpythonsdk/models/cards/actions.py index 9f1462c..a391172 100644 --- a/src/webexpythonsdk/models/cards/actions.py +++ b/src/webexpythonsdk/models/cards/actions.py @@ -153,9 +153,7 @@ def __init__( super().__init__( serializable_properties=[ - *( - ["fallback"] if hasattr(fallback, "to_dict") else [] - ), + *(["fallback"] if hasattr(fallback, "to_dict") else []), ], simple_properties=[ "type", @@ -164,9 +162,7 @@ def __init__( "iconUrl", "id", "style", - *( - [] if hasattr(fallback, "to_dict") else ["fallback"] - ), + *([] if hasattr(fallback, "to_dict") else ["fallback"]), "requires", ], ) @@ -246,7 +242,7 @@ def __init__( str, object, ), - optional=True + optional=True, ) validate_input( @@ -315,9 +311,7 @@ def __init__( super().__init__( serializable_properties=[ - *( - ["fallback"] if hasattr(fallback, "to_dict") else [] - ), + *(["fallback"] if hasattr(fallback, "to_dict") else []), ], simple_properties=[ "type", @@ -327,9 +321,7 @@ def __init__( "iconUrl", "id", "style", - *( - [] if hasattr(fallback, "to_dict") else ["fallback"] - ), + *([] if hasattr(fallback, "to_dict") else ["fallback"]), "requires", ], ) @@ -461,9 +453,7 @@ def __init__( super().__init__( serializable_properties=[ "card", - *( - ["fallback"] if hasattr(fallback, "to_dict") else [] - ), + *(["fallback"] if hasattr(fallback, "to_dict") else []), ], simple_properties=[ "type", @@ -471,9 +461,7 @@ def __init__( "iconUrl", "id", "style", - *( - [] if hasattr(fallback, "to_dict") else ["fallback"] - ), + *([] if hasattr(fallback, "to_dict") else ["fallback"]), "requires", ], ) @@ -608,9 +596,7 @@ def __init__( super().__init__( serializable_properties=[ "targetElements", - *( - ["fallback"] if hasattr(fallback, "to_dict") else [] - ), + *(["fallback"] if hasattr(fallback, "to_dict") else []), ], simple_properties=[ "type", @@ -618,9 +604,7 @@ def __init__( "iconUrl", "id", "style", - *( - [] if hasattr(fallback, "to_dict") else ["fallback"] - ), + *([] if hasattr(fallback, "to_dict") else ["fallback"]), "requires", ], ) diff --git a/src/webexpythonsdk/models/cards/card_elements.py b/src/webexpythonsdk/models/cards/card_elements.py index 6507e93..f6d86aa 100644 --- a/src/webexpythonsdk/models/cards/card_elements.py +++ b/src/webexpythonsdk/models/cards/card_elements.py @@ -267,9 +267,7 @@ def __init__( super().__init__( serializable_properties=[ - *( - ["fallback"] if hasattr(fallback, "to_dict") else [] - ), + *(["fallback"] if hasattr(fallback, "to_dict") else []), ], simple_properties=[ "type", @@ -282,9 +280,7 @@ def __init__( "size", "weight", "wrap", - *( - [] if hasattr(fallback, "to_dict") else ["fallback"] - ), + *([] if hasattr(fallback, "to_dict") else ["fallback"]), "height", "separator", "spacing", @@ -531,9 +527,7 @@ def __init__( super().__init__( serializable_properties=[ "selectAction", - *( - ["fallback"] if hasattr(fallback, "to_dict") else [] - ), + *(["fallback"] if hasattr(fallback, "to_dict") else []), ], simple_properties=[ "type", @@ -545,9 +539,7 @@ def __init__( "size", "style", "width", - *( - [] if hasattr(fallback, "to_dict") else ["fallback"] - ), + *([] if hasattr(fallback, "to_dict") else ["fallback"]), "separator", "spacing", "id", @@ -726,17 +718,13 @@ def __init__( super().__init__( serializable_properties=[ "sources", - *( - ["fallback"] if hasattr(fallback, "to_dict") else [] - ), + *(["fallback"] if hasattr(fallback, "to_dict") else []), ], simple_properties=[ "type", "poster", "altText", - *( - [] if hasattr(fallback, "to_dict") else ["fallback"] - ), + *([] if hasattr(fallback, "to_dict") else ["fallback"]), "height", "separator", "spacing", @@ -954,16 +942,12 @@ def __init__( super().__init__( serializable_properties=[ "inlines", - *( - ["fallback"] if hasattr(fallback, "to_dict") else [] - ), + *(["fallback"] if hasattr(fallback, "to_dict") else []), ], simple_properties=[ "type", "horizontalAlignment", - *( - [] if hasattr(fallback, "to_dict") else ["fallback"] - ), + *([] if hasattr(fallback, "to_dict") else ["fallback"]), "height", "separator", "spacing", diff --git a/src/webexpythonsdk/models/cards/cards.py b/src/webexpythonsdk/models/cards/cards.py index 5e44d85..64f17fc 100644 --- a/src/webexpythonsdk/models/cards/cards.py +++ b/src/webexpythonsdk/models/cards/cards.py @@ -219,7 +219,8 @@ def __init__( "actions", "selectAction", *( - ["backgroundImage"] if hasattr(backgroundImage, "to_dict") + ["backgroundImage"] + if hasattr(backgroundImage, "to_dict") else [] ), ], @@ -228,7 +229,8 @@ def __init__( "version", "fallbackText", *( - [] if hasattr(backgroundImage, "to_dict") + [] + if hasattr(backgroundImage, "to_dict") else ["backgroundImage"] ), "minHeight", diff --git a/src/webexpythonsdk/models/cards/containers.py b/src/webexpythonsdk/models/cards/containers.py index f5e5294..3bf5659 100644 --- a/src/webexpythonsdk/models/cards/containers.py +++ b/src/webexpythonsdk/models/cards/containers.py @@ -186,15 +186,11 @@ def __init__( super().__init__( serializable_properties=[ "actions", - *( - ["fallback"] if hasattr(fallback, "to_dict") else [] - ), + *(["fallback"] if hasattr(fallback, "to_dict") else []), ], simple_properties=[ "type", - *( - [] if hasattr(fallback, "to_dict") else ["fallback"] - ), + *([] if hasattr(fallback, "to_dict") else ["fallback"]), "height", "separator", "spacing", @@ -455,12 +451,11 @@ def __init__( "items", "selectAction", *( - ["backgroundImage"] if hasattr(backgroundImage, "to_dict") + ["backgroundImage"] + if hasattr(backgroundImage, "to_dict") else [] ), - *( - ["fallback"] if hasattr(fallback, "to_dict") else [] - ), + *(["fallback"] if hasattr(fallback, "to_dict") else []), ], simple_properties=[ "type", @@ -468,13 +463,12 @@ def __init__( "verticalContentAlignment", "bleed", *( - [] if hasattr(backgroundImage, "to_dict") + [] + if hasattr(backgroundImage, "to_dict") else ["backgroundImage"] ), "minHeight", - *( - [] if hasattr(fallback, "to_dict") else ["fallback"] - ), + *([] if hasattr(fallback, "to_dict") else ["fallback"]), "height", "separator", "spacing", @@ -504,7 +498,7 @@ def __init__( minHeight: str = None, horizontalAlignment: OPTIONS.HorizontalAlignment = None, fallback: object = None, - height: OPTIONS.BlockElementHeight=None, + height: OPTIONS.BlockElementHeight = None, separator: bool = None, spacing: OPTIONS.Spacing = None, id: str = None, @@ -698,9 +692,7 @@ def __init__( serializable_properties=[ "columns", "selectAction", - *( - ["fallback"] if hasattr(fallback, "to_dict") else [] - ), + *(["fallback"] if hasattr(fallback, "to_dict") else []), ], simple_properties=[ "type", @@ -708,9 +700,7 @@ def __init__( "bleed", "minHeight", "horizontalAlignment", - *( - [] if hasattr(fallback, "to_dict") else ["fallback"] - ), + *([] if hasattr(fallback, "to_dict") else ["fallback"]), "height", "separator", "spacing", @@ -959,24 +949,22 @@ def __init__( serializable_properties=[ "items", *( - ["backgroundImage"] if hasattr(backgroundImage, "to_dict") + ["backgroundImage"] + if hasattr(backgroundImage, "to_dict") else [] ), - *( - ["fallback"] if hasattr(fallback, "to_dict") else [] - ), + *(["fallback"] if hasattr(fallback, "to_dict") else []), "selectAction", ], simple_properties=[ "type", *( - [] if hasattr(backgroundImage, "to_dict") + [] + if hasattr(backgroundImage, "to_dict") else ["backgroundImage"] ), "bleed", - *( - [] if hasattr(fallback, "to_dict") else ["fallback"] - ), + *([] if hasattr(fallback, "to_dict") else ["fallback"]), "minHeight", "separator", "spacing", @@ -1135,15 +1123,11 @@ def __init__( super().__init__( serializable_properties=[ "facts", - *( - ["fallback"] if hasattr(fallback, "to_dict") else [] - ), + *(["fallback"] if hasattr(fallback, "to_dict") else []), ], simple_properties=[ "type", - *( - [] if hasattr(fallback, "to_dict") else ["fallback"] - ), + *([] if hasattr(fallback, "to_dict") else ["fallback"]), "height", "separator", "id", @@ -1215,7 +1199,7 @@ def __init__( height: OPTIONS.BlockElementHeight = None, separator: bool = None, spacing: OPTIONS.Spacing = None, - id: str = None, + id: str = None, isVisible: bool = True, requires: dict[str, str] = None, ): @@ -1357,16 +1341,12 @@ def __init__( super().__init__( serializable_properties=[ "images", - *( - ["fallback"] if hasattr(fallback, "to_dict") else [] - ), + *(["fallback"] if hasattr(fallback, "to_dict") else []), ], simple_properties=[ "type", "imageSize", - *( - [] if hasattr(fallback, "to_dict") else ["fallback"] - ), + *([] if hasattr(fallback, "to_dict") else ["fallback"]), "height", "separator", "spacing", diff --git a/src/webexpythonsdk/models/cards/inputs.py b/src/webexpythonsdk/models/cards/inputs.py index 72a2c79..b1b2233 100644 --- a/src/webexpythonsdk/models/cards/inputs.py +++ b/src/webexpythonsdk/models/cards/inputs.py @@ -277,9 +277,7 @@ def __init__( super().__init__( serializable_properties=[ "inlineAction", - *( - ["fallback"] if hasattr(fallback, "to_dict") else [] - ), + *(["fallback"] if hasattr(fallback, "to_dict") else []), ], simple_properties=[ "type", @@ -293,9 +291,7 @@ def __init__( "errorMessage", "isRequired", "label", - *( - [] if hasattr(fallback, "to_dict") else ["fallback"] - ), + *([] if hasattr(fallback, "to_dict") else ["fallback"]), "height", "separator", "spacing", @@ -506,9 +502,7 @@ def __init__( super().__init__( serializable_properties=[ - *( - ["fallback"] if hasattr(fallback, "to_dict") else [] - ), + *(["fallback"] if hasattr(fallback, "to_dict") else []), ], simple_properties=[ "type", @@ -520,9 +514,7 @@ def __init__( "errorMessage", "isRequired", "label", - *( - [] if hasattr(fallback, "to_dict") else ["fallback"] - ), + *([] if hasattr(fallback, "to_dict") else ["fallback"]), "height", "separator", "spacing", @@ -733,9 +725,7 @@ def __init__( super().__init__( serializable_properties=[ - *( - ["fallback"] if hasattr(fallback, "to_dict") else [] - ), + *(["fallback"] if hasattr(fallback, "to_dict") else []), ], simple_properties=[ "type", @@ -747,9 +737,7 @@ def __init__( "errorMessage", "isRequired", "label", - *( - [] if hasattr(fallback, "to_dict") else ["fallback"] - ), + *([] if hasattr(fallback, "to_dict") else ["fallback"]), "height", "separator", "spacing", @@ -777,7 +765,7 @@ def __init__( value: str = None, errorMessage: str = None, isRequired: bool = None, - label: str = None, + label: str = None, fallback: object = None, height: OPTIONS.BlockElementHeight = None, separator: bool = None, @@ -960,9 +948,7 @@ def __init__( super().__init__( serializable_properties=[ - *( - ["fallback"] if hasattr(fallback, "to_dict") else [] - ), + *(["fallback"] if hasattr(fallback, "to_dict") else []), ], simple_properties=[ "id", @@ -974,9 +960,7 @@ def __init__( "errorMessage", "isRequired", "label", - *( - [] if hasattr(fallback, "to_dict") else ["fallback"] - ), + *([] if hasattr(fallback, "to_dict") else ["fallback"]), "height", "separator", "spacing", @@ -1196,9 +1180,7 @@ def __init__( super().__init__( serializable_properties=[ - *( - ["fallback"] if hasattr(fallback, "to_dict") else [] - ), + *(["fallback"] if hasattr(fallback, "to_dict") else []), ], simple_properties=[ "type", @@ -1211,9 +1193,7 @@ def __init__( "errorMessage", "isRequired", "label", - *( - [] if hasattr(fallback, "to_dict") else ["fallback"] - ), + *([] if hasattr(fallback, "to_dict") else ["fallback"]), "height", "separator", "spacing", @@ -1450,9 +1430,7 @@ def __init__( super().__init__( serializable_properties=[ "choices", - *( - ["fallback"] if hasattr(fallback, "to_dict") else [] - ), + *(["fallback"] if hasattr(fallback, "to_dict") else []), ], simple_properties=[ "type", @@ -1465,9 +1443,7 @@ def __init__( "errorMessage", "isRequired", "label", - *( - [] if hasattr(fallback, "to_dict") else ["fallback"] - ), + *([] if hasattr(fallback, "to_dict") else ["fallback"]), "height", "separator", "spacing", diff --git a/src/webexpythonsdk/models/cards/utils.py b/src/webexpythonsdk/models/cards/utils.py index 2b850fc..1ee2594 100644 --- a/src/webexpythonsdk/models/cards/utils.py +++ b/src/webexpythonsdk/models/cards/utils.py @@ -27,11 +27,11 @@ def check_type( - obj: object, - acceptable_types: Any, - optional: bool = False, - is_list: bool = False, - ): + obj: object, + acceptable_types: Any, + optional: bool = False, + is_list: bool = False, +): """ Object is an instance of one of the acceptable types or None. @@ -59,7 +59,7 @@ def check_type( "We were expecting to receive a list of objects of the " "following types: " f"{', '.join([repr(t.__name__) for t in acceptable_types])}" - f"{' or \'None\'' if optional else ''}; instead we received " + f"{' or None' if optional else ''}; instead we received " f"{obj} which is a {repr(type(obj).__name__)}." ) raise TypeError(error_message) @@ -70,7 +70,7 @@ def check_type( "We were expecting to receive an object of one of the " "following types: " f"{', '.join(repr(t.__name__) for t in acceptable_types)}" - f"{' or \'None\'' if optional else ''}; instead we " + f"{' or None' if optional else ''}; instead we " f"received {o} which is a {repr(type(o).__name__)}." ) raise TypeError(error_message) @@ -82,7 +82,7 @@ def check_type( error_message = ( "We were expecting to receive an instance of one of the following " f"types: {', '.join(repr(t.__name__) for t in acceptable_types)}" - f"{' or \'None\'' if optional else ''}; but instead we received " + f"{' or None' if optional else ''}; but instead we received " f"{obj} which is a {repr(type(obj).__name__)}." ) @@ -90,10 +90,10 @@ def check_type( def validate_input( - input_value: Any, - allowed_values: Any, - optional: bool = False, - ): + input_value: Any, + allowed_values: Any, + optional: bool = False, +): """ Validate if the input value is in the tuple of allowed values. @@ -117,9 +117,7 @@ def validate_input( expected_values = tuple( f"{item.__class__.__name__}.{item.name}" for item in allowed_values ) - allowed_values = tuple( - item.value for item in allowed_values - ) + allowed_values = tuple(item.value for item in allowed_values) # Convert a single string to a tuple of one string if isinstance(allowed_values, str): @@ -141,18 +139,18 @@ def validate_input( if value_to_check not in allowed_values: raise ValueError( f"Invalid value: '{input_value}'. " - f"Must be one of {expected_values}." + f"Must be one of '{expected_values}'." ) return def validate_dict_str( - input_value: Any, - key_type: Type, - value_type: Type, - optional: bool = False, - ): + input_value: Any, + key_type: Type, + value_type: Type, + optional: bool = False, +): """ Validate that the input is a dictionary and that all keys and values in the dictionary are of the specified types. @@ -201,9 +199,9 @@ class URIException(Exception): def validate_uri( - uri: Any, - optional=False, - ): + uri: Any, + optional=False, +): """ Validate the given URI and raise an exception if it is invalid. @@ -228,8 +226,8 @@ def validate_uri( if not parsed_uri.scheme: raise URIException("Invalid URI: Missing scheme") - # Check if the URI has a heir-part location - if not parsed_uri.netloc: + # Check if the URI has a heir-part location if scheme isn't "data" + if parsed_uri.scheme != "data" and not parsed_uri.netloc: raise URIException("Invalid URI: Missing heir part location") # Return if every check is passed diff --git a/tests/api/test_messages.py b/tests/api/test_messages.py index 67d1043..7b82310 100644 --- a/tests/api/test_messages.py +++ b/tests/api/test_messages.py @@ -364,18 +364,21 @@ def test_get_message_by_id(api, group_room_text_message): message = api.messages.get(group_room_text_message.id) assert is_valid_message(message) + def test_delete_message(api, group_room, send_group_room_message): text = create_string("Message") message = api.messages.create(group_room.id, text=text) assert is_valid_message(message) api.messages.delete(message.id) + def test_edit_message(api, group_room): text = create_string("Edit this Message") message = api.messages.create(group_room.id, text=text) text = create_string("Message Edited") assert text == api.messages.edit(message.id, group_room.id, text).text + def test_update_message(api, group_room): text = create_string("Update this Message") message = api.messages.create(group_room.id, text=text)