Skip to content

Releases: david-lev/pywa

2.0.2

30 Oct 15:13
Compare
Choose a tag to compare

What's Changed

Update with pip: pip3 install -U pywa

Important

Breaking Changes! Please see the Migration Guide for instructions on updating from earlier versions.

  • [listeners]: Listeners are a new way to handle incoming user updates (messages, callbacks, etc.). They are more flexible, faster, and easier to use than handlers.
  • [sent_message]: The SentMessage object returned by send_message, send_image, etc., contains the message ID and allows to act on the sent message with methods like reply_x, wait_for_x etc.
  • [filters]: Filters are now objects that can be combined using logical operators. They are more powerful and flexible than the previous filter system.
  • [handlers] allow to register callbacks without WhatsApp instance
  • [flows] allow pythonic conditionals in If component
  • [flows]: A new method FlowCompletion.get_media(types.Image, key="img") allows you to construct a media object and perform actions like .download() on it.
  • [flows]: Decrypt media directly from FlowRequest using .decrypt_media(key, index).
  • [flows] rename .data_key and .from_ref to .ref
  • [client]: The client can run without a token but won’t allow API operations (only webhook listening).
  • [server] rely on update hash instead of update id to avid duplicate updates
  • [callback] allow to override callback data id
  • [utils] bump graph-api version to 21.0
# my_handlers.py

from pywa import WhatsApp, filters, types, listeners

# You can register callback without th instance!
@WhatsApp.on_message(filters.text & filters.matches("store")) # combine filters with & operator
def store(_: WhatsApp, m: types.Message):
    try:
        age = m.reply("How old are you?", buttons=[types.Button("Cancel", "cancel")]).wait_for_reply( # wait for a reply
            filters=filters.text & filters.new(lambda _, m: m.text.isdigit()), # create a new filter
            cancelers=filters.callback_button & filters.matches("cancel"), # cancel the listener if the user clicks the cancel button
            timeout=10, # set a timeout
        )
        m.reply(f"Your age is {age.text}")
    except listeners.ListenerCanceled:
        m.reply("You cancelled the store")
    except listeners.ListenerTimeout:
        m.reply("You took too long to reply")

# main.py

from pywa import WhatsApp
from . import my_handlers

wa = WhatsApp(..., handlers_modules=[my_handlers])

# run the server
from pywa.types.flows import *

FlowJSON(
    version=pywa.Version.FLOW_JSON,
    screens=[
        Screen(
            id="WELCOME",
            title="Welcome",
            data=[is_admin := ScreenData(key="is_admin", example=True)],
            layout=Layout(
                children=[
                    animal := TextInput(
                        name="animal",
                        label="Animal",
                        helper_text="Type: cat",
                    ),
                    If(
                        condition=is_admin.ref & (animal.ref == "cat"), # we can use python operators
                        then=[TextHeading(text="It is a cat")],
                        else_=[TextHeading(text="It is not a cat")],
                    ),
                ]
            ),
        )
    ],
)

Full Changelog: 1.26.0...2.0.2

2.0.0-rc.2

29 Oct 18:45
Compare
Choose a tag to compare
2.0.0-rc.2 Pre-release
Pre-release

What's Changed

Update with pip: pip3 install -U pywa==2.0.0-rc.2

Caution

Make sure to read the migration guide before updating to this version!

Important

Please let us know in the Telegram group or the GitHub discussion if you encountered any problems during the migration

Note

Updated documentation to the new version is available here

[listeners]: Listeners are a new way to handle incoming user updates (messages, callbacks, etc.). They are more flexible, faster, and easier to use than handlers.
[filters]: Filters are now objects that can be combined using logical operators. They are more powerful and flexible than the previous filter system.
[handlers]: Now you can register handlers with decorators without the need to use the add_handlers method.
[flows] allow pythonic conditionals in If component.
[flows]: A new method FlowCompletion.get_media(types.Image, key="img") allows you to construct a media object and perform actions like .download() on it.
[flows]: Decrypt media directly from FlowRequest using .decrypt_media(key, index).
[client]: The client can run without a token but won’t allow API operations (only webhook listening).
[sent_message]: The SentMessage object returned by send_message, send_image, etc., contains the message ID and allows to act on the sent message with methods like reply_x, wait_for_x etc.
[callback] allow to override callback data id
[docs] docs updates

# my_handlers.py

from pywa import WhatsApp, filters, types, listeners

