From be2add4271ed16158a76430de650107cd90e716f Mon Sep 17 00:00:00 2001 From: Cheng-Lung Sung Date: Sun, 9 Feb 2020 12:58:49 +0800 Subject: [PATCH 01/14] support narrowcast api --- linebot/api.py | 40 ++++++++ linebot/models/__init__.py | 25 +++++ linebot/models/filter.py | 133 +++++++++++++++++++++++++++ linebot/models/limit.py | 41 +++++++++ linebot/models/operator.py | 110 ++++++++++++++++++++++ linebot/models/recipient.py | 54 +++++++++++ tests/api/test_narrowcast_message.py | 112 ++++++++++++++++++++++ tests/models/serialize_test_case.py | 5 + tests/models/test_filter.py | 76 +++++++++++++++ 9 files changed, 596 insertions(+) create mode 100644 linebot/models/filter.py create mode 100644 linebot/models/limit.py create mode 100644 linebot/models/operator.py create mode 100644 linebot/models/recipient.py create mode 100644 tests/api/test_narrowcast_message.py create mode 100644 tests/models/test_filter.py diff --git a/linebot/api.py b/linebot/api.py index cbf96798..31dffbf2 100644 --- a/linebot/api.py +++ b/linebot/api.py @@ -210,6 +210,46 @@ def broadcast(self, messages, notification_disabled=False, timeout=None): return BroadcastResponse(request_id=response.headers.get('X-Line-Request-Id')) + def narrowcast(self, messages, recipient=None, filter=None, limit=None, timeout=None): + """Call multicast API. + + https://developers.line.biz/en/reference/messaging-api/#send-narrowcast-message + + Sends push messages to multiple users at any time. + Messages cannot be sent to groups or rooms. + + :param messages: Messages. + Max: 5 + :type messages: T <= :py:class:`linebot.models.send_messages.SendMessage` | + list[T <= :py:class:`linebot.models.send_messages.SendMessage`] + :param recipient: audience object of recipient + :type recipient: T <= :py:class:`linebot.models.recipient.AudienceRecipient` + :param filter: demographic filter of recipient + :type filter: T <= :py:class:`linebot.models.filter.DemographicFilter` + :param limit: limit on this narrowcast + :type limit: T <= :py:class:`linebot.models.limit.Limit` + :param timeout: (optional) How long to wait for the server + to send data before giving up, as a float, + or a (connect timeout, read timeout) float tuple. + Default is self.http_client.timeout + :type timeout: float | tuple(float, float) + """ + if not isinstance(messages, (list, tuple)): + messages = [messages] + + data = { + 'messages': [message.as_json_dict() for message in messages], + 'recipient': recipient.as_json_dict(), + 'filter': filter.as_json_dict(), + 'limit': limit.as_json_dict(), + } + + response = self._post( + '/v2/bot/message/narrowcast', data=json.dumps(data), timeout=timeout + ) + + return BroadcastResponse(request_id=response.headers.get('X-Line-Request-Id')) + def get_message_delivery_broadcast(self, date, timeout=None): """Get number of sent broadcast messages. diff --git a/linebot/models/__init__.py b/linebot/models/__init__.py index 84601b10..ad0adc1c 100644 --- a/linebot/models/__init__.py +++ b/linebot/models/__init__.py @@ -54,6 +54,15 @@ Beacon, Link, ) +from .filter import( # noqa + DemographicFilter, + GenderFilter, + AppTypeFilter, + AreaFilter, + AgeFilter, + SubscriptionPeriodFilter, +) + from .flex_message import ( # noqa FlexSendMessage, FlexContainer, @@ -93,6 +102,11 @@ MessageInsight, ClickInsight, ) + +from .limit import ( # noqa + Limit, +) + from .messages import ( # noqa Message, TextMessage, @@ -103,6 +117,17 @@ StickerMessage, FileMessage, ) + +from .operator import ( # noqa + OpAND, + OpOR, + OpNOT +) + +from .recipient import ( # noqa + AudienceRecipient +) + from .responses import ( # noqa Profile, MemberIds, diff --git a/linebot/models/filter.py b/linebot/models/filter.py new file mode 100644 index 00000000..3d462071 --- /dev/null +++ b/linebot/models/filter.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- + +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""linebot.models.filter module.""" + +from __future__ import unicode_literals + +from abc import ABCMeta + +from future.utils import with_metaclass + +from .base import Base + + +class Filter(with_metaclass(ABCMeta, Base)): + """Filter. + + https://developers.line.biz/en/reference/messaging-api/#narrowcast-demographic-filter + + A filter is the top-level structure of a demographic element. + """ + + def __init__(self, **kwargs): + """__init__ method. + + :param kwargs: + """ + super(Filter, self).__init__(**kwargs) + + self.type = None + + +class DemographicFilter(Filter): + """Demographic. + + https://developers.line.biz/en/reference/messaging-api/#narrowcast-demographic-filter + + A demogrphic filter is the top-level structure of a demographic element. + """ + + def __init__(self, condition=None, **kwargs): + """__init__ method. + + :param kwargs: + """ + super(DemographicFilter, self).__init__(**kwargs) + + self.demographic = condition + + +class GenderFilter(Filter): + """GenderFilter + """ + + def __init__(self, one_of=[], **kwargs): + """__init__ method. + + :param header: Style of the header block + :type header: :py:class:`linebot.models.flex_message.BlockStyle` + """ + super(GenderFilter, self).__init__(**kwargs) + + self.type = "gender" + self.one_of = one_of + + +class AppTypeFilter(Filter): + """AppTypeFilter + """ + + def __init__(self, one_of=[], **kwargs): + """__init__ method. + + """ + super(AppTypeFilter, self).__init__(**kwargs) + + self.type = "appType" + self.one_of = one_of + + +class AreaFilter(Filter): + """AreaFilter + """ + + def __init__(self, one_of=[], **kwargs): + """__init__ method. + + """ + super(AreaFilter, self).__init__(**kwargs) + + self.type = "area" + self.one_of = one_of + + +class AgeFilter(Filter): + """AgeFilter + """ + + def __init__(self, gte=None, lt=None, **kwargs): + """__init__ method. + + """ + super(AgeFilter, self).__init__(**kwargs) + + self.type = "age" + self.gte = gte + self.lt = lt + + +class SubscriptionPeriodFilter(Filter): + """SubscriptionPeriodFilter + """ + + def __init__(self, gte=None, lt=None, **kwargs): + """__init__ method. + + """ + super(SubscriptionPeriodFilter, self).__init__(**kwargs) + + self.type = "subscriptionPeriod" + self.gte = gte + self.lt = lt diff --git a/linebot/models/limit.py b/linebot/models/limit.py new file mode 100644 index 00000000..5bc30ffd --- /dev/null +++ b/linebot/models/limit.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- + +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""linebot.models.recipient module.""" + +from __future__ import unicode_literals + +from abc import ABCMeta + +from future.utils import with_metaclass + +from .base import Base + + +class Limit(with_metaclass(ABCMeta, Base)): + """Limit. + + https://developers.line.biz/en/reference/messaging-api/#send-narrowcast-message + + """ + + def __init__(self, max=None, **kwargs): + """__init__ method. + + :param kwargs: + """ + super(Limit, self).__init__(**kwargs) + + self.type = None + self.max = max diff --git a/linebot/models/operator.py b/linebot/models/operator.py new file mode 100644 index 00000000..727fad16 --- /dev/null +++ b/linebot/models/operator.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- + +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""linebot.models.filter module.""" + +from __future__ import unicode_literals + +from abc import ABCMeta + +from future.utils import with_metaclass + +from .base import Base + + +class Operator(with_metaclass(ABCMeta, Base)): + """Operator. + + https://developers.line.biz/en/reference/messaging-api/#narrowcast-demographic-filter + + A operator is the top-level structure of a demographic element. + """ + + def __init__(self, **kwargs): + """__init__ method. + + :param kwargs: + """ + super(Operator, self).__init__(**kwargs) + + self.type = "operator" + + def as_json_dict(self): + """Return dictionary from this object. + + This converts 'AND', 'OR' and 'NOT' to lowercases. + :return: dict + """ + data = {} + for key, value in self.__dict__.items(): + lower_key = key.lower() + if isinstance(value, (list, tuple, set)): + data[lower_key] = list() + for item in value: + if hasattr(item, 'as_json_dict'): + data[lower_key].append(item.as_json_dict()) + else: + data[lower_key].append(item) + + elif hasattr(value, 'as_json_dict'): + data[lower_key] = value.as_json_dict() + elif value is not None: + data[lower_key] = value + + return data + + +class OpAND(Operator): + """OpAND + """ + + def __init__(self, *args, **kwargs): + """__init__ method. + + :param args: + :param kwargs: + """ + super(OpAND, self).__init__(**kwargs) + + self.AND = args + + +class OpOR(Operator): + """OpOR + """ + + def __init__(self, *args, **kwargs): + """__init__ method. + + :param args: + :param kwargs: + """ + super(OpOR, self).__init__(**kwargs) + + self.OR = args + + +class OpNOT(Operator): + """OpNOT + """ + + def __init__(self, arg, **kwargs): + """__init__ method. + + :param arg: + :param kwargs: + """ + super(OpNOT, self).__init__(**kwargs) + + self.NOT = arg diff --git a/linebot/models/recipient.py b/linebot/models/recipient.py new file mode 100644 index 00000000..1445cc6f --- /dev/null +++ b/linebot/models/recipient.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- + +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""linebot.models.limit module.""" + +from __future__ import unicode_literals + +from abc import ABCMeta + +from future.utils import with_metaclass + +from .base import Base + + +class Recipient(with_metaclass(ABCMeta, Base)): + """Recipient. + + https://developers.line.biz/en/reference/messaging-api/#narrowcast-recipient + + """ + + def __init__(self, **kwargs): + """__init__ method. + + :param kwargs: + """ + super(Recipient, self).__init__(**kwargs) + + self.type = None + + +class AudienceRecipient(Recipient): + """AudienceRecipient + """ + + def __init__(self, group_id=None, **kwargs): + """__init__ method. + + """ + super(AudienceRecipient, self).__init__(**kwargs) + + self.type = "audience" + self.audience_group_id = group_id diff --git a/tests/api/test_narrowcast_message.py b/tests/api/test_narrowcast_message.py new file mode 100644 index 00000000..926d26ca --- /dev/null +++ b/tests/api/test_narrowcast_message.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- + +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from __future__ import unicode_literals, absolute_import + +import json +import unittest + +import responses + +from linebot import ( + LineBotApi +) +from linebot.models import ( + TextSendMessage, + Limit, + OpAND, + OpOR, + OpNOT, + GenderFilter, + DemographicFilter, + AppTypeFilter, + AreaFilter, + AgeFilter, + AudienceRecipient, + SubscriptionPeriodFilter +) + + +class TestNarrowcastMessage(unittest.TestCase): + def setUp(self): + self.tested = LineBotApi('channel_secret') + self.maxDiff = None + + # test data + self.text_message = TextSendMessage(text='Hello, world') + self.message = [{"type": "text", "text": "Hello, world"}] + + @responses.activate + def test_narrowcast_text_message(self): + responses.add( + responses.POST, + LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/message/narrowcast', + json={}, status=200 + ) + + self.tested.narrowcast( + self.text_message, + recipient=AudienceRecipient(group_id='1234'), + filter=DemographicFilter( + OpAND( + AgeFilter(gte="age_35", lt="age_40"), + OpNOT(GenderFilter(one_of=["male"])) + ) + ), + limit=Limit(max=10), + ) + + request = responses.calls[0].request + self.assertEqual( + request.url, + LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/message/narrowcast') + self.assertEqual(request.method, 'POST') + self.assertEqual( + json.loads(request.body), + { + "messages": self.message, + "recipient": { + 'audienceGroupId': '1234', + 'type': 'audience' + }, + "filter": { + "demographic": { + "type": "operator", + "and": [ + { + "type": "age", + "gte": "age_35", + "lt": "age_40" + }, + { + "type": "operator", + "not": { + "type": "gender", + "oneOf": [ + "male" + ] + } + } + ] + } + }, + "limit": { + "max": 10 + } + } + ) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/models/serialize_test_case.py b/tests/models/serialize_test_case.py index d9fdeeac..7784dcdc 100644 --- a/tests/models/serialize_test_case.py +++ b/tests/models/serialize_test_case.py @@ -36,6 +36,11 @@ class SerializeTestCase(unittest.TestCase): URI = 'uri' LOCATION = 'location' FLEX = 'flex' + GENDER = "gender" + APP_TYPE = "appType" + AGE = "age" + AREA = "area" + SUBSCRIPTION_PERIOD = "subscriptionPeriod" SPACER = 'spacer' SPAN = 'span' BUBBLE = 'bubble' diff --git a/tests/models/test_filter.py b/tests/models/test_filter.py new file mode 100644 index 00000000..e09240da --- /dev/null +++ b/tests/models/test_filter.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- + +# Licensed under the Apache License, Version 2.0 (the 'License'); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from __future__ import unicode_literals, absolute_import + +import unittest + +from linebot.models import ( + DemographicFilter, + GenderFilter, + AppTypeFilter, + AreaFilter, + AgeFilter, + SubscriptionPeriodFilter +) +from tests.models.serialize_test_case import SerializeTestCase + + +class TestFilter(SerializeTestCase): + def test_gender_filter(self): + arg = { + "one_of": ["male", "female"] + } + self.assertEqual( + self.serialize_as_dict(arg, type=self.GENDER), + GenderFilter(**arg).as_json_dict() + ) + + def test_app_type_filter(self): + arg = { + "one_of": ["ios", "android"] + } + self.assertEqual( + self.serialize_as_dict(arg, type=self.APP_TYPE), + AppTypeFilter(**arg).as_json_dict() + ) + + def test_age_filter(self): + arg = { + "gte": "age_35", + "lt": "age_40", + } + self.assertEqual( + self.serialize_as_dict(arg, type=self.AGE), + AgeFilter(**arg).as_json_dict() + ) + + def test_area_filter(self): + arg = { + "one_of": ["jp_34", "jp_05"] + } + self.assertEqual( + self.serialize_as_dict(arg, type=self.AREA), + AreaFilter(**arg).as_json_dict() + ) + + def test_subscription_period_filter(self): + arg = { + "gte": "day_7", + "lt": "day_30", + } + self.assertEqual( + self.serialize_as_dict(arg, type=self.SUBSCRIPTION_PERIOD), + SubscriptionPeriodFilter(**arg).as_json_dict() + ) From 33c671d6241ab16912166fd2bb8a888e425dbff1 Mon Sep 17 00:00:00 2001 From: Cheng-Lung Sung Date: Thu, 13 Feb 2020 00:14:39 +0800 Subject: [PATCH 02/14] fix test --- tests/models/test_filter.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/models/test_filter.py b/tests/models/test_filter.py index e09240da..cd852c9e 100644 --- a/tests/models/test_filter.py +++ b/tests/models/test_filter.py @@ -17,7 +17,6 @@ import unittest from linebot.models import ( - DemographicFilter, GenderFilter, AppTypeFilter, AreaFilter, @@ -74,3 +73,7 @@ def test_subscription_period_filter(self): self.serialize_as_dict(arg, type=self.SUBSCRIPTION_PERIOD), SubscriptionPeriodFilter(**arg).as_json_dict() ) + + +if __name__ == '__main__': + unittest.main() From bea6fd4bdbd1ce3687d1c6a0a79cec4c84fdb64f Mon Sep 17 00:00:00 2001 From: Cheng-Lung Sung Date: Thu, 13 Feb 2020 00:27:10 +0800 Subject: [PATCH 03/14] add test --- tests/api/test_narrowcast_message.py | 101 ++++++++++++++++++++++----- 1 file changed, 83 insertions(+), 18 deletions(-) diff --git a/tests/api/test_narrowcast_message.py b/tests/api/test_narrowcast_message.py index 926d26ca..8e0f1fd7 100644 --- a/tests/api/test_narrowcast_message.py +++ b/tests/api/test_narrowcast_message.py @@ -57,14 +57,26 @@ def test_narrowcast_text_message(self): self.tested.narrowcast( self.text_message, - recipient=AudienceRecipient(group_id='1234'), + recipient=OpAND( + AudienceRecipient(group_id=5614991017776), + OpNOT(AudienceRecipient(group_id=4389303728991)) + ), filter=DemographicFilter( - OpAND( - AgeFilter(gte="age_35", lt="age_40"), - OpNOT(GenderFilter(one_of=["male"])) + OpOR( + OpAND( + GenderFilter(one_of=["male", "female"]), + AgeFilter(gte="age_20", lt="age_25"), + AppTypeFilter(one_of=["android", "ios"]), + AreaFilter(one_of=["jp_23", "jp_05"]), + SubscriptionPeriodFilter(gte="day_7", lt="day_30") + ), + OpAND( + AgeFilter(gte="age_35", lt="age_40"), + OpNOT(GenderFilter(one_of=["male"])) + ) ) ), - limit=Limit(max=10), + limit=Limit(max=100), ) request = responses.calls[0].request @@ -77,32 +89,85 @@ def test_narrowcast_text_message(self): { "messages": self.message, "recipient": { - 'audienceGroupId': '1234', - 'type': 'audience' + "type": "operator", + "and": [ + { + 'audienceGroupId': 5614991017776, + 'type': 'audience' + }, + { + "type": "operator", + "not": { + "type": "audience", + "audienceGroupId": 4389303728991 + } + } + ] }, "filter": { "demographic": { "type": "operator", - "and": [ + "or": [ { - "type": "age", - "gte": "age_35", - "lt": "age_40" + "type": "operator", + "and": [ + { + "type": "gender", + "oneOf": [ + "male", + "female" + ] + }, + { + "type": "age", + "gte": "age_20", + "lt": "age_25" + }, + { + "type": "appType", + "oneOf": [ + "android", + "ios" + ] + }, + { + "type": "area", + "oneOf": [ + "jp_23", + "jp_05" + ] + }, + { + "type": "subscriptionPeriod", + "gte": "day_7", + "lt": "day_30" + } + ] }, { "type": "operator", - "not": { - "type": "gender", - "oneOf": [ - "male" - ] - } + "and": [ + { + "type": "age", + "gte": "age_35", + "lt": "age_40" + }, + { + "type": "operator", + "not": { + "type": "gender", + "oneOf": [ + "male" + ] + } + } + ] } ] } }, "limit": { - "max": 10 + "max": 100 } } ) From b6f77c93d05fb075eb05628e72b0ad5d57d1bf9e Mon Sep 17 00:00:00 2001 From: Cheng-Lung Sung Date: Sat, 15 Feb 2020 01:03:31 +0800 Subject: [PATCH 04/14] add MessageProgressNarrowcastResponse --- linebot/api.py | 27 +++++++++++++++-- linebot/models/__init__.py | 2 ++ linebot/models/limit.py | 1 - linebot/models/responses.py | 45 ++++++++++++++++++++++++++++ tests/api/test_narrowcast_message.py | 10 +++++-- 5 files changed, 79 insertions(+), 6 deletions(-) diff --git a/linebot/api.py b/linebot/api.py index 31dffbf2..02258893 100644 --- a/linebot/api.py +++ b/linebot/api.py @@ -27,7 +27,7 @@ MessageDeliveryBroadcastResponse, MessageDeliveryMulticastResponse, MessageDeliveryPushResponse, MessageDeliveryReplyResponse, InsightMessageDeliveryResponse, InsightFollowersResponse, InsightDemographicResponse, - InsightMessageEventResponse, BroadcastResponse, + InsightMessageEventResponse, BroadcastResponse, NarrowcastResponse, ) @@ -233,6 +233,7 @@ def narrowcast(self, messages, recipient=None, filter=None, limit=None, timeout= or a (connect timeout, read timeout) float tuple. Default is self.http_client.timeout :type timeout: float | tuple(float, float) + :rtype: :py:class:`linebot.models.responses.NarrowcastResponse` """ if not isinstance(messages, (list, tuple)): messages = [messages] @@ -248,7 +249,29 @@ def narrowcast(self, messages, recipient=None, filter=None, limit=None, timeout= '/v2/bot/message/narrowcast', data=json.dumps(data), timeout=timeout ) - return BroadcastResponse(request_id=response.headers.get('X-Line-Request-Id')) + return NarrowcastResponse(request_id=response.headers.get('X-Line-Request-Id')) + + def get_progress_status_narrowcast(self, request_id, timeout=None): + """Get progress status of narrowcast messages sent. + + https://developers.line.biz/en/reference/messaging-api/#get-narrowcast-progress-status + + Gets the number of messages sent with the /bot/message/progress/narrowcast endpoint. + + :param str request_id: request ID of narrowcast. + :param timeout: (optional) How long to wait for the server + to send data before giving up, as a float, + or a (connect timeout, read timeout) float tuple. + Default is self.http_client.timeout + :type timeout: float | tuple(float, float) + :rtype: :py:class:`linebot.models.responses.MessageDeliveryBroadcastResponse` + """ + response = self._get( + '/v2/bot/message/progress/narrowcast?requestId={request_id}'.format(request_id=request_id), + timeout=timeout + ) + + return MessageProgressNarrowcastResponse.new_from_json_dict(response.json) def get_message_delivery_broadcast(self, date, timeout=None): """Get number of sent broadcast messages. diff --git a/linebot/models/__init__.py b/linebot/models/__init__.py index ad0adc1c..996403f8 100644 --- a/linebot/models/__init__.py +++ b/linebot/models/__init__.py @@ -147,6 +147,8 @@ InsightDemographicResponse, InsightMessageEventResponse, BroadcastResponse, + NarrowcastResponse, + MessageProgressNarrowcastResponse, ) from .rich_menu import ( # noqa RichMenu, diff --git a/linebot/models/limit.py b/linebot/models/limit.py index 5bc30ffd..7f967674 100644 --- a/linebot/models/limit.py +++ b/linebot/models/limit.py @@ -37,5 +37,4 @@ def __init__(self, max=None, **kwargs): """ super(Limit, self).__init__(**kwargs) - self.type = None self.max = max diff --git a/linebot/models/responses.py b/linebot/models/responses.py index 7e699709..aad1f494 100644 --- a/linebot/models/responses.py +++ b/linebot/models/responses.py @@ -276,6 +276,34 @@ def __init__(self, status=None, success=None, **kwargs): self.success = success +class MessageProgressNarrowcastResponse(Base): + """MessageProgressNarrowcastResponse.""" + + def __init__(self, phase=None, success_count=None, failure_count=None, + target_count=None, failed_description=None, error_code=None, **kwargs): + """__init__ method. + + :param str phase: Progress status. One of `waiting`, `sending`, + `succeeded`, or `failed`. + :param int success_count: Number of narrowcast messages sent successful. + :param int failure_count: Number of narrowcast messages sent failed. + :param int target_count: Number of targeted messages sent. + :param str failed_description: Reaseon why narrowcast failed, useful when + phase is `failed`. + :param int error_code: Summary of the error. One of `1` or `2`. `1` + means internal error, whereas `2` indicates too few targets. + :param kwargs: + """ + super(MessageProgressNarrowcastResponse, self).__init__(**kwargs) + + self.phase = phase + self.success_count = success_count + self.failure_count = failure_count + self.target_count = target_count + self.failed_description = failed_description + self.error_code = error_code + + class IssueLinkTokenResponse(Base): """IssueLinkTokenResponse. @@ -428,3 +456,20 @@ def __init__(self, overview=None, messages=None, clicks=None, **kwargs): self.overview = self.get_or_new_from_json_dict(overview, MessageStatistics) self.messages = [self.get_or_new_from_json_dict(it, MessageInsight) for it in messages] self.clicks = [self.get_or_new_from_json_dict(it, ClickInsight) for it in clicks] + + +class NarrowcastResponse(Base): + """NarrowcastResponse. + + https://developers.line.biz/en/reference/messaging-api/#send-narrowcast-message + """ + + def __init__(self, request_id=None, **kwargs): + """__init__ method. + + :param str request_id: Request ID. A unique ID is generated for each request + :param kwargs: + """ + super(NarrowcastResponse, self).__init__(**kwargs) + + self.request_id = request_id diff --git a/tests/api/test_narrowcast_message.py b/tests/api/test_narrowcast_message.py index 8e0f1fd7..fbc1a29c 100644 --- a/tests/api/test_narrowcast_message.py +++ b/tests/api/test_narrowcast_message.py @@ -34,7 +34,9 @@ AreaFilter, AgeFilter, AudienceRecipient, - SubscriptionPeriodFilter + SubscriptionPeriodFilter, + NarrowcastResponse, + MessageProgressNarrowcastResponse, ) @@ -52,10 +54,11 @@ def test_narrowcast_text_message(self): responses.add( responses.POST, LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/message/narrowcast', - json={}, status=200 + json={}, status=200, + headers={'X-Line-Request-Id': 'request_id_test'}, ) - self.tested.narrowcast( + response = self.tested.narrowcast( self.text_message, recipient=OpAND( AudienceRecipient(group_id=5614991017776), @@ -171,6 +174,7 @@ def test_narrowcast_text_message(self): } } ) + self.assertEqual('request_id_test', response.request_id) if __name__ == '__main__': From a8fbacf8163a854e28d3cc91fb9115d9124eb9fb Mon Sep 17 00:00:00 2001 From: Cheng-Lung Sung Date: Sat, 15 Feb 2020 01:12:53 +0800 Subject: [PATCH 05/14] update operator --- linebot/models/operator.py | 30 +++--------------------------- 1 file changed, 3 insertions(+), 27 deletions(-) diff --git a/linebot/models/operator.py b/linebot/models/operator.py index 727fad16..d6322cf4 100644 --- a/linebot/models/operator.py +++ b/linebot/models/operator.py @@ -40,30 +40,6 @@ def __init__(self, **kwargs): self.type = "operator" - def as_json_dict(self): - """Return dictionary from this object. - - This converts 'AND', 'OR' and 'NOT' to lowercases. - :return: dict - """ - data = {} - for key, value in self.__dict__.items(): - lower_key = key.lower() - if isinstance(value, (list, tuple, set)): - data[lower_key] = list() - for item in value: - if hasattr(item, 'as_json_dict'): - data[lower_key].append(item.as_json_dict()) - else: - data[lower_key].append(item) - - elif hasattr(value, 'as_json_dict'): - data[lower_key] = value.as_json_dict() - elif value is not None: - data[lower_key] = value - - return data - class OpAND(Operator): """OpAND @@ -77,7 +53,7 @@ def __init__(self, *args, **kwargs): """ super(OpAND, self).__init__(**kwargs) - self.AND = args + setattr(self, 'and', args) class OpOR(Operator): @@ -92,7 +68,7 @@ def __init__(self, *args, **kwargs): """ super(OpOR, self).__init__(**kwargs) - self.OR = args + setattr(self, 'or', args) class OpNOT(Operator): @@ -107,4 +83,4 @@ def __init__(self, arg, **kwargs): """ super(OpNOT, self).__init__(**kwargs) - self.NOT = arg + setattr(self, 'not', arg) From 15d48bc6dd3f5ecd833ec7f8435039bf020c6af6 Mon Sep 17 00:00:00 2001 From: Cheng-Lung Sung Date: Sat, 15 Feb 2020 01:16:41 +0800 Subject: [PATCH 06/14] fix by flake8 --- linebot/api.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/linebot/api.py b/linebot/api.py index 02258893..ea8eccf6 100644 --- a/linebot/api.py +++ b/linebot/api.py @@ -28,6 +28,7 @@ MessageDeliveryPushResponse, MessageDeliveryReplyResponse, InsightMessageDeliveryResponse, InsightFollowersResponse, InsightDemographicResponse, InsightMessageEventResponse, BroadcastResponse, NarrowcastResponse, + MessageProgressNarrowcastResponse, ) @@ -267,7 +268,8 @@ def get_progress_status_narrowcast(self, request_id, timeout=None): :rtype: :py:class:`linebot.models.responses.MessageDeliveryBroadcastResponse` """ response = self._get( - '/v2/bot/message/progress/narrowcast?requestId={request_id}'.format(request_id=request_id), + '/v2/bot/message/progress/narrowcast?requestId={request_id}'.format( + request_id=request_id), timeout=timeout ) From d7b8d0ac7a96341b9cc781669b2dbc41f98014d7 Mon Sep 17 00:00:00 2001 From: Cheng-Lung Sung Date: Sun, 23 Feb 2020 17:49:40 +0800 Subject: [PATCH 07/14] OpAND, OpNOT, OpOR to AND, NOT, OR --- linebot/models/__init__.py | 6 +++--- linebot/models/operator.py | 18 +++++++++--------- tests/api/test_narrowcast_message.py | 18 +++++++++--------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/linebot/models/__init__.py b/linebot/models/__init__.py index 996403f8..41cc2771 100644 --- a/linebot/models/__init__.py +++ b/linebot/models/__init__.py @@ -119,9 +119,9 @@ ) from .operator import ( # noqa - OpAND, - OpOR, - OpNOT + AND, + OR, + NOT ) from .recipient import ( # noqa diff --git a/linebot/models/operator.py b/linebot/models/operator.py index d6322cf4..66102363 100644 --- a/linebot/models/operator.py +++ b/linebot/models/operator.py @@ -41,8 +41,8 @@ def __init__(self, **kwargs): self.type = "operator" -class OpAND(Operator): - """OpAND +class AND(Operator): + """AND """ def __init__(self, *args, **kwargs): @@ -51,13 +51,13 @@ def __init__(self, *args, **kwargs): :param args: :param kwargs: """ - super(OpAND, self).__init__(**kwargs) + super(AND, self).__init__(**kwargs) setattr(self, 'and', args) -class OpOR(Operator): - """OpOR +class OR(Operator): + """OR """ def __init__(self, *args, **kwargs): @@ -66,13 +66,13 @@ def __init__(self, *args, **kwargs): :param args: :param kwargs: """ - super(OpOR, self).__init__(**kwargs) + super(OR, self).__init__(**kwargs) setattr(self, 'or', args) -class OpNOT(Operator): - """OpNOT +class NOT(Operator): + """NOT """ def __init__(self, arg, **kwargs): @@ -81,6 +81,6 @@ def __init__(self, arg, **kwargs): :param arg: :param kwargs: """ - super(OpNOT, self).__init__(**kwargs) + super(NOT, self).__init__(**kwargs) setattr(self, 'not', arg) diff --git a/tests/api/test_narrowcast_message.py b/tests/api/test_narrowcast_message.py index fbc1a29c..78ff96ba 100644 --- a/tests/api/test_narrowcast_message.py +++ b/tests/api/test_narrowcast_message.py @@ -25,9 +25,9 @@ from linebot.models import ( TextSendMessage, Limit, - OpAND, - OpOR, - OpNOT, + AND, + OR, + NOT, GenderFilter, DemographicFilter, AppTypeFilter, @@ -60,22 +60,22 @@ def test_narrowcast_text_message(self): response = self.tested.narrowcast( self.text_message, - recipient=OpAND( + recipient=AND( AudienceRecipient(group_id=5614991017776), - OpNOT(AudienceRecipient(group_id=4389303728991)) + NOT(AudienceRecipient(group_id=4389303728991)) ), filter=DemographicFilter( - OpOR( - OpAND( + OR( + AND( GenderFilter(one_of=["male", "female"]), AgeFilter(gte="age_20", lt="age_25"), AppTypeFilter(one_of=["android", "ios"]), AreaFilter(one_of=["jp_23", "jp_05"]), SubscriptionPeriodFilter(gte="day_7", lt="day_30") ), - OpAND( + AND( AgeFilter(gte="age_35", lt="age_40"), - OpNOT(GenderFilter(one_of=["male"])) + NOT(GenderFilter(one_of=["male"])) ) ) ), From 8fa09f1c6a7d5db51bb96583aaf2eab1b24d947f Mon Sep 17 00:00:00 2001 From: Cheng-Lung Sung Date: Sun, 23 Feb 2020 18:19:14 +0800 Subject: [PATCH 08/14] add test for get_progress_status_narrowcast --- tests/api/test_narrowcast_message.py | 51 +++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/tests/api/test_narrowcast_message.py b/tests/api/test_narrowcast_message.py index 78ff96ba..7ba07a15 100644 --- a/tests/api/test_narrowcast_message.py +++ b/tests/api/test_narrowcast_message.py @@ -43,7 +43,7 @@ class TestNarrowcastMessage(unittest.TestCase): def setUp(self): self.tested = LineBotApi('channel_secret') - self.maxDiff = None + self.request_id = 'f70dd685-499a-4231-a441-f24b8d4fba21' # test data self.text_message = TextSendMessage(text='Hello, world') @@ -176,6 +176,55 @@ def test_narrowcast_text_message(self): ) self.assertEqual('request_id_test', response.request_id) + @responses.activate + def test_get_progress_status_narrowcast(self): + responses.add( + responses.GET, + LineBotApi.DEFAULT_API_ENDPOINT + + '/v2/bot/message/progress/narrowcast?requestId={request_id}'.format( + request_id=self.request_id), + json={'phase': 'waiting'}, status=200, + ) + responses.add( + responses.GET, + LineBotApi.DEFAULT_API_ENDPOINT + + '/v2/bot/message/progress/narrowcast?requestId={request_id}'.format( + request_id=self.request_id), + json={ + 'phase': 'succeeded', + 'successCount': 10, + 'failureCount': 0, + 'targetCount': 10, + }, status=200, + ) + + res = self.tested.get_progress_status_narrowcast(self.request_id) + request = responses.calls[0].request + self.assertEqual('GET', request.method) + self.assertEqual( + request.url, + LineBotApi.DEFAULT_API_ENDPOINT + + '/v2/bot/message/progress/narrowcast?requestId={request_id}'.format( + request_id=self.request_id) + ) + + self.assertEqual(res.phase, 'waiting') + + res = self.tested.get_progress_status_narrowcast(self.request_id) + request = responses.calls[1].request + self.assertEqual('GET', request.method) + self.assertEqual( + request.url, + LineBotApi.DEFAULT_API_ENDPOINT + + '/v2/bot/message/progress/narrowcast?requestId={request_id}'.format( + request_id=self.request_id) + ) + + self.assertEqual(res.phase, 'succeeded') + self.assertEqual(res.success_count, 10) + self.assertEqual(res.failure_count, 0) + self.assertEqual(res.target_count, 10) + if __name__ == '__main__': unittest.main() From 05399fd0ae88578befde1e5aab0fe428d18ed628 Mon Sep 17 00:00:00 2001 From: Cheng-Lung Sung Date: Sun, 23 Feb 2020 20:19:36 +0800 Subject: [PATCH 09/14] Add English comments --- linebot/models/filter.py | 60 ++++++++++++++++++++++++++++--------- linebot/models/operator.py | 19 +++++++++--- linebot/models/recipient.py | 8 +++-- 3 files changed, 67 insertions(+), 20 deletions(-) diff --git a/linebot/models/filter.py b/linebot/models/filter.py index 3d462071..c8c81007 100644 --- a/linebot/models/filter.py +++ b/linebot/models/filter.py @@ -42,32 +42,40 @@ def __init__(self, **kwargs): class DemographicFilter(Filter): - """Demographic. + """DemographicFilter. https://developers.line.biz/en/reference/messaging-api/#narrowcast-demographic-filter - A demogrphic filter is the top-level structure of a demographic element. + Demographic filter objects represent criteria (e.g. age, gender, OS, region, + and friendship duration) on which to filter the list of recipients. + You can filter recipients based on a combination of different criteria using + logical operator objects. """ - def __init__(self, condition=None, **kwargs): + def __init__(self, criteria=None, **kwargs): """__init__ method. + :param criteria: Combination of different criteria using logical + operator objects. + :type criteria: :py:class:`linebot.model.DemographicFilter` | + :py:class:`linebot.model.Operator` :param kwargs: """ super(DemographicFilter, self).__init__(**kwargs) - self.demographic = condition + self.demographic = criteria class GenderFilter(Filter): - """GenderFilter - """ + """GenderFilter.""" def __init__(self, one_of=[], **kwargs): """__init__ method. - :param header: Style of the header block - :type header: :py:class:`linebot.models.flex_message.BlockStyle` + :param one_of: Send messages to users of a given gender. One of: + male: Users who identify as male + female: Users who identify as female + :type one_of: list[str] """ super(GenderFilter, self).__init__(**kwargs) @@ -76,12 +84,15 @@ def __init__(self, one_of=[], **kwargs): class AppTypeFilter(Filter): - """AppTypeFilter - """ + """AppTypeFilter.""" def __init__(self, one_of=[], **kwargs): """__init__ method. + :param one_of: Send messages to users of the specified OS. One of: + ios: Users who using iOS. + android: Users who using Android. + :type one_of: list[str] """ super(AppTypeFilter, self).__init__(**kwargs) @@ -90,12 +101,13 @@ def __init__(self, one_of=[], **kwargs): class AreaFilter(Filter): - """AreaFilter - """ + """AreaFilter.""" def __init__(self, one_of=[], **kwargs): """__init__ method. + :param one_of: Send messages to users in the specified region. + :type one_of: list[str] """ super(AreaFilter, self).__init__(**kwargs) @@ -104,12 +116,21 @@ def __init__(self, one_of=[], **kwargs): class AgeFilter(Filter): - """AgeFilter + """AgeFilter. + + This lets you filter recipients with a given age range. """ def __init__(self, gte=None, lt=None, **kwargs): """__init__ method. + Be sure to specify either gte, lt, or both. + + :param gte: Send messages to users at least as old as the specified age. + :type gte: str + :param lt: Send messages to users younger than the specified age. + You can specify the same values as for the gte property. + :type lt: str """ super(AgeFilter, self).__init__(**kwargs) @@ -119,12 +140,23 @@ def __init__(self, gte=None, lt=None, **kwargs): class SubscriptionPeriodFilter(Filter): - """SubscriptionPeriodFilter + """SubscriptionPeriodFilter. + + This lets you filter recipients with a given range of friendship durations. """ def __init__(self, gte=None, lt=None, **kwargs): """__init__ method. + Be sure to specify either gte, lt, or both. + + :param gte: Send messages to users who have been friends of yours for + at least the specified number of days + :type gte: str + :param lt: Send messages to users who have been friends of yours for + less than the specified number of days. + You can specify the same values as for the gte property. + :type lt: str """ super(SubscriptionPeriodFilter, self).__init__(**kwargs) diff --git a/linebot/models/operator.py b/linebot/models/operator.py index 66102363..d8179f87 100644 --- a/linebot/models/operator.py +++ b/linebot/models/operator.py @@ -28,7 +28,9 @@ class Operator(with_metaclass(ABCMeta, Base)): https://developers.line.biz/en/reference/messaging-api/#narrowcast-demographic-filter - A operator is the top-level structure of a demographic element. + Use logical AND, OR, and NOT operators to combine multiple recipient objects or + demographic filter objects together. + You can specify up to 10 recipient objects or demographic filter objects per request. """ def __init__(self, **kwargs): @@ -42,7 +44,10 @@ def __init__(self, **kwargs): class AND(Operator): - """AND + """AND. + + Create a new recipient object or demographic filter object by taking the + logical conjunction (AND) of the specified array of objects. """ def __init__(self, *args, **kwargs): @@ -57,7 +62,10 @@ def __init__(self, *args, **kwargs): class OR(Operator): - """OR + """OR. + + Create a new recipient object or demographic filter object by taking the + logical disjunction (OR) of the specified array of objects. """ def __init__(self, *args, **kwargs): @@ -72,7 +80,10 @@ def __init__(self, *args, **kwargs): class NOT(Operator): - """NOT + """NOT. + + Create a new recipient object or demographic filter object that excludes + in the specified object. """ def __init__(self, arg, **kwargs): diff --git a/linebot/models/recipient.py b/linebot/models/recipient.py index 1445cc6f..4855d91c 100644 --- a/linebot/models/recipient.py +++ b/linebot/models/recipient.py @@ -28,6 +28,8 @@ class Recipient(with_metaclass(ABCMeta, Base)): https://developers.line.biz/en/reference/messaging-api/#narrowcast-recipient + Recipient objects represent audiences. You can specify recipients based on + a combination of criteria using logical operator objects. """ def __init__(self, **kwargs): @@ -41,12 +43,14 @@ def __init__(self, **kwargs): class AudienceRecipient(Recipient): - """AudienceRecipient - """ + """AudienceRecipient.""" def __init__(self, group_id=None, **kwargs): """__init__ method. + :param int group_id: The audience ID. Create audiences with the + Manage Audience API. + :param kwargs: """ super(AudienceRecipient, self).__init__(**kwargs) From cd93889f50e5274bdb82cf0c64f907a9351e8882 Mon Sep 17 00:00:00 2001 From: Cheng-Lung Sung Date: Sun, 23 Feb 2020 20:22:57 +0800 Subject: [PATCH 10/14] AND/OR/NOT to And/Or/Not --- linebot/models/__init__.py | 6 +++--- linebot/models/operator.py | 18 +++++++++--------- tests/api/test_narrowcast_message.py | 19 ++++++++++--------- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/linebot/models/__init__.py b/linebot/models/__init__.py index 41cc2771..c7760ca8 100644 --- a/linebot/models/__init__.py +++ b/linebot/models/__init__.py @@ -119,9 +119,9 @@ ) from .operator import ( # noqa - AND, - OR, - NOT + And, + Or, + Not ) from .recipient import ( # noqa diff --git a/linebot/models/operator.py b/linebot/models/operator.py index d8179f87..e0861e72 100644 --- a/linebot/models/operator.py +++ b/linebot/models/operator.py @@ -43,8 +43,8 @@ def __init__(self, **kwargs): self.type = "operator" -class AND(Operator): - """AND. +class And(Operator): + """And. Create a new recipient object or demographic filter object by taking the logical conjunction (AND) of the specified array of objects. @@ -56,13 +56,13 @@ def __init__(self, *args, **kwargs): :param args: :param kwargs: """ - super(AND, self).__init__(**kwargs) + super(And, self).__init__(**kwargs) setattr(self, 'and', args) -class OR(Operator): - """OR. +class Or(Operator): + """Or. Create a new recipient object or demographic filter object by taking the logical disjunction (OR) of the specified array of objects. @@ -74,13 +74,13 @@ def __init__(self, *args, **kwargs): :param args: :param kwargs: """ - super(OR, self).__init__(**kwargs) + super(Or, self).__init__(**kwargs) setattr(self, 'or', args) -class NOT(Operator): - """NOT. +class Not(Operator): + """Not. Create a new recipient object or demographic filter object that excludes in the specified object. @@ -92,6 +92,6 @@ def __init__(self, arg, **kwargs): :param arg: :param kwargs: """ - super(NOT, self).__init__(**kwargs) + super(Not, self).__init__(**kwargs) setattr(self, 'not', arg) diff --git a/tests/api/test_narrowcast_message.py b/tests/api/test_narrowcast_message.py index 7ba07a15..36eca5b0 100644 --- a/tests/api/test_narrowcast_message.py +++ b/tests/api/test_narrowcast_message.py @@ -25,9 +25,9 @@ from linebot.models import ( TextSendMessage, Limit, - AND, - OR, - NOT, + And, + Or, + Not, GenderFilter, DemographicFilter, AppTypeFilter, @@ -43,6 +43,7 @@ class TestNarrowcastMessage(unittest.TestCase): def setUp(self): self.tested = LineBotApi('channel_secret') + self.maxDiff = None self.request_id = 'f70dd685-499a-4231-a441-f24b8d4fba21' # test data @@ -60,22 +61,22 @@ def test_narrowcast_text_message(self): response = self.tested.narrowcast( self.text_message, - recipient=AND( + recipient=And( AudienceRecipient(group_id=5614991017776), - NOT(AudienceRecipient(group_id=4389303728991)) + Not(AudienceRecipient(group_id=4389303728991)) ), filter=DemographicFilter( - OR( - AND( + Or( + And( GenderFilter(one_of=["male", "female"]), AgeFilter(gte="age_20", lt="age_25"), AppTypeFilter(one_of=["android", "ios"]), AreaFilter(one_of=["jp_23", "jp_05"]), SubscriptionPeriodFilter(gte="day_7", lt="day_30") ), - AND( + And( AgeFilter(gte="age_35", lt="age_40"), - NOT(GenderFilter(one_of=["male"])) + Not(GenderFilter(one_of=["male"])) ) ) ), From a748fa1593438a1d0e4adb69c9b7591f04127843 Mon Sep 17 00:00:00 2001 From: Cheng-Lung Sung Date: Sun, 23 Feb 2020 20:28:42 +0800 Subject: [PATCH 11/14] fix flake8 --- tests/api/test_narrowcast_message.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/api/test_narrowcast_message.py b/tests/api/test_narrowcast_message.py index 36eca5b0..7b5f64d7 100644 --- a/tests/api/test_narrowcast_message.py +++ b/tests/api/test_narrowcast_message.py @@ -35,8 +35,6 @@ AgeFilter, AudienceRecipient, SubscriptionPeriodFilter, - NarrowcastResponse, - MessageProgressNarrowcastResponse, ) From 338eab66704e0a5f88c866a6b7ad97da1596909b Mon Sep 17 00:00:00 2001 From: Cheng-Lung Sung Date: Mon, 24 Feb 2020 19:37:41 +0800 Subject: [PATCH 12/14] make DemographicFilter as base class of demographic filters --- linebot/models/__init__.py | 1 + linebot/models/filter.py | 26 ++++++++-------- tests/api/test_narrowcast_message.py | 46 ++++++++++++++++++++++++++-- 3 files changed, 58 insertions(+), 15 deletions(-) diff --git a/linebot/models/__init__.py b/linebot/models/__init__.py index c7760ca8..e173ec94 100644 --- a/linebot/models/__init__.py +++ b/linebot/models/__init__.py @@ -55,6 +55,7 @@ Link, ) from .filter import( # noqa + Filter, DemographicFilter, GenderFilter, AppTypeFilter, diff --git a/linebot/models/filter.py b/linebot/models/filter.py index c8c81007..e755eb2b 100644 --- a/linebot/models/filter.py +++ b/linebot/models/filter.py @@ -31,14 +31,18 @@ class Filter(with_metaclass(ABCMeta, Base)): A filter is the top-level structure of a demographic element. """ - def __init__(self, **kwargs): + def __init__(self, demographic=None, **kwargs): """__init__ method. + :param demographic: Combination of different criteria using logical + operator objects. + :type demographic: :py:class:`linebot.model.DemographicFilter` | + :py:class:`linebot.model.Operator` :param kwargs: """ super(Filter, self).__init__(**kwargs) - self.type = None + self.demographic = demographic class DemographicFilter(Filter): @@ -52,21 +56,17 @@ class DemographicFilter(Filter): logical operator objects. """ - def __init__(self, criteria=None, **kwargs): + def __init__(self, **kwargs): """__init__ method. - :param criteria: Combination of different criteria using logical - operator objects. - :type criteria: :py:class:`linebot.model.DemographicFilter` | - :py:class:`linebot.model.Operator` :param kwargs: """ super(DemographicFilter, self).__init__(**kwargs) - self.demographic = criteria + self.type = None -class GenderFilter(Filter): +class GenderFilter(DemographicFilter): """GenderFilter.""" def __init__(self, one_of=[], **kwargs): @@ -83,7 +83,7 @@ def __init__(self, one_of=[], **kwargs): self.one_of = one_of -class AppTypeFilter(Filter): +class AppTypeFilter(DemographicFilter): """AppTypeFilter.""" def __init__(self, one_of=[], **kwargs): @@ -100,7 +100,7 @@ def __init__(self, one_of=[], **kwargs): self.one_of = one_of -class AreaFilter(Filter): +class AreaFilter(DemographicFilter): """AreaFilter.""" def __init__(self, one_of=[], **kwargs): @@ -115,7 +115,7 @@ def __init__(self, one_of=[], **kwargs): self.one_of = one_of -class AgeFilter(Filter): +class AgeFilter(DemographicFilter): """AgeFilter. This lets you filter recipients with a given age range. @@ -139,7 +139,7 @@ def __init__(self, gte=None, lt=None, **kwargs): self.lt = lt -class SubscriptionPeriodFilter(Filter): +class SubscriptionPeriodFilter(DemographicFilter): """SubscriptionPeriodFilter. This lets you filter recipients with a given range of friendship durations. diff --git a/tests/api/test_narrowcast_message.py b/tests/api/test_narrowcast_message.py index 7b5f64d7..076e3a31 100644 --- a/tests/api/test_narrowcast_message.py +++ b/tests/api/test_narrowcast_message.py @@ -28,8 +28,8 @@ And, Or, Not, + Filter, GenderFilter, - DemographicFilter, AppTypeFilter, AreaFilter, AgeFilter, @@ -48,6 +48,48 @@ def setUp(self): self.text_message = TextSendMessage(text='Hello, world') self.message = [{"type": "text", "text": "Hello, world"}] + @responses.activate + def test_narrowcast_simple_text_message(self): + responses.add( + responses.POST, + LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/message/narrowcast', + json={}, status=200, + headers={'X-Line-Request-Id': 'request_id_test'}, + ) + + response = self.tested.narrowcast( + self.text_message, + recipient=AudienceRecipient(group_id=5614991017776), + filter=Filter(demographic=AgeFilter(gte="age_35", lt="age_40")), + limit=Limit(max=10), + ) + + request = responses.calls[0].request + self.assertEqual( + request.url, + LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/message/narrowcast') + self.assertEqual(request.method, 'POST') + self.assertEqual( + json.loads(request.body), + { + "messages": self.message, + "recipient": { + 'audienceGroupId': 5614991017776, + 'type': 'audience' + }, + "filter": { + "demographic": { + "type": "age", + "gte": "age_35", + "lt": "age_40" + } + }, + "limit": { + "max": 10 + } + } + ) + @responses.activate def test_narrowcast_text_message(self): responses.add( @@ -63,7 +105,7 @@ def test_narrowcast_text_message(self): AudienceRecipient(group_id=5614991017776), Not(AudienceRecipient(group_id=4389303728991)) ), - filter=DemographicFilter( + filter=Filter(demographic= Or( And( GenderFilter(one_of=["male", "female"]), From eda653c6c80f1b97f9ecde1670983cadf08f1fe9 Mon Sep 17 00:00:00 2001 From: Cheng-Lung Sung Date: Mon, 24 Feb 2020 19:41:17 +0800 Subject: [PATCH 13/14] flake8 --- tests/api/test_narrowcast_message.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/api/test_narrowcast_message.py b/tests/api/test_narrowcast_message.py index 076e3a31..0f01d06a 100644 --- a/tests/api/test_narrowcast_message.py +++ b/tests/api/test_narrowcast_message.py @@ -57,7 +57,7 @@ def test_narrowcast_simple_text_message(self): headers={'X-Line-Request-Id': 'request_id_test'}, ) - response = self.tested.narrowcast( + self.tested.narrowcast( self.text_message, recipient=AudienceRecipient(group_id=5614991017776), filter=Filter(demographic=AgeFilter(gte="age_35", lt="age_40")), @@ -105,8 +105,8 @@ def test_narrowcast_text_message(self): AudienceRecipient(group_id=5614991017776), Not(AudienceRecipient(group_id=4389303728991)) ), - filter=Filter(demographic= - Or( + filter=Filter( + demographic=Or( And( GenderFilter(one_of=["male", "female"]), AgeFilter(gte="age_20", lt="age_25"), From 7c17cae1dd39140a6c36ad19631cdf864ae72116 Mon Sep 17 00:00:00 2001 From: Cheng-Lung Sung Date: Tue, 25 Feb 2020 21:11:52 +0800 Subject: [PATCH 14/14] fix typo --- linebot/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/linebot/api.py b/linebot/api.py index ea8eccf6..77f20344 100644 --- a/linebot/api.py +++ b/linebot/api.py @@ -212,7 +212,7 @@ def broadcast(self, messages, notification_disabled=False, timeout=None): return BroadcastResponse(request_id=response.headers.get('X-Line-Request-Id')) def narrowcast(self, messages, recipient=None, filter=None, limit=None, timeout=None): - """Call multicast API. + """Call narrowcast API. https://developers.line.biz/en/reference/messaging-api/#send-narrowcast-message