merge
This commit is contained in:
		| @@ -7,3 +7,4 @@ syntax: glob | ||||
| samples/cmdline/*.dat | ||||
| htmlcov/* | ||||
| .coverage | ||||
| database.sqlite3 | ||||
|   | ||||
| @@ -45,13 +45,21 @@ except ImportError: # pragma: no cover | ||||
|     import json as simplejson | ||||
|  | ||||
|  | ||||
| class HttpError(Exception): | ||||
| class Error(Exception): | ||||
|   """Base error for this module.""" | ||||
|   pass | ||||
|  | ||||
|  | ||||
| class UnknownLinkType(Exception): | ||||
| class HttpError(Error): | ||||
|   """HTTP data was invalid or unexpected.""" | ||||
|   pass | ||||
|  | ||||
|  | ||||
| class UnknownLinkType(Error): | ||||
|   """Link type unknown or unexpected.""" | ||||
|   pass | ||||
|  | ||||
|  | ||||
| DISCOVERY_URI = ('http://www.googleapis.com/discovery/0.1/describe' | ||||
|   '{?api,apiVersion}') | ||||
|  | ||||
| @@ -85,13 +93,16 @@ class JsonModel(object): | ||||
|     headers['user-agent'] += 'google-api-python-client/1.0' | ||||
|     if body_value is None: | ||||
|       return (headers, path_params, query, None) | ||||
|     else: | ||||
|       if len(body_value) == 1 and 'data' in body_value: | ||||
|         model = body_value | ||||
|       else: | ||||
|         model = {'data': body_value} | ||||
|       headers['content-type'] = 'application/json' | ||||
|       return (headers, path_params, query, simplejson.dumps(model)) | ||||
|  | ||||
|   def build_query(self, params): | ||||
|     params.update({'alt': 'json', 'prettyprint': 'true'}) | ||||
|     params.update({'alt': 'json'}) | ||||
|     astuples = [] | ||||
|     for key, value in params.iteritems(): | ||||
|       if getattr(value, 'encode', False) and callable(value.encode): | ||||
| @@ -103,6 +114,9 @@ class JsonModel(object): | ||||
|     # Error handling is TBD, for example, do we retry | ||||
|     # for some operation/error combinations? | ||||
|     if resp.status < 300: | ||||
|       if resp.status == 204: | ||||
|         # A 204: No Content response should be treated differently to all the other success states | ||||
|         return simplejson.loads('{}') | ||||
|       return simplejson.loads(content)['data'] | ||||
|     else: | ||||
|       logging.debug('Content from bad request was: %s' % content) | ||||
|   | ||||
| @@ -21,7 +21,17 @@ except ImportError: | ||||
|     from cgi import parse_qs, parse_qsl | ||||
|  | ||||
|  | ||||
| class MissingParameter(Exception): | ||||
| class Error(Exception): | ||||
|   """Base error for this module.""" | ||||
|   pass | ||||
|  | ||||
|  | ||||
| class RequestError(Error): | ||||
|   """Error occurred during request.""" | ||||
|   pass | ||||
|  | ||||
|  | ||||
| class MissingParameter(Error): | ||||
|   pass | ||||
|  | ||||
|  | ||||
| @@ -120,7 +130,9 @@ class OAuthCredentials(Credentials): | ||||
|       if headers == None: | ||||
|         headers = {} | ||||
|       headers.update(req.to_header()) | ||||
|       if 'user-agent' not in headers: | ||||
|       if 'user-agent' in headers: | ||||
|         headers['user-agent'] =  self.user_agent + ' ' + headers['user-agent'] | ||||
|       else: | ||||
|         headers['user-agent'] =  self.user_agent | ||||
|       return request_orig(uri, method, body, headers, | ||||
|                           redirections, connection_type) | ||||
| @@ -185,7 +197,7 @@ class FlowThreeLegged(object): | ||||
|                                    body=body) | ||||
|     if resp['status'] != '200': | ||||
|       logging.error('Failed to retrieve temporary authorization: %s' % content) | ||||
|       raise Exception('Invalid response %s.' % resp['status']) | ||||
|       raise RequestError('Invalid response %s.' % resp['status']) | ||||
|  | ||||
|     self.request_token = dict(parse_qsl(content)) | ||||
|  | ||||
| @@ -222,7 +234,7 @@ class FlowThreeLegged(object): | ||||
|     resp, content = client.request(uri, 'POST', headers=headers) | ||||
|     if resp['status'] != '200': | ||||
|       logging.error('Failed to retrieve access token: %s' % content) | ||||
|       raise Exception('Invalid response %s.' % resp['status']) | ||||
|       raise RequestError('Invalid response %s.' % resp['status']) | ||||
|  | ||||
|     oauth_params = dict(parse_qsl(content)) | ||||
|     token = oauth.Token( | ||||
|   | ||||
| @@ -36,6 +36,17 @@ REQUEST_TOKEN_URL = 'https://www.google.com/accounts/OAuthGetRequestToken?domain | ||||
| AUTHORIZE_URL = 'https://www.google.com/buzz/api/auth/OAuthAuthorizeToken?domain=anonymous&scope=https://www.googleapis.com/auth/buzz' | ||||
| ACCESS_TOKEN_URL = 'https://www.google.com/accounts/OAuthGetAccessToken' | ||||
|  | ||||
|  | ||||
| class Error(Exception): | ||||
|   """Base error for this module.""" | ||||
|   pass | ||||
|  | ||||
|  | ||||
| class RequestError(Error): | ||||
|   """Request returned failure or unexpected data.""" | ||||
|   pass | ||||
|  | ||||
|  | ||||
| # TODO(ade) This class is really a BuzzGaeBuilder. Rename it. | ||||
| class BuzzGaeClient(object): | ||||
|   def __init__(self, consumer_key='anonymous', consumer_secret='anonymous'): | ||||
| @@ -49,7 +60,7 @@ class BuzzGaeClient(object): | ||||
|  | ||||
|     if resp['status'] != '200': | ||||
|       logging.warn('Request: %s failed with status: %s. Content was: %s' % (url, resp['status'], content)) | ||||
|       raise Exception('Invalid response %s.' % resp['status']) | ||||
|       raise RequestError('Invalid response %s.' % resp['status']) | ||||
|     return resp, content | ||||
|  | ||||
|   def get_request_token(self, callback_url, display_name = None): | ||||
|   | ||||
| @@ -10,6 +10,7 @@ These tests are read-only in order to ensure they're repeatable. They also | ||||
| only work with publicly visible data in order to avoid dealing with OAuth. | ||||
| """ | ||||
| import httplib2 | ||||
| import pprint | ||||
|  | ||||
| __author__ = 'ade@google.com (Ade Oshineye)' | ||||
|  | ||||
| @@ -18,6 +19,7 @@ import httplib2 | ||||
| import logging | ||||
| import pickle | ||||
| import os | ||||
| import time | ||||
| import unittest | ||||
|  | ||||
| # TODO(ade) Remove this mock once the bug in the discovery document is fixed | ||||
| @@ -34,29 +36,43 @@ class HttpMock(object): | ||||
|     return httplib2.Response(self.headers), self.data | ||||
|  | ||||
| class BuzzFunctionalTest(unittest.TestCase): | ||||
|   def test_can_get_specific_activity(self): | ||||
|     buzz = build('buzz', 'v1') | ||||
|     activity = buzz.activities().get(userId='105037104815911535953', | ||||
|                                      postId='B:z12sspviqyakfvye123wehng0muwz5jzq04').execute() | ||||
|  | ||||
|     self.assertTrue(activity is not None) | ||||
|  | ||||
|   def test_can_get_specific_activity_with_tag_id(self): | ||||
|     buzz = build('buzz', 'v1') | ||||
|     activity = buzz.activities().get(userId='105037104815911535953', | ||||
|                                      postId='tag:google.com,2010:buzz:z13ptnw5usmnv15ey22fzlswnuqoebasu').execute() | ||||
|  | ||||
|     self.assertTrue(activity is not None) | ||||
|  | ||||
|   def test_can_get_buzz_activities_with_many_params(self): | ||||
|     buzz = build('buzz', 'v1') | ||||
|     max_results = 2 | ||||
|     actcol = buzz.activities() | ||||
|     activities = actcol.list(userId='googlebuzz', scope='@self', | ||||
|     activities_command = buzz.activities() | ||||
|     activities = activities_command.list(userId='googlebuzz', scope='@self', | ||||
|                              max_comments=max_results*2 ,max_liked=max_results*3, | ||||
|                              max_results=max_results).execute() | ||||
|     activity_count = len(activities['items']) | ||||
|     self.assertEquals(max_results, activity_count) | ||||
|  | ||||
|     activities = actcol.list_next(activities).execute() | ||||
|     activities = activities_command.list_next(activities).execute() | ||||
|     activity_count = len(activities['items']) | ||||
|     self.assertEquals(max_results, activity_count) | ||||
|  | ||||
|   def test_can_get_multiple_pages_of_buzz_activities(self): | ||||
|     buzz = build('buzz', 'v1') | ||||
|     max_results = 2 | ||||
|     actcol = buzz.activities() | ||||
|     activities_command = buzz.activities() | ||||
|      | ||||
|     activities = actcol.list(userId='adewale', scope='@self', | ||||
|     activities = activities_command.list(userId='adewale', scope='@self', | ||||
|                              max_results=max_results).execute() | ||||
|     for count in range(10): | ||||
|       activities = actcol.list_next(activities).execute() | ||||
|       activities = activities_command.list_next(activities).execute() | ||||
|       activity_count = len(activities['items']) | ||||
|       self.assertEquals(max_results, activity_count, 'Failed after %s pages' % str(count)) | ||||
|  | ||||
| @@ -86,6 +102,31 @@ class BuzzFunctionalTest(unittest.TestCase): | ||||
|     self.assertEquals('111062888259659218284', person['id']) | ||||
|     self.assertEquals('http://www.google.com/profiles/googlebuzz', person['profileUrl']) | ||||
|  | ||||
|   def test_can_get_followees_of_user(self): | ||||
|     buzz = build('buzz', 'v1') | ||||
|     expected_followees = 30 | ||||
|     following = buzz.people().list(userId='googlebuzz', groupId='@following', max_results=expected_followees).execute() | ||||
|  | ||||
|     self.assertEquals(expected_followees, following['totalResults']) | ||||
|     self.assertEquals(expected_followees, len(following['entry'])) | ||||
|  | ||||
|   def test_can_efficiently_get_follower_count_of_user(self): | ||||
|     buzz = build('buzz', 'v1') | ||||
|  | ||||
|     # Restricting max_results to 1 means only a tiny amount of data comes back but the totalResults still has the total. | ||||
|     following = buzz.people().list(userId='googlebuzz', groupId='@followers', max_results=1).execute() | ||||
|  | ||||
|     # @googlebuzz has a large but fluctuating number of followers | ||||
|     # It is sufficient if the result is bigger than 10, 000 | ||||
|     follower_count = following['totalResults'] | ||||
|     self.assertTrue(follower_count > 10000, follower_count) | ||||
|  | ||||
|   def test_follower_count_is_zero_for_user_with_hidden_follower_count(self): | ||||
|     buzz = build('buzz', 'v1') | ||||
|     following = buzz.people().list(userId='adewale', groupId='@followers').execute() | ||||
|      | ||||
|     self.assertEquals(0, following['totalResults']) | ||||
|  | ||||
|  | ||||
| class BuzzAuthenticatedFunctionalTest(unittest.TestCase): | ||||
|   def __init__(self, method_name): | ||||
| @@ -97,8 +138,36 @@ class BuzzAuthenticatedFunctionalTest(unittest.TestCase): | ||||
|  | ||||
|     self.http = credentials.authorize(httplib2.Http()) | ||||
|  | ||||
|   def test_can_list_groups_belonging_to_user(self): | ||||
|     # TODO(ade) This should not require authentication. It does because we're adding a spurious @self to the URL | ||||
|   def test_can_create_activity(self): | ||||
|     buzz = build('buzz', 'v1', http=self.http) | ||||
|  | ||||
|     activity = buzz.activities().insert(userId='@me', body={ | ||||
|       'title': 'Testing insert', | ||||
|       'object': { | ||||
|         'content': u'Just a short note to show that insert is working. ?', | ||||
|         'type': 'note'} | ||||
|       } | ||||
|     ).execute() | ||||
|     self.assertTrue(activity is not None) | ||||
|  | ||||
|   def test_can_create_private_activity(self): | ||||
|     buzz = build('buzz', 'v1', http=self.http) | ||||
|  | ||||
|     activity = buzz.activities().insert(userId='@me', body={ | ||||
|         'title': 'Testing insert', | ||||
|         'object': { | ||||
|         'content': 'This is a private post.' | ||||
|       }, | ||||
|       'visibility': { | ||||
|         'entries': [ | ||||
|           { 'id': 'tag:google.com,2010:buzz-group:108242092577082601423:13' } | ||||
|         ] | ||||
|       } | ||||
|     } | ||||
|     ).execute() | ||||
|     self.assertTrue(activity is not None) | ||||
|  | ||||
|   def test_can_identify_number_of_groups_belonging_to_user(self): | ||||
|     buzz = build('buzz', 'v1', http=self.http) | ||||
|     groups = buzz.groups().list(userId='108242092577082601423').execute() | ||||
|  | ||||
| @@ -106,14 +175,75 @@ class BuzzAuthenticatedFunctionalTest(unittest.TestCase): | ||||
|     expected_default_number_of_groups = 4 | ||||
|     self.assertEquals(expected_default_number_of_groups, len(groups['items'])) | ||||
|  | ||||
|   def IGNORE__test_can_get_followees_of_user(self): | ||||
|     # This currently fails with: | ||||
|     # Attempting to access self view of a different user. | ||||
|     # and URL:  | ||||
|   def IGNORE__test_can_like_activity(self): | ||||
|     buzz = build('buzz', 'v1', http=self.http) | ||||
|     following = buzz.groups().get(userId='googlebuzz', groupId='@following').execute() | ||||
|     activity = buzz.activities().insert(userId='@me', body={ | ||||
|       'title': 'Testing insert', | ||||
|       'object': { | ||||
|         'content': u'Just a short note to show that insert is working. ?', | ||||
|         'type': 'note'} | ||||
|       } | ||||
|     ).execute() | ||||
|     pprint.pprint(activity) | ||||
|     id = activity['id'] | ||||
|     likers = buzz.people().liked(userId='105037104815911535953', postId=id, groupId='@liked', scope='@self').execute() | ||||
|     # Todo(ade) Insert the new liker once the Buzz back-end bug is fixed | ||||
|  | ||||
|     self.assertEquals(17, len(following)) | ||||
|   def test_can_comment_on_activity(self): | ||||
|     buzz = build('buzz', 'v1', http=self.http) | ||||
|  | ||||
|     activity = buzz.activities().insert(userId='@me', body={ | ||||
|       'title': 'A new activity', | ||||
|       'object': { | ||||
|         'content': u'The body of the new activity', | ||||
|         'type': 'note'} | ||||
|       } | ||||
|     ).execute() | ||||
|  | ||||
|     id = activity['id'] | ||||
|     comment = buzz.comments().insert(userId='@me', postId=id, body={ | ||||
|       "content": "A comment on the new activity" | ||||
|     }).execute() | ||||
|  | ||||
|   def IGNORE__test_can_list_groups_belonging_to_user(self): | ||||
|     # TODO(ade) Uncomment this test once the related Buzz back-end bug is fixed | ||||
|     buzz = build('buzz', 'v1', http=self.http) | ||||
|     groups = buzz.groups().list(userId='108242092577082601423').execute() | ||||
|     pprint.pprint(groups) | ||||
|  | ||||
|     group = buzz.groups().get(userId='108242092577082601423', groupId='G:108242092577082601423:15').execute() | ||||
|     self.assertEquals('G:108242092577082601423:15', group['id'], group) | ||||
|  | ||||
|     group = buzz.groups().get(userId='108242092577082601423', groupId='G:108242092577082601423:14').execute() | ||||
|     self.assertEquals('G:108242092577082601423:14', group['id'], group) | ||||
|  | ||||
|     group = buzz.groups().get(userId='108242092577082601423', groupId='G:108242092577082601423:13').execute() | ||||
|     self.assertEquals('G:108242092577082601423:13', group['id'], group) | ||||
|  | ||||
|     group = buzz.groups().get(userId='108242092577082601423', groupId='G:108242092577082601423:6').execute() | ||||
|     self.assertEquals('G:108242092577082601423:6', group['id'], group) | ||||
|  | ||||
|     group = buzz.groups().get(userId='108242092577082601423', groupId='G:108242092577082601423:9999999').execute() | ||||
|     self.assertEquals(None, group, group) | ||||
|  | ||||
|   def test_can_delete_activity(self): | ||||
|     buzz = build('buzz', 'v1', http=self.http) | ||||
|  | ||||
|     activity = buzz.activities().insert(userId='@me', body={ | ||||
|       'title': 'Activity to be deleted', | ||||
|       'object': { | ||||
|         'content': u'Created this activity so that it can be deleted.', | ||||
|         'type': 'note'} | ||||
|       } | ||||
|     ).execute() | ||||
|     id = activity['id'] | ||||
|  | ||||
|     buzz.activities().delete(scope='@self', userId='@me', postId=id).execute() | ||||
|     time.sleep(2) | ||||
|  | ||||
|     activity_url = activity['links']['self'][0]['href'] | ||||
|     resp, content = self.http.request(activity_url, 'GET') | ||||
|     self.assertEquals(404, resp.status) | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|   unittest.main() | ||||
|   | ||||
| @@ -83,7 +83,7 @@ def has_timeout(timeout): # python 2.6 | ||||
| __all__ = ['Http', 'Response', 'ProxyInfo', 'HttpLib2Error', | ||||
|   'RedirectMissingLocation', 'RedirectLimit', 'FailedToDecompressContent', | ||||
|   'UnimplementedDigestAuthOptionError', 'UnimplementedHmacDigestAuthOptionError', | ||||
|   'debuglevel'] | ||||
|   'debuglevel', 'ProxiesUnavailableError'] | ||||
|  | ||||
|  | ||||
| # The httplib debug level, set to a non-zero value to get debug output | ||||
| @@ -125,6 +125,7 @@ class UnimplementedHmacDigestAuthOptionError(HttpLib2ErrorWithResponse): pass | ||||
|  | ||||
| class RelativeURIError(HttpLib2Error): pass | ||||
| class ServerNotFoundError(HttpLib2Error): pass | ||||
| class ProxiesUnavailableError(HttpLib2Error): pass | ||||
|  | ||||
| # Open Items: | ||||
| # ----------- | ||||
| @@ -721,6 +722,9 @@ class HTTPConnectionWithTimeout(httplib.HTTPConnection): | ||||
|     def connect(self): | ||||
|         """Connect to the host and port specified in __init__.""" | ||||
|         # Mostly verbatim from httplib.py. | ||||
|         if self.proxy_info and socks is None: | ||||
|             raise ProxiesUnavailableError( | ||||
|                 'Proxy support missing but proxy use was requested!') | ||||
|         msg = "getaddrinfo returns an empty list" | ||||
|         for res in socket.getaddrinfo(self.host, self.port, 0, | ||||
|                 socket.SOCK_STREAM): | ||||
|   | ||||
| @@ -41,12 +41,13 @@ mainly to merge bug fixes found in Sourceforge | ||||
| """ | ||||
|  | ||||
| import socket | ||||
|  | ||||
| if getattr(socket, 'socket', None) is None: | ||||
|     raise ImportError('socket.socket missing, proxy support unusable') | ||||
|  | ||||
| import struct | ||||
| import sys | ||||
|  | ||||
| if not hasattr(socket, 'socket'): | ||||
|   raise ImportError("Running on App Engine?") | ||||
|  | ||||
| PROXY_TYPE_SOCKS4 = 1 | ||||
| PROXY_TYPE_SOCKS5 = 2 | ||||
| PROXY_TYPE_HTTP = 3 | ||||
|   | ||||
							
								
								
									
										6
									
								
								samples/api-python-client-doc/README
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								samples/api-python-client-doc/README
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| This sample is the code that drives | ||||
|  | ||||
|    http://api-python-client-doc.appspot.com/ | ||||
|  | ||||
| It is an application that serves up the Python help documentation | ||||
| for each API. | ||||
							
								
								
									
										66
									
								
								samples/buzz/buzz.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								samples/buzz/buzz.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | ||||
| #!/usr/bin/python2.4 | ||||
| # -*- coding: utf-8 -*- | ||||
| # | ||||
| # Copyright 2010 Google Inc. All Rights Reserved. | ||||
|  | ||||
| """Simple command-line example for Buzz. | ||||
|  | ||||
| Command-line application that retrieves the users | ||||
| latest content and then adds a new entry. | ||||
| """ | ||||
|  | ||||
| __author__ = 'jcgregorio@google.com (Joe Gregorio)' | ||||
|  | ||||
| from apiclient.discovery import build | ||||
|  | ||||
| import httplib2 | ||||
| import pickle | ||||
| import pprint | ||||
|  | ||||
| # Uncomment the next line to get very detailed logging | ||||
| # httplib2.debuglevel = 4 | ||||
|  | ||||
| def main(): | ||||
|   f = open("buzz.dat", "r") | ||||
|   credentials = pickle.loads(f.read()) | ||||
|   f.close() | ||||
|  | ||||
|   http = httplib2.Http() | ||||
|   http = credentials.authorize(http) | ||||
|  | ||||
|   p = build("buzz", "v1", http=http) | ||||
|   activities = p.activities() | ||||
|  | ||||
|   # Retrieve the first two activities | ||||
|   activitylist = activities.list(max_results='2', scope='@self', userId='@me').execute() | ||||
|   print "Retrieved the first two activities" | ||||
|  | ||||
|   # Retrieve the next two activities | ||||
|   activitylist = activities.list_next(activitylist).execute() | ||||
|   print "Retrieved the next two activities" | ||||
|  | ||||
|   # Add a new activity | ||||
|   new_activity_body = { | ||||
|       'title': 'Testing insert', | ||||
|       'object': { | ||||
|           'content': u'Just a short note to show that insert is working. ☄', | ||||
|           'type': 'note'} | ||||
|       } | ||||
|   activity = activities.insert(userId='@me', body=new_activity_body).execute() | ||||
|   print "Added a new activity" | ||||
|  | ||||
|   activitylist = activities.list(max_results='2', scope='@self', userId='@me').execute() | ||||
|  | ||||
|   # Add a comment to that activity | ||||
|   comment_body = { | ||||
|           "content": "This is a comment" | ||||
|       } | ||||
|   item = activitylist['items'][0] | ||||
|   comment = p.comments().insert( | ||||
|       userId=item['actor']['id'], postId=item['id'], body=comment_body | ||||
|       ).execute() | ||||
|   print 'Added a comment to the new activity' | ||||
|   pprint.pprint(comment) | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|   main() | ||||
| @@ -22,11 +22,6 @@ other example apps in the same directory. | ||||
| 
 | ||||
| __author__ = 'jcgregorio@google.com (Joe Gregorio)' | ||||
| 
 | ||||
| # Enable this sample to be run from the top-level directory | ||||
| import os | ||||
| import sys | ||||
| sys.path.insert(0, os.getcwd()) | ||||
| 
 | ||||
| from apiclient.discovery import build | ||||
| from apiclient.oauth import FlowThreeLegged | ||||
| 
 | ||||
| @@ -1,53 +0,0 @@ | ||||
| #!/usr/bin/python2.4 | ||||
| # -*- coding: utf-8 -*- | ||||
| # | ||||
| # Copyright 2010 Google Inc. All Rights Reserved. | ||||
|  | ||||
| """Simple command-line example for Buzz. | ||||
|  | ||||
| Command-line application that retrieves the users | ||||
| latest content and then adds a new entry. | ||||
| """ | ||||
|  | ||||
| __author__ = 'jcgregorio@google.com (Joe Gregorio)' | ||||
|  | ||||
| # Enable this sample to be run from the top-level directory | ||||
| import os | ||||
| import sys | ||||
| sys.path.insert(0, os.getcwd()) | ||||
|  | ||||
| from apiclient.discovery import build | ||||
|  | ||||
| import httplib2 | ||||
| # httplib2.debuglevel = 4 | ||||
| import pickle | ||||
| import pprint | ||||
|  | ||||
| def main(): | ||||
|   f = open("buzz.dat", "r") | ||||
|   credentials = pickle.loads(f.read()) | ||||
|   f.close() | ||||
|  | ||||
|   http = httplib2.Http() | ||||
|   http = credentials.authorize(http) | ||||
|  | ||||
|   p = build("buzz", "v1", http=http) | ||||
|   activities = p.activities() | ||||
|   activitylist = activities.list(max_results='2', scope='@self', userId='@me').execute() | ||||
|   print activitylist['items'][0]['title'] | ||||
|   activitylist = activities.list_next(activitylist).execute() | ||||
|   print activitylist['items'][0]['title'] | ||||
|  | ||||
|   activity = activities.insert(userId='@me', body={ | ||||
|     'title': 'Testing insert', | ||||
|     'object': { | ||||
|       'content': u'Just a short note to show that insert is working. ☄', | ||||
|       'type': 'note'} | ||||
|     } | ||||
|   ).execute() | ||||
|   pprint.pprint(activity) | ||||
|   print | ||||
|   print 'Just created: ', activity['links']['alternate'][0]['href'] | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|   main() | ||||
| @@ -1,33 +0,0 @@ | ||||
| #!/usr/bin/python2.4 | ||||
| # -*- coding: utf-8 -*- | ||||
| # | ||||
| # Copyright 2010 Google Inc. All Rights Reserved. | ||||
|  | ||||
| """Simple command-line example for Buzz. | ||||
|  | ||||
| Command-line application that retrieves the users | ||||
| latest content and then adds a new entry. | ||||
| """ | ||||
|  | ||||
| __author__ = 'jcgregorio@google.com (Joe Gregorio)' | ||||
|  | ||||
|  | ||||
| from apiclient.discovery import build | ||||
|  | ||||
| import httplib2 | ||||
| import pickle | ||||
|  | ||||
|  | ||||
| def main(): | ||||
|   f = open("moderator.dat", "r") | ||||
|   credentials = pickle.loads(f.read()) | ||||
|   f.close() | ||||
|  | ||||
|   http = httplib2.Http() | ||||
|   http = credentials.authorize(http) | ||||
|  | ||||
|   p = build("moderator", "v1", http=http) | ||||
|   print p.submissions().list(seriesId="7035", topicId="64").execute() | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|   main() | ||||
							
								
								
									
										0
									
								
								samples/django_sample/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								samples/django_sample/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								samples/django_sample/buzz/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								samples/django_sample/buzz/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										62
									
								
								samples/django_sample/buzz/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								samples/django_sample/buzz/models.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | ||||
| import pickle | ||||
| import base64 | ||||
|  | ||||
| import apiclient.oauth | ||||
| from django.contrib import admin | ||||
| from django.contrib.auth.models import User | ||||
| from django.db import models | ||||
|  | ||||
| # Create your models here. | ||||
|  | ||||
| class OAuthCredentialsField(models.Field): | ||||
|  | ||||
|   __metaclass__ = models.SubfieldBase | ||||
|  | ||||
|   def db_type(self): | ||||
|     return 'VARCHAR' | ||||
|  | ||||
|   def to_python(self, value): | ||||
|     if value is None: | ||||
|       return None | ||||
|     if isinstance(value, apiclient.oauth.Credentials): | ||||
|       return value | ||||
|     return pickle.loads(base64.b64decode(value)) | ||||
|  | ||||
|   def get_db_prep_value(self, value): | ||||
|     return base64.b64encode(pickle.dumps(value)) | ||||
|  | ||||
| class FlowThreeLeggedField(models.Field): | ||||
|  | ||||
|   __metaclass__ = models.SubfieldBase | ||||
|  | ||||
|   def db_type(self): | ||||
|     return 'VARCHAR' | ||||
|  | ||||
|   def to_python(self, value): | ||||
|     print "In to_python", value | ||||
|     if value is None: | ||||
|       return None | ||||
|     if isinstance(value, apiclient.oauth.FlowThreeLegged): | ||||
|       return value | ||||
|     return pickle.loads(base64.b64decode(value)) | ||||
|  | ||||
|   def get_db_prep_value(self, value): | ||||
|     return base64.b64encode(pickle.dumps(value)) | ||||
|  | ||||
| # The Flow could also be stored in memcache since it is short lived. | ||||
| class Flow(models.Model): | ||||
|   id = models.ForeignKey(User, primary_key=True) | ||||
|   flow = FlowThreeLeggedField() | ||||
|  | ||||
| class Credential(models.Model): | ||||
|   id = models.ForeignKey(User, primary_key=True) | ||||
|   credential = OAuthCredentialsField() | ||||
|  | ||||
| class CredentialAdmin(admin.ModelAdmin): | ||||
|     pass | ||||
|  | ||||
| class FlowAdmin(admin.ModelAdmin): | ||||
|     pass | ||||
|  | ||||
| admin.site.register(Credential, CredentialAdmin) | ||||
| admin.site.register(Flow, FlowAdmin) | ||||
							
								
								
									
										23
									
								
								samples/django_sample/buzz/tests.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								samples/django_sample/buzz/tests.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| """ | ||||
| This file demonstrates two different styles of tests (one doctest and one | ||||
| unittest). These will both pass when you run "manage.py test". | ||||
|  | ||||
| Replace these with more appropriate tests for your application. | ||||
| """ | ||||
|  | ||||
| from django.test import TestCase | ||||
|  | ||||
| class SimpleTest(TestCase): | ||||
|     def test_basic_addition(self): | ||||
|         """ | ||||
|         Tests that 1 + 1 always equals 2. | ||||
|         """ | ||||
|         self.failUnlessEqual(1 + 1, 2) | ||||
|  | ||||
| __test__ = {"doctest": """ | ||||
| Another way to test that 1 + 1 is equal to 2. | ||||
|  | ||||
| >>> 1 + 1 == 2 | ||||
| True | ||||
| """} | ||||
|  | ||||
							
								
								
									
										62
									
								
								samples/django_sample/buzz/views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								samples/django_sample/buzz/views.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | ||||
| import os | ||||
| import logging | ||||
| import httplib2 | ||||
|  | ||||
| from django.http import HttpResponse | ||||
| from django.core.urlresolvers import reverse | ||||
| from django.contrib.auth.decorators import login_required | ||||
| from django_sample.buzz.models import Credential, Flow | ||||
| from apiclient.discovery import build | ||||
| from apiclient.oauth import FlowThreeLegged | ||||
| from django.http import HttpResponseRedirect | ||||
| from django.shortcuts import render_to_response | ||||
|  | ||||
| print os.environ | ||||
| STEP2_URI = 'http://localhost:8000/auth_return' | ||||
|  | ||||
| @login_required | ||||
| def index(request): | ||||
|   try: | ||||
|     c = Credential.objects.get(id=request.user) | ||||
|     http = httplib2.Http() | ||||
|     http = c.credential.authorize(http) | ||||
|     p = build("buzz", "v1", http=http) | ||||
|     activities = p.activities() | ||||
|     activitylist = activities.list(scope='@consumption', | ||||
|                                    userId='@me').execute() | ||||
|     logging.info(activitylist) | ||||
|  | ||||
|     return render_to_response('buzz/welcome.html', { | ||||
|                 'activitylist': activitylist, | ||||
|                 }) | ||||
|  | ||||
|   except Credential.DoesNotExist: | ||||
|     p = build("buzz", "v1") | ||||
|     flow = FlowThreeLegged(p.auth_discovery(), | ||||
|                    consumer_key='anonymous', | ||||
|                    consumer_secret='anonymous', | ||||
|                    user_agent='google-api-client-python-buzz-django/1.0', | ||||
|                    domain='anonymous', | ||||
|                    scope='https://www.googleapis.com/auth/buzz', | ||||
|                    xoauth_displayname='Django Example Web App') | ||||
|  | ||||
|     authorize_url = flow.step1_get_authorize_url(STEP2_URI) | ||||
|     f = Flow(id=request.user, flow=flow) | ||||
|     f.save() | ||||
|     return HttpResponseRedirect(authorize_url) | ||||
|  | ||||
| @login_required | ||||
| def auth_return(request): | ||||
|     try: | ||||
|       f = Flow.objects.get(id=request.user) | ||||
|       print f | ||||
|       print f.flow | ||||
|       print dir(f.flow) | ||||
|       print type(f.flow) | ||||
|       credential = f.flow.step2_exchange(request.REQUEST) | ||||
|       c = Credential(id=request.user, credential=credential) | ||||
|       c.save() | ||||
|       f.delete() | ||||
|       return HttpResponseRedirect("/") | ||||
|     except Flow.DoesNotExist: | ||||
|       pass | ||||
							
								
								
									
										11
									
								
								samples/django_sample/manage.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										11
									
								
								samples/django_sample/manage.py
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| #!/usr/bin/python | ||||
| from django.core.management import execute_manager | ||||
| try: | ||||
|     import settings # Assumed to be in the same directory. | ||||
| except ImportError: | ||||
|     import sys | ||||
|     sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) | ||||
|     sys.exit(1) | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     execute_manager(settings) | ||||
							
								
								
									
										83
									
								
								samples/django_sample/settings.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								samples/django_sample/settings.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,83 @@ | ||||
| # Django settings for django_sample project. | ||||
| import os | ||||
|  | ||||
| DEBUG = True | ||||
| TEMPLATE_DEBUG = DEBUG | ||||
|  | ||||
| ADMINS = ( | ||||
|     # ('Your Name', 'your_email@domain.com'), | ||||
| ) | ||||
|  | ||||
| MANAGERS = ADMINS | ||||
|  | ||||
| DATABASE_ENGINE = 'sqlite3'           # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. | ||||
| DATABASE_NAME = 'database.sqlite3'             # Or path to database file if using sqlite3. | ||||
| DATABASE_USER = ''             # Not used with sqlite3. | ||||
| DATABASE_PASSWORD = ''         # Not used with sqlite3. | ||||
| DATABASE_HOST = ''             # Set to empty string for localhost. Not used with sqlite3. | ||||
| DATABASE_PORT = ''             # Set to empty string for default. Not used with sqlite3. | ||||
|  | ||||
| # Local time zone for this installation. Choices can be found here: | ||||
| # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name | ||||
| # although not all choices may be available on all operating systems. | ||||
| # If running in a Windows environment this must be set to the same as your | ||||
| # system time zone. | ||||
| TIME_ZONE = 'America/New_York' | ||||
|  | ||||
| # Language code for this installation. All choices can be found here: | ||||
| # http://www.i18nguy.com/unicode/language-identifiers.html | ||||
| LANGUAGE_CODE = 'en-us' | ||||
|  | ||||
| SITE_ID = 1 | ||||
|  | ||||
| # If you set this to False, Django will make some optimizations so as not | ||||
| # to load the internationalization machinery. | ||||
| USE_I18N = True | ||||
|  | ||||
| # Absolute path to the directory that holds media. | ||||
| # Example: "/home/media/media.lawrence.com/" | ||||
| MEDIA_ROOT = '' | ||||
|  | ||||
| # URL that handles the media served from MEDIA_ROOT. Make sure to use a | ||||
| # trailing slash if there is a path component (optional in other cases). | ||||
| # Examples: "http://media.lawrence.com", "http://example.com/media/" | ||||
| MEDIA_URL = '' | ||||
|  | ||||
| # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a | ||||
| # trailing slash. | ||||
| # Examples: "http://foo.com/media/", "/media/". | ||||
| ADMIN_MEDIA_PREFIX = '/media/' | ||||
|  | ||||
| # Make this unique, and don't share it with anybody. | ||||
| SECRET_KEY = '_=9hq-$t_uv1ckf&s!y2$9g$1dm*6p1cl%*!^mg=7gr)!zj32d' | ||||
|  | ||||
| # List of callables that know how to import templates from various sources. | ||||
| TEMPLATE_LOADERS = ( | ||||
|     'django.template.loaders.filesystem.load_template_source', | ||||
|     'django.template.loaders.app_directories.load_template_source', | ||||
| #     'django.template.loaders.eggs.load_template_source', | ||||
| ) | ||||
|  | ||||
| MIDDLEWARE_CLASSES = ( | ||||
|     'django.middleware.common.CommonMiddleware', | ||||
|     'django.contrib.sessions.middleware.SessionMiddleware', | ||||
|     'django.contrib.auth.middleware.AuthenticationMiddleware', | ||||
| ) | ||||
|  | ||||
| ROOT_URLCONF = 'django_sample.urls' | ||||
|  | ||||
| TEMPLATE_DIRS = ( | ||||
|     # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". | ||||
|     # Always use forward slashes, even on Windows. | ||||
|     # Don't forget to use absolute paths, not relative paths. | ||||
|     os.path.join(os.path.dirname(__file__), 'templates') | ||||
| ) | ||||
|  | ||||
| INSTALLED_APPS = ( | ||||
|     'django.contrib.admin', | ||||
|     'django.contrib.auth', | ||||
|     'django.contrib.contenttypes', | ||||
|     'django.contrib.sessions', | ||||
|     'django.contrib.sites', | ||||
|     'django_sample.buzz' | ||||
| ) | ||||
							
								
								
									
										
											BIN
										
									
								
								samples/django_sample/static/go.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								samples/django_sample/static/go.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.1 KiB | 
							
								
								
									
										23
									
								
								samples/django_sample/templates/buzz/login.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								samples/django_sample/templates/buzz/login.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| {% block content %} | ||||
|  | ||||
| {% if form.errors %} | ||||
| <p>Your username and password didn't match. Please try again.</p> | ||||
| {% endif %} | ||||
|  | ||||
| <form method="post" action="{% url django.contrib.auth.views.login %}"> | ||||
| <table> | ||||
| <tr> | ||||
|     <td>{{ form.username.label_tag }}</td> | ||||
|     <td>{{ form.username }}</td> | ||||
| </tr> | ||||
| <tr> | ||||
|     <td>{{ form.password.label_tag }}</td> | ||||
|     <td>{{ form.password }}</td> | ||||
| </tr> | ||||
| </table> | ||||
|  | ||||
| <input type="submit" value="login" /> | ||||
| <input type="hidden" name="next" value="{{ next }}" /> | ||||
| </form> | ||||
|  | ||||
| {% endblock %} | ||||
							
								
								
									
										33
									
								
								samples/django_sample/templates/buzz/welcome.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								samples/django_sample/templates/buzz/welcome.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
|  | ||||
| <html> | ||||
|   <head> | ||||
|     <title>Buzz Stuff</title> | ||||
|     <style type=text/css> | ||||
|       td  { vertical-align: top; padding: 0.5em } | ||||
|       img { border:0 } | ||||
|     </style> | ||||
|   </head> | ||||
|   <body> | ||||
|       <table border=0> | ||||
|       {% for item in activitylist.items %} | ||||
|       <tr valign=top> | ||||
|         <td> | ||||
|           {% if item.actor.thumbnailUrl %} | ||||
|             <a href="{{ item.actor.profileUrl }}"> | ||||
|               <img src="{{ item.actor.thumbnailUrl }}"> | ||||
|             </a> | ||||
|           {% endif %} | ||||
|           <br> | ||||
|           <a href="{{ item.actor.profileUrl }}">{{ item.actor.name }}</a></td> | ||||
|       <td> | ||||
|         {{ item.object.content|safe }} | ||||
|         </td> | ||||
|         <td> | ||||
|           <a href="{{ item.object.links.alternate.0.href }}"><img | ||||
|             src="/static/go.png"></a> | ||||
|         </td> | ||||
|       </tr> | ||||
|       {% endfor %} | ||||
|       </table> | ||||
|   </body> | ||||
| </html> | ||||
							
								
								
									
										25
									
								
								samples/django_sample/urls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								samples/django_sample/urls.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| import os | ||||
| from django.conf.urls.defaults import * | ||||
|  | ||||
| # Uncomment the next two lines to enable the admin: | ||||
| from django.contrib import admin | ||||
| admin.autodiscover() | ||||
|  | ||||
| urlpatterns = patterns('', | ||||
|     # Example: | ||||
|     (r'^$', 'django_sample.buzz.views.index'), | ||||
|     (r'^auth_return', 'django_sample.buzz.views.auth_return'), | ||||
|  | ||||
|     # Uncomment the admin/doc line below and add 'django.contrib.admindocs' | ||||
|     # to INSTALLED_APPS to enable admin documentation: | ||||
|     # (r'^admin/doc/', include('django.contrib.admindocs.urls')), | ||||
|  | ||||
|     # Uncomment the next line to enable the admin: | ||||
|     (r'^admin/', include(admin.site.urls)), | ||||
|     (r'^accounts/login/$', 'django.contrib.auth.views.login', | ||||
|                         {'template_name': 'buzz/login.html'}), | ||||
|  | ||||
|     (r'^static/(?P<path>.*)$', 'django.views.static.serve', | ||||
|         {'document_root': os.path.join(os.path.dirname(__file__), 'static') | ||||
| }), | ||||
| ) | ||||
							
								
								
									
										75
									
								
								samples/moderator/moderator.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								samples/moderator/moderator.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | ||||
| #!/usr/bin/python2.4 | ||||
| # -*- coding: utf-8 -*- | ||||
| # | ||||
| # Copyright 2010 Google Inc. All Rights Reserved. | ||||
|  | ||||
| """Simple command-line example for Buzz. | ||||
|  | ||||
| Command-line application that retrieves the users | ||||
| latest content and then adds a new entry. | ||||
| """ | ||||
|  | ||||
| __author__ = 'jcgregorio@google.com (Joe Gregorio)' | ||||
|  | ||||
|  | ||||
| from apiclient.discovery import build | ||||
|  | ||||
| import httplib2 | ||||
| import pickle | ||||
|  | ||||
| # Uncomment to get detailed logging | ||||
| # httplib2.debuglevel = 4 | ||||
|  | ||||
| def main(): | ||||
|   f = open("moderator.dat", "r") | ||||
|   credentials = pickle.loads(f.read()) | ||||
|   f.close() | ||||
|  | ||||
|   http = httplib2.Http() | ||||
|   http = credentials.authorize(http) | ||||
|  | ||||
|   p = build("moderator", "v1", http=http) | ||||
|  | ||||
|   series_body = { | ||||
|         "description": "Share and rank tips for eating healthily on the cheaps!", | ||||
|         "name": "Eating Healthy & Cheap", | ||||
|         "videoSubmissionAllowed": False | ||||
|       } | ||||
|   series = p.series().insert(body=series_body).execute() | ||||
|   print "Created a new series" | ||||
|  | ||||
|   topic_body = { | ||||
|       "data": { | ||||
|         "description": "Share your ideas on eating healthy!", | ||||
|         "name": "Ideas", | ||||
|         "presenter": "liz" | ||||
|         } | ||||
|       } | ||||
|   topic = p.topics().insert(seriesId=series['id']['seriesId'], body=topic_body).execute() | ||||
|   print "Created a new topic" | ||||
|  | ||||
|   submission_body = { | ||||
|       "data": { | ||||
|         "attachmentUrl": "http://www.youtube.com/watch?v=1a1wyc5Xxpg", | ||||
|         "attribution": { | ||||
|           "displayName": "Bashan", | ||||
|           "location": "Bainbridge Island, WA" | ||||
|           }, | ||||
|         "text": "Charlie Ayers @ Google" | ||||
|         } | ||||
|       } | ||||
|   submission = p.submissions().insert(seriesId=topic['id']['seriesId'], | ||||
|       topicId=topic['id']['topicId'], body=submission_body).execute() | ||||
|   print "Inserted a new submisson on the topic" | ||||
|  | ||||
|   vote_body = { | ||||
|       "data": { | ||||
|         "vote": "PLUS" | ||||
|         } | ||||
|       } | ||||
|   p.votes().insert(seriesId=topic['id']['seriesId'], submissionId=submission['id']['submissionId'], body=vote_body) | ||||
|   print "Voted on the submission" | ||||
|  | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|   main() | ||||
| @@ -35,6 +35,7 @@ flow = FlowThreeLegged(moderator_discovery, | ||||
|                        user_agent='google-api-client-python-mdrtr-cmdline/1.0', | ||||
|                        domain='anonymous', | ||||
|                        scope='https://www.googleapis.com/auth/moderator', | ||||
|                        #scope='tag:google.com,2010:auth/moderator', | ||||
|                        xoauth_displayname='Google API Client Example App') | ||||
| 
 | ||||
| authorize_url = flow.step1_get_authorize_url() | ||||
| @@ -65,6 +65,22 @@ class Model(unittest.TestCase): | ||||
|     self.assertNotEqual(query, '') | ||||
|     self.assertEqual(body, '{"data": {}}') | ||||
|  | ||||
|   def test_json_body_default_data(self): | ||||
|     """Test that a 'data' wrapper doesn't get added if one is already present.""" | ||||
|     model = JsonModel() | ||||
|  | ||||
|     headers = {} | ||||
|     path_params = {} | ||||
|     query_params = {} | ||||
|     body = {'data': 'foo'} | ||||
|  | ||||
|     headers, params, query, body = model.request(headers, path_params, query_params, body) | ||||
|  | ||||
|     self.assertEqual(headers['accept'], 'application/json') | ||||
|     self.assertEqual(headers['content-type'], 'application/json') | ||||
|     self.assertNotEqual(query, '') | ||||
|     self.assertEqual(body, '{"data": "foo"}') | ||||
|  | ||||
|   def test_json_build_query(self): | ||||
|     model = JsonModel() | ||||
|  | ||||
|   | ||||
| @@ -487,7 +487,7 @@ group.add_option("--rev", action="store", dest="revision", | ||||
|                  help="Base revision/branch/tree to diff against. Use " | ||||
|                       "rev1:rev2 range to review already committed changeset.") | ||||
| group.add_option("--send_mail", action="store_true", | ||||
|                  dest="send_mail", default=False, | ||||
|                  dest="send_mail", default=True, | ||||
|                  help="Send notification email to reviewers.") | ||||
| group.add_option("--vcs", action="store", dest="vcs", | ||||
|                  metavar="VCS", default=None, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Tom Miller
					Tom Miller