# You can register callback without th instance!
@WhatsApp.on_message(filters.text & filters.matches("store")) # combine filters with & operator
def store(_: WhatsApp, m: types.Message):
    try:
        age = m.reply("How old are you?", buttons=[types.Button("Cancel", "cancel")]).wait_for_reply( # wait for a reply
            filters=filters.text & filters.new(lambda _, m: m.text.isdigit()), # create a new filter
            cancelers=filters.callback_button & filters.matches("cancel"), # cancel the listener if the user clicks the cancel button
            timeout=10, # set a timeout
        )
        m.reply(f"Your age is {age.text}")
    except listeners.ListenerCanceled:
        m.reply("You cancelled the store")
    except listeners.ListenerTimeout:
        m.reply("You took too long to reply")

# main.py

from pywa import WhatsApp
from . import my_handlers

wa = WhatsApp(..., handlers_modules=[my_handlers])

# run the server
from pywa.types.flows import *

FlowJSON(
    version=pywa.Version.FLOW_JSON,
    screens=[
        Screen(
            id="WELCOME",
            title="Welcome",
            data=[is_admin := ScreenData(key="is_admin", example=True)],
            layout=Layout(
                children=[
                    animal := TextInput(
                        name="animal",
                        label="Animal",
                        helper_text="Type: cat",
                    ),
                    If(
                        condition=is_admin.ref & (animal.ref == "cat"), # we can use python operators
                        then=[TextHeading(text="It is a cat")],
                        else_=[TextHeading(text="It is not a cat")],
                    ),
                ]
            ),
        )
    ],
)

Full Changelog: 1.26.0...2.0.0-rc.2

2.0.0-rc.1

27 Oct 14:22
Compare
Choose a tag to compare
2.0.0-rc.1 Pre-release
Pre-release

What's Changed

Update with pip: pip3 install -U pywa==2.0.0-rc.1

Caution

Make sure to read the migration guide before updating to this version!

Important

Please let us know in the Telegram group or the GitHub discussion if you encountered any problems during the migration

Note

Updated documentation to the new version is available here

[listeners]: Listeners are a new way to handle incoming user updates (messages, callbacks, etc.). They are more flexible, faster, and easier to use than handlers.
[filters]: Filters are now objects that can be combined using logical operators. They are more powerful and flexible than the previous filter system.
[handlers]: Now you can register handlers with decorators without the need to use the add_handlers method.
[flows]: A new method FlowCompletion.get_media(types.Image, key="img") allows you to construct a media object and perform actions like .download() on it.
[flows]: Decrypt media directly from FlowRequest using .decrypt_media(key, index).
[client]: The client can run without a token but won’t allow API operations (only webhook listening).
[sent_message]: The SentMessage object returned by send_message, send_image, etc., contains the message ID and allows to act on the sent message with methods like reply_x, wait_for_x etc.
[callback] allow to override callback data id
[docs] docs updates

# my_handlers.py

from pywa import WhatsApp, filters, types, listeners

# You can register callback without th instance!
@WhatsApp.on_message(filters.text & filters.matches("store")) # combine filters with & operator
def store(_: WhatsApp, m: types.Message):
    try:
        age = m.reply("How old are you?", buttons=[types.Button("Cancel", "cancel")]).wait_for_reply( # wait for a reply
            filters=filters.text & filters.new(lambda _, m: m.text.isdigit()), # create a new filter
            cancelers=filters.callback_button & filters.matches("cancel"), # cancel the listener if the user clicks the cancel button
            timeout=10, # set a timeout
        )
        m.reply(f"Your age is {age.text}")
    except listeners.ListenerCanceled:
        m.reply("You cancelled the store")
    except listeners.ListenerTimeout:
        m.reply("You took too long to reply")

# main.py

from pywa import WhatsApp
from . import my_handlers

wa = WhatsApp(..., handlers_modules=[my_handlers])

# run the server

Full Changelog: 1.26.0...2.0.0-rc.1

2.0.0-beta.1

25 Oct 13:34
Compare
Choose a tag to compare
2.0.0-beta.1 Pre-release
Pre-release

What's Changed

Update with pip: pip3 install -U pywa==2.0.0-beta.1

Caution

  • Make sure to read the migration guide before updating to this version!
  • This is a beta version, use it with caution!

Important

  • Please let us know in the Telegram group or the GitHub discussion if you encountered any problems during the migration

[listeners]: Listeners are a new way to handle incoming user updates (messages, callbacks, etc.). They are more flexible, faster, and easier to use than handlers.
[filters]: Filters are now objects that can be combined using logical operators. They are more powerful and flexible than the previous filter system.
[handlers]: Now you can register handlers with decorators without the need to use the add_handler method.
[flows]: A new method FlowCompletion.get_media(types.Image, key="img") allows you to construct a media object and perform actions like .download() on it.
[flows]: Decrypt media directly from FlowRequest using .decrypt_media(key, index).
[client]: The client can run without a token but won’t allow API operations (only webhook listening).
[sent_message]: The SentMessage object returned by send_message, send_image, etc., contains the message ID and allows to act on the sent message with methods like reply_x, wait_for_x etc.

