Skip to content

Commit

Permalink
Merge pull request #97 from david-lev/flows-01-2025
Browse files Browse the repository at this point in the history
Flows 01 2025
  • Loading branch information
david-lev authored Jan 18, 2025
2 parents 81027da + c56e2ef commit 3d21648
Show file tree
Hide file tree
Showing 9 changed files with 213 additions and 38 deletions.
2 changes: 1 addition & 1 deletion pywa/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ def resolve_flow_json_param(
to_dump = flow_json
else:
raise TypeError(
"`flow_json` must be a FlowJSON object, dict, json string, json file path or json bytes"
f"`flow_json` must be a FlowJSON object, dict, json string, json file path or json bytes. not {type(flow_json)}"
)

if to_dump is not None:
Expand Down
39 changes: 36 additions & 3 deletions pywa/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,8 @@ def create_flow(
categories: tuple[str, ...],
clone_flow_id: str | None = None,
endpoint_uri: str | None = None,
flow_json: str = None,
publish: bool = None,
) -> dict[str, str]:
"""
Create or clone a flow.
Expand All @@ -808,18 +810,49 @@ def create_flow(
categories: The categories of the flow.
clone_flow_id: The ID of the flow to clone.
endpoint_uri: The endpoint URI of the flow.
flow_json: Flow's JSON encoded as string.
publish: Whether to publish the flow. Only works if ``flow_json`` is also provided with valid Flow JSON.
Return example::
{
"id": "<Flow-ID>"
"id": "<Flow-ID>"
"success": True,
"validation_errors": [
{
"error": "INVALID_PROPERTY_VALUE" ,
"error_type": "FLOW_JSON_ERROR",
"message": "Invalid value found for property 'type'.",
"line_start": 10,
"line_end": 10,
"column_start": 21,
"column_end": 34,
"pointers": [
{
"line_start": 10,
"line_end": 10,
"column_start": 21,
"column_end": 34,
"path": "screens [0].layout.children[0].type"
}
]
}
]
}
"""
data = {
"name": name,
"categories": categories,
**({"clone_flow_id": clone_flow_id} if clone_flow_id else {}),
**({"endpoint_uri": endpoint_uri} if endpoint_uri else {}),
**{
k: v
for k, v in {
"clone_flow_id": clone_flow_id,
"endpoint_uri": endpoint_uri,
"flow_json": flow_json,
"publish": publish,
}.items()
if v is not None
},
}
return self._make_request(
method="POST",
Expand Down
60 changes: 47 additions & 13 deletions pywa/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
FlowDetails,
FlowValidationError,
FlowAsset,
CreatedFlow,
)
from .types.sent_message import SentMessage, SentTemplate
from .types.others import InteractiveType
Expand Down Expand Up @@ -2011,52 +2012,85 @@ def send_template(
from_phone_id=sender,
)

# fmt: off
def create_flow(
self,
name: str,
categories: Iterable[FlowCategory | str],
clone_flow_id: str | None = None,
endpoint_uri: str | None = None,
waba_id: str | int | None = None,
) -> str:
flow_json: FlowJSON | dict | str | pathlib.Path | bytes | BinaryIO | None = None,
publish: bool | None = None,
*,
return_only_id: bool = True,
) -> CreatedFlow | str:
"""
Create a flow.
For backward compatibility, when ``flow_json`` is not provided, the method will return the ID of the created flow.
Set ``return_only_id=False`` to return the created flow object instead.
- This method requires the WhatsApp Business account ID to be provided when initializing the client.
- New Flows are created in :class:`FlowStatus.DRAFT` status.
- New Flows are created in :class:`FlowStatus.DRAFT` status unless ``flow_json`` is provided and ``publish`` is True.
- To update the flow json, use :py:func:`~pywa.client.WhatsApp.update_flow`.
- To send a flow, use :py:func:`~pywa.client.WhatsApp.send_flow`.
Args:
name: The name of the flow.
name: The name of the flow (must be unique, can be used later to update and send the flow).
categories: The categories of the flow.
flow_json: The JSON of the flow (optional, if provided, the flow will be created with the provided JSON).
publish: Whether to publish the flow after creating it, only works if ``flow_json`` is provided.
clone_flow_id: The flow ID to clone (optional).
endpoint_uri: The URL of the FlowJSON Endpoint. Starting from Flow 3.0 this property should be
specified only gere. Do not provide this field if you are cloning a Flow with version below 3.0.
waba_id: The WhatsApp Business account ID (Overrides the client's business account ID).
return_only_id: Only for backward compatibility. Switch to False to return the created flow object. ignored when flow_json provided.
Example:
>>> from pywa.types.flows import FlowCategory
>>> from pywa.types.flows import *
>>> wa = WhatsApp(...)
>>> wa.create_flow(
... name='Feedback',
... categories=[FlowCategory.SURVEY, FlowCategory.OTHER]
... categories=[FlowCategory.SURVEY, FlowCategory.OTHER],
... flow_json=FlowJSON(...),
... publish=True,
... )
Returns:
The flow ID.
The created flow or the ID of the created flow (if ``return_only_id`` is True).
Raises:
FlowBlockedByIntegrity: If you can't create a flow because of integrity issues.
"""
return self.api.create_flow(
name=name,
categories=tuple(map(str, categories)),
clone_flow_id=clone_flow_id,
endpoint_uri=endpoint_uri,
waba_id=helpers.resolve_waba_id_param(self, waba_id),
)["id"]
if return_only_id:
if flow_json:
return_only_id = False
else:
warnings.warn(
"The `return_only_id` argument is for backward compatibility and will be removed in a future version.\n"
">>> Set `return_only_id=False` and access the `.id` attribute of the returned object instead.",
DeprecationWarning,
stacklevel=2,
)

