Skip to content

Commit 5272dd4

Browse files
author
j.s@google.com
committed
Added methods and unit tests for Blogger v2 client. Also corrected XML parsing logic when looking for a v2 XML element inside of a non-versioned XML element.
1 parent 1b183ea commit 5272dd4

8 files changed

Lines changed: 276 additions & 12 deletions

File tree

src/atom/core.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -272,12 +272,13 @@ def _harvest_tree(self, tree, version=1):
272272
if getattr(self, definition[0]) is None:
273273
setattr(self, definition[0], [])
274274
getattr(self, definition[0]).append(_xml_element_from_tree(element,
275-
definition[1]))
275+
definition[1], version))
276276
else:
277277
setattr(self, definition[0], _xml_element_from_tree(element,
278-
definition[1]))
278+
definition[1], version))
279279
else:
280-
self._other_elements.append(_xml_element_from_tree(element, XmlElement))
280+
self._other_elements.append(_xml_element_from_tree(element, XmlElement,
281+
version))
281282
for attrib, value in tree.attrib.iteritems():
282283
if attributes and attrib in attributes:
283284
setattr(self, attributes[attrib], value)

src/gdata/blogger/client.py

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,19 @@
1515
# limitations under the License.
1616

1717

18-
"""Contains a client to communicate with the Blogger servers."""
18+
"""Contains a client to communicate with the Blogger servers.
19+
20+
For documentation on the Blogger API, see:
21+
http://code.google.com/apis/blogger/
22+
"""
1923

2024

2125
__author__ = 'j.s@google.com (Jeff Scudder)'
2226

2327

2428
import gdata.client
25-
import gdata.data.blogger
29+
import gdata.blogger.data
30+
import atom.data
2631

2732

2833
# List user's blogs, takes a user ID, or 'default'.
@@ -36,6 +41,7 @@
3641
# Takes a blog ID.
3742
BLOG_ARCHIVE_URL = 'http://www.blogger.com/feeds/%s/archive/full'
3843