# my_handlers.py

from pywa import WhatsApp, filters, types, listeners

# You can register callback without th instance!
@WhatsApp.on_message(filters.text & filters.matches("store")) # combine filters with & operator
def store(_: WhatsApp, m: types.Message):
    try:
        age = m.reply("How old are you?", buttons=[types.Button("Cancel", "cancel")]).wait_for_reply( # wait for a reply
            filters=filters.text & filters.new(lambda _, m: m.text.isdigit()), # create a new filter
            cancelers=filters.callback_button & filters.matches("cancel"), # cancel the listener if the user clicks the cancel button
            timeout=10, # set a timeout
        )
        m.reply(f"Your age is {age.text}")
    except listeners.ListenerCanceled:
        m.reply("You cancelled the store")
    except listeners.ListenerTimeout:
        m.reply("You took too long to reply")

# main.py

from pywa import WhatsApp
from . import my_handlers

wa = WhatsApp(..., handlers_modules=[my_handlers])

# run the server

Full Changelog: 1.26.0...2.0.0-beta.1

1.26.0

21 Sep 22:55
Compare
Choose a tag to compare

What's Changed

Update with pip: pip3 install -U pywa

  • [flows] adding support of RichText
  • [flows] adding support of markdown in TextBody and TextCaption
  • [flows] adding sensitive attr to Screen, allowing to hide specific fields from the response summary
  • [client] adding reply_to_message arg to send_location, request_location, send_sticker, and send_audio
  • [message] adding reply_location_request
