From 598e86c1b7413e95280b24964d917554924e6ef2 Mon Sep 17 00:00:00 2001 From: Shohei Maeda Date: Tue, 8 Oct 2019 04:32:14 +0900 Subject: [PATCH] Minor updates (#227) * Separate analytics class from resouce.py * Add some missing endpoints * bump version --- tests/test_analytics_async.py | 2 +- twitter_ads/__init__.py | 2 +- twitter_ads/analytics.py | 163 +++++++++++++++++++++++++++++++++ twitter_ads/campaign.py | 59 ++++++++++-- twitter_ads/creative.py | 3 +- twitter_ads/resource.py | 167 +++------------------------------- 6 files changed, 231 insertions(+), 165 deletions(-) create mode 100644 twitter_ads/analytics.py diff --git a/tests/test_analytics_async.py b/tests/test_analytics_async.py index 9ff60b8..0b6e123 100644 --- a/tests/test_analytics_async.py +++ b/tests/test_analytics_async.py @@ -5,7 +5,7 @@ from twitter_ads.account import Account from twitter_ads.client import Client from twitter_ads.campaign import Campaign -from twitter_ads.resource import Analytics +from twitter_ads.analytics import Analytics from twitter_ads.enum import ENTITY, METRIC_GROUP, GRANULARITY from twitter_ads import API_VERSION diff --git a/twitter_ads/__init__.py b/twitter_ads/__init__.py index d4098fc..d5e5d21 100644 --- a/twitter_ads/__init__.py +++ b/twitter_ads/__init__.py @@ -1,6 +1,6 @@ # Copyright (C) 2015 Twitter, Inc. -VERSION = (6, 0, 0) +VERSION = (6, 0, 1) API_VERSION = '6' from twitter_ads.utils import get_version diff --git a/twitter_ads/analytics.py b/twitter_ads/analytics.py new file mode 100644 index 0000000..1690b9f --- /dev/null +++ b/twitter_ads/analytics.py @@ -0,0 +1,163 @@ +# Copyright (C) 2015 Twitter, Inc. + +"""Container for all plugable resource object logic used by the Ads API SDK.""" + +from datetime import datetime, timedelta +try: + from urllib.parse import urlparse +except ImportError: + from urlparse import urlparse + +from twitter_ads.utils import to_time, validate_whole_hours +from twitter_ads.enum import ENTITY, GRANULARITY, PLACEMENT, TRANSFORM +from twitter_ads.http import Request +from twitter_ads.cursor import Cursor +from twitter_ads.resource import Resource, resource_property +from twitter_ads import API_VERSION +from twitter_ads.utils import FlattenParams + + +class Analytics(Resource): + """ + Container for all analytics related logic used by API resource objects. + """ + PROPERTIES = {} + + ANALYTICS_MAP = { + 'Campaign': ENTITY.CAMPAIGN, + 'FundingInstrument': ENTITY.FUNDING_INSTRUMENT, + 'LineItem': ENTITY.LINE_ITEM, + 'MediaCreative': ENTITY.MEDIA_CREATIVE, + 'OrganicTweet': ENTITY.ORGANIC_TWEET, + 'PromotedTweet': ENTITY.PROMOTED_TWEET, + 'PromotedAccount': ENTITY.PROMOTED_ACCOUNT + } + + RESOURCE_SYNC = '/' + API_VERSION + '/stats/accounts/{account_id}' + RESOURCE_ASYNC = '/' + API_VERSION + '/stats/jobs/accounts/{account_id}' + RESOURCE_ACTIVE_ENTITIES = '/' + API_VERSION + '/stats/accounts/{account_id}/active_entities' + + def stats(self, metrics, **kwargs): # noqa + """ + Pulls a list of metrics for the current object instance. + """ + return self.__class__.all_stats(self.account, [self.id], metrics, **kwargs) + + @classmethod + def _standard_params(klass, ids, metric_groups, **kwargs): + """ + Sets the standard params for a stats request + """ + end_time = kwargs.get('end_time', datetime.utcnow()) + start_time = kwargs.get('start_time', end_time - timedelta(seconds=604800)) + granularity = kwargs.get('granularity', GRANULARITY.HOUR) + placement = kwargs.get('placement', PLACEMENT.ALL_ON_TWITTER) + entity = kwargs.get('entity', None) + + params = { + 'metric_groups': ','.join(map(str, metric_groups)), + 'start_time': to_time(start_time, granularity), + 'end_time': to_time(end_time, granularity), + 'granularity': granularity.upper(), + 'entity': entity or klass.ANALYTICS_MAP[klass.__name__], + 'placement': placement + } + + params['entity_ids'] = ','.join(map(str, ids)) + + return params + + @classmethod + def all_stats(klass, account, ids, metric_groups, **kwargs): + """ + Pulls a list of metrics for a specified set of object IDs. + """ + params = klass._standard_params(ids, metric_groups, **kwargs) + + resource = klass.RESOURCE_SYNC.format(account_id=account.id) + response = Request(account.client, 'get', resource, params=params).perform() + return response.body['data'] + + @classmethod + def queue_async_stats_job(klass, account, ids, metric_groups, **kwargs): + """ + Queues a list of metrics for a specified set of object IDs asynchronously + """ + params = klass._standard_params(ids, metric_groups, **kwargs) + + params['platform'] = kwargs.get('platform', None) + params['country'] = kwargs.get('country', None) + params['segmentation_type'] = kwargs.get('segmentation_type', None) + + resource = klass.RESOURCE_ASYNC.format(account_id=account.id) + response = Request(account.client, 'post', resource, params=params).perform() + return Analytics(account).from_response(response.body['data'], headers=response.headers) + + @classmethod + @FlattenParams + def async_stats_job_result(klass, account, **kwargs): + """ + Returns the results of the specified async job IDs + """ + resource = klass.RESOURCE_ASYNC.format(account_id=account.id) + request = Request(account.client, 'get', resource, params=kwargs) + + return Cursor(Analytics, request, init_with=[account]) + + @classmethod + def async_stats_job_data(klass, account, url, **kwargs): + """ + Returns the results of the specified async job IDs + """ + resource = urlparse(url) + domain = '{0}://{1}'.format(resource.scheme, resource.netloc) + + response = Request(account.client, 'get', resource.path, domain=domain, + raw_body=True, stream=True).perform() + + return response.body + + @classmethod + @FlattenParams + def active_entities(klass, account, start_time, end_time, **kwargs): + """ + Returns the details about which entities' analytics metrics + have changed in a given time period. + """ + entity = kwargs.get('entity') or klass.ANALYTICS_MAP[klass.__name__] + if entity == klass.ANALYTICS_MAP['OrganicTweet']: + raise ValueError("'OrganicTweet' not support with 'active_entities'") + + # The start and end times must be expressed in whole hours + validate_whole_hours(start_time) + validate_whole_hours(end_time) + + params = { + 'entity': entity, + 'start_time': to_time(start_time, None), + 'end_time': to_time(end_time, None) + } + params.update(kwargs) + + resource = klass.RESOURCE_ACTIVE_ENTITIES.format(account_id=account.id) + response = Request(account.client, 'get', resource, params=params).perform() + return response.body['data'] + + +# Analytics properties +# read-only +resource_property(Analytics, 'id', readonly=True) +resource_property(Analytics, 'id_str', readonly=True) +resource_property(Analytics, 'status', readonly=True) +resource_property(Analytics, 'url', readonly=True) +resource_property(Analytics, 'created_at', readonly=True, transform=TRANSFORM.TIME) +resource_property(Analytics, 'expires_at', readonly=True, transform=TRANSFORM.TIME) +resource_property(Analytics, 'updated_at', readonly=True, transform=TRANSFORM.TIME) + +resource_property(Analytics, 'start_time', readonly=True, transform=TRANSFORM.TIME) +resource_property(Analytics, 'end_time', readonly=True, transform=TRANSFORM.TIME) +resource_property(Analytics, 'entity', readonly=True) +resource_property(Analytics, 'entity_ids', readonly=True) +resource_property(Analytics, 'placement', readonly=True) +resource_property(Analytics, 'granularity', readonly=True) +resource_property(Analytics, 'metric_groups', readonly=True) diff --git a/twitter_ads/campaign.py b/twitter_ads/campaign.py index b580a7f..42c859f 100644 --- a/twitter_ads/campaign.py +++ b/twitter_ads/campaign.py @@ -3,7 +3,8 @@ """Container for all campaign management logic used by the Ads API SDK.""" from twitter_ads.enum import TRANSFORM -from twitter_ads.resource import resource_property, Resource, Persistence, Batch, Analytics +from twitter_ads.analytics import Analytics +from twitter_ads.resource import resource_property, Resource, Persistence, Batch from twitter_ads.http import Request from twitter_ads.cursor import Cursor from twitter_ads.utils import FlattenParams @@ -200,13 +201,6 @@ class AppList(Resource, Persistence): RESOURCE_COLLECTION = '/' + API_VERSION + '/accounts/{account_id}/app_lists' RESOURCE = '/' + API_VERSION + '/accounts/{account_id}/app_lists/{id}' - @classmethod - @FlattenParams - def create(klass, account, **kwargs): - resource = klass.RESOURCE_COLLECTION.format(account_id=account.id) - response = Request(account.client, 'post', resource, params=kwargs).perform() - return klass(account).from_response(response.body['data']) - def apps(self): if self.id and not hasattr(self, '_apps'): self.reload() @@ -306,6 +300,33 @@ def targeting_criteria(self, id=None, **kwargs): resource_property(LineItem, 'to_delete', transform=TRANSFORM.BOOL) +class LineItemApps(Resource, Persistence): + + PROPERTIES = {} + + RESOURCE_COLLECTION = '/' + API_VERSION + '/accounts/{account_id}/line_item_apps' + RESOURCE = '/' + API_VERSION + '/accounts/{account_id}/line_item_apps/{id}' + + +resource_property(LineItemApps, 'created_at', readonly=True, transform=TRANSFORM.TIME) +resource_property(LineItemApps, 'updated_at', readonly=True, transform=TRANSFORM.TIME) +resource_property(LineItemApps, 'deleted', readonly=True, transform=TRANSFORM.BOOL) +resource_property(LineItemApps, 'id', readonly=True) +resource_property(LineItemApps, 'os_type', readonly=True) +resource_property(LineItemApps, 'app_store_identifier', readonly=True) +resource_property(LineItemApps, 'line_item_id', readonly=True) + + +class LineItemPlacements(Resource): + + PROPERTIES = {} + RESOURCE_COLLECTION = '/' + API_VERSION + '/line_items/placements' + + +resource_property(LineItemPlacements, 'product_type', readonly=True) +resource_property(LineItemPlacements, 'placements', readonly=True) + + class ScheduledPromotedTweet(Resource, Persistence): PROPERTIES = {} @@ -415,3 +436,25 @@ def save(self): resource_property(TaxSettings, 'tax_category') resource_property(TaxSettings, 'tax_exemption_id') resource_property(TaxSettings, 'tax_id') + + +class ContentCategories(Resource): + + PROPERTIES = {} + RESOURCE_COLLECTION = '/' + API_VERSION + '/content_categories' + + +resource_property(ContentCategories, 'id', readonly=True) +resource_property(ContentCategories, 'name', readonly=True) +resource_property(ContentCategories, 'iab_categories', readonly=True) + + +class IabCategories(Resource): + + PROPERTIES = {} + RESOURCE_COLLECTION = '/' + API_VERSION + '/iab_categories' + + +resource_property(IabCategories, 'id', readonly=True) +resource_property(IabCategories, 'name', readonly=True) +resource_property(IabCategories, 'parent_id', readonly=True) diff --git a/twitter_ads/creative.py b/twitter_ads/creative.py index ff5732f..230fa21 100644 --- a/twitter_ads/creative.py +++ b/twitter_ads/creative.py @@ -7,7 +7,8 @@ from twitter_ads.cursor import Cursor from twitter_ads.enum import TRANSFORM from twitter_ads.http import Request -from twitter_ads.resource import resource_property, Resource, Persistence, Analytics +from twitter_ads.analytics import Analytics +from twitter_ads.resource import resource_property, Resource, Persistence from twitter_ads.utils import Deprecated, FlattenParams diff --git a/twitter_ads/resource.py b/twitter_ads/resource.py index 75a90da..95f55cf 100644 --- a/twitter_ads/resource.py +++ b/twitter_ads/resource.py @@ -3,18 +3,13 @@ """Container for all plugable resource object logic used by the Ads API SDK.""" import dateutil.parser -from datetime import datetime, timedelta -try: - from urllib.parse import urlparse -except ImportError: - from urlparse import urlparse import json -from twitter_ads.utils import format_time, to_time, validate_whole_hours -from twitter_ads.enum import ENTITY, GRANULARITY, PLACEMENT, TRANSFORM +from datetime import datetime +from twitter_ads.utils import format_time +from twitter_ads.enum import ENTITY, TRANSFORM from twitter_ads.http import Request from twitter_ads.cursor import Cursor -from twitter_ads import API_VERSION from twitter_ads.utils import extract_response_headers, FlattenParams @@ -190,6 +185,16 @@ class Persistence(object): Container for all persistence related logic used by API resource objects. """ + @classmethod + @FlattenParams + def create(self, account, **kwargs): + """ + Create a new item. + """ + resource = self.RESOURCE_COLLECTION.format(account_id=account.id) + response = Request(account.client, 'post', resource, params=kwargs).perform() + return self(account).from_response(response.body['data']) + def save(self): """ Saves or updates the current object instance depending on the @@ -216,149 +221,3 @@ def delete(self): resource = self.RESOURCE.format(account_id=self.account.id, id=self.id) response = Request(self.account.client, 'delete', resource).perform() self.from_response(response.body['data']) - - -class Analytics(Resource): - """ - Container for all analytics related logic used by API resource objects. - """ - PROPERTIES = {} - - ANALYTICS_MAP = { - 'Campaign': ENTITY.CAMPAIGN, - 'FundingInstrument': ENTITY.FUNDING_INSTRUMENT, - 'LineItem': ENTITY.LINE_ITEM, - 'MediaCreative': ENTITY.MEDIA_CREATIVE, - 'OrganicTweet': ENTITY.ORGANIC_TWEET, - 'PromotedTweet': ENTITY.PROMOTED_TWEET, - 'PromotedAccount': ENTITY.PROMOTED_ACCOUNT - } - - RESOURCE_SYNC = '/' + API_VERSION + '/stats/accounts/{account_id}' - RESOURCE_ASYNC = '/' + API_VERSION + '/stats/jobs/accounts/{account_id}' - RESOURCE_ACTIVE_ENTITIES = '/' + API_VERSION + '/stats/accounts/{account_id}/active_entities' - - def stats(self, metrics, **kwargs): # noqa - """ - Pulls a list of metrics for the current object instance. - """ - return self.__class__.all_stats(self.account, [self.id], metrics, **kwargs) - - @classmethod - def _standard_params(klass, ids, metric_groups, **kwargs): - """ - Sets the standard params for a stats request - """ - end_time = kwargs.get('end_time', datetime.utcnow()) - start_time = kwargs.get('start_time', end_time - timedelta(seconds=604800)) - granularity = kwargs.get('granularity', GRANULARITY.HOUR) - placement = kwargs.get('placement', PLACEMENT.ALL_ON_TWITTER) - entity = kwargs.get('entity', None) - - params = { - 'metric_groups': ','.join(map(str, metric_groups)), - 'start_time': to_time(start_time, granularity), - 'end_time': to_time(end_time, granularity), - 'granularity': granularity.upper(), - 'entity': entity or klass.ANALYTICS_MAP[klass.__name__], - 'placement': placement - } - - params['entity_ids'] = ','.join(map(str, ids)) - - return params - - @classmethod - def all_stats(klass, account, ids, metric_groups, **kwargs): - """ - Pulls a list of metrics for a specified set of object IDs. - """ - params = klass._standard_params(ids, metric_groups, **kwargs) - - resource = klass.RESOURCE_SYNC.format(account_id=account.id) - response = Request(account.client, 'get', resource, params=params).perform() - return response.body['data'] - - @classmethod - def queue_async_stats_job(klass, account, ids, metric_groups, **kwargs): - """ - Queues a list of metrics for a specified set of object IDs asynchronously - """ - params = klass._standard_params(ids, metric_groups, **kwargs) - - params['platform'] = kwargs.get('platform', None) - params['country'] = kwargs.get('country', None) - params['segmentation_type'] = kwargs.get('segmentation_type', None) - - resource = klass.RESOURCE_ASYNC.format(account_id=account.id) - response = Request(account.client, 'post', resource, params=params).perform() - return Analytics(account).from_response(response.body['data'], headers=response.headers) - - @classmethod - @FlattenParams - def async_stats_job_result(klass, account, **kwargs): - """ - Returns the results of the specified async job IDs - """ - resource = klass.RESOURCE_ASYNC.format(account_id=account.id) - request = Request(account.client, 'get', resource, params=kwargs) - - return Cursor(Analytics, request, init_with=[account]) - - @classmethod - def async_stats_job_data(klass, account, url, **kwargs): - """ - Returns the results of the specified async job IDs - """ - resource = urlparse(url) - domain = '{0}://{1}'.format(resource.scheme, resource.netloc) - - response = Request(account.client, 'get', resource.path, domain=domain, - raw_body=True, stream=True).perform() - - return response.body - - @classmethod - @FlattenParams - def active_entities(klass, account, start_time, end_time, **kwargs): - """ - Returns the details about which entities' analytics metrics - have changed in a given time period. - """ - entity = kwargs.get('entity') or klass.ANALYTICS_MAP[klass.__name__] - if entity == klass.ANALYTICS_MAP['OrganicTweet']: - raise ValueError("'OrganicTweet' not support with 'active_entities'") - - # The start and end times must be expressed in whole hours - validate_whole_hours(start_time) - validate_whole_hours(end_time) - - params = { - 'entity': entity, - 'start_time': to_time(start_time, None), - 'end_time': to_time(end_time, None) - } - params.update(kwargs) - - resource = klass.RESOURCE_ACTIVE_ENTITIES.format(account_id=account.id) - response = Request(account.client, 'get', resource, params=params).perform() - return response.body['data'] - - -# Analytics properties -# read-only -resource_property(Analytics, 'id', readonly=True) -resource_property(Analytics, 'id_str', readonly=True) -resource_property(Analytics, 'status', readonly=True) -resource_property(Analytics, 'url', readonly=True) -resource_property(Analytics, 'created_at', readonly=True, transform=TRANSFORM.TIME) -resource_property(Analytics, 'expires_at', readonly=True, transform=TRANSFORM.TIME) -resource_property(Analytics, 'updated_at', readonly=True, transform=TRANSFORM.TIME) - -resource_property(Analytics, 'start_time', readonly=True, transform=TRANSFORM.TIME) -resource_property(Analytics, 'end_time', readonly=True, transform=TRANSFORM.TIME) -resource_property(Analytics, 'entity', readonly=True) -resource_property(Analytics, 'entity_ids', readonly=True) -resource_property(Analytics, 'placement', readonly=True) -resource_property(Analytics, 'granularity', readonly=True) -resource_property(Analytics, 'metric_groups', readonly=True)