templates/python/facebook_business/adobjects/abstractcrudobject.py.versioned.mustache (662 lines of code) (raw):
# Copyright 2014 Facebook, Inc.
# You are hereby granted a non-exclusive, worldwide, royalty-free license to
# use, copy, modify, and distribute this software in source code or binary
# form for use in connection with the web services and APIs provided by
# Facebook.
# As with any software that integrates with the Facebook platform, your use
# of this software is subject to the Facebook Developer Principles and
# Policies [http://developers.facebook.com/policy/]. This copyright notice
# shall be included in all copies or substantial portions of the software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
from facebook_business.exceptions import (
FacebookBadObjectError,
)
from facebook_business.api import (
FacebookAdsApi,
Cursor,
FacebookRequest,
)
from facebook_business.adobjects.abstractobject import AbstractObject
from facebook_business.adobjects.objectparser import ObjectParser
{{#version}}
{{#has_deprecate_implicit_creation_new}}
import logging
{{/has_deprecate_implicit_creation_new}}
{{/version}}
class AbstractCrudObject(AbstractObject):
"""
Extends AbstractObject and implements methods to create, read, update,
and delete.
Attributes:
parent_id: The object's parent's id. (default None)
api: The api instance associated with this object. (default None)
"""
def __init__(self, fbid=None, parent_id=None, api=None):
"""Initializes a CRUD object.
Args:
fbid (optional): The id of the object ont the Graph.
parent_id (optional): The id of the object's parent.
api (optional): An api object which all calls will go through. If
an api object is not specified, api calls will revert to going
through the default api.
"""
super(AbstractCrudObject, self).__init__()
self._api = api or FacebookAdsApi.get_default_api()
self._changes = {}
{{#version}}
{{#has_deprecate_implicit_creation_new}}
if (parent_id is not None):
warning_message = "parent_id as a parameter of constructor is " \
"being deprecated."
logging.warning(warning_message)
{{/has_deprecate_implicit_creation_new}}
{{/version}}
self._parent_id = parent_id
self._data['id'] = fbid
self._include_summary = True
def __setitem__(self, key, value):
"""Sets an item in this CRUD object while maintaining a changelog."""
if key not in self._data or self._data[key] != value:
self._changes[key] = value
super(AbstractCrudObject, self).__setitem__(key, value)
if '_setitem_trigger' in dir(self):
self._setitem_trigger(key, value)
return self
def __delitem__(self, key):
del self._data[key]
self._changes.pop(key, None)
def __eq__(self, other):
"""Two objects are the same if they have the same fbid."""
return (
# Same class
isinstance(other, self.__class__) and
# Both have id's
self.get_id() and other.get_id() and
# Both have same id
self.get_id() == other.get_id()
)
def __ne__(self, other):
return not self.__eq__(other)
@classmethod
def get_by_ids(cls, ids, params=None, fields=None, api=None):
api = api or FacebookAdsApi.get_default_api()
params = dict(params or {})
cls._assign_fields_to_params(fields, params)
params['ids'] = ','.join(map(str, ids))
response = api.call(
'GET',
['/'],
params=params,
)
result = []
for fbid, data in response.json().items():
obj = cls(fbid, api=api)
obj._set_data(data)
result.append(obj)
return result
# Getters
def get_id(self):
"""Returns the object's fbid if set. Else, it returns None."""
return self[self.Field.id] if hasattr(self, 'Field') and hasattr(self.Field, 'Field') else self['id']
{{#version}}
{{#has_deprecate_implicit_creation_new_v2}}
# @deprecated deprecate parent_id in AbstractCrudObject
{{/has_deprecate_implicit_creation_new_v2}}
{{/version}}
def get_parent_id(self):
{{#version}}
{{#has_deprecate_implicit_creation_new_v2}}
warning_message = "parent_id is being deprecated."
logging.warning(warning_message)
{{/has_deprecate_implicit_creation_new_v2}}
{{/version}}
"""Returns the object's parent's id."""
return self._parent_id or FacebookAdsApi.get_default_account_id()
def get_api(self):
"""
Returns the api associated with the object.
"""
return self._api
def get_id_assured(self):
"""Returns the fbid of the object.
Raises:
FacebookBadObjectError if the object does not have an id.
"""
if not self.get(self.Field.id):
raise FacebookBadObjectError(
"%s object needs an id for this operation."
% self.__class__.__name__,
)
return self.get_id()
{{#version}}
{{#has_deprecate_implicit_creation_new_v2}}
# @deprecated deprecate parent_id in AbstractCrudObject
{{/has_deprecate_implicit_creation_new_v2}}
{{/version}}
def get_parent_id_assured(self):
"""Returns the object's parent's fbid.
Raises:
FacebookBadObjectError if the object does not have a parent id.
"""
{{#version}}
{{#has_deprecate_implicit_creation_new_v2}}
warning_message = "parent_id is being deprecated."
logging.warning(warning_message)
{{/has_deprecate_implicit_creation_new_v2}}
{{/version}}
if not self.get_parent_id():
raise FacebookBadObjectError(
"%s object needs a parent_id for this operation."
% self.__class__.__name__,
)
return self.get_parent_id()
def get_api_assured(self):
"""Returns the fbid of the object.
Raises:
FacebookBadObjectError if get_api returns None.
"""
api = self.get_api()
if not api:
raise FacebookBadObjectError(
"%s does not yet have an associated api object.\n"
"Did you forget to instantiate an API session with: "
"FacebookAdsApi.init(app_id, app_secret, access_token)"
% self.__class__.__name__,
)
return api
# Data management
def _clear_history(self):
self._changes = {}
if 'filename' in self._data:
del self._data['filename']
return self
def _set_data(self, data):
"""
Sets object's data as if it were read from the server.
Warning: Does not log changes.
"""
for key in map(str, data):
self[key] = data[key]
# clear history due to the update
self._changes.pop(key, None)
self._json = data
return self
def export_changed_data(self):
"""
Returns a dictionary of property names mapped to their values for
properties modified from their original values.
"""
return self.export_value(self._changes)
def export_data(self):
"""
Deprecated. Use export_all_data() or export_changed_data() instead.
"""
return self.export_changed_data()
# CRUD Helpers
def clear_id(self):
"""Clears the object's fbid."""
del self[self.Field.id]
return self
def get_node_path(self):
"""Returns the node's relative path as a tuple of tokens."""
return (self.get_id_assured(),)
def get_node_path_string(self):
"""Returns the node's path as a tuple."""
return '/'.join(self.get_node_path())
# CRUD
{{#version}}
{{#has_deprecate_implicit_creation_new_v2}}
# @deprecated
# use Object(parent_id).create_xxx() instead
{{/has_deprecate_implicit_creation_new_v2}}
{{/version}}
def remote_create(
self,
batch=None,
failure=None,
files=None,
params=None,
success=None,
api_version=None,
):
"""Creates the object by calling the API.
Args:
batch (optional): A FacebookAdsApiBatch object. If specified,
the call will be added to the batch.
params (optional): A mapping of request parameters where a key
is the parameter name and its value is a string or an object
which can be JSON-encoded.
files (optional): An optional mapping of file names to binary open
file objects. These files will be attached to the request.
success (optional): A callback function which will be called with
the FacebookResponse of this call if the call succeeded.
failure (optional): A callback function which will be called with
the FacebookResponse of this call if the call failed.
Returns:
self if not a batch call.
the return value of batch.add if a batch call.
"""
{{#version}}
{{#has_deprecate_implicit_creation_new_v2}}
warning_message = "`remote_create` is being deprecated, please update your code with new function."
logging.warning(warning_message)
{{/has_deprecate_implicit_creation_new_v2}}
{{/version}}
if self.get_id():
raise FacebookBadObjectError(
"This %s object was already created."
% self.__class__.__name__,
)
if not 'get_endpoint' in dir(self):
raise TypeError('Cannot create object of type %s.'
% self.__class__.__name__)
params = {} if not params else params.copy()
params.update(self.export_all_data())
request = None
if hasattr(self, 'api_create'):
request = self.api_create(self.get_parent_id_assured(), pending=True)
else:
request = FacebookRequest(
node_id=self.get_parent_id_assured(),
method='POST',
endpoint=self.get_endpoint(),
api=self._api,
target_class=self.__class__,
response_parser=ObjectParser(
reuse_object=self
),
)
request.add_params(params)
request.add_files(files)
if batch is not None:
def callback_success(response):
self._set_data(response.json())
self._clear_history()
if success:
success(response)
def callback_failure(response):
if failure:
failure(response)
return batch.add_request(
request=request,
success=callback_success,
failure=callback_failure,
)
else:
response = request.execute()
self._set_data(response._json)
self._clear_history()
return self
{{#version}}
{{#has_deprecate_implicit_creation_new_v2}}
# @deprecated
# use Object(id).api_get() instead
{{/has_deprecate_implicit_creation_new_v2}}
{{/version}}
def remote_read(
self,
batch=None,
failure=None,
fields=None,
params=None,
success=None,
api_version=None,
):
"""Reads the object by calling the API.
Args:
batch (optional): A FacebookAdsApiBatch object. If specified,
the call will be added to the batch.
fields (optional): A list of fields to read.
params (optional): A mapping of request parameters where a key
is the parameter name and its value is a string or an object
which can be JSON-encoded.
files (optional): An optional mapping of file names to binary open
file objects. These files will be attached to the request.
success (optional): A callback function which will be called with
the FacebookResponse of this call if the call succeeded.
failure (optional): A callback function which will be called with
the FacebookResponse of this call if the call failed.
Returns:
self if not a batch call.
the return value of batch.add if a batch call.
"""
{{#version}}
{{#has_deprecate_implicit_creation_new_v2}}
warning_message = "`remote_read` is being deprecated, please update your code with new function."
logging.warning(warning_message)
{{/has_deprecate_implicit_creation_new_v2}}
{{/version}}
params = dict(params or {})
if hasattr(self, 'api_get'):
request = self.api_get(pending=True)
else:
request = FacebookRequest(
node_id=self.get_id_assured(),
method='GET',
endpoint='/',
api=self._api,
target_class=self.__class__,
response_parser=ObjectParser(
reuse_object=self
),
)
request.add_params(params)
request.add_fields(fields)
if batch is not None:
def callback_success(response):
self._set_data(response.json())
if success:
success(response)
def callback_failure(response):
if failure:
failure(response)
batch_call = batch.add_request(
request=request,
success=callback_success,
failure=callback_failure,
)
return batch_call
else:
self = request.execute()
return self
{{#version}}
{{#has_deprecate_implicit_creation_new_v2}}
# @deprecated
# use Object(id).api_update() instead
{{/has_deprecate_implicit_creation_new_v2}}
{{/version}}
def remote_update(
self,
batch=None,
failure=None,
files=None,
params=None,
success=None,
api_version=None,
):
"""Updates the object by calling the API with only the changes recorded.
Args:
batch (optional): A FacebookAdsApiBatch object. If specified,
the call will be added to the batch.
params (optional): A mapping of request parameters where a key
is the parameter name and its value is a string or an object
which can be JSON-encoded.
files (optional): An optional mapping of file names to binary open
file objects. These files will be attached to the request.
success (optional): A callback function which will be called with
the FacebookResponse of this call if the call succeeded.
failure (optional): A callback function which will be called with
the FacebookResponse of this call if the call failed.
Returns:
self if not a batch call.
the return value of batch.add if a batch call.
"""
{{#version}}
{{#has_deprecate_implicit_creation_new_v2}}
warning_message = "`remote_update` is being deprecated, please update your code with new function."
logging.warning(warning_message)
{{/has_deprecate_implicit_creation_new_v2}}
{{/version}}
params = {} if not params else params.copy()
params.update(self.export_changed_data())
self._set_data(params)
if hasattr(self, 'api_update'):
request = self.api_update(pending=True)
else:
request = FacebookRequest(
node_id=self.get_id_assured(),
method='POST',
endpoint='/',
api=self._api,
target_class=self.__class__,
response_parser=ObjectParser(
reuse_object=self
),
)
request.add_params(params)
request.add_files(files)
if batch is not None:
def callback_success(response):
self._clear_history()
if success:
success(response)
def callback_failure(response):
if failure:
failure(response)
batch_call = batch.add_request(
request=request,
success=callback_success,
failure=callback_failure,
)
return batch_call
else:
request.execute()
self._clear_history()
return self
{{#version}}
{{#has_deprecate_implicit_creation_new_v2}}
# @deprecated
# use Object(id).api_delete() instead
{{/has_deprecate_implicit_creation_new_v2}}
{{/version}}
def remote_delete(
self,
batch=None,
failure=None,
params=None,
success=None,
api_version=None,
):
"""Deletes the object by calling the API with the DELETE http method.
Args:
batch (optional): A FacebookAdsApiBatch object. If specified,
the call will be added to the batch.
params (optional): A mapping of request parameters where a key
is the parameter name and its value is a string or an object
which can be JSON-encoded.
success (optional): A callback function which will be called with
the FacebookResponse of this call if the call succeeded.
failure (optional): A callback function which will be called with
the FacebookResponse of this call if the call failed.
Returns:
self if not a batch call.
the return value of batch.add if a batch call.
"""
{{#version}}
{{#has_deprecate_implicit_creation_new_v2}}
warning_message = "`remote_delete` is being deprecated, please update your code with new function."
logging.warning(warning_message)
{{/has_deprecate_implicit_creation_new_v2}}
{{/version}}
if hasattr(self, 'api_delete'):
request = self.api_delete(pending=True)
else:
request = FacebookRequest(
node_id=self.get_id_assured(),
method='DELETE',
endpoint='/',
api=self._api,
)
request.add_params(params)
if batch is not None:
def callback_success(response):
self.clear_id()
if success:
success(response)
def callback_failure(response):
if failure:
failure(response)
batch_call = batch.add_request(
request=request,
success=callback_success,
failure=callback_failure,
)
return batch_call
else:
request.execute()
self.clear_id()
return self
# Helpers
{{#version}}
{{#has_deprecate_implicit_creation_new_v2}}
# @deprecated
{{/has_deprecate_implicit_creation_new_v2}}
{{/version}}
def remote_save(self, *args, **kwargs):
"""
Calls remote_create method if object has not been created. Else, calls
the remote_update method.
"""
{{#version}}
{{#has_deprecate_implicit_creation_new_v2}}
warning_message = "`remote_save` is being deprecated, please update your code with new function."
logging.warning(warning_message)
{{/has_deprecate_implicit_creation_new_v2}}
{{/version}}
if self.get_id():
return self.remote_update(*args, **kwargs)
else:
return self.remote_create(*args, **kwargs)
def remote_archive(
self,
batch=None,
failure=None,
success=None
):
if 'Status' not in dir(self) or 'archived' not in dir(self.Status):
raise TypeError('Cannot archive object of type %s.'
% self.__class__.__name__)
{{#version}}
{{#has_deprecate_implicit_creation_new_v2}}
return self.api_create(
params={
'status': self.Status.archived,
},
batch=batch,
failure=failure,
success=success,
)
{{/has_deprecate_implicit_creation_new_v2}}
{{/version}}
{{#version}}
{{^has_deprecate_implicit_creation_new_v2}}
return self.remote_update(
params={
'status': self.Status.archived,
},
batch=batch,
failure=failure,
success=success,
)
{{/has_deprecate_implicit_creation_new_v2}}
{{/version}}
{{#version}}
{{#has_deprecate_implicit_creation_new_v2}}
# @deprecated
{{/has_deprecate_implicit_creation_new_v2}}
{{/version}}
save = remote_save
def iterate_edge(
self,
target_objects_class,
fields=None,
params=None,
fetch_first_page=True,
include_summary=True,
endpoint=None
):
"""
Returns Cursor with argument self as source_object and
the rest as given __init__ arguments.
Note: list(iterate_edge(...)) can prefetch all the objects.
"""
source_object = self
cursor = Cursor(
source_object,
target_objects_class,
fields=fields,
params=params,
include_summary=include_summary,
endpoint=endpoint,
)
if fetch_first_page:
cursor.load_next_page()
return cursor
def iterate_edge_async(self, target_objects_class, fields=None,
params=None, is_async=False, include_summary=True,
endpoint=None):
from facebook_business.adobjects.adreportrun import AdReportRun
"""
Behaves as iterate_edge(...) if parameter is_async if False
(Default value)
If is_async is True:
Returns an AsyncJob which can be checked using remote_read()
to verify when the job is completed and the result ready to query
or download using get_result()
Example:
>>> job = object.iterate_edge_async(
TargetClass, fields, params, is_async=True)
>>> time.sleep(10)
>>> job.remote_read()
>>> if job:
result = job.read_result()
print result
"""
synchronous = not is_async
synchronous_iterator = self.iterate_edge(
target_objects_class,
fields,
params,
fetch_first_page=synchronous,
include_summary=include_summary,
)
if synchronous:
return synchronous_iterator
if not params:
params = {}
else:
params = dict(params)
self.__class__._assign_fields_to_params(fields, params)
# To force an async response from an edge, do a POST instead of GET.
# The response comes in the format of an AsyncJob which
# indicates the progress of the async request.
if endpoint is None:
endpoint = target_objects_class.get_endpoint()
response = self.get_api_assured().call(
'POST',
(self.get_id_assured(), endpoint),
params=params,
).json()
# AsyncJob stores the real iterator
# for when the result is ready to be queried
result = AdReportRun()
if 'report_run_id' in response:
response['id'] = response['report_run_id']
result._set_data(response)
return result
def edge_object(self, target_objects_class, fields=None, params=None, endpoint=None):
"""
Returns first object when iterating over Cursor with argument
self as source_object and the rest as given __init__ arguments.
"""
params = {} if not params else params.copy()
params['limit'] = '1'
for obj in self.iterate_edge(
target_objects_class,
fields=fields,
params=params,
endpoint=endpoint,
):
return obj
# if nothing found, return None
return None
def assure_call(self):
if not self._api:
raise FacebookBadObjectError(
'Api call cannot be made if api is not set')