Skip to content

Commit

Permalink
Added ObjectStorySpec
Browse files Browse the repository at this point in the history
Summary:
Added ObjectStorySpec (and related Objects) to make it easier for developers
to build specs for API without having to write out a giant JSON string.

Test Plan:
Update the config.json file
Run tests
python -m facebookads.test.unit
python -m facebookads.test.integration <ACCESS_TOKEN>
  • Loading branch information
Evan Chen committed Oct 23, 2014
1 parent 8c4455b commit 9ee253e
Show file tree
Hide file tree
Showing 16 changed files with 382 additions and 73 deletions.
14 changes: 12 additions & 2 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
== 0.1.1 ==
0.2.0
- Added ObjectStorySpec and specs module
- Moved integration tests to their own file
- Added ValidatesFields mixin
- Added bootstrap.auth() function to make using REPL easier
- Deprecated remote_create_with_filename
- Deprecated AbstractObject child method
- Renamed _read_update() to _set_data()


0.1.1
- Increase version requirement for required configparser package (Python 3 compatibility fix)
- Misc issue fix requests

== 0.1.0 ==
0.1.0
Initial release.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ The rest of the example code given will assume you have bootstrapped the api
into your program like the following sample app:

```python
name=my_ads_app.py
from facebookads.session import FacebookSession
from facebookads.api import FacebookAdsApi
from facebookads import objects
Expand Down Expand Up @@ -317,8 +316,12 @@ the SDK.

## Tests

Copy the `config.json.example` to `config.json` and fill in the appropriate
details.

```
python -m facebookads.test.tests app_id app_secret access_token account_id
python -m facebookads.test.unit
python -m facebookads.test.integration <ACCESS_TOKEN>
```

## Examples
Expand Down
6 changes: 5 additions & 1 deletion config.json.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
{
"app_id": "<YOUR_APP_ID>",
"app_secret": "<YOUR_APP_SECRET>"
"app_secret": "<YOUR_APP_SECRET>",

/* For running tests */
"act_id": "act_<YOUR_ACCOUNT_ID>",
"page_id": "<YOUR_PAGE_ID>"
}
3 changes: 2 additions & 1 deletion examples/ad_creation_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ def create_multiple_website_clicks_ads(
image_hashes = []
for image_path in image_paths:
img = AdImage(parent_id=account.get_id_assured())
img.remote_create_from_filename(image_path)
img[AdImage.Field.filename] = image_path
img.remote_create()
image_hashes.append(img.get_hash())

ADGROUP_BATCH_CREATE_LIMIT = 10
Expand Down
2 changes: 1 addition & 1 deletion examples/batch_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
"The Next City",
],
urls=["http://www.seattle.gov/visiting/"],
image_paths=[os.path.join(this_dir, "puget_sound.jpg")],
image_paths=[os.path.join(this_dir, "test.png")],

bid_type=AdSet.BidType.cpm,
bid_info={AdSet.Field.BidInfo.impressions: 53}, # $0.53 / thousand
Expand Down
3 changes: 2 additions & 1 deletion examples/create_ad.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@

### Upload an image to an account.
img = AdImage(parent_id=my_account.get_id_assured())
img.remote_create_from_filename(os.path.join(this_dir, 'puget_sound.jpg'))
img[AdImage.Field.filename] = os.path.join(this_dir, 'test.png')
img.remote_create()
print("**** DONE: Image uploaded:")
pp.pprint(img) # The image hash can be found using img[AdImage.Field.hash]

Expand Down
2 changes: 1 addition & 1 deletion examples/simple_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
title="Visit Seattle", # How it looks
body="Beautiful Puget Sound.",
url="http://www.seattle.gov/visiting/",
image_path=os.path.join(this_dir, 'puget_sound.jpg'),
image_path=os.path.join(this_dir, 'test.png'),

bid_type=AdSet.BidType.cpm,
bid_info={AdSet.Field.BidInfo.impressions: 53}, # $0.53 / thousand
Expand Down
2 changes: 1 addition & 1 deletion facebookads/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ class FacebookAdsApi(object):
this sdk.
"""

SDK_VERSION = '0.1.1'
SDK_VERSION = '0.2.0'

API_VERSION = 'v2.1'

Expand Down
32 changes: 32 additions & 0 deletions facebookads/mixins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# 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 facebookads.exceptions import FacebookBadObjectError


class ValidatesFields(object):
def __setitem__(self, key, value):
if key not in self.Field.__dict__:
raise FacebookBadObjectError(
"\"%s\" is not a valid field of %s"
% (key, self.__class__.__name__)
)
else:
super(ValidatesFields, self).__setitem__(key, value)
88 changes: 37 additions & 51 deletions facebookads/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ def load_next_page(self):
num_added = 0
for json_obj in response['data']:
obj = self._target_objects_class()
obj._read_update(json_obj)
obj._set_data(json_obj)
self._queue.append(obj)
num_added += 1

Expand Down Expand Up @@ -206,24 +206,32 @@ def set_default_read_fields(cls, fields):
"""
cls._default_read_fields = fields

def _read_update(self, data):
def _set_data(self, data):
"""
An AbstractObject does not keep history so _read_update is an alias for
a MutableMapping's update() method. _read_update elsewhere may have a
An AbstractObject does not keep history so _set_data is an alias for
a MutableMapping's update() method. _set_data elsewhere may have a
different behavior depending on the type of the object and how the data
should be processed.
"""
self.update(data)

def export_data(self):
"""Returns a dictionary of property names mapped to their values."""
data = {}
def export_value(self, data):
if isinstance(data, AbstractObject):
data = data.export_data()
elif isinstance(data, dict):
for key, value in data.items():
if value is None:
del data[key]
else:
data[key] = self.export_value(value)
elif isinstance(data, list):
for i, value in enumerate(data):
data[i] = self.export_value(value)
return data

for key in self:
if self[key] is not None:
data[key] = self[key]
def export_data(self):
return self.export_value(self._data)

return data


class AbstractCrudObject(AbstractObject):
Expand Down Expand Up @@ -261,37 +269,11 @@ def __init__(self, fbid=None, parent_id=None, api=None):

def __setitem__(self, key, value):
"""Sets an item in this CRUD object while maintaining a changelog."""
key = str(key)

key_already_set = key in self._data
if key_already_set:
old_data_value = self._data[key]
if key not in self._data or self._data[key] != value:
self._changes[key] = value

self._data[key] = value

if key_already_set:
if key in self._changes:
# key already set and has been a result of user change
if (
'original' in self._changes[key] and
value == self._changes['key']['original']
):
# value is being set back to original
del self._changes['key']
else:
# There's a new value
self._changes[key]['new'] = value
elif value != old_data_value:
# key already set and not in changes (has original value)
self._changes[key] = {
'original': old_data_value,
'new': value,
}
else:
# There's a new value
self._changes[key] = {
'new': value,
}
super(AbstractCrudObject, self).__setitem__(key, value)

return self

Expand Down Expand Up @@ -386,7 +368,7 @@ def _clear_history(self):
self._changes = {}
return self

def _read_update(self, data):
def _set_data(self, data):
"""
Sets object's data as if it were read from the server.
Warning: Does not log changes.
Expand All @@ -410,8 +392,7 @@ def export_data(self):
"""
data = {}

for key in self._changes:
value = self._changes[key]['new']
for key, value in self._changes.items():
if isinstance(value, AbstractObject):
data[key] = value.export_data()
else:
Expand Down Expand Up @@ -465,15 +446,15 @@ def remote_create(
"""
if self.__class__.Field.id in self:
raise FacebookBadObjectError(
"This %s object was alread created." % self.__class__.__name__
"This %s object was already created." % self.__class__.__name__
)

params = {} if params is None else params.copy()
params.update(self.export_data())

if batch is not None:
def callback_success(response):
self._read_update(response.json())
self._set_data(response.json())
self._clear_history()

if success is not None:
Expand All @@ -499,7 +480,7 @@ def callback_failure(response):
params=params,
files=files,
)
self._read_update(response.json())
self._set_data(response.json())

return self

Expand Down Expand Up @@ -546,7 +527,7 @@ def remote_read(

if batch is not None:
def callback_success(response):
self._read_update(response.json())
self._set_data(response.json())

if success is not None:
success(response)
Expand All @@ -569,7 +550,7 @@ def callback_failure(response):
self.get_node_path(),
params=params,
)
self._read_update(response.json())
self._set_data(response.json())

return self

Expand Down Expand Up @@ -1338,6 +1319,7 @@ class Field(object):
object_id = 'object_id'
object_store_url = 'object_store_url'
object_story_id = 'object_story_id'
object_story_spec = 'object_story_spec'
object_type = 'object_type'
object_url = 'object_url'
preview_url = 'preview_url'
Expand Down Expand Up @@ -1370,7 +1352,7 @@ def get_endpoint(cls):
def get_node_path(self):
return (self.get_parent_id_assured(), self.get_endpoint())

def _read_update(self, data):
def _set_data(self, data):
data = list(data['images'].values())[0]

for key in data:
Expand Down Expand Up @@ -1401,9 +1383,13 @@ def remote_create(
"""Uploads filename and creates the AdImage object from it.
It has same arguments as AbstractCrudObject.remote_create except it does
not have a 'files' keyword argument. Instead, it has a required
'filename' argument.
not have the files argument but requires the 'filename' property to be
defined.
"""
if self[self.__class__.Field.filename] is None:
raise FacebookBadObjectError(
"AdImage required a filename to be defined."
)
filename = self[self.__class__.Field.filename]
open_file = open(filename, 'rb')
return_val = super(AdImage, self).remote_create(
Expand Down
Loading

0 comments on commit 9ee253e

Please sign in to comment.