flow = FlowJSON(
    screens=[
        screen_1 := Screen(
            layout=Layout(
                children=[
                    RichText(
                        text=[
                            "# Heading 1",
                            "## Heading 2",
                            "Let’s make a **bold** statement",
                            "Let's make this text *italic*",
                            "Let's make this text ~~Strikethrough~~",
                            "This text is ~~***really important***~~",
                            "This is a [pywa docs](https://pywa.readthedocs.io/)",
                            "This is a ordered list:",
                            "1. Item 1",
                            "2. Item 2",
                            "This is a unordered list:",
                            "- Item 1",
                            "- Item 2",
                            "![image](data:image/png;base64,<base64 content>)",
                            "| Tables        | Are           | Cool  |",
                            "| ------------- | ------------- | ----- |",
                            "| col 3 is      | right-aligned | $1600 |",
                            "| col 2 is      | centered      |   $12 |",
                        ],
                    ),
                ]
            ),
        )
    ]

Full Changelog: 1.25.0...1.26.0

1.25.0

15 Aug 16:38
Compare
Choose a tag to compare

What's Changed

Update with pip: pip3 install -U pywa

  • [handlers] adding priority
  • [client] adding QR methods (create update get and delete)
  • [client] adding get_flow_metrics method
  • [flows] adding to DataSource support for images/colors
  • [flows] support datetime objs in DatePicker
  • [flows] support Screen when using .data_key_of(...) and .form_ref_of(...)
  • [flows] update flow json with indent and without ensuring ascii
  • [flows] adding health_status to FlowDetails
from pywa import WhatsApp, types

wa = WhatsApp(...)

@wa.on_message(filters.text, priority=2)
def one(_: WhatsApp, m: types.Message):
    ...

@wa.on_message(filters.text, priority=3) # will be called before `one`
def two(_: WhatsApp, m: types.Message):
    ...

wa.create_qr_code(prefilled_message="Hi, I am interested in your product", image_type="SVG")

wa.get_flow_metrics(
    flow_id=...,
    metric_name=types.FlowMetricName.ENDPOINT_REQUEST_COUNT,
    granularity=types.FlowMetricGranularity.DAY,
    since=datetime.date(2024, 8, 1),
)

flow = FlowJSON(
    screens=[
        screen_1 := Screen(
            data=[name := ScreenData(key="name", example="David")], # data key in screen_1
            layout=...
        ),
        screen_2 := Screen(
            layout=Layout(
                children=[
                    TextHeading(text=name.data_key_of(screen_1)), # reference to data key in screen_1
                    date := DatePicker(
                        name="date",
                        label="When?",
                        min_date=datetime.date(2024, 8, 1), # date obj in DatePicker 
                        max_date=datetime.date(2024, 8, 30),
                    )
                ]
            ),
        )
    ]
)

Full Changelog: 1.24.0...1.25.0

1.24.0

26 Jul 11:38
Compare
Choose a tag to compare

What's Changed

Update with pip: pip3 install -U pywa

  • [server] validating X-Hub-Signature-256 header
  • [requirements] removing requests
  • [server] default callback url registration delay to 3 sec
wa = WhatsApp(
    validate_updates=True, # Default: True
    app_secret="122hifewyhf9we372d93676",
    ...
)

Full Changelog: 1.23.0...1.24.0

1.23.0

13 Jul 21:27
Compare
Choose a tag to compare

What's Changed

Update with pip: pip3 install -U pywa

  • [client] allowing to manage multiple numbers from the same client (Partner solutions)
  • [flows] adding .respond() shortcut for FlowRequest
  • [flows] allowing body in FlowResponseError subclasses
from pywa import WhatsApp, types, filters

wa = WhatsApp(token="xyzxyz", ...) # `phone_id` is now optional

wa.send_message(
    sender=..., # now you can specify sender
    to=...,
    text="Hello from PyWa!"
)

@wa.on_message(filters.text.command("start"))
def on_start(_: WhatsApp, msg: types.Message):
    if msg.recipient == ...:  # or check the recipient
        ...
    elif msg.recipient == ...:
        ...
    msg.reply(...) # .reply() is automatically replaying to the sender
from pywa import WhatsApp, types

wa = WhatsApp(...)

@wa.on_flow_request("/my-flow")
def my_flow_handler(_: WhatsApp, req: types.FlowRequest) -> types.FlowResponse:
    return req.respond( # instead of instantiating FlowResponse with req.version
        screen="SIGN_UP",
        data={}
    )

Full Changelog: 1.22.0...1.23.0

1.22.0

16 Jun 16:21
Compare
Choose a tag to compare

What's Changed

Update with pip: pip3 install -U pywa

  • [handlers] introducing new way to help split flow endpoint logic to multiple handlers (see example)
  • [client] adding add_flow_request_handler method to register FlowRequestHandlers
  • [flows] pop flow_token from FlowCompletion.response
  • [docs] update examples
from pywa import WhatsApp, types

wa = WhatsApp(...)

@wa.on_flow_request("/my-flow")
def my_flow_handler(_: WhatsApp, flow: types.FlowRequest) -> types.FlowResponse | None:
    ...

@my_flow_handler.on(action=types.FlowRequestActionType.INIT)
def on_init(_: WhatsApp, request: types.FlowRequest) -> types.FlowResponse:
    ...

@my_flow_handler.on(action=types.FlowRequestActionType.DATA_EXCHANGE, screen="SIGN_UP")
def on_signup(_: WhatsApp, request: types.FlowRequest) -> types.FlowResponse:
    ...

@my_flow_handler.on(
    action=types.FlowRequestActionType.DATA_EXCHANGE,
    screen="LOGIN",
    data_filter=lambda _, data: not user_repository.exists(data["email"]),
)
def if_not_registered(_: WhatsApp, request: types.FlowRequest) -> types.FlowResponse | None:
    ...

Full Changelog: 1.21.0...1.22.0

1.21.0

14 Jun 12:20
Compare
Choose a tag to compare

What's Changed

Update with pip: pip3 install -U pywa

Caution

  • [server] This version drops support for non-asynchronous WSGI applications (Flask without asgiref)
  • [flows] version is now required in FlowJSON
  • [flows] added new components PhotoPicker, DocumentPicker, If and Switch
  • [flows] added .data_key_of and .form_ref_of to refer from other screens
  • [flows] added description to CheckboxGroup and to RadioButtonsGroup
  • [utils] adding flow_request_media_decryptor function to decrypt medias from flow requests
  • [client] allow updating flow application id with update_flow_metadata
  • [server] remove event loop
  • [docs] update examples
  • [version] bump FLOW_JSON version to 4.0
from pywa import WhatsApp, utils, types
from pywa.types.flows import *

wa = WhatsApp(...)

my_flow = FlowJSON(
    version="4.0",
    screens=[
        Screen(
            id="UPLOAD_DRIVER_LICENSE",
            title="Upload Driver License",
            layout=Layout(
                children=[
                    doc := DocumentPicker(
                        name="driver_license",
                        label="Driver License",
                        min_uploaded_documents=1,
                        max_uploaded_documents=1,
                        allowed_mime_types=["application/pdf", "image/jpeg"],
                    ),
                    Footer(
                        label="Next",
                        enabled=True,
                        on_click_action=Action(
                            name=FlowActionType.DATA_EXCHANGE,
                            payload={"doc": doc.form_ref_of("UPLOAD_DRIVER_LICENSE")},
                        ),
                    ),
                ]
            )
        )
    ]
)


@wa.on_flow_request("/flow")
def on_flow_request(_, flow: types.FlowRequest):
    media_id, filename, media_bytes = utils.flow_request_media_decryptor_sync(flow.data["doc"])
    with open(filename, "wb") as f:
        f.write(media_bytes)
    ...

Full Changelog: 1.20.2...1.21.0