diff --git a/README.rst b/README.rst index de2e4d6a..6cc509cd 100644 --- a/README.rst +++ b/README.rst @@ -460,6 +460,42 @@ https://developers.line.biz/en/reference/messaging-api/#revoke-channel-access-to line_bot_api.revoke_channel_token() +get\_insight\_message\_delivery(self, date, timeout=None) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Get the number of messages sent on a specified day. + +https://developers.line.biz/en/reference/messaging-api/#get-number-of-delivery-messages + +.. code:: python + + insight = line_bot_api.get_insight_message_delivery('20191231') + print(insight.api_broadcast) + +get\_insight\_followers(self, date, timeout=None) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Get the number of users who have added the bot on or before a specified date. + +https://developers.line.biz/en/reference/messaging-api/#get-number-of-followers + +.. code:: python + + insight = line_bot_api.get_insight_followers('20191231') + print(insight.followers) + +get\_insight\_demographic(self, timeout=None) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Retrieve the demographic attributes for a bot's friends. + +https://developers.line.biz/en/reference/messaging-api/#get-demographic + +.. code:: python + + insight = line_bot_api.get_insight_demographic() + print(insight.genders) + ※ Error handling ^^^^^^^^^^^^^^^^ diff --git a/docs/source/linebot.models.rst b/docs/source/linebot.models.rst index 6dbca334..7828f09a 100644 --- a/docs/source/linebot.models.rst +++ b/docs/source/linebot.models.rst @@ -32,6 +32,11 @@ linebot.models.imagemap module .. automodule:: linebot.models.imagemap +linebot.models.insight module +------------------------------ + +.. automodule:: linebot.models.insight + linebot.models.messages module ------------------------------ diff --git a/examples/flask-kitchensink/app.py b/examples/flask-kitchensink/app.py index c11d447a..8fb48ef9 100644 --- a/examples/flask-kitchensink/app.py +++ b/examples/flask-kitchensink/app.py @@ -14,6 +14,7 @@ from __future__ import unicode_literals +import datetime import errno import os import sys @@ -366,6 +367,37 @@ def handle_text_message(event): TextSendMessage(text='link_token: ' + link_token_response.link_token) ] ) + elif text == 'insight_message_delivery': + today = datetime.date.today().strftime("%Y%m%d") + response = line_bot_api.get_insight_message_delivery(today) + if response.status == 'ready': + messages = [ + TextSendMessage(text='broadcast: ' + str(response.broadcast)), + TextSendMessage(text='targeting: ' + str(response.targeting)), + ] + else: + messages = [TextSendMessage(text='status: ' + response.status)] + line_bot_api.reply_message(event.reply_token, messages) + elif text == 'insight_followers': + today = datetime.date.today().strftime("%Y%m%d") + response = line_bot_api.get_insight_followers(today) + if response.status == 'ready': + messages = [ + TextSendMessage(text='followers: ' + str(response.followers)), + TextSendMessage(text='targetedReaches: ' + str(response.targeted_reaches)), + TextSendMessage(text='blocks: ' + str(response.blocks)), + ] + else: + messages = [TextSendMessage(text='status: ' + response.status)] + line_bot_api.reply_message(event.reply_token, messages) + elif text == 'insight_demographic': + response = line_bot_api.get_insight_demographic() + if response.available: + messages = ["{gender}: {percentage}".format(gender=it.gender, percentage=it.percentage) + for it in response.genders] + else: + messages = [TextSendMessage(text='available: false')] + line_bot_api.reply_message(event.reply_token, messages) else: line_bot_api.reply_message( event.reply_token, TextSendMessage(text=event.message.text)) diff --git a/linebot/api.py b/linebot/api.py index e5bfa019..4542be8b 100644 --- a/linebot/api.py +++ b/linebot/api.py @@ -26,6 +26,7 @@ MessageQuotaConsumptionResponse, IssueLinkTokenResponse, IssueChannelTokenResponse, MessageDeliveryBroadcastResponse, MessageDeliveryMulticastResponse, MessageDeliveryPushResponse, MessageDeliveryReplyResponse, + InsightMessageDeliveryResponse, InsightFollowersResponse, InsightDemographicResponse, ) @@ -875,6 +876,63 @@ def revoke_channel_token(self, access_token, timeout=None): timeout=timeout ) + def get_insight_message_delivery(self, date, timeout=None): + """Get the number of messages sent on a specified day. + + https://developers.line.biz/en/reference/messaging-api/#get-number-of-delivery-messages + + :param str date: Date for which to retrieve number of sent messages. + :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) + """ + response = self._get( + '/v2/bot/insight/message/delivery?date={date}'.format(date=date), + timeout=timeout + ) + + return InsightMessageDeliveryResponse.new_from_json_dict(response.json) + + def get_insight_followers(self, date, timeout=None): + """Get the number of users who have added the bot on or before a specified date. + + https://developers.line.biz/en/reference/messaging-api/#get-number-of-followers + + :param str date: Date for which to retrieve the number of followers. + :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) + """ + response = self._get( + '/v2/bot/insight/followers?date={date}'.format(date=date), + timeout=timeout + ) + + return InsightFollowersResponse.new_from_json_dict(response.json) + + def get_insight_demographic(self, timeout=None): + """Retrieve the demographic attributes for a bot's friends. + + https://developers.line.biz/en/reference/messaging-api/#get-demographic + + :param str date: Date for which to retrieve the number of followers. + :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) + """ + response = self._get( + '/v2/bot/insight/demographic', + timeout=timeout + ) + + return InsightDemographicResponse.new_from_json_dict(response.json) + def _get(self, path, params=None, headers=None, stream=False, timeout=None): url = self.endpoint + path diff --git a/linebot/models/__init__.py b/linebot/models/__init__.py index ce6ce1d1..a83fcf8d 100644 --- a/linebot/models/__init__.py +++ b/linebot/models/__init__.py @@ -81,6 +81,14 @@ Video, ExternalLink, ) +from .insight import ( # noqa + DemographicInsight, + AgeInsight, + AreaInsight, + AppTypeInsight, + GenderInsight, + SubscriptionPeriodInsight, +) from .messages import ( # noqa Message, TextMessage, @@ -105,6 +113,9 @@ Content as MessageContent, # backward compatibility, IssueLinkTokenResponse, IssueChannelTokenResponse, + InsightMessageDeliveryResponse, + InsightFollowersResponse, + InsightDemographicResponse, ) from .rich_menu import ( # noqa RichMenu, diff --git a/linebot/models/insight.py b/linebot/models/insight.py new file mode 100644 index 00000000..399a290c --- /dev/null +++ b/linebot/models/insight.py @@ -0,0 +1,111 @@ +# -*- 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.insight module.""" + +from __future__ import unicode_literals + +from abc import ABCMeta + +from future.utils import with_metaclass + +from .base import Base + + +class DemographicInsight(with_metaclass(ABCMeta, Base)): + """Abstract Base Class of DemographicInsight.""" + + def __init__(self, percentage=None, **kwargs): + """__init__ method. + + :param float percentage: Percentage. + :param kwargs: + """ + super(DemographicInsight, self).__init__(**kwargs) + self.percentage = percentage + + +class GenderInsight(DemographicInsight): + """GenderInsight.""" + + def __init__(self, percentage=None, gender=None, **kwargs): + """__init__ method. + + :param float percentage: Percentage. + :param str gender: Gender + :param kwargs: + """ + super(GenderInsight, self).__init__(percentage=percentage, **kwargs) + + self.gender = gender + + +class AgeInsight(DemographicInsight): + """AgeInsight.""" + + def __init__(self, percentage=None, age=None, **kwargs): + """__init__ method. + + :param float percentage: Percentage. + :param str age: Age + :param kwargs: + """ + super(AgeInsight, self).__init__(percentage=percentage, **kwargs) + + self.age = age + + +class AreaInsight(DemographicInsight): + """AreaInsight.""" + + def __init__(self, percentage=None, area=None, **kwargs): + """__init__ method. + + :param float percentage: Percentage. + :param str area: Area + :param kwargs: + """ + super(AreaInsight, self).__init__(percentage=percentage, **kwargs) + + self.area = area + + +class AppTypeInsight(DemographicInsight): + """AppTypeInsight.""" + + def __init__(self, percentage=None, app_type=None, **kwargs): + """__init__ method. + + :param float percentage: Percentage. + :param str app_type: OS + :param kwargs: + """ + super(AppTypeInsight, self).__init__(percentage=percentage, **kwargs) + + self.app_type = app_type + + +class SubscriptionPeriodInsight(DemographicInsight): + """SubscriptionPeriodInsight.""" + + def __init__(self, percentage=None, subscription_period=None, **kwargs): + """__init__ method. + + :param float percentage: Percentage. + :param str subscription_period: Friendship duration + :param kwargs: + """ + super(SubscriptionPeriodInsight, self).__init__(percentage=percentage, **kwargs) + + self.subscription_period = subscription_period diff --git a/linebot/models/responses.py b/linebot/models/responses.py index dc644e23..d46059a8 100644 --- a/linebot/models/responses.py +++ b/linebot/models/responses.py @@ -17,6 +17,10 @@ from __future__ import unicode_literals from .base import Base +from .insight import ( + SubscriptionPeriodInsight, AppTypeInsight, + AgeInsight, GenderInsight, AreaInsight +) from .rich_menu import RichMenuSize, RichMenuArea @@ -294,3 +298,97 @@ def __init__(self, access_token=None, expires_in=None, token_type=None, **kwargs self.access_token = access_token self.expires_in = expires_in self.token_type = token_type + + +class InsightMessageDeliveryResponse(Base): + """InsightMessageDeliveryResponse.""" + + def __init__(self, status=None, broadcast=None, targeting=None, auto_response=None, + welcome_response=None, chat=None, api_broadcast=None, api_push=None, + api_multicast=None, api_reply=None, **kwargs): + """__init__ method. + + :param str status: Calculation status. One of `ready`, `unready`, or `out_of_service`. + :param int broadcast: Number of broadcast messages sent. + :param int targeting: Number of targeted/segmented messages sent. + :param int auto_response: Number of auto-response messages sent. + :param int welcome_response: Number of greeting messages sent. + :param int chat: Number of messages sent from LINE Official Account Manager Chat screen. + :param int api_broadcast: Number of broadcast messages sent with + the Send broadcast message Messaging API operation. + :param int api_push: Number of push messages sent + with the Send push message Messaging API operation. + :param int api_multicast: Number of multicast messages sent with + the Send multicast message Messaging API operation. + :param int api_reply: Number of replies sent + with the Send reply message Messaging API operation. + :param int success: The number of messages sent with the Messaging API + on the date specified in date. + :param kwargs: + """ + super(InsightMessageDeliveryResponse, self).__init__(**kwargs) + + self.status = status + self.broadcast = broadcast + self.targeting = targeting + self.auto_response = auto_response + self.welcome_response = welcome_response + self.chat = chat + self.api_broadcast = api_broadcast + self.api_push = api_push + self.api_multicast = api_multicast + self.api_reply = api_reply + + +class InsightFollowersResponse(Base): + """InsightFollowersResponse.""" + + def __init__(self, status=None, followers=None, targeted_reaches=None, blocks=None, **kwargs): + """__init__ method. + + :param str status: Calculation status. One of `ready`, `unready`, or `out_of_service`. + :param int followers: The number of times, as of the specified date, + that a user added this LINE official account as a friend for the first time. + :param int targeted_reaches: The number of users, as of the specified date, + that the official account can reach through targeted messages based + on gender, age, and/or region. + :param int blocks: The number of users blocking the account as of the specified date. + :param kwargs: + """ + super(InsightFollowersResponse, self).__init__(**kwargs) + + self.status = status + self.followers = followers + self.targeted_reaches = targeted_reaches + self.blocks = blocks + + +class InsightDemographicResponse(Base): + """InsightDemographicResponse.""" + + def __init__(self, available=None, genders=None, ages=None, + areas=None, app_types=None, subscription_periods=None, **kwargs): + """__init__ method. + + :param bool available: `true` if friend demographic information is available. + :param genders: Percentage per gender. + :type genders: list[T <= :py:class:`linebot.models.GenderInsight`] + :param ages: Percentage per age group. + :type ages: list[T <= :py:class:`linebot.models.AgeInsight`] + :param areas: Percentage per area. + :type areas: list[T <= :py:class:`linebot.models.AreaInsight`] + :param app_types: Percentage by OS. + :type app_types: list[T <= :py:class:`linebot.models.AppTypeInsight`] + :param subscription_periods: Percentage per friendship duration. + :type subscription_periods: list[T <= :py:class:`linebot.models.SubscriptionPeriodInsight`] + :param kwargs: + """ + super(InsightDemographicResponse, self).__init__(**kwargs) + + self.available = available + self.genders = [self.get_or_new_from_json_dict(it, GenderInsight) for it in genders] + self.ages = [self.get_or_new_from_json_dict(it, AgeInsight) for it in ages] + self.areas = [self.get_or_new_from_json_dict(it, AreaInsight) for it in areas] + self.app_types = [self.get_or_new_from_json_dict(it, AppTypeInsight) for it in app_types] + self.subscription_periods = [self.get_or_new_from_json_dict(it, SubscriptionPeriodInsight) + for it in subscription_periods] diff --git a/tests/api/test_get_insight.py b/tests/api/test_get_insight.py new file mode 100644 index 00000000..1d0af1cc --- /dev/null +++ b/tests/api/test_get_insight.py @@ -0,0 +1,192 @@ +# -*- 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 + +import responses + +from linebot import ( + LineBotApi +) +from linebot.models import ( + GenderInsight, AreaInsight, AgeInsight, AppTypeInsight, SubscriptionPeriodInsight +) + + +class TestLineBotApi(unittest.TestCase): + def setUp(self): + self.tested = LineBotApi('channel_secret') + self.date = '20190101' + + @responses.activate + def test_get_insight_message_delivery(self): + responses.add( + responses.GET, + LineBotApi.DEFAULT_API_ENDPOINT + + '/v2/bot/insight/message/delivery?date={date}'.format(date=self.date), + json={ + 'status': 'ready', + 'broadcast': 100, + 'targeting': 200, + 'autoResponse': 300, + 'welcomeResponse': 400, + 'chat': 500, + 'apiBroadcast': 600, + 'apiPush': 700, + 'apiMulticast': 800, + }, + status=200 + ) + + res = self.tested.get_insight_message_delivery(self.date) + request = responses.calls[0].request + self.assertEqual('GET', request.method) + self.assertEqual('ready', res.status) + self.assertEqual(100, res.broadcast) + self.assertEqual(200, res.targeting) + self.assertEqual(300, res.auto_response) + self.assertEqual(400, res.welcome_response) + self.assertEqual(500, res.chat) + self.assertEqual(600, res.api_broadcast) + self.assertEqual(700, res.api_push) + self.assertEqual(800, res.api_multicast) + self.assertEqual(None, res.api_reply) + + @responses.activate + def test_get_insight_followers(self): + responses.add( + responses.GET, + LineBotApi.DEFAULT_API_ENDPOINT + + '/v2/bot/insight/followers?date={date}'.format(date=self.date), + json={ + 'status': 'ready', + 'followers': 100, + 'targetedReaches': 200, + 'blocks': 300 + }, + status=200 + ) + + res = self.tested.get_insight_followers(self.date) + request = responses.calls[0].request + self.assertEqual('GET', request.method) + self.assertEqual('ready', res.status) + self.assertEqual(100, res.followers) + self.assertEqual(200, res.targeted_reaches) + self.assertEqual(300, res.blocks) + + @responses.activate + def test_get_insight_demographic(self): + responses.add( + responses.GET, + LineBotApi.DEFAULT_API_ENDPOINT + + '/v2/bot/insight/demographic', + json={ + 'available': True, + 'genders': [ + { + 'gender': 'unknown', + 'percentage': 37.6 + }, + { + 'gender': 'male', + 'percentage': 31.8 + }, + { + 'gender': 'female', + 'percentage': 30.6 + } + ], + 'ages': [ + { + 'age': 'unknown', + 'percentage': 37.6 + }, + { + 'age': 'from50', + 'percentage': 17.3 + }, + ], + 'areas': [ + { + 'area': 'unknown', + 'percentage': 50.5 + }, + { + 'area': 'Tokyo', + 'percentage': 49.5 + }, + ], + 'appTypes': [ + { + 'appType': 'ios', + 'percentage': 62.4 + }, + { + 'appType': 'android', + 'percentage': 27.7 + }, + { + 'appType': 'others', + 'percentage': 9.9 + } + ], + 'subscriptionPeriods': [ + { + 'subscriptionPeriod': 'over365days', + 'percentage': 96.4 + }, + { + 'subscriptionPeriod': 'within365days', + 'percentage': 1.9 + }, + { + 'subscriptionPeriod': 'within180days', + 'percentage': 1.2 + }, + { + 'subscriptionPeriod': 'within90days', + 'percentage': 0.5 + }, + { + 'subscriptionPeriod': 'within30days', + 'percentage': 0.1 + }, + { + 'subscriptionPeriod': 'within7days', + 'percentage': 0 + } + ] + }, + status=200 + ) + + res = self.tested.get_insight_demographic() + request = responses.calls[0].request + self.assertEqual('GET', request.method) + self.assertEqual(True, res.available) + self.assertIn(GenderInsight(gender='male', percentage=31.8), res.genders) + self.assertIn(AgeInsight(age='from50', percentage=17.3), res.ages) + self.assertIn(AreaInsight(area='Tokyo', percentage=49.5), res.areas) + self.assertIn(AppTypeInsight(app_type='ios', percentage=62.4), res.app_types) + self.assertIn( + SubscriptionPeriodInsight(subscription_period='over365days', percentage=96.4), + res.subscription_periods + ) + + +if __name__ == '__main__': + unittest.main() diff --git a/tox.ini b/tox.ini index 0815ab7f..ff2a9d58 100644 --- a/tox.ini +++ b/tox.ini @@ -19,6 +19,7 @@ deps = six flake8 flake8-docstrings + pydocstyle<4 commands = flake8 linebot/ [testenv:py37-flake8-other]