44+
3945
class BloggerClient(gdata.client.GDClient):
4046
api_version = '2'
4147
auth_serice = 'blogger'
@@ -46,3 +52,86 @@ def get_blogs(self, user_id='default', auth_token=None,
4652
return self.get_feed(BLOGS_URL % user_id, auth_token=auth_token,
4753
desired_class=desired_class, **kwargs)
4854

55+
GetBlogs = get_blogs
56+
57+
def get_posts(self, blog_id, auth_token=None,
58+
desired_class=gdata.blogger.data.BlogPostFeed, query=None,
59+
**kwargs):
60+
return self.get_feed(BLOG_POST_URL % blog_id, auth_token=auth_token,
61+
desired_class=desired_class, query=query, **kwargs)
62+
63+
GetPosts = get_posts
64+
65+
def get_post_comments(self, blog_id, post_id, auth_token=None,
66+
desired_class=gdata.blogger.data.CommentFeed,
67+
query=None, **kwargs):
68+
return self.get_feed(BLOG_POST_COMMENTS_URL % (blog_id, post_id),
69+
auth_token=auth_token, desired_class=desired_class,
70+
query=query, **kwargs)
71+
72+
GetPostComments = get_post_comments
73+
74+
def get_blog_comments(self, blog_id, auth_token=None,
75+
desired_class=gdata.blogger.data.CommentFeed,
76+
query=None, **kwargs):
77+
return self.get_feed(BLOG_COMMENTS_URL % blog_id, auth_token=auth_token,
78+
desired_class=desired_class, query=query, **kwargs)
79+
80+
GetBlogComments = get_blog_comments
81+
82+
def get_blog_archive(self, blog_id, auth_token=None, **kwargs):
83+
return self.get_feed(BLOG_ARCHIVE_URL % blog_id, auth_token=auth_token,
84+
**kwargs)
85+
86+
GetBlogArchive = get_blog_archive
87+
88+
def add_post(self, blog_id, title, body, labels=None, draft=False,
89+
auth_token=None, title_type='text', body_type='html', **kwargs):
90+
# Construct an atom Entry for the blog post to be sent to the server.
91+
new_entry = gdata.blogger.data.BlogPost(
92+
title=atom.data.Title(text=title, type=title_type),
93+
content=atom.data.Content(text=body, type=body_type))
94+
if labels:
95+
for label in labels:
96+
new_entry.add_label(label)
97+
if draft:
98+
new_entry.control = atom.data.Control(draft=atom.data.Draft(text='yes'))
99+
print 'here is your new post:'
100+
print str(new_entry)
101+
return self.post(new_entry, BLOG_POST_URL % blog_id, auth_token=auth_token, **kwargs)
102+
103+
AddPost = add_post
104+
105+
def add_comment(self, blog_id, post_id, title, body, auth_token=None,
106+
title_type='text', body_type='html', **kwargs):
107+
new_entry = gdata.blogger.data.Comment(
108+
title=atom.data.Title(text=title, type=title_type),
109+
content=atom.data.Content(text=body, type=body_type))
110+
return self.post(new_entry, BLOG_POST_COMMENTS_URL % (blog_id, post_id),
111+
auth_token=auth_token, **kwargs)
112+
113+
AddComment = add_comment
114+
115+
def update(self, entry, auth_token=None, **kwargs):
116+
# The Blogger API does not currently support ETags, so for now remove
117+
# the ETag before performing an update.
118+
old_etag = entry.etag
119+
entry.etag = None
120+
response = gdata.client.GDClient.update(self, entry,
121+
auth_token=auth_token, **kwargs)
122+
entry.etag = old_etag
123+
return response
124+
125+
Update = update
126+
127+
def delete(self, entry, auth_token=None, **kwargs):
128+
# The Blogger API does not currently support ETags, so for now remove
129+
# the ETag before performing a delete.
130+
old_etag = entry.etag
131+
entry.etag = None
132+
response = gdata.client.GDClient.delete(self, entry,
133+
auth_token=auth_token, **kwargs)
134+
entry.etag = old_etag
135+
return response
136+
137+
Delete = delete

src/gdata/client.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,19 @@ def error_from_response(message, http_response, error_class, response_body=None)
140140
return error
141141

142142

143+
def get_xml_version(version):
144+
"""Determines which XML schema to use based on the client API version.
145+
146+
Args:
147+
version: string which is converted to an int. The version string is in
148+
the form 'Major.Minor.x.y.z' and only the major version number
149+
is considered. If None is provided assume version 1.
150+
"""
151+
if version is None:
152+
return 1
153+
return int(version.split('.')[0])
154+
155+
143156
class GDClient(atom.client.AtomPubClient):
144157
"""Communicates with Google Data servers to perform CRUD operations.
145158
@@ -276,7 +289,7 @@ def request(self, method=None, uri=None, auth_token=None,
276289
elif desired_class is not None:
277290
if self.api_version is not None:
278291
return atom.core.parse(response.read(), desired_class,
279-
version=self.api_version)
292+
version=get_xml_version(self.api_version))
280293
else:
281294
# No API version was specified, so allow parse to
282295
# use the default version.
@@ -484,7 +497,9 @@ def post(self, entry, uri, auth_token=None, converter=None,
484497
if converter is None and desired_class is None:
485498
desired_class = entry.__class__
486499
http_request = atom.http_core.HttpRequest()
487-
http_request.add_body_part(entry.to_string(), 'application/atom+xml')
500+
http_request.add_body_part(
501+
entry.to_string(get_xml_version(self.api_version)),
502+
'application/atom+xml')
488503
return self.request(method='POST', uri=uri, auth_token=auth_token,
489504
http_request=http_request, converter=converter,
490505
desired_class=desired_class, **kwargs)
@@ -511,7 +526,9 @@ def update(self, entry, auth_token=None, force=False, **kwargs):
511526
A new Entry object of a matching type to the entry which was passed in.
512527
"""
513528
http_request = atom.http_core.HttpRequest()
514-
http_request.add_body_part(entry.to_string(), 'application/atom+xml')
529+
http_request.add_body_part(
530+
entry.to_string(get_xml_version(self.api_version)),
531+
'application/atom+xml')
515532
# Include the ETag in the request if this is version 2 of the API.
516533
if self.api_version and self.api_version.startswith('2'):
517534
if force:

tests/all_tests.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import gdata_tests.live_client_test
3535
import gdata_tests.gauth_test
3636
import gdata_tests.blogger.data_test
37+
import gdata_tests.blogger.live_client_test
3738
# Compatibility tests for requests to v1 feeds.
3839
import gdata_tests.contacts.service_test
3940
# Tests for v1 classes.
@@ -52,14 +53,15 @@ def suite():
5253
return unittest.TestSuite((atom_tests.core_test.suite(),
5354
atom_tests.data_test.suite(),
5455
atom_tests.http_core_test.suite(),
56+
atom_tests.auth_test.suite(),
5557
atom_tests.mock_http_core_test.suite(),
5658
atom_tests.client_test.suite(),
57-
atom_tests.auth_test.suite(),
5859
gdata_tests.client_test.suite(),
5960
gdata_tests.data_test.suite(),
61+
gdata_tests.live_client_test.suite(),
6062
gdata_tests.gauth_test.suite(),
6163
gdata_tests.blogger.data_test.suite(),
62-
gdata_tests.live_client_test.suite(),
64+
gdata_tests.blogger.live_client_test.suite(),
6365
gdata_tests.contacts.service_test.suite(),
6466
atom_test.suite(), gdata_test.suite(),
6567
atom_tests.service_test.suite(),

tests/all_tests_coverage.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
import gdata.gauth
3333
import gdata.client
3434
import gdata.data
35+
import gdata.blogger.data
36+
import gdata.blogger.client
3537
from gdata.test_config import settings
3638

3739

@@ -51,4 +53,4 @@ def suite():
5153
coverage.stop()
5254
coverage.report([atom.core, atom.http_core, atom.auth, atom.data,
5355
atom.mock_http_core, atom.client, gdata.gauth, gdata.client,
54-
gdata.data])
56+
gdata.data, gdata.blogger.data, gdata.blogger.client])

tests/atom_tests/data_test.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -722,6 +722,35 @@ def testConvertExampleXML(self):
722722
self.fail('Error when converting XML')
723723

724724

725+
class VersionedXmlTest(unittest.TestCase):
726+
727+
def test_monoversioned_parent_with_multiversioned_child(self):
728+
v2_rules = atom.data.Entry._get_rules(2)
729+
self.assertTrue('{http://www.w3.org/2007/app}control' in v2_rules[1])
730+
731+
entry_xml = """<entry xmlns='http://www.w3.org/2005/Atom'>
732+
<app:control xmlns:app='http://www.w3.org/2007/app'>
733+
<app:draft>yes</app:draft>
734+
</app:control>
735+
</entry>"""
736+
737+
entry = e = atom.core.parse(entry_xml, atom.data.Entry, version=2)
738+
self.assertTrue(entry is not None)
739+
self.assertTrue(entry.control is not None)
740+
self.assertTrue(entry.control.draft is not None)
741+
self.assertEqual(entry.control.draft.text, 'yes')
742+
743+
# v1 rules should not parse v2 XML.
744+
entry = e = atom.core.parse(entry_xml, atom.data.Entry, version=1)
745+
self.assertTrue(entry is not None)
746+
self.assertTrue(entry.control is None)
747+
748+
# The default version should be v1.
749+
entry = e = atom.core.parse(entry_xml, atom.data.Entry)
750+
self.assertTrue(entry is not None)
751+
self.assertTrue(entry.control is None)
752+
753+
725754
def suite():
726755
return conf.build_suite([AuthorTest, EmailTest, NameTest,
727756
ExtensionElementTest, LinkTest, GeneratorTest,
@@ -730,7 +759,7 @@ def suite():
730759
PublishedTest, FeedEntryParentTest, EntryTest,
731760
ContentEntryParentTest, PreserveUnkownElementTest,
732761
FeedTest, LinkFinderTest, AtomBaseTest,
733-
UtfParsingTest])
762+
UtfParsingTest, VersionedXmlTest])
734763