created = CreatedFlow.from_dict(
self.api.create_flow(
name=name,
categories=tuple(map(str, categories)),
clone_flow_id=clone_flow_id,
endpoint_uri=endpoint_uri,
waba_id=helpers.resolve_waba_id_param(self, waba_id),
flow_json=helpers.resolve_flow_json_param(flow_json)
if flow_json
else None,
publish=publish,
)
)
if return_only_id:
return created.id
return created

def update_flow_metadata(
self,
Expand Down
27 changes: 17 additions & 10 deletions pywa/types/callback.py
Original file line number Diff line number Diff line change
Expand Up @@ -517,20 +517,19 @@ class FlowButton:
Attributes:
title: Text on the CTA button. e.g ``SignUp``, Up to 20 characters, no emojis)
flow_id: Unique ID of the Flow provided by WhatsApp.
flow_token: Flow token generated by the business to serve as an identifier for data exchange.
flow_id: Unique ID of the Flow provided by WhatsApp (You can provide either ``flow_id`` or ``flow_name``).
flow_token: Flow token generated by the business to serve as an identifier (Default value: ``unused``)
flow_message_version: Version of the flow message. Default is the latest version.
flow_action_type: Type of action to be performed when the user clicks on the CTA button.
flow_action_screen: The ID of the first Screen.
Required when ``flow_action_type`` is ``FlowActionType.NAVIGATE`` (default).
flow_action_payload: The payload to send when the user clicks on the button
(optional, only when ``flow_action_type`` is ``FlowActionType.NAVIGATE``).
flow_action_type: Type of action to be performed when the user clicks on the button.
flow_action_screen: The ID of the screen to navigate to. Required when ``flow_action_type`` is ``FlowActionType.NAVIGATE`` (default).
flow_action_payload: The data to provide to the navigation screen, if the screen requires it.
mode: The mode of the flow. ``FlowStatus.PUBLISHED`` (default) or ``FlowStatus.DRAFT`` (for testing).
flow_name: Name of the Flow provided by WhatsApp (You can provide either ``flow_id`` or ``flow_name``).
"""

title: str
flow_id: str | int
flow_token: str
flow_id: str | int | None = None
flow_token: str | None = None
flow_action_type: (
Literal[FlowActionType.NAVIGATE, FlowActionType.DATA_EXCHANGE] | None
) = None
Expand All @@ -540,6 +539,7 @@ class FlowButton:
utils.Version.FLOW_MSG
)
mode: Literal[FlowStatus.PUBLISHED, FlowStatus.DRAFT] = FlowStatus.PUBLISHED
flow_name: str | None = None

def __post_init__(self):
utils.Version.FLOW_MSG.validate_min_version(str(self.flow_message_version))
Expand All @@ -550,6 +550,12 @@ def __post_init__(self):
raise ValueError(
"flow_action_screen cannot be None when flow_action_type is FlowActionType.NAVIGATE"
)
if (not self.flow_id and not self.flow_name) or (
self.flow_id and self.flow_name
):
raise ValueError(
"Either flow_id or flow_name must be provided, but not both."
)

def to_dict(self) -> dict:
return {
Expand All @@ -558,7 +564,8 @@ def to_dict(self) -> dict:
"mode": self.mode.lower(),
"flow_message_version": str(self.flow_message_version),
"flow_token": self.flow_token,
"flow_id": self.flow_id,
"flow_id" if self.flow_id else "flow_name": self.flow_id
or self.flow_name,
"flow_cta": self.title,
**(
{"flow_action": str(self.flow_action_type)}
Expand Down
29 changes: 29 additions & 0 deletions pywa/types/flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"FlowPreview",
"FlowValidationError",
"FlowAsset",
"CreatedFlow",
"FlowJSON",
"Screen",
"ScreenData",
Expand Down Expand Up @@ -916,6 +917,34 @@ def from_dict(cls, data: dict):
)


@dataclasses.dataclass(frozen=True, slots=True)
class CreatedFlow:
"""
Represents a created flow.
Attributes:
id: The ID of the created flow.
success: Whether the flow json is valid (Only if created with flow json).
validation_errors: The validation errors of the flow json. Only available if success is ``False``.
"""

id: str
success: bool
validation_errors: tuple[FlowValidationError, ...] | None

@classmethod
def from_dict(cls, data: dict):
return cls(
id=data["id"],
success=data.get("success", True),
validation_errors=tuple(
FlowValidationError.from_dict(e)
for e in data.get("validation_errors", [])
)
or None,
)


_UNDERSCORE_FIELDS = {
"routing_model",
"data_api_version",
Expand Down
39 changes: 36 additions & 3 deletions pywa_async/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,8 @@ async def create_flow(
categories: tuple[str, ...],
clone_flow_id: str | None = None,
endpoint_uri: str | None = None,
flow_json: str = None,
publish: bool = None,
) -> dict[str, str]:
"""
Create or clone a flow.
Expand All @@ -798,18 +800,49 @@ async def create_flow(
categories: The categories of the flow.
clone_flow_id: The ID of the flow to clone.
endpoint_uri: The endpoint URI of the flow.
flow_json: Flow's JSON encoded as string.
publish: Whether to publish the flow. Only works if ``flow_json`` is also provided with valid Flow JSON.
Return example::
{
"id": "<Flow-ID>"
"id": "<Flow-ID>"
"success": True,
"validation_errors": [
{
"error": "INVALID_PROPERTY_VALUE" ,
"error_type": "FLOW_JSON_ERROR",
"message": "Invalid value found for property 'type'.",
"line_start": 10,
"line_end": 10,
"column_start": 21,
"column_end": 34,
"pointers": [
{
"line_start": 10,
"line_end": 10,
"column_start": 21,
"column_end": 34,
"path": "screens [0].layout.children[0].type"
}
]
}
]
}
"""
data = {
"name": name,
"categories": categories,
**({"clone_flow_id": clone_flow_id} if clone_flow_id else {}),
**({"endpoint_uri": endpoint_uri} if endpoint_uri else {}),
**{
k: v
for k, v in {
"clone_flow_id": clone_flow_id,
"endpoint_uri": endpoint_uri,
"flow_json": flow_json,
"publish": publish,
}.items()
if v is not None
},
}
return await self._make_request(
method="POST",
Expand Down
Loading

0 comments on commit 3d21648

Please sign in to comment.