735764

736765
if __name__ == '__main__':
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
#!/usr/bin/env python
2+
#
3+
# Copyright (C) 2009 Google Inc.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
18+
# This module is used for version 2 of the Google Data APIs.
19+
# These tests attempt to connect to Google servers.
20+
21+
22+
__author__ = 'j.s@google.com (Jeff Scudder)'
23+
24+
25+
import unittest
26+
import gdata.blogger.client
27+
import gdata.blogger.data
28+
import gdata.gauth
29+
import gdata.client
30+
import atom.http_core
31+
import atom.mock_http_core
32+
import atom.core
33+
import gdata.data
34+
import gdata.test_config as conf
35+
36+
37+
class BloggerClientTest(unittest.TestCase):
38+
39+
def setUp(self):
40+
self.client = None
41+
if conf.settings.RUN_LIVE_TESTS:
42+
self.client = gdata.blogger.client.BloggerClient()
43+
conf.configure_client(self.client, conf.settings.BloggerConfig,
44+
'BloggerTest')
45+
46+
def tearDown(self):
47+
conf.close_client(self.client)
48+
49+
def test_create_update_delete(self):
50+
if not conf.settings.RUN_LIVE_TESTS:
51+
return
52+
# Either load the recording or prepare to make a live request.
53+
conf.configure_cache(self.client, 'test_create_update_delete')
54+
55+
# Add a blog post.
56+
created = self.client.add_post(conf.settings.BloggerConfig.blog_id,
57+
conf.settings.BloggerConfig.title,
58+
conf.settings.BloggerConfig.content,
59+
labels=['test', 'python'])
60+
61+
self.assertEqual(created.title.text, conf.settings.BloggerConfig.title)
62+
self.assertEqual(created.content.text, conf.settings.BloggerConfig.content)
63+
self.assertEqual(len(created.category), 2)
64+
self.assertTrue(created.control is None)
65+
66+
# Change the title of the blog post we just added.
67+
created.title.text = 'Edited'
68+
updated = self.client.update(created)
69+
70+
self.assertEqual(updated.title.text, 'Edited')
71+
self.assertTrue(isinstance(updated, gdata.blogger.data.BlogPost))
72+
self.assertEqual(updated.content.text, created.content.text)
73+
74+
# Delete the test entry from the blog.
75+
self.client.delete(updated)
76+
77+
def test_create_draft_post(self):
78+
if not conf.settings.RUN_LIVE_TESTS:
79+
return
80+
conf.configure_cache(self.client, 'test_create_draft_post')
81+
82+
# Add a draft blog post.
83+
created = self.client.add_post(conf.settings.BloggerConfig.blog_id,
84+
conf.settings.BloggerConfig.title,
85+
conf.settings.BloggerConfig.content,
86+
labels=['test2', 'python'], draft=True)
87+
88+
self.assertEqual(created.title.text, conf.settings.BloggerConfig.title)
89+
self.assertEqual(created.content.text, conf.settings.BloggerConfig.content)
90+
self.assertEqual(len(created.category), 2)
91+
self.assertTrue(created.control is not None)
92+
self.assertTrue(created.control.draft is not None)
93+
self.assertEqual(created.control.draft.text, 'yes')
94+
95+
# Publish the blog post.
96+
created.control.draft.text = 'no'
97+
updated = self.client.update(created)
98+
99+
if updated.control is not None and updated.control.draft is not None:
100+
self.assertNotEqual(updated.control.draft.text, 'yes')
101+
102+
raw_input('pause')
103+
104+
# Delete the test entry from the blog.
105+
self.client.delete(updated)
106+
107+
108+
def suite():
109+
return conf.build_suite([BloggerClientTest])
110+
111+
112+
if __name__ == '__main__':
113+
unittest.main()

0 commit comments

Comments
 (0)