From 0ab58f123304f9dc1866a4d8b31072f97e2f66dd Mon Sep 17 00:00:00 2001 From: htormey Date: Tue, 6 Apr 2010 10:57:11 -0700 Subject: [PATCH 01/26] -Adding installer setup.py --- setup.py | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 setup.py diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..0cc50c0 --- /dev/null +++ b/setup.py @@ -0,0 +1,62 @@ +# Copyright (c) 2010 Harry Tormey +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +'''The setup and build script for the python-snaptic library.''' + +__author__ = 'harry@snaptic.com' +__version__ = '0.4-devel' + + +METADATA = dict( + name = "python-snaptic", + version = __version__, + py_modules = ['snaptic'], + author='Harry Tormey', + author_email='harry@snaptic.com', + description='A python wrapper around the Snaptic API', + license=' MIT License', + url='http://github.com/snaptic/python-api', + keywords='snaptic api', +) + +# Extra package metadata to be used only if setuptools is installed +SETUPTOOLS_METADATA = dict( + install_requires = ['setuptools', 'simplejson'], + include_package_data = True, + classifiers = [ + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Topic :: Software Development :: Libraries :: Python Modules', + 'Topic :: Internet', + ], +) + +def Main(): + + # Use setuptools if available, otherwise fallback and use distutils + try: + import setuptools + METADATA.update(SETUPTOOLS_METADATA) + setuptools.setup(**METADATA) + except ImportError: + import distutils.core + distutils.core.setup(**METADATA) + + +if __name__ == '__main__': + Main() + + From 00c65eaa7fa95bdfd82563bb1ae789ea3eede170 Mon Sep 17 00:00:00 2001 From: htormey Date: Tue, 6 Apr 2010 16:16:24 -0700 Subject: [PATCH 02/26] Fix bug in tags. First pass at script to generate python api docs using sphinx --- docs/index.txt | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 docs/index.txt diff --git a/docs/index.txt b/docs/index.txt new file mode 100644 index 0000000..2c18e00 --- /dev/null +++ b/docs/index.txt @@ -0,0 +1,20 @@ +.. snaptic documentation master file, created by + sphinx-quickstart on Tue Apr 6 11:18:53 2010. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +snaptic (version 0.4) +=================================== + +A library that provies a python inferface to the Snaptic API + + +.. toctree:: + :maxdepth: 2 + +Classes +================== + +.. automodule:: snaptic + :members: Api, Note, User, Image, SnapticError + From c39d9af95534272c62e9bf6cd89d34a84dc5be0f Mon Sep 17 00:00:00 2001 From: htormey Date: Tue, 6 Apr 2010 16:38:25 -0700 Subject: [PATCH 03/26] forgot to push snaptic.py --- snaptic.py | 213 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 123 insertions(+), 90 deletions(-) diff --git a/snaptic.py b/snaptic.py index 079dc74..b043d56 100644 --- a/snaptic.py +++ b/snaptic.py @@ -13,7 +13,7 @@ # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -'''A library that provides a python interface to the Snaptic API''' +'''A python interface to the Snaptic API''' __author__ = 'harry@snaptic.com' __version__ = '0.4-devel' @@ -88,13 +88,15 @@ class Image(object): '''A class representing the Image structure which is an attribute of a note retruned via the Snaptic API. The Image structure exposes the following properties: - image.type - image.md5 - image.id - image.width - image.height - image.src - image.data + + image.type + image.md5 + image.id + image.width + image.height + image.src + image.data + ''' def __init__(self, type="image", md5=None, id=None, revision_id=None, width=0, height=0, src=None, data=None): @@ -110,21 +112,24 @@ def __init__(self, type="image", md5=None, id=None, revision_id=None, width=0, h class Note(object): '''A class representing the Note structure used by the Snaptic API. - The Note structure exposes the following properties: - note.created_at - note.modified_at - note.reminder_at - note.note_id - note.text - note.summary - note.source - note.source_url - note.user - note.children - note.media - note.labels - note.location - note.has_media + The Note structure exposes the following properties: + + note.created_at + note.modified_at + note.reminder_at + note.note_id + note.text + note.summary + note.source + note.source_url + note.user + note.children + note.media + note.labels + note.location + note.has_media # read only + note.dictionary # read only + ''' def __init__(self, created_at, modified_at, reminder_at, note_id, text, @@ -145,80 +150,106 @@ def __init__(self, created_at, modified_at, reminder_at, note_id, text, @property def has_media(self): + ''' + Check to see if Note has any media (images) associated with it. + + Returns: + True/False + ''' return len(self.media) > 0 @property def dictionary(self): ''' - return a dictionary version of the note + Returns text from the note packaged as a dictionary. + + Returns: + A dictionary containing selected attributes from the note. ''' #Working on adding dates/location/media and other fields to this dictionary. Right now you can just update text. -htormey return dict(text=self.text) class Api(object): - '''A python interface into the Snaptic API - - Example usage: - To create an instance of the snaptic.Api class with basic authentication: - >> import snaptic - >> api = snaptic.Api("username", "password") - - To fetch users notes and print an attribute: - >> print [n.created_at for n in api.notes] - - ['2010-03-08T17:49:08.850Z', '2010-03-06T20:02:32.501Z', '2010-03-06T01:35:14.851Z', '2010-03-05T04:13:00.616Z', '2010-03-01T00:09:38.566Z', '2010-02-18T04:09:55.471Z', '2010-02-18T02:26:35.990Z', - '2010-02-12T23:28:22.612Z', '2010-02-10T03:06:50.590Z', '2010-02-10T06:02:57.068Z', '2010-02-08T05:14:07.000Z', '2010-02-08T02:28:20.391Z', '2010-02-05T06:57:54.323Z', '2010-02-07T07:26:34.469Z', - '2010-01-25T02:11:24.075Z', '2010-01-24T23:37:07.411Z'] - - To post a note: - >> r = api.post_note("Harry says snaptic is da bomb") - >> print r - { - "notes":[ - { - "created_at": "2010-03-30T05:12:15.395Z", - "modified_at": "2010-03-30T05:12:19.260Z", - "reminder_at": "", - "id": "1760036", - "text": "Harry says snaptic is da bomb", - "summary": "Harry says snaptic is da bomb", - "source": "3banana", - "source_url": "https://snaptic.com/", - "user": { - "id": "913202", - "user_name": "ht" - }, - "children": "0", - "labels": {}, - "tags": {}, - "location": {} - } - ]} - - To delete a note: - >> id = api.notes[1].note_id - >> api.delete_note(id) - - To add an image to the above note - >> id = api.notes[1].note_id - >> api.load_image_and_add_to_note_with_id("myimage.jpg", id) - - To edit a note: - >> n[0].text='Harry says coolio' - >> api.edit_note(n[0]) - - To download image data from a note: - >> api.notes[1].has_media - True - >> id = api.notes[1].note_id - >> d = api.get_image_with_id(id) - >> filename = "/Users/harrytormey/%s.jpg" % id - >> fout = open(filename, "wb") - >> fout.write(d) - >> fout.close() - - To get a json object of a users tags - >> api.get_tags() + ''' + Example usage: + + To create an instance of the snaptic.Api class with basic authentication: + + >>> import snaptic + >>> api = snaptic.Api("username", "password") + + To fetch users notes and print an attribute: + + >>> [n.created_at for n in api.notes] + ['2010-03-08T17:49:08.850Z', '2010-03-06T20:02:32.501Z', '2010-03-06T01:35:14.851Z', '2010-03-05T04:13:00.616Z', '2010-03-01T00:09:38.566Z', '2010-02-18T04:09:55.471Z', '2010-02-18T02:26:35.990Z', + '2010-02-12T23:28:22.612Z', '2010-02-10T03:06:50.590Z', '2010-02-10T06:02:57.068Z', '2010-02-08T05:14:07.000Z', '2010-02-08T02:28:20.391Z', '2010-02-05T06:57:54.323Z', '2010-02-07T07:26:34.469Z', + '2010-01-25T02:11:24.075Z', '2010-01-24T23:37:07.411Z'] + + To post a note: + + >>> api.post_note("Harry says snaptic is da bomb") + { + "notes":[ + { + "created_at": "2010-04-06T22:59:12.093Z", + "modified_at": "2010-04-06T22:59:12.093Z", + "reminder_at": "", + "id": "1926387", + "text": "Harry says snaptic is da bomb", + "summary": "Harry says snaptic is da bomb", + "source": "3banana", + "source_url": "https://snaptic.com/", + "user": { + "id": "1813083", + "user_name": "harry12" + }, + "children": "0", + "labels": {}, + "tags": {}, + "location": {} + } + ]} + + To delete a note: + + >>> id = api.notes[1].note_id + >>> api.delete_note(id) + + To add an image to the above note + + >>> id = api.notes[1].note_id + >>> api.load_image_and_add_to_note_with_id("myimage.jpg", id) + + To edit a note: + + >>> n[0].text='Harry says coolio' + >>> api.edit_note(n[0]) + + To download image data from a note: + + >>> api.notes[1].has_media + True + >>> id = api.notes[1].note_id + >>> d = api.get_image_with_id(id) + >>> filename = "/Users/harrytormey/%s.jpg" % id + >>> fout = open(filename, "wb") + >>> fout.write(d) + >>> fout.close() + + To get a json object of a users tags + + >>> api.get_tags() + { + "tags":[ + { + "name":"food", + "count":"1", + }, + { + "name":"ice", + "count":"1", + }, + ]} ''' API_SERVER = "api.snaptic.com" @@ -250,8 +281,10 @@ def set_credentials(self, username, password): Set username/password Args: - username: snaptic username - password: snaptic password + username: + snaptic username + password: + snaptic password ''' self._username = username self._password = password @@ -446,7 +479,7 @@ def get_tags(self): ''' Fetch json object containing tags from users account ''' - url = "/" + self.API_VERSION + API_ENDPOINT_TAGS_JSON + url = "/" + self.API_VERSION + self.API_ENDPOINT_TAGS_JSON tags = self._fetch_url(url) return tags From 44270cf065b3ff42593596da14c19e394cdb91a5 Mon Sep 17 00:00:00 2001 From: htormey Date: Tue, 6 Apr 2010 18:00:48 -0700 Subject: [PATCH 04/26] updating doc strings to allow for auto generated docs. --- snaptic.py | 213 ++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 178 insertions(+), 35 deletions(-) diff --git a/snaptic.py b/snaptic.py index b043d56..ad3988e 100644 --- a/snaptic.py +++ b/snaptic.py @@ -112,24 +112,23 @@ def __init__(self, type="image", md5=None, id=None, revision_id=None, width=0, h class Note(object): '''A class representing the Note structure used by the Snaptic API. - The Note structure exposes the following properties: - - note.created_at - note.modified_at - note.reminder_at - note.note_id - note.text - note.summary - note.source - note.source_url - note.user - note.children - note.media - note.labels - note.location - note.has_media # read only - note.dictionary # read only - + The Note structure exposes the following properties: + + note.created_at + note.modified_at + note.reminder_at + note.note_id + note.text + note.summary + note.source + note.source_url + note.user + note.children + note.media + note.labels + note.location + note.has_media # read only + note.dictionary # read only ''' def __init__(self, created_at, modified_at, reminder_at, note_id, text, @@ -266,6 +265,15 @@ class Api(object): API_ENDPOINT_CURSOR = "?cursor=" def __init__(self, username, password=None, url=API_SERVER, use_ssl=True, port=443, timeout=10): + ''' + Args: + username: The username of the snaptic account. + password: The password of the snaptic account. + url: The url of the api server which will handle the http(s) API requests. + use_ssl: Use ssl for basic auth or not. + port: The port to make http(s) requests on. + timeout: number of seconds to wait before giving up on a request. + ''' self._url = url self._use_ssl = use_ssl self._port = port @@ -276,7 +284,6 @@ def __init__(self, username, password=None, url=API_SERVER, use_ssl=True, port=4 self.set_credentials(username, password) def set_credentials(self, username, password): - ''' Set username/password @@ -292,6 +299,10 @@ def set_credentials(self, username, password): def load_image_and_add_to_note_with_id(self, filename, id): ''' Load image from filename and append to note. + + Args: + filename: filename of image to load data from. + id: id of note to which image will be appended. ''' try: fin = open(filename, 'r') @@ -302,7 +313,14 @@ def load_image_and_add_to_note_with_id(self, filename, id): def add_image_to_note_with_id(self, filename, data, id): ''' - Add image data to note + Add image data to note. + + Args: + filename: filename of image. + data: loaded image data to be appended to note. + id: id of note to which image data will be appended. + Returns: + The server's response page. ''' page = "/" + self.API_VERSION + self.API_ENDPOINT_IMAGES + id +".json" return self._post_multi_part(self._url, page, [("image", filename, data)]) @@ -310,8 +328,13 @@ def add_image_to_note_with_id(self, filename, data, id): def _post_multi_part(self, host, selector, files): """ Post files to an http host as multipart/form-data. - files is a sequence of (name, filename, value) elements for data to be uploaded as files - Return the server's response page. + + Args: + host: server to send request to + selector: API endpoint to send to the server + files: sequence of (name, filename, value) elements for data to be uploaded as files + Returns: + Return the server's response page. """ content_type, body = self._encode_multi_part_form_data(files) handler = httplib.HTTPConnection(host) @@ -330,8 +353,12 @@ def _post_multi_part(self, host, selector, files): def _encode_multi_part_form_data(self, files): """ - Files is a sequence of (name, filename, value) elements for data to be uploaded as files - Return (content_type, body) ready for httplib.HTTPConnection instance + Encode multi part form data to be posted to server. + + Args: + Files is a sequence of (name, filename, value) elements for data to be uploaded as files + Return: + sequence of (content_type, body) ready for httplib.HTTPConnection instance """ BOUNDARY = '----------ThIs_Is_tHe_bouNdaRY_$' CRLF = '\r\n' @@ -349,18 +376,58 @@ def _encode_multi_part_form_data(self, files): return content_type, body def _get_content_type(self, filename): + """ + Attempt to guess mimetype of file. + + Args: + filename: filename to be guessed. + Returns: + File type or default value. + """ return mimetypes.guess_type(filename)[0] or 'application/octet-stream' def delete_note(self, id):#Change this to just take a note + """ + Delete a note. + + Args: + id: id of note to be deleted. + Returns: + The server's response page. + """ return self._request(self.HTTP_DELETE, id) def edit_note(self, note): + """ + Edit a note. + + Args: + note: note object to be edited + Returns: + The server's response page. + """ return self._request(self.HTTP_POST, note) def post_note(self, note): + """ + Post a note. + + Args: + note: text of note to be posted. + Returns: + The server's response page. + """ return self._request(self.HTTP_POST, note) #change this to note_text to be a little clearer -htormey def _request(self, http_method, note): #Clean this up a little -htormey + """ + Perform a http request on a note + + Args: + http_metod: what kind of http request is being made (i.e POST/DELETE/GET) + Returns: + The server's response page. + """ if http_method == self.HTTP_POST: headers = { 'Content-type' : "application/x-www-form-urlencoded" } if isinstance(note, Note): @@ -385,7 +452,12 @@ def _request(self, http_method, note): #Clean this up a little -htormey def get_image_with_id(self, id): ''' - Get image data using the following id + Get image data associated with a given id. + + Args: + id: id of image to be fetched. + Returns: + Data associated with image id. ''' url = self.API_ENDPOINT_IMAGES_VIEW + id return self._fetch_url(url) @@ -393,6 +465,9 @@ def get_image_with_id(self, id): def get_user_id(self): ''' Get ID of API user. + + Returns: + Id of snaptic user associated with Api instance. ''' if self._user: return self._user.id @@ -411,7 +486,10 @@ def fget(self): def get_notes(self): ''' - Get notes and update the cache + Get notes and update the Api's internal cache. + + Returns: + A list of Note objects from the snaptic users account. ''' url = "/" + self.API_VERSION + self.API_ENDPOINT_NOTES_JSON json_notes = self._fetch_url(url) @@ -423,6 +501,11 @@ def get_notes_from_cursor(self, cursor_position): Get a batch of upto 20 notes from a given cursor position. See description given for json_cursor for further details on how cursors work with snaptic. + + Args: + cursor_position: cursor position to grab 20 notes from (i.e -1 is most recent 20) + Returns: + A list of note objects based on the contents of the users account. ''' json_notes = self.json_cursor(cursor_position) notes = self._parse_notes(json_notes) @@ -430,10 +513,13 @@ def get_notes_from_cursor(self, cursor_position): def get_cursor_information(self, cursor_position): ''' - Return dictionary containing previous_cursor, next_cursor and note count - for a given cursor_position. This information can be used to calculate - how to navigate through a users notes. See json_cursor for further details - on how cursors work with snaptic. + Gets information which can be used to calculate to navigate through a users notes. See + json_cursor for further details on how cursors work with snaptic. + + Args: + cursor_position: cursor position you want to find out about. + Returns: + A dictionary containing previous_cursor, next_cursor and note count. ''' json_notes = self.json_cursor(cursor_position) return self._parse_cursor_info(json_notes) @@ -441,6 +527,11 @@ def get_cursor_information(self, cursor_position): def _parse_cursor_info(self, source): ''' Parse cursor information with notes returned from snaptic. + + Args: + source: A json object consisting of notes and cursor information + Returns: + A dictionary containing previous_cursor, next_cursor and note count. ''' cursor_info = json.loads(source) if 'next_cursor' in cursor_info and 'previous_cursor' in cursor_info and 'count' in cursor_info: @@ -450,7 +541,10 @@ def _parse_cursor_info(self, source): def get_user(self): ''' - Get user info + Get user info. + + Returns: + A user object. ''' url = "/" + self.API_VERSION + self.API_ENDPOINT_USER_JSON user_info = self._fetch_url(url) @@ -470,6 +564,9 @@ def fget(self): def get_json(self): ''' Get json object and update the cache + + Returns: + A json object representing all notes in a users account. ''' url = "/" + self.API_VERSION + self.API_ENDPOINT_NOTES_JSON self._json = self._fetch_url(url) @@ -478,6 +575,9 @@ def get_json(self): def get_tags(self): ''' Fetch json object containing tags from users account + + Returns: + A json object containing tags and related information (number of notes per tag, etc). ''' url = "/" + self.API_VERSION + self.API_ENDPOINT_TAGS_JSON tags = self._fetch_url(url) @@ -485,16 +585,29 @@ def get_tags(self): def json_cursor(self, cursor_position): ''' - Return batches of 20 notes in JSON format from a given cursor position i.e -1, 1, + Get batches of 20 notes in JSON format from a given cursor position i.e -1, 1, etc. For example: -1 returns the most recent 20 notes, 1 returns the previous 20 before that, etc. One exeption to note is that 0 returns a JSON object for all notes in a given account. + + Args: + cursor_position: cursor position to grab 20 notes from (i.e -1 is most recent 20) + Returns: + A json object containing notes from cursor position requested ''' url = "/" + self.API_VERSION + self.API_ENDPOINT_NOTES_JSON + self.API_ENDPOINT_CURSOR + str(cursor_position) cursor = self._fetch_url(url) return cursor def _fetch_url(self, url): + ''' + Perform a basic auth request on a given snaptic API endpoint. + + Args: + url: Snaptic Api endpoint (i.e /v1/notes.json etc) + Returns: + The server's response page. + ''' handler = self._basic_auth_request(url) response = handler.getresponse() data = response.read() @@ -504,6 +617,15 @@ def _fetch_url(self, url): return data def _make_basic_auth_headers(self, username, password): + ''' + Encode headers for basic auth request. + + Args: + username: snaptic username to be used. + password: password to be used. + Returns: + Dictionary with encoded basic auth values. + ''' if username and password: headers = dict(Authorization="Basic %s" %(base64.b64encode("%s:%s" %(username, password)))) @@ -512,8 +634,18 @@ def _make_basic_auth_headers(self, username, password): return headers def _basic_auth_request(self, path, method=HTTP_GET, headers={}, params={}): - ''' Make a HTTP request with basic auth header and supplied method. - Defaults to operating over SSL. ''' + ''' + Make a HTTP request with basic auth header and supplied method. + Defaults to operating over SSL. + + Args: + path: Snaptic API endpoint + metthod: which http method to use (PUT/DELETE/GET) + headers: Additional header to use with request. + params: Other parameters to use + Returns: + The server's response page. + ''' h = self._make_basic_auth_headers(self._username, self._password) h.update(headers) if self._use_ssl: @@ -531,7 +663,12 @@ def _basic_auth_request(self, path, method=HTTP_GET, headers={}, params={}): def _parse_user_info(self, source): ''' - parse JSON user returned from snaptic, instantiate a User object from it. + Parse JSON user returned from snaptic, instantiate a User object from it. + + Args: + source: Json object representing a user + Returns: + A User object. ''' user_info = json.loads(source) @@ -543,6 +680,12 @@ def _parse_user_info(self, source): def _parse_notes( self, source, get_image_data=False): ''' parse JSON notes returned from snaptic, instantiate a list of note objects from it. + + Args: + source: A json object representing a list of notes. + get_images: if images are associated with notes, download them now. + Returns: + A list of note objects. ''' notes = [] json_notes = json.loads(source) From 51052c0a4dd7604f75642c090f8bbc276f641441 Mon Sep 17 00:00:00 2001 From: htormey Date: Tue, 6 Apr 2010 20:09:02 -0700 Subject: [PATCH 05/26] Adding more docs stuff --- docs/Makefile | 89 +++++++++++++++++++++++ docs/conf.py | 194 ++++++++++++++++++++++++++++++++++++++++++++++++++ docs/make.bat | 113 +++++++++++++++++++++++++++++ 3 files changed, 396 insertions(+) create mode 100644 docs/Makefile create mode 100644 docs/conf.py create mode 100644 docs/make.bat diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..8beca86 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,89 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + -rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/snaptic.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/snaptic.qhc" + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ + "run these through (pdf)latex." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..a85e1b4 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,194 @@ +# -*- coding: utf-8 -*- +# +# snaptic documentation build configuration file, created by +# sphinx-quickstart on Tue Apr 6 11:18:53 2010. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys, os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.append(os.path.abspath('.')) + +# -- General configuration ----------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.autodoc'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.txt' + +# The encoding of source files. +#source_encoding = 'utf-8' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'snaptic' +copyright = u'2010, Harry Tormey' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '0.4-devel' +# The full version, including alpha/beta/rc tags. +release = '0.4-devel' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of documents that shouldn't be included in the build. +#unused_docs = [] + +# List of directories, relative to source directory, that shouldn't be searched +# for source files. +exclude_trees = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. Major themes that come with +# Sphinx are currently 'default' and 'sphinxdoc'. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_use_modindex = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = '' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'snapticdoc' + + +# -- Options for LaTeX output -------------------------------------------------- + +# The paper size ('letter' or 'a4'). +#latex_paper_size = 'letter' + +# The font size ('10pt', '11pt' or '12pt'). +#latex_font_size = '10pt' + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('index', 'snaptic.tex', u'snaptic Documentation', + u'Harry Tormey', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# Additional stuff for the LaTeX preamble. +#latex_preamble = '' + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_use_modindex = True diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..a27ad8b --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,113 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +set SPHINXBUILD=sphinx-build +set BUILDDIR=_build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. changes to make an overview over all changed/added/deprecated items + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\snaptic.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\snaptic.ghc + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +:end From 5858ea99c8e170b3d9b6c3bdfc3ac00ab49d0a8a Mon Sep 17 00:00:00 2001 From: htormey Date: Wed, 7 Apr 2010 11:43:52 -0700 Subject: [PATCH 06/26] Work on doc strings to improve auto generated docs --- snaptic.py | 195 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 116 insertions(+), 79 deletions(-) diff --git a/snaptic.py b/snaptic.py index ad3988e..2fef267 100644 --- a/snaptic.py +++ b/snaptic.py @@ -31,30 +31,42 @@ def Property(func): return property(**func()) class SnapticError(Exception): - '''Base class for Snaptic errors''' + """ + Base class for Snaptic errors. + + The SnaptiError class exposes the following properties:: + + snaptic_error.message # read only + snaptic_error.status # read only + snaptic_error.response # read only + """ @property def message(self): - '''Returns the first argument used to construct this error.''' + """Returns the first argument used to construct this error.""" return self.args[0] @property def status(self): - '''Returns the HTTP status code used to construct this error.''' + """Returns the HTTP status code used to construct this error.""" return self.args[1] @property def response(self): - '''Returns HTTP response body used to construct this error.''' + """Returns HTTP response body used to construct this error.""" return self.args[2] class User(object): - '''A class representing the User structure used by the Snaptic API. + """ + A class representing the User structure used by the Snaptic API. - The User structure exposes the following properties: - user.id - user.user_name - ''' + The User class exposes the following properties:: + user.id # read only + user.user_name # read only + user.created_at # read only + user.auth_token # read only + user.email # read only + """ def __init__(self, id=None, user_name=None, created_at=None, auth_token=None, email=None): self._id = id @@ -85,9 +97,10 @@ def email(self): #Perhaps I should refactor this into a class hierarchy and subclass for image/sound/etc? -htormey class Image(object): - '''A class representing the Image structure which is an attribute of a note retruned via the Snaptic API. + """ + A class representing the Image structure which is an attribute of a note retruned via the Snaptic API. - The Image structure exposes the following properties: + The Image structure exposes the following properties:: image.type image.md5 @@ -96,8 +109,7 @@ class Image(object): image.height image.src image.data - - ''' + """ def __init__(self, type="image", md5=None, id=None, revision_id=None, width=0, height=0, src=None, data=None): self.type = type @@ -110,9 +122,10 @@ def __init__(self, type="image", md5=None, id=None, revision_id=None, width=0, h self.data = data class Note(object): - '''A class representing the Note structure used by the Snaptic API. + """ + A class representing the Note structure used by the Snaptic API. - The Note structure exposes the following properties: + The Note structure exposes the following properties:: note.created_at note.modified_at @@ -129,7 +142,7 @@ class Note(object): note.location note.has_media # read only note.dictionary # read only - ''' + """ def __init__(self, created_at, modified_at, reminder_at, note_id, text, summary, source, source_url, user, children, media = [], labels = [], location = []): @@ -149,27 +162,27 @@ def __init__(self, created_at, modified_at, reminder_at, note_id, text, @property def has_media(self): - ''' + """ Check to see if Note has any media (images) associated with it. Returns: True/False - ''' + """ return len(self.media) > 0 @property def dictionary(self): - ''' + """ Returns text from the note packaged as a dictionary. Returns: A dictionary containing selected attributes from the note. - ''' + """ #Working on adding dates/location/media and other fields to this dictionary. Right now you can just update text. -htormey return dict(text=self.text) class Api(object): - ''' + """ Example usage: To create an instance of the snaptic.Api class with basic authentication: @@ -177,13 +190,26 @@ class Api(object): >>> import snaptic >>> api = snaptic.Api("username", "password") - To fetch users notes and print an attribute: + To fetch all users notes and print an attribute: >>> [n.created_at for n in api.notes] ['2010-03-08T17:49:08.850Z', '2010-03-06T20:02:32.501Z', '2010-03-06T01:35:14.851Z', '2010-03-05T04:13:00.616Z', '2010-03-01T00:09:38.566Z', '2010-02-18T04:09:55.471Z', '2010-02-18T02:26:35.990Z', '2010-02-12T23:28:22.612Z', '2010-02-10T03:06:50.590Z', '2010-02-10T06:02:57.068Z', '2010-02-08T05:14:07.000Z', '2010-02-08T02:28:20.391Z', '2010-02-05T06:57:54.323Z', '2010-02-07T07:26:34.469Z', '2010-01-25T02:11:24.075Z', '2010-01-24T23:37:07.411Z'] + To fetch a subset of a users notes use a cursor. To get the first 20 notes and print an attribute: + + >>> [n.text for n in api.get_notes_from_cursor(-1)] + ['Harry says snaptic is da bomb #food #ice', 'Harry says snaptic is da bomb #food #ice', 'Harry says snaptic is da bomb', 'Harry says snaptic is da bomb', 'post number 99', 'post number 98', + 'post number 97', 'post number 96', 'post number 95', 'post number 94', 'post number 93', 'post number 92', 'post number 91', 'post number 90', 'post number 89', 'post number 88', 'post number 87', + 'post number 86', 'post number 85', 'post number 84'] + + To get the next 20 notes use cursor 1 (cursor 0 returns all notes in a users account): + + >>> [n.text for n in api.get_notes_from_cursor(1)] + ['post number 83', 'post number 82', 'post number 81', 'post number 80', 'post number 79', 'post number 78', 'post number 77', 'post number 76', 'post number 75', 'post number 74', 'post number 73', + 'post number 72', 'post number 71', post number 70', 'post number 69', 'post number 68', 'post number 67', 'post number 66', 'post number 65', 'post number 64'] + To post a note: >>> api.post_note("Harry says snaptic is da bomb") @@ -249,7 +275,7 @@ class Api(object): "count":"1", }, ]} - ''' + """ API_SERVER = "api.snaptic.com" API_VERSION = "v1" @@ -265,7 +291,7 @@ class Api(object): API_ENDPOINT_CURSOR = "?cursor=" def __init__(self, username, password=None, url=API_SERVER, use_ssl=True, port=443, timeout=10): - ''' + """ Args: username: The username of the snaptic account. password: The password of the snaptic account. @@ -273,7 +299,7 @@ def __init__(self, username, password=None, url=API_SERVER, use_ssl=True, port=4 use_ssl: Use ssl for basic auth or not. port: The port to make http(s) requests on. timeout: number of seconds to wait before giving up on a request. - ''' + """ self._url = url self._use_ssl = use_ssl self._port = port @@ -284,26 +310,27 @@ def __init__(self, username, password=None, url=API_SERVER, use_ssl=True, port=4 self.set_credentials(username, password) def set_credentials(self, username, password): - ''' + """ Set username/password Args: username: - snaptic username + snaptic username. password: - snaptic password - ''' + snaptic password. + """ self._username = username self._password = password def load_image_and_add_to_note_with_id(self, filename, id): - ''' + """ Load image from filename and append to note. - Args: + Args:: + filename: filename of image to load data from. id: id of note to which image will be appended. - ''' + """ try: fin = open(filename, 'r') data = fin.read() @@ -312,16 +339,18 @@ def load_image_and_add_to_note_with_id(self, filename, id): raise SnapticError("Error reading filename") def add_image_to_note_with_id(self, filename, data, id): - ''' + """ Add image data to note. - Args: + Args:: + filename: filename of image. data: loaded image data to be appended to note. id: id of note to which image data will be appended. + Returns: The server's response page. - ''' + """ page = "/" + self.API_VERSION + self.API_ENDPOINT_IMAGES + id +".json" return self._post_multi_part(self._url, page, [("image", filename, data)]) @@ -329,10 +358,12 @@ def _post_multi_part(self, host, selector, files): """ Post files to an http host as multipart/form-data. - Args: + Args:: + host: server to send request to selector: API endpoint to send to the server files: sequence of (name, filename, value) elements for data to be uploaded as files + Returns: Return the server's response page. """ @@ -421,7 +452,7 @@ def post_note(self, note): def _request(self, http_method, note): #Clean this up a little -htormey """ - Perform a http request on a note + Perform a http request on a note. Args: http_metod: what kind of http request is being made (i.e POST/DELETE/GET) @@ -451,24 +482,24 @@ def _request(self, http_method, note): #Clean this up a little -htormey return data def get_image_with_id(self, id): - ''' + """ Get image data associated with a given id. Args: id: id of image to be fetched. Returns: Data associated with image id. - ''' + """ url = self.API_ENDPOINT_IMAGES_VIEW + id return self._fetch_url(url) def get_user_id(self): - ''' + """ Get ID of API user. Returns: - Id of snaptic user associated with Api instance. - ''' + Id of snaptic user associated with API instance. + """ if self._user: return self._user.id else: @@ -485,19 +516,19 @@ def fget(self): return locals() def get_notes(self): - ''' + """ Get notes and update the Api's internal cache. Returns: A list of Note objects from the snaptic users account. - ''' + """ url = "/" + self.API_VERSION + self.API_ENDPOINT_NOTES_JSON json_notes = self._fetch_url(url) self._notes = self._parse_notes(json_notes) return self._notes def get_notes_from_cursor(self, cursor_position): - ''' + """ Get a batch of upto 20 notes from a given cursor position. See description given for json_cursor for further details on how cursors work with snaptic. @@ -506,33 +537,33 @@ def get_notes_from_cursor(self, cursor_position): cursor_position: cursor position to grab 20 notes from (i.e -1 is most recent 20) Returns: A list of note objects based on the contents of the users account. - ''' + """ json_notes = self.json_cursor(cursor_position) notes = self._parse_notes(json_notes) return notes def get_cursor_information(self, cursor_position): - ''' - Gets information which can be used to calculate to navigate through a users notes. See - json_cursor for further details on how cursors work with snaptic. + """ + Gets information about cursor at a given position. See json_cursor for further + details on how cursors work with snaptic. Args: cursor_position: cursor position you want to find out about. Returns: A dictionary containing previous_cursor, next_cursor and note count. - ''' + """ json_notes = self.json_cursor(cursor_position) return self._parse_cursor_info(json_notes) def _parse_cursor_info(self, source): - ''' + """ Parse cursor information with notes returned from snaptic. Args: source: A json object consisting of notes and cursor information Returns: A dictionary containing previous_cursor, next_cursor and note count. - ''' + """ cursor_info = json.loads(source) if 'next_cursor' in cursor_info and 'previous_cursor' in cursor_info and 'count' in cursor_info: return {"previous_cursor": cursor_info['previous_cursor'], "next_cursor": cursor_info['next_cursor'], "count": cursor_info['count'] } @@ -540,12 +571,12 @@ def _parse_cursor_info(self, source): SnapticError("Error keys missing from source JSON passed to _parse_cursor_info") def get_user(self): - ''' + """ Get user info. Returns: A user object. - ''' + """ url = "/" + self.API_VERSION + self.API_ENDPOINT_USER_JSON user_info = self._fetch_url(url) self._parse_user_info(user_info) @@ -553,7 +584,7 @@ def get_user(self): @Property def json(): - doc = "Json object of notes stored in account" + doc = "Json object of notes stored in account." def fget(self): if self._json: return self._json #should I return json.load(sef._json) ? -htormey @@ -562,52 +593,52 @@ def fget(self): return locals() def get_json(self): - ''' - Get json object and update the cache + """ + Get json object and update the cache. Returns: A json object representing all notes in a users account. - ''' + """ url = "/" + self.API_VERSION + self.API_ENDPOINT_NOTES_JSON self._json = self._fetch_url(url) return self._json def get_tags(self): - ''' - Fetch json object containing tags from users account + """ + Fetch json object containing tags from users account. Returns: A json object containing tags and related information (number of notes per tag, etc). - ''' + """ url = "/" + self.API_VERSION + self.API_ENDPOINT_TAGS_JSON tags = self._fetch_url(url) return tags def json_cursor(self, cursor_position): - ''' + """ Get batches of 20 notes in JSON format from a given cursor position i.e -1, 1, etc. For example: -1 returns the most recent 20 notes, 1 returns the previous 20 before that, etc. One exeption to note is that 0 returns a JSON object for all notes in a given account. Args: - cursor_position: cursor position to grab 20 notes from (i.e -1 is most recent 20) + cursor_position: cursor position to grab 20 notes from (i.e -1 is most recent 20). Returns: - A json object containing notes from cursor position requested - ''' + A json object containing notes from cursor position requested. + """ url = "/" + self.API_VERSION + self.API_ENDPOINT_NOTES_JSON + self.API_ENDPOINT_CURSOR + str(cursor_position) cursor = self._fetch_url(url) return cursor def _fetch_url(self, url): - ''' + """ Perform a basic auth request on a given snaptic API endpoint. Args: - url: Snaptic Api endpoint (i.e /v1/notes.json etc) + url: Snaptic Api endpoint (i.e /v1/notes.json etc). Returns: The server's response page. - ''' + """ handler = self._basic_auth_request(url) response = handler.getresponse() data = response.read() @@ -617,15 +648,17 @@ def _fetch_url(self, url): return data def _make_basic_auth_headers(self, username, password): - ''' + """ Encode headers for basic auth request. - Args: + Args:: + username: snaptic username to be used. password: password to be used. + Returns: Dictionary with encoded basic auth values. - ''' + """ if username and password: headers = dict(Authorization="Basic %s" %(base64.b64encode("%s:%s" %(username, password)))) @@ -634,18 +667,20 @@ def _make_basic_auth_headers(self, username, password): return headers def _basic_auth_request(self, path, method=HTTP_GET, headers={}, params={}): - ''' + """ Make a HTTP request with basic auth header and supplied method. Defaults to operating over SSL. - Args: + Args:: + path: Snaptic API endpoint metthod: which http method to use (PUT/DELETE/GET) headers: Additional header to use with request. params: Other parameters to use + Returns: The server's response page. - ''' + """ h = self._make_basic_auth_headers(self._username, self._password) h.update(headers) if self._use_ssl: @@ -662,14 +697,14 @@ def _basic_auth_request(self, path, method=HTTP_GET, headers={}, params={}): return conn def _parse_user_info(self, source): - ''' + """ Parse JSON user returned from snaptic, instantiate a User object from it. Args: source: Json object representing a user Returns: A User object. - ''' + """ user_info = json.loads(source) if 'user' in user_info: @@ -678,15 +713,17 @@ def _parse_user_info(self, source): SnapticError("Error no user key found in source JSON passed to _parse_user_info") def _parse_notes( self, source, get_image_data=False): - ''' + """ parse JSON notes returned from snaptic, instantiate a list of note objects from it. - Args: + Args:: + source: A json object representing a list of notes. get_images: if images are associated with notes, download them now. Returns: A list of note objects. - ''' + """ + notes = [] json_notes = json.loads(source) From f6f6cae4db1128b26c392f3840ee7bc271ea53a0 Mon Sep 17 00:00:00 2001 From: htormey Date: Wed, 7 Apr 2010 15:42:29 -0700 Subject: [PATCH 07/26] Further documentation related changes. --- index.txt | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 index.txt diff --git a/index.txt b/index.txt new file mode 100644 index 0000000..e18b2ce --- /dev/null +++ b/index.txt @@ -0,0 +1,24 @@ +.. Snaptic documentation master file, created by + sphinx-quickstart on Mon Apr 5 22:41:22 2010. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to Snaptic's documentation! +=================================== + +Contents: + +.. toctree:: + :maxdepth: 2 + + api + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + + + From 3a8facd10cbbe6043b6796f9c45abeb966809921 Mon Sep 17 00:00:00 2001 From: htormey Date: Wed, 7 Apr 2010 16:26:27 -0700 Subject: [PATCH 08/26] further changes to docs. --- docs/index.txt | 92 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 87 insertions(+), 5 deletions(-) diff --git a/docs/index.txt b/docs/index.txt index 2c18e00..2325d3b 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -3,18 +3,100 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -snaptic (version 0.4) +python-snaptic =================================== -A library that provies a python inferface to the Snaptic API +*A python wrapper around the Snaptic API* +Author: *Harry Tormey * + + +Introduction +================== + +This library provides a pure python interface for the Snaptic API. + +Snaptic (https://snaptic.com/) provides a service that allows people to build cross platform productivity applications to +capture, organize, and share information. Snaptic exposes a web services API (http://wiki.github.com/snaptic/docs-api/snaptic-rest-api) +which this library implements for python programmers. + +Build instructions +=================== + +**From source:** + +Install the dependencies: + +http://pypi.python.org/pypi/simplejson + +Download the latest python-snaptic library from: + +*todo: place holder, work out with NJO how we are going to do deployment of tar files. -htormey* + +Untar the source distribution and run: + +python setup.py install + +**Testing:** + +*todo: checkin unit tests for python api -htormey* + +Source code +================== + +**View the trunk here:** + +http://github.com/snaptic/python-api + +Check out the latest version of the snaptic python API:: + + git clone git@github.com:snaptic/python-api.git + cd python-snaptic + +Documentation +================== + +View the latest python-snaptic API documentation here: .. toctree:: :maxdepth: 2 -Classes + api.txt + +Usage +================== + +.. autoclass:: snaptic.Api + +Further information +==================== + +For more information on the Snaptic REST API see here: + +http://wiki.github.com/snaptic/docs-api/snaptic-rest-api + +Contributors ================== -.. automodule:: snaptic - :members: Api, Note, User, Image, SnapticError +Additional thanks to Niall O'Higgins, Casey Duncan. + +License +================== + +:: + +# Copyright (c) 2010 Harry Tormey +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + From a9305c4afbc12bb530581fde43bffe65cf1230b3 Mon Sep 17 00:00:00 2001 From: htormey Date: Wed, 7 Apr 2010 23:23:08 -0700 Subject: [PATCH 09/26] Adding basic unit tests for API. --- tests/config.ini | 9 +++++++++ tests/test_api.py | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 tests/config.ini create mode 100644 tests/test_api.py diff --git a/tests/config.ini b/tests/config.ini new file mode 100644 index 0000000..fa1d012 --- /dev/null +++ b/tests/config.ini @@ -0,0 +1,9 @@ +[api] +host = api.snaptic.com +port = 443 +# comment out the following line or set to non-True for plaintext +use_ssl = True +email = +username = +password = +timeout = 10 diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..f53681e --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,46 @@ +# backwards compatible with Python < 2.6 +try: + import json +except ImportError: + import simplejson as json +import sys + +from nose.tools import assert_equals, assert_true +from testconfig import config +import snaptic + + +def test_get_notes(): + """ + Verify that notes returns a list of notes greater than 0 from test account with notes in it. + """ + api = snaptic.Api(username=config['api']['email'], password=config['api']['password']) + n = api.notes + assert len(n) > 0 + return n + +def test_post_note(): + """ + Verify that you get back the approrpiate Json object when you post a note. + """ + api = snaptic.Api(username=config['api']['email'], password=config['api']['password']) + data_before_post = test_get_notes() + r = api.post_note("Testing 123") + data_after_post = test_get_notes() + assert_equals(len(data_before_post) +1, len(data_after_post), "Note count from API indicates note not written to backend") + return r + +def test_post_note_json_schema_valid(): + """ + Verify that Json returned by post notes service contains expected fields + """ + r = test_post_note() + data = json.loads(r) + assert_true(len(data['notes']) > 0) + note_fields = ('id', 'created_at', 'modified_at', 'reminder_at', 'text', + 'summary', 'source', 'source_url', 'user', 'children', 'tags', 'location') + for note in data['notes']: + for field in note_fields: + assert_true(field in note) + + From a71e78a90d8167fad6715e5103747603e3287f17 Mon Sep 17 00:00:00 2001 From: htormey Date: Wed, 7 Apr 2010 23:24:21 -0700 Subject: [PATCH 10/26] Adding test usage readme --- tests/README | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 tests/README diff --git a/tests/README b/tests/README new file mode 100644 index 0000000..4a869ae --- /dev/null +++ b/tests/README @@ -0,0 +1,39 @@ +REQUIREMENTS +------------ + +- Python with JSON support. Python 2.6+ has it built in, Python 2.5 and under + need 'simplejson' module. You can install simplejson with command: + 'easy_install simplejson'. + +- 'nose' test runner. http://somethingaboutorange.com/mrl/projects/nose/. + +- 'nose-testconfig' plugin. To install nose-testconfig, run command: + 'easy_install nose-testconfig'. + +TEST CONFIGURATION +------------------ + +Test configuration for things like which host to point at, usernames, +passwords, etc - are all stored in Python ConfigParser files (INI-style +format). + +Provided along with the tests are configs to run against: + +- https://snaptic.com (config.ini) + +You specify the configuration file to use via the '--tc-file' option. +You also need to add username/password/email of account you want to test +against to config.ini. + +HOW TO RUN THE TESTS +-------------------- + +To start the tests, type 'nosetests --tc-file=' in the same +directory as the test source files. More verbose output can be seen by passing +the -v flag: 'nosetests -v --tc-file='. + +E.g. to run the tests against https://snaptic-info.test you would execute: + +$ nosetests -v --tc-file=config.ini + + From fa8d1c361cd9b7210d2c4e2124d1eef5ddf9bec1 Mon Sep 17 00:00:00 2001 From: htormey Date: Thu, 8 Apr 2010 10:37:51 -0700 Subject: [PATCH 11/26] further unit tests for API. --- tests/test_api.py | 77 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 72 insertions(+), 5 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index f53681e..c871531 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -10,29 +10,96 @@ import snaptic -def test_get_notes(): +def test_notes_property(): """ - Verify that notes returns a list of notes greater than 0 from test account with notes in it. + Verify that .notes returns a list of notes greater than 0 from test account with notes in it. """ api = snaptic.Api(username=config['api']['email'], password=config['api']['password']) n = api.notes assert len(n) > 0 return n +def test_get_notes(): + """ + Verify that get_notes returns a list of notes greater than 0 from test account with notes in it. + """ + api = snaptic.Api(username=config['api']['email'], password=config['api']['password']) + n = api.get_notes() + assert len(n) > 0 + return n + def test_post_note(): """ - Verify that you get back the approrpiate Json object when you post a note. + Verify posting a note. """ api = snaptic.Api(username=config['api']['email'], password=config['api']['password']) data_before_post = test_get_notes() r = api.post_note("Testing 123") data_after_post = test_get_notes() - assert_equals(len(data_before_post) +1, len(data_after_post), "Note count from API indicates note not written to backend") + assert_equals(len(data_before_post) +1, len(data_after_post), "Note count from API indicates note not posted to backend") return r +def test_edit_note(): + """ + Verify editing a note. + """ + api = snaptic.Api(username=config['api']['email'], password=config['api']['password']) + data_before_post = test_get_notes() + r = api.post_note("Testing 123") + data_after_post = test_get_notes() + assert_equals(len(data_before_post) +1, len(data_after_post), "Note count from API indicates note not posted to backend") + #Now try editing note + test_string = "changed notes rock" + data_after_post[0].text = test_string + api.edit_note(data_after_post[0]) + data_after_edit = test_get_notes() + assert_equals(data_after_edit[0].text, test_string) + +def test_delete_note(): + """ + Verify deleting a note. + """ + api = snaptic.Api(username=config['api']['email'], password=config['api']['password']) + r = api.post_note("Testing 123") + data_after_post = test_get_notes() + api.delete_note(data_after_post[0].note_id) + data_after_delete = test_get_notes() + assert_equals(len(data_after_post) -1, len(data_after_delete), "Note count from API indicates a problem deleting note") + +def test_get_json_cursor_schema_valid(): + """ + Verify that Json returned by the get cursor service contains expected fields. + """ + api = snaptic.Api(username=config['api']['email'], password=config['api']['password']) + r = api.json_cursor(-1) #Get the first 20 note + data = json.loads(r) + envelope_fields = ('count', 'previous_cursor', 'next_cursor', 'notes') + for field in envelope_fields: + assert_true(field in data) + assert_true(len(data['notes']) > 0) + note_fields = ('id', 'created_at', 'modified_at', 'reminder_at', 'text', + 'summary', 'source', 'source_url', 'user', 'children', 'tags', 'location') + for note in data['notes']: + for field in note_fields: + assert_true(field in note) + +def test_get_json_notes_schema_valid(): + """ + Verify that Json returned by the get notes service contains expected fields. + """ + api = snaptic.Api(username=config['api']['email'], password=config['api']['password']) + r = api.get_json() + data = json.loads(r) + assert_true(len(data['notes']) > 0) + note_fields = ('id', 'created_at', 'modified_at', 'reminder_at', 'text', + 'summary', 'source', 'source_url', 'user', 'children', 'tags', 'location') + for note in data['notes']: + for field in note_fields: + assert_true(field in note) + def test_post_note_json_schema_valid(): """ - Verify that Json returned by post notes service contains expected fields + Verify that Json returned by the post notes service contains expected fields. """ r = test_post_note() data = json.loads(r) From c32fe9ddc3d912941b8a654639ec1dacec6e723f Mon Sep 17 00:00:00 2001 From: htormey Date: Thu, 8 Apr 2010 10:49:51 -0700 Subject: [PATCH 12/26] Correct some typos in testing readme, update documentation to have testing instructions it it --- docs/index.txt | 39 +++++++++++++++++++++++++++++++++++++-- tests/README | 4 ++-- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/docs/index.txt b/docs/index.txt index 2325d3b..5eed19c 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -37,9 +37,44 @@ Untar the source distribution and run: python setup.py install -**Testing:** +Testing +========== + +**Requirements** + +- Python with JSON support. Python 2.6+ has it built in, Python 2.5 and under + need 'simplejson' module. You can install simplejson with command: + 'easy_install simplejson'. + +- 'nose' test runner. http://somethingaboutorange.com/mrl/projects/nose/. + +- 'nose-testconfig' plugin. To install nose-testconfig, run command: + 'easy_install nose-testconfig'. + +**Test configuration** + +Test configuration for things like which host to point at, usernames, +passwords, etc - are all stored in Python ConfigParser files (INI-style +format). + +Provided along with the tests are configs to run against: + +- https://snaptic.com (config.ini) + +You specify the configuration file to use via the '--tc-file' option. +You also need to add username/password/email of the account you want to test +against to config.ini. + +**How to run the tests** + +To start the tests, type 'nosetests --tc-file=' in the same +directory as the test source files. More verbose output can be seen by passing +the -v flag: 'nosetests -v --tc-file='. + +E.g. to run the tests against https://api.snaptic.com you would execute: + +$ nosetests -v --tc-file=config.ini -*todo: checkin unit tests for python api -htormey* Source code ================== diff --git a/tests/README b/tests/README index 4a869ae..ef911c6 100644 --- a/tests/README +++ b/tests/README @@ -22,7 +22,7 @@ Provided along with the tests are configs to run against: - https://snaptic.com (config.ini) You specify the configuration file to use via the '--tc-file' option. -You also need to add username/password/email of account you want to test +You also need to add username/password/email of the account you want to test against to config.ini. HOW TO RUN THE TESTS @@ -32,7 +32,7 @@ To start the tests, type 'nosetests --tc-file=' in the same directory as the test source files. More verbose output can be seen by passing the -v flag: 'nosetests -v --tc-file='. -E.g. to run the tests against https://snaptic-info.test you would execute: +E.g. to run the tests against https://api.snaptic.com you would execute: $ nosetests -v --tc-file=config.ini From 4b17c1d84873a9a8f1b6def4af7734a63ff132ac Mon Sep 17 00:00:00 2001 From: htormey Date: Wed, 21 Apr 2010 21:54:56 -0700 Subject: [PATCH 13/26] Fix bugs post api changes from Andreas --- docs/index.txt | 4 ++-- snaptic.py | 43 ++++++++++--------------------------------- 2 files changed, 12 insertions(+), 35 deletions(-) diff --git a/docs/index.txt b/docs/index.txt index 5eed19c..4d395c5 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -17,7 +17,7 @@ Introduction This library provides a pure python interface for the Snaptic API. Snaptic (https://snaptic.com/) provides a service that allows people to build cross platform productivity applications to -capture, organize, and share information. Snaptic exposes a web services API (http://wiki.github.com/snaptic/docs-api/snaptic-rest-api) +capture, organize, and share information. Snaptic exposes a web services API(http://wiki.github.com/snaptic/docs-api/snaptic-api-quickstart-guide) which this library implements for python programmers. Build instructions @@ -108,7 +108,7 @@ Further information For more information on the Snaptic REST API see here: -http://wiki.github.com/snaptic/docs-api/snaptic-rest-api +http://wiki.github.com/snaptic/docs-api/snaptic-api-quickstart-guide Contributors ================== diff --git a/snaptic.py b/snaptic.py index 2fef267..7eec0ee 100644 --- a/snaptic.py +++ b/snaptic.py @@ -64,15 +64,13 @@ class User(object): user.id # read only user.user_name # read only user.created_at # read only - user.auth_token # read only user.email # read only """ - def __init__(self, id=None, user_name=None, created_at=None, auth_token=None, email=None): + def __init__(self, id=None, user_name=None, created_at=None, email=None): self._id = id self._user_name = user_name self._created_at = created_at - self._auth_token = auth_token self._email = email @property @@ -87,10 +85,6 @@ def user_name(self): def created_at(self): return self._created_at - @property - def auth_token(self): - return self._auth_token - @property def email(self): return self._email @@ -213,27 +207,10 @@ class Api(object): To post a note: >>> api.post_note("Harry says snaptic is da bomb") - { - "notes":[ - { - "created_at": "2010-04-06T22:59:12.093Z", - "modified_at": "2010-04-06T22:59:12.093Z", - "reminder_at": "", - "id": "1926387", - "text": "Harry says snaptic is da bomb", - "summary": "Harry says snaptic is da bomb", - "source": "3banana", - "source_url": "https://snaptic.com/", - "user": { - "id": "1813083", - "user_name": "harry12" - }, - "children": "0", - "labels": {}, - "tags": {}, - "location": {} - } - ]} + '{\n\n\t"notes":[\n{\n\n\t"summary":"Harry says snaptic is da + bomb",\n\t"user":{\n\n\t"user_name":"harry12",\n\t"id":1813083}\n,\n\t"created_at":"2010-04-22T04:19:16.543Z",\n\t"mode":"private",\n\t"modified_at":"2010-04-22T04:19:16.543Z",\n\t"labels":[\n]\n,\n\t"reminder_at":null,\n\t"id":2276722,\n\t"text":"Harry + says snaptic is da + bomb",\n\t"tags":[\n]\n,\n\t"source":"3banana",\n\t"location":null,\n\t"source_url":"https://snaptic.com/",\n\t"children":0}\n]\n}\n' To delete a note: @@ -351,7 +328,7 @@ def add_image_to_note_with_id(self, filename, data, id): Returns: The server's response page. """ - page = "/" + self.API_VERSION + self.API_ENDPOINT_IMAGES + id +".json" + page = "/" + self.API_VERSION + self.API_ENDPOINT_IMAGES + str(id) +".json" return self._post_multi_part(self._url, page, [("image", filename, data)]) def _post_multi_part(self, host, selector, files): @@ -464,13 +441,13 @@ def _request(self, http_method, note): #Clean this up a little -htormey if isinstance(note, Note): #Edit an existing note params = urlencode(note.dictionary) - page = "/" + self.API_VERSION + self.API_ENDPOINT_NOTES + note.note_id + '.json' + page = "/" + self.API_VERSION + self.API_ENDPOINT_NOTES + str(note.note_id) + '.json' else: params = urlencode(dict(text=note)) page = "/" + self.API_VERSION + self.API_ENDPOINT_NOTES_JSON handle = self._basic_auth_request(page, headers=headers, method=self.HTTP_POST, params=params) elif http_method == self.HTTP_DELETE: - page = "/" + self.API_VERSION + self.API_ENDPOINT_NOTES + note + page = "/" + self.API_VERSION + self.API_ENDPOINT_NOTES + str(note) handle = self._basic_auth_request(page, method=self.HTTP_DELETE) response = handle.getresponse() @@ -490,7 +467,7 @@ def get_image_with_id(self, id): Returns: Data associated with image id. """ - url = self.API_ENDPOINT_IMAGES_VIEW + id + url = self.API_ENDPOINT_IMAGES_VIEW + str(id) return self._fetch_url(url) def get_user_id(self): @@ -708,7 +685,7 @@ def _parse_user_info(self, source): user_info = json.loads(source) if 'user' in user_info: - self._user = User(user_info['user']['id'], user_info['user']['user_name'], user_info['user']['created_at'], user_info['user']['auth_token'], user_info['user']['email']) + self._user = User(user_info['user']['id'], user_info['user']['user_name'], user_info['user']['created_at'], user_info['user']['email']) else: SnapticError("Error no user key found in source JSON passed to _parse_user_info") From f75e77779147f5def6cd8dbee6ab0699168444b2 Mon Sep 17 00:00:00 2001 From: htormey Date: Wed, 21 Apr 2010 22:11:11 -0700 Subject: [PATCH 14/26] =Further docstring tweaks for sphinx --- snaptic.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/snaptic.py b/snaptic.py index 7eec0ee..e74efac 100644 --- a/snaptic.py +++ b/snaptic.py @@ -207,10 +207,25 @@ class Api(object): To post a note: >>> api.post_note("Harry says snaptic is da bomb") - '{\n\n\t"notes":[\n{\n\n\t"summary":"Harry says snaptic is da - bomb",\n\t"user":{\n\n\t"user_name":"harry12",\n\t"id":1813083}\n,\n\t"created_at":"2010-04-22T04:19:16.543Z",\n\t"mode":"private",\n\t"modified_at":"2010-04-22T04:19:16.543Z",\n\t"labels":[\n]\n,\n\t"reminder_at":null,\n\t"id":2276722,\n\t"text":"Harry - says snaptic is da - bomb",\n\t"tags":[\n]\n,\n\t"source":"3banana",\n\t"location":null,\n\t"source_url":"https://snaptic.com/",\n\t"children":0}\n]\n}\n' + { + "notes":[ + { + "summary":"Harry says snaptic is da bomb", + "user": { + "user_name":"harry12", + "id":1813083}, + "created_at":"2010-04-22T04:19:16.543Z", + "mode":"private", + "modified_at":"2010-04-22T04:19:16.543Z", + "reminder_at":null, + "id":2276722, + "text":"Harry says snaptic is da bomb", + "tags":[], + "source":"3banana", + "location":null, + "source_url":"https://snaptic.com/", + "children":0 + }]} To delete a note: From 0703b5dc7bc1fecf1e662e90b2cc60d70f959a01 Mon Sep 17 00:00:00 2001 From: Tadhg O'Higgins Date: Wed, 30 Jun 2010 15:01:04 -0700 Subject: [PATCH 15/26] Minor name and doc changes --- docs/index.txt | 46 +++++++++++++++++++++++----------------------- setup.py | 4 ++-- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/docs/index.txt b/docs/index.txt index 4d395c5..9538bab 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -16,8 +16,8 @@ Introduction This library provides a pure python interface for the Snaptic API. -Snaptic (https://snaptic.com/) provides a service that allows people to build cross platform productivity applications to -capture, organize, and share information. Snaptic exposes a web services API(http://wiki.github.com/snaptic/docs-api/snaptic-api-quickstart-guide) +Snaptic (https://snaptic.com/) provides a service that allows people to build cross-platform productivity applications to +capture, organize, and share information. Snaptic exposes a web services API (http://wiki.github.com/snaptic/docs-api/snaptic-api-quickstart-guide) which this library implements for python programmers. Build instructions @@ -25,17 +25,17 @@ Build instructions **From source:** -Install the dependencies: +The Snaptic API requires simplejson. Python 2.6+ has it built in, Python 2.5 and under +need the ``simplejson`` module. You can install simplejson with this command:: -http://pypi.python.org/pypi/simplejson + easy_install simplejson -Download the latest python-snaptic library from: +To install, check out the latest version of the snaptic python API and +run ``setup.py`` :: -*todo: place holder, work out with NJO how we are going to do deployment of tar files. -htormey* - -Untar the source distribution and run: - -python setup.py install + git clone git://github.com/snaptic/python-api.git + cd python-api + python setup.py install Testing ========== @@ -43,13 +43,13 @@ Testing **Requirements** - Python with JSON support. Python 2.6+ has it built in, Python 2.5 and under - need 'simplejson' module. You can install simplejson with command: - 'easy_install simplejson'. + need the ``simplejson`` module. You can install simplejson with the command: + ``easy_install simplejson``. -- 'nose' test runner. http://somethingaboutorange.com/mrl/projects/nose/. +- ``nose`` test runner. http://somethingaboutorange.com/mrl/projects/nose/. -- 'nose-testconfig' plugin. To install nose-testconfig, run command: - 'easy_install nose-testconfig'. +- ``nose-testconfig`` plugin. To install nose-testconfig, run the command: + ``easy_install nose-testconfig``. **Test configuration** @@ -59,21 +59,21 @@ format). Provided along with the tests are configs to run against: -- https://snaptic.com (config.ini) +- https://api.snaptic.com (config.ini) -You specify the configuration file to use via the '--tc-file' option. +You specify the configuration file to use via the ``--tc-file`` option. You also need to add username/password/email of the account you want to test against to config.ini. **How to run the tests** -To start the tests, type 'nosetests --tc-file=' in the same +To start the tests, type ``nosetests --tc-file=`` in the same directory as the test source files. More verbose output can be seen by passing -the -v flag: 'nosetests -v --tc-file='. +the -v flag: ``nosetests -v --tc-file=``. -E.g. to run the tests against https://api.snaptic.com you would execute: +E.g. to run the tests against https://api.snaptic.com you would execute:: -$ nosetests -v --tc-file=config.ini + $ nosetests -v --tc-file=config.ini Source code @@ -85,8 +85,8 @@ http://github.com/snaptic/python-api Check out the latest version of the snaptic python API:: - git clone git@github.com:snaptic/python-api.git - cd python-snaptic + git clone git://github.com/snaptic/python-api.git + cd python-api Documentation ================== diff --git a/setup.py b/setup.py index 0cc50c0..bd16d48 100644 --- a/setup.py +++ b/setup.py @@ -16,11 +16,11 @@ '''The setup and build script for the python-snaptic library.''' __author__ = 'harry@snaptic.com' -__version__ = '0.4-devel' +__version__ = '0.4' METADATA = dict( - name = "python-snaptic", + name = "py-snaptic", version = __version__, py_modules = ['snaptic'], author='Harry Tormey', From 606e59a316bcaca08b8d688b1a003f9caee975af Mon Sep 17 00:00:00 2001 From: Tadhg O'Higgins Date: Thu, 1 Jul 2010 12:27:33 -0700 Subject: [PATCH 16/26] Add cookie auth to python-api; make tests respect host parameter in config. --- snaptic.py | 60 ++++++++++++++++++++++++++++++++++++++++------- tests/test_api.py | 40 +++++++++++++++++++++++++------ 2 files changed, 85 insertions(+), 15 deletions(-) diff --git a/snaptic.py b/snaptic.py index e74efac..0bbf681 100644 --- a/snaptic.py +++ b/snaptic.py @@ -282,7 +282,8 @@ class Api(object): API_ENDPOINT_USER_JSON = "/user.json" API_ENDPOINT_CURSOR = "?cursor=" - def __init__(self, username, password=None, url=API_SERVER, use_ssl=True, port=443, timeout=10): + def __init__(self, username=None, password=None, url=API_SERVER, + use_ssl=True, port=443, timeout=10, cookie_epass=None): """ Args: username: The username of the snaptic account. @@ -299,20 +300,31 @@ def __init__(self, username, password=None, url=API_SERVER, use_ssl=True, port=4 self._user = None self._notes = None self._json = None - self.set_credentials(username, password) + if cookie_epass: + self.set_credentials(cookie_epass=cookie_epass) + else: + self.set_credentials(username=username, password=password) - def set_credentials(self, username, password): + def set_credentials(self, username=None, password=None, cookie_epass=None): """ - Set username/password + Set username/password or cookie. Args: username: snaptic username. password: snaptic password. + cookie_epass: + snaptic authentication cookie """ - self._username = username - self._password = password + if username and password: + self._username = username + self._password = password + elif cookie_epass: + self._cookie_epass = cookie_epass + else: + raise SnapticError("No username/password combination\ + or cookie authentication provided") def load_image_and_add_to_note_with_id(self, filename, id): """ @@ -361,7 +373,7 @@ def _post_multi_part(self, host, selector, files): """ content_type, body = self._encode_multi_part_form_data(files) handler = httplib.HTTPConnection(host) - headers = self._make_basic_auth_headers(self._username, self._password) + headers = self._get_auth_headers() h = { 'User-Agent': 'INSERT USERAGENTNAME',#Change this to library version? -htormey 'Content-Type': content_type @@ -639,6 +651,19 @@ def _fetch_url(self, url): raise SnapticError("Http error", response.status, data) return data + def _get_auth_headers(self): + """ + Switch between basic auth and cookie auth depending on which properties + self has. + """ + if hasattr(self, "_username") and hasattr(self, "_password"): + return self._make_basic_auth_headers(self._username, self._password) + elif hasattr(self, "_cookie_epass"): + return self._make_cookie_auth_headers(self._cookie_epass) + else: + raise SnapticError("No username/password combination\ + or cookie authentication provided") + def _make_basic_auth_headers(self, username, password): """ Encode headers for basic auth request. @@ -658,6 +683,25 @@ def _make_basic_auth_headers(self, username, password): raise SnapticError("Error making basic auth headers with username: %s, password: %s" % (username, password)) return headers + def _make_cookie_auth_headers(self, cookie_epass): + """ + Encode headers for cookie auth request. + + Args:: + + cookie_epass: cookie auth token to be used. + + Returns: + Dictionary with encoded basic auth values. + """ + if cookie_epass: + return { + "Cookie": "cookie_epass={0}".format(cookie_epass) + } + else: + raise SnapticError("Error making cookie auth headers with\ + cookie:{0}".format(cookie_epass)) + def _basic_auth_request(self, path, method=HTTP_GET, headers={}, params={}): """ Make a HTTP request with basic auth header and supplied method. @@ -673,7 +717,7 @@ def _basic_auth_request(self, path, method=HTTP_GET, headers={}, params={}): Returns: The server's response page. """ - h = self._make_basic_auth_headers(self._username, self._password) + h = self._get_auth_headers() h.update(headers) if self._use_ssl: handler = httplib.HTTPSConnection diff --git a/tests/test_api.py b/tests/test_api.py index c871531..be86c36 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -14,7 +14,9 @@ def test_notes_property(): """ Verify that .notes returns a list of notes greater than 0 from test account with notes in it. """ - api = snaptic.Api(username=config['api']['email'], password=config['api']['password']) + cfg = config["api"] + api = snaptic.Api(username=cfg['email'], password=cfg['password'], + url=cfg["host"]) n = api.notes assert len(n) > 0 return n @@ -23,7 +25,9 @@ def test_get_notes(): """ Verify that get_notes returns a list of notes greater than 0 from test account with notes in it. """ - api = snaptic.Api(username=config['api']['email'], password=config['api']['password']) + cfg = config["api"] + api = snaptic.Api(username=cfg['email'], password=cfg['password'], + url=cfg["host"]) n = api.get_notes() assert len(n) > 0 return n @@ -32,18 +36,34 @@ def test_post_note(): """ Verify posting a note. """ - api = snaptic.Api(username=config['api']['email'], password=config['api']['password']) + cfg = config["api"] + api = snaptic.Api(username=cfg['email'], password=cfg['password'], + url=cfg["host"]) data_before_post = test_get_notes() r = api.post_note("Testing 123") data_after_post = test_get_notes() assert_equals(len(data_before_post) +1, len(data_after_post), "Note count from API indicates note not posted to backend") return r +def test_post_note_cookie(): + """ + Verify posting a note using cookie authentication. + """ + cfg = config["api"] + api = snaptic.Api(url=cfg["host"], cookie_epass=cfg["cookie_epass"]) + data_before_post = test_get_notes() + r = api.post_note("Testing 123 cookie") + data_after_post = test_get_notes() + assert_equals(len(data_before_post) +1, len(data_after_post), "Note count from API indicates note not posted to backend") + return r + def test_edit_note(): """ Verify editing a note. """ - api = snaptic.Api(username=config['api']['email'], password=config['api']['password']) + cfg = config["api"] + api = snaptic.Api(username=cfg['email'], password=cfg['password'], + url=cfg["host"]) data_before_post = test_get_notes() r = api.post_note("Testing 123") data_after_post = test_get_notes() @@ -59,7 +79,9 @@ def test_delete_note(): """ Verify deleting a note. """ - api = snaptic.Api(username=config['api']['email'], password=config['api']['password']) + cfg = config["api"] + api = snaptic.Api(username=cfg['email'], password=cfg['password'], + url=cfg["host"]) r = api.post_note("Testing 123") data_after_post = test_get_notes() api.delete_note(data_after_post[0].note_id) @@ -70,7 +92,9 @@ def test_get_json_cursor_schema_valid(): """ Verify that Json returned by the get cursor service contains expected fields. """ - api = snaptic.Api(username=config['api']['email'], password=config['api']['password']) + cfg = config["api"] + api = snaptic.Api(username=cfg['email'], password=cfg['password'], + url=cfg["host"]) r = api.json_cursor(-1) #Get the first 20 note data = json.loads(r) envelope_fields = ('count', 'previous_cursor', 'next_cursor', 'notes') @@ -87,7 +111,9 @@ def test_get_json_notes_schema_valid(): """ Verify that Json returned by the get notes service contains expected fields. """ - api = snaptic.Api(username=config['api']['email'], password=config['api']['password']) + cfg = config["api"] + api = snaptic.Api(username=cfg['email'], password=cfg['password'], + url=cfg["host"]) r = api.get_json() data = json.loads(r) assert_true(len(data['notes']) > 0) From 73f8db3581d8c323dd6c404f4ffc024044a2aeb5 Mon Sep 17 00:00:00 2001 From: Gergely Imreh Date: Tue, 19 Oct 2010 14:09:57 +0800 Subject: [PATCH 17/26] load note tags renamed labels to tags as per website and JSON data, and get them from response Signed-off-by: Gergely Imreh --- snaptic.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/snaptic.py b/snaptic.py index 0bbf681..b9019aa 100644 --- a/snaptic.py +++ b/snaptic.py @@ -132,14 +132,14 @@ class Note(object): note.user note.children note.media - note.labels + note.tags note.location note.has_media # read only note.dictionary # read only """ def __init__(self, created_at, modified_at, reminder_at, note_id, text, - summary, source, source_url, user, children, media = [], labels = [], location = []): + summary, source, source_url, user, children, media = [], tags = [], location = []): self.created_at = created_at self.modified_at = modified_at self.reminder_at = reminder_at @@ -151,7 +151,7 @@ def __init__(self, created_at, modified_at, reminder_at, note_id, text, self.user = user self.children = children self.media = media - self.labels = labels + self.tags = tags self.location = location @property @@ -766,7 +766,7 @@ def _parse_notes( self, source, get_image_data=False): for note in json_notes['notes']: media = [] location = [] - labels = [] + tags = [] user = None source = None @@ -778,10 +778,9 @@ def _parse_notes( self, source, get_image_data=False): user = self._user.id if 'location' in note: pass - if 'labels' in note: - labels = [] - for label in note['labels']: - labels.append(label) + if 'tags' in note: + for tag in note['tags']: + tags.append(tag) if 'media' in note: for item in note['media']: if item['type'] == 'image': @@ -791,5 +790,5 @@ def _parse_notes( self, source, get_image_data=False): media.append(Image(item['type'], item['md5'], item['id'], item['revision_id'], item['width'], item['height'], item['src'], image_data)) notes.append(Note(note['created_at'], note['modified_at'], note['reminder_at'], note['id'], note['text'], note['summary'], note['source'], - note['source_url'], user, note['children'], media, labels, location)) + note['source_url'], user, note['children'], media, tags, location)) return notes From eeec42cf713ef50341fb9274ebca066962136b49 Mon Sep 17 00:00:00 2001 From: Gergely Imreh Date: Tue, 19 Oct 2010 14:18:04 +0800 Subject: [PATCH 18/26] API does not return md5 for images so don't check Signed-off-by: Gergely Imreh --- snaptic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snaptic.py b/snaptic.py index b9019aa..6a189e5 100644 --- a/snaptic.py +++ b/snaptic.py @@ -787,7 +787,7 @@ def _parse_notes( self, source, get_image_data=False): image_data = None if get_image_data: image_data = self._fetch_url(item['src']) - media.append(Image(item['type'], item['md5'], item['id'], item['revision_id'], item['width'], item['height'], item['src'], image_data)) + media.append(Image(item['type'], None, item['id'], item['revision_id'], item['width'], item['height'], item['src'], image_data)) notes.append(Note(note['created_at'], note['modified_at'], note['reminder_at'], note['id'], note['text'], note['summary'], note['source'], note['source_url'], user, note['children'], media, tags, location)) From 9482731f45161a8bdb3599a84520aa95d563f60b Mon Sep 17 00:00:00 2001 From: arielbackenroth Date: Thu, 5 May 2011 10:22:25 -0700 Subject: [PATCH 19/26] removing nose dependency, rebranding from snaptic to catch, minor cleanup --- .gitignore | 4 + snaptic.py => catchapi/__init__.py | 117 ++++++++++++------------ setup.py | 93 +++++++------------ tests/README | 39 -------- tests/config.ini | 9 -- tests/test_api.py | 139 ----------------------------- 6 files changed, 96 insertions(+), 305 deletions(-) create mode 100644 .gitignore rename snaptic.py => catchapi/__init__.py (86%) delete mode 100644 tests/README delete mode 100644 tests/config.ini delete mode 100644 tests/test_api.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c65acff --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.egg-info +_install +*.pyc + diff --git a/snaptic.py b/catchapi/__init__.py similarity index 86% rename from snaptic.py rename to catchapi/__init__.py index 6a189e5..cf5af6e 100644 --- a/snaptic.py +++ b/catchapi/__init__.py @@ -1,22 +1,21 @@ -# Copyright (c) 2010 Harry Tormey +# Copyright 2011 Catch.com, Inc. # -# Permission to use, copy, modify, and distribute this software for any -# purpose with or without fee is hereby granted, provided that the above -# copyright notice and this permission notice appear in all copies. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. -'''A python interface to the Snaptic API''' +'''A python interface to the Catch API''' -__author__ = 'harry@snaptic.com' -__version__ = '0.4-devel' +__author__ = 'ariel@catch.com' +__version__ = '0.5' import mimetypes import base64 @@ -30,15 +29,15 @@ def Property(func): return property(**func()) -class SnapticError(Exception): +class CatchError(Exception): """ - Base class for Snaptic errors. + Base class for Catch errors. The SnaptiError class exposes the following properties:: - snaptic_error.message # read only - snaptic_error.status # read only - snaptic_error.response # read only + catch_error.message # read only + catch_error.status # read only + catch_error.response # read only """ @property @@ -58,7 +57,7 @@ def response(self): class User(object): """ - A class representing the User structure used by the Snaptic API. + A class representing the User structure used by the Catch API. The User class exposes the following properties:: user.id # read only @@ -92,7 +91,7 @@ def email(self): #Perhaps I should refactor this into a class hierarchy and subclass for image/sound/etc? -htormey class Image(object): """ - A class representing the Image structure which is an attribute of a note retruned via the Snaptic API. + A class representing the Image structure which is an attribute of a note retruned via the Catch API. The Image structure exposes the following properties:: @@ -117,7 +116,7 @@ def __init__(self, type="image", md5=None, id=None, revision_id=None, width=0, h class Note(object): """ - A class representing the Note structure used by the Snaptic API. + A class representing the Note structure used by the Catch API. The Note structure exposes the following properties:: @@ -179,10 +178,10 @@ class Api(object): """ Example usage: - To create an instance of the snaptic.Api class with basic authentication: + To create an instance of the catch.Api class with basic authentication: - >>> import snaptic - >>> api = snaptic.Api("username", "password") + >>> import catchapi + >>> api = catch.Api("username", "password") To fetch all users notes and print an attribute: @@ -194,7 +193,7 @@ class Api(object): To fetch a subset of a users notes use a cursor. To get the first 20 notes and print an attribute: >>> [n.text for n in api.get_notes_from_cursor(-1)] - ['Harry says snaptic is da bomb #food #ice', 'Harry says snaptic is da bomb #food #ice', 'Harry says snaptic is da bomb', 'Harry says snaptic is da bomb', 'post number 99', 'post number 98', + ['Harry says catch is da bomb #food #ice', 'Harry says catch is da bomb #food #ice', 'Harry says catch is da bomb', 'Harry says catch is da bomb', 'post number 99', 'post number 98', 'post number 97', 'post number 96', 'post number 95', 'post number 94', 'post number 93', 'post number 92', 'post number 91', 'post number 90', 'post number 89', 'post number 88', 'post number 87', 'post number 86', 'post number 85', 'post number 84'] @@ -206,11 +205,11 @@ class Api(object): To post a note: - >>> api.post_note("Harry says snaptic is da bomb") + >>> api.post_note("Harry says catch is da bomb") { "notes":[ { - "summary":"Harry says snaptic is da bomb", + "summary":"Harry says catch is da bomb", "user": { "user_name":"harry12", "id":1813083}, @@ -219,11 +218,11 @@ class Api(object): "modified_at":"2010-04-22T04:19:16.543Z", "reminder_at":null, "id":2276722, - "text":"Harry says snaptic is da bomb", + "text":"Harry says catch is da bomb", "tags":[], "source":"3banana", "location":null, - "source_url":"https://snaptic.com/", + "source_url":"https://catch.com/", "children":0 }]} @@ -269,7 +268,7 @@ class Api(object): ]} """ - API_SERVER = "api.snaptic.com" + API_SERVER = "api.catch.com" API_VERSION = "v1" HTTP_GET = "GET" HTTP_POST = "POST" @@ -286,8 +285,8 @@ def __init__(self, username=None, password=None, url=API_SERVER, use_ssl=True, port=443, timeout=10, cookie_epass=None): """ Args: - username: The username of the snaptic account. - password: The password of the snaptic account. + username: The username of the catch account. + password: The password of the catch account. url: The url of the api server which will handle the http(s) API requests. use_ssl: Use ssl for basic auth or not. port: The port to make http(s) requests on. @@ -311,11 +310,11 @@ def set_credentials(self, username=None, password=None, cookie_epass=None): Args: username: - snaptic username. + catch username. password: - snaptic password. + catch password. cookie_epass: - snaptic authentication cookie + catch authentication cookie """ if username and password: self._username = username @@ -323,7 +322,7 @@ def set_credentials(self, username=None, password=None, cookie_epass=None): elif cookie_epass: self._cookie_epass = cookie_epass else: - raise SnapticError("No username/password combination\ + raise CatchError("No username/password combination\ or cookie authentication provided") def load_image_and_add_to_note_with_id(self, filename, id): @@ -340,7 +339,7 @@ def load_image_and_add_to_note_with_id(self, filename, id): data = fin.read() self.add_image_to_note_with_id(filename, data, id) except IOError: - raise SnapticError("Error reading filename") + raise CatchError("Error reading filename") def add_image_to_note_with_id(self, filename, data, id): """ @@ -384,7 +383,7 @@ def _post_multi_part(self, host, selector, files): data = response.read() handler.close() if response.status != 200: - raise SnapticError("Error posting files ", response.status, data) + raise CatchError("Error posting files ", response.status, data) def _encode_multi_part_form_data(self, files): """ @@ -482,7 +481,7 @@ def _request(self, http_method, note): #Clean this up a little -htormey handle.close() if response.status != 200: - raise SnapticError("Http error posting/editing/deleting note ", response.status, data) + raise CatchError("Http error posting/editing/deleting note ", response.status, data) return data def get_image_with_id(self, id): @@ -502,12 +501,12 @@ def get_user_id(self): Get ID of API user. Returns: - Id of snaptic user associated with API instance. + Id of catch user associated with API instance. """ if self._user: return self._user.id else: - raise SnapticError("Error user id not set, try calling GetNotes.") + raise CatchError("Error user id not set, try calling GetNotes.") @Property def notes(): @@ -524,7 +523,7 @@ def get_notes(self): Get notes and update the Api's internal cache. Returns: - A list of Note objects from the snaptic users account. + A list of Note objects from the catch users account. """ url = "/" + self.API_VERSION + self.API_ENDPOINT_NOTES_JSON json_notes = self._fetch_url(url) @@ -535,7 +534,7 @@ def get_notes_from_cursor(self, cursor_position): """ Get a batch of upto 20 notes from a given cursor position. See description given for json_cursor for further details on how - cursors work with snaptic. + cursors work with catch. Args: cursor_position: cursor position to grab 20 notes from (i.e -1 is most recent 20) @@ -549,7 +548,7 @@ def get_notes_from_cursor(self, cursor_position): def get_cursor_information(self, cursor_position): """ Gets information about cursor at a given position. See json_cursor for further - details on how cursors work with snaptic. + details on how cursors work with catch. Args: cursor_position: cursor position you want to find out about. @@ -561,7 +560,7 @@ def get_cursor_information(self, cursor_position): def _parse_cursor_info(self, source): """ - Parse cursor information with notes returned from snaptic. + Parse cursor information with notes returned from catch. Args: source: A json object consisting of notes and cursor information @@ -572,7 +571,7 @@ def _parse_cursor_info(self, source): if 'next_cursor' in cursor_info and 'previous_cursor' in cursor_info and 'count' in cursor_info: return {"previous_cursor": cursor_info['previous_cursor'], "next_cursor": cursor_info['next_cursor'], "count": cursor_info['count'] } else: - SnapticError("Error keys missing from source JSON passed to _parse_cursor_info") + CatchError("Error keys missing from source JSON passed to _parse_cursor_info") def get_user(self): """ @@ -636,10 +635,10 @@ def json_cursor(self, cursor_position): def _fetch_url(self, url): """ - Perform a basic auth request on a given snaptic API endpoint. + Perform a basic auth request on a given catch API endpoint. Args: - url: Snaptic Api endpoint (i.e /v1/notes.json etc). + url: Catch Api endpoint (i.e /v1/notes.json etc). Returns: The server's response page. """ @@ -648,7 +647,7 @@ def _fetch_url(self, url): data = response.read() handler.close() if response.status != 200: - raise SnapticError("Http error", response.status, data) + raise CatchError("Http error", response.status, data) return data def _get_auth_headers(self): @@ -661,7 +660,7 @@ def _get_auth_headers(self): elif hasattr(self, "_cookie_epass"): return self._make_cookie_auth_headers(self._cookie_epass) else: - raise SnapticError("No username/password combination\ + raise CatchError("No username/password combination\ or cookie authentication provided") def _make_basic_auth_headers(self, username, password): @@ -670,7 +669,7 @@ def _make_basic_auth_headers(self, username, password): Args:: - username: snaptic username to be used. + username: catch username to be used. password: password to be used. Returns: @@ -680,7 +679,7 @@ def _make_basic_auth_headers(self, username, password): headers = dict(Authorization="Basic %s" %(base64.b64encode("%s:%s" %(username, password)))) else: - raise SnapticError("Error making basic auth headers with username: %s, password: %s" % (username, password)) + raise CatchError("Error making basic auth headers with username: %s, password: %s" % (username, password)) return headers def _make_cookie_auth_headers(self, cookie_epass): @@ -699,7 +698,7 @@ def _make_cookie_auth_headers(self, cookie_epass): "Cookie": "cookie_epass={0}".format(cookie_epass) } else: - raise SnapticError("Error making cookie auth headers with\ + raise CatchError("Error making cookie auth headers with\ cookie:{0}".format(cookie_epass)) def _basic_auth_request(self, path, method=HTTP_GET, headers={}, params={}): @@ -709,7 +708,7 @@ def _basic_auth_request(self, path, method=HTTP_GET, headers={}, params={}): Args:: - path: Snaptic API endpoint + path: Catch API endpoint metthod: which http method to use (PUT/DELETE/GET) headers: Additional header to use with request. params: Other parameters to use @@ -734,7 +733,7 @@ def _basic_auth_request(self, path, method=HTTP_GET, headers={}, params={}): def _parse_user_info(self, source): """ - Parse JSON user returned from snaptic, instantiate a User object from it. + Parse JSON user returned from catch, instantiate a User object from it. Args: source: Json object representing a user @@ -746,11 +745,11 @@ def _parse_user_info(self, source): if 'user' in user_info: self._user = User(user_info['user']['id'], user_info['user']['user_name'], user_info['user']['created_at'], user_info['user']['email']) else: - SnapticError("Error no user key found in source JSON passed to _parse_user_info") + raise CatchError("Error no user key found in source JSON passed to _parse_user_info") def _parse_notes( self, source, get_image_data=False): """ - parse JSON notes returned from snaptic, instantiate a list of note objects from it. + parse JSON notes returned from catch, instantiate a list of note objects from it. Args:: diff --git a/setup.py b/setup.py index bd16d48..500bc50 100644 --- a/setup.py +++ b/setup.py @@ -1,62 +1,37 @@ -# Copyright (c) 2010 Harry Tormey +# Copyright 2011 Catch.com, Inc. # -# Permission to use, copy, modify, and distribute this software for any -# purpose with or without fee is hereby granted, provided that the above -# copyright notice and this permission notice appear in all copies. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - - -'''The setup and build script for the python-snaptic library.''' - -__author__ = 'harry@snaptic.com' -__version__ = '0.4' - - -METADATA = dict( - name = "py-snaptic", - version = __version__, - py_modules = ['snaptic'], - author='Harry Tormey', - author_email='harry@snaptic.com', - description='A python wrapper around the Snaptic API', - license=' MIT License', - url='http://github.com/snaptic/python-api', - keywords='snaptic api', -) - -# Extra package metadata to be used only if setuptools is installed -SETUPTOOLS_METADATA = dict( - install_requires = ['setuptools', 'simplejson'], - include_package_data = True, - classifiers = [ - 'Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Topic :: Software Development :: Libraries :: Python Modules', - 'Topic :: Internet', - ], -) - -def Main(): - - # Use setuptools if available, otherwise fallback and use distutils - try: - import setuptools - METADATA.update(SETUPTOOLS_METADATA) - setuptools.setup(**METADATA) - except ImportError: - import distutils.core - distutils.core.setup(**METADATA) - - -if __name__ == '__main__': - Main() - +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from setuptools import setup + +__version__ = "0.5" +setup(install_requires=('simplejson',), + include_package_data=True, + name='py-catchapi', + version=__version__, + author="Ariel Backenroth", + author_email="ariel@catch.com", + description='A python wrapper around the Catch API', + license='Apache License', + url='http://github.com/catch/python-api', + keywords='catch snaptic api', + test_suite="test_catchapi", + packages=('catchapi',), + classifiers = [ + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Topic :: Software Development :: Libraries :: Python Modules', + 'Topic :: Internet', + ]) diff --git a/tests/README b/tests/README deleted file mode 100644 index ef911c6..0000000 --- a/tests/README +++ /dev/null @@ -1,39 +0,0 @@ -REQUIREMENTS ------------- - -- Python with JSON support. Python 2.6+ has it built in, Python 2.5 and under - need 'simplejson' module. You can install simplejson with command: - 'easy_install simplejson'. - -- 'nose' test runner. http://somethingaboutorange.com/mrl/projects/nose/. - -- 'nose-testconfig' plugin. To install nose-testconfig, run command: - 'easy_install nose-testconfig'. - -TEST CONFIGURATION ------------------- - -Test configuration for things like which host to point at, usernames, -passwords, etc - are all stored in Python ConfigParser files (INI-style -format). - -Provided along with the tests are configs to run against: - -- https://snaptic.com (config.ini) - -You specify the configuration file to use via the '--tc-file' option. -You also need to add username/password/email of the account you want to test -against to config.ini. - -HOW TO RUN THE TESTS --------------------- - -To start the tests, type 'nosetests --tc-file=' in the same -directory as the test source files. More verbose output can be seen by passing -the -v flag: 'nosetests -v --tc-file='. - -E.g. to run the tests against https://api.snaptic.com you would execute: - -$ nosetests -v --tc-file=config.ini - - diff --git a/tests/config.ini b/tests/config.ini deleted file mode 100644 index fa1d012..0000000 --- a/tests/config.ini +++ /dev/null @@ -1,9 +0,0 @@ -[api] -host = api.snaptic.com -port = 443 -# comment out the following line or set to non-True for plaintext -use_ssl = True -email = -username = -password = -timeout = 10 diff --git a/tests/test_api.py b/tests/test_api.py deleted file mode 100644 index be86c36..0000000 --- a/tests/test_api.py +++ /dev/null @@ -1,139 +0,0 @@ -# backwards compatible with Python < 2.6 -try: - import json -except ImportError: - import simplejson as json -import sys - -from nose.tools import assert_equals, assert_true -from testconfig import config -import snaptic - - -def test_notes_property(): - """ - Verify that .notes returns a list of notes greater than 0 from test account with notes in it. - """ - cfg = config["api"] - api = snaptic.Api(username=cfg['email'], password=cfg['password'], - url=cfg["host"]) - n = api.notes - assert len(n) > 0 - return n - -def test_get_notes(): - """ - Verify that get_notes returns a list of notes greater than 0 from test account with notes in it. - """ - cfg = config["api"] - api = snaptic.Api(username=cfg['email'], password=cfg['password'], - url=cfg["host"]) - n = api.get_notes() - assert len(n) > 0 - return n - -def test_post_note(): - """ - Verify posting a note. - """ - cfg = config["api"] - api = snaptic.Api(username=cfg['email'], password=cfg['password'], - url=cfg["host"]) - data_before_post = test_get_notes() - r = api.post_note("Testing 123") - data_after_post = test_get_notes() - assert_equals(len(data_before_post) +1, len(data_after_post), "Note count from API indicates note not posted to backend") - return r - -def test_post_note_cookie(): - """ - Verify posting a note using cookie authentication. - """ - cfg = config["api"] - api = snaptic.Api(url=cfg["host"], cookie_epass=cfg["cookie_epass"]) - data_before_post = test_get_notes() - r = api.post_note("Testing 123 cookie") - data_after_post = test_get_notes() - assert_equals(len(data_before_post) +1, len(data_after_post), "Note count from API indicates note not posted to backend") - return r - -def test_edit_note(): - """ - Verify editing a note. - """ - cfg = config["api"] - api = snaptic.Api(username=cfg['email'], password=cfg['password'], - url=cfg["host"]) - data_before_post = test_get_notes() - r = api.post_note("Testing 123") - data_after_post = test_get_notes() - assert_equals(len(data_before_post) +1, len(data_after_post), "Note count from API indicates note not posted to backend") - #Now try editing note - test_string = "changed notes rock" - data_after_post[0].text = test_string - api.edit_note(data_after_post[0]) - data_after_edit = test_get_notes() - assert_equals(data_after_edit[0].text, test_string) - -def test_delete_note(): - """ - Verify deleting a note. - """ - cfg = config["api"] - api = snaptic.Api(username=cfg['email'], password=cfg['password'], - url=cfg["host"]) - r = api.post_note("Testing 123") - data_after_post = test_get_notes() - api.delete_note(data_after_post[0].note_id) - data_after_delete = test_get_notes() - assert_equals(len(data_after_post) -1, len(data_after_delete), "Note count from API indicates a problem deleting note") - -def test_get_json_cursor_schema_valid(): - """ - Verify that Json returned by the get cursor service contains expected fields. - """ - cfg = config["api"] - api = snaptic.Api(username=cfg['email'], password=cfg['password'], - url=cfg["host"]) - r = api.json_cursor(-1) #Get the first 20 note - data = json.loads(r) - envelope_fields = ('count', 'previous_cursor', 'next_cursor', 'notes') - for field in envelope_fields: - assert_true(field in data) - assert_true(len(data['notes']) > 0) - note_fields = ('id', 'created_at', 'modified_at', 'reminder_at', 'text', - 'summary', 'source', 'source_url', 'user', 'children', 'tags', 'location') - for note in data['notes']: - for field in note_fields: - assert_true(field in note) - -def test_get_json_notes_schema_valid(): - """ - Verify that Json returned by the get notes service contains expected fields. - """ - cfg = config["api"] - api = snaptic.Api(username=cfg['email'], password=cfg['password'], - url=cfg["host"]) - r = api.get_json() - data = json.loads(r) - assert_true(len(data['notes']) > 0) - note_fields = ('id', 'created_at', 'modified_at', 'reminder_at', 'text', - 'summary', 'source', 'source_url', 'user', 'children', 'tags', 'location') - for note in data['notes']: - for field in note_fields: - assert_true(field in note) - -def test_post_note_json_schema_valid(): - """ - Verify that Json returned by the post notes service contains expected fields. - """ - r = test_post_note() - data = json.loads(r) - assert_true(len(data['notes']) > 0) - note_fields = ('id', 'created_at', 'modified_at', 'reminder_at', 'text', - 'summary', 'source', 'source_url', 'user', 'children', 'tags', 'location') - for note in data['notes']: - for field in note_fields: - assert_true(field in note) - - From 25c3e0b0a6ceb7df1213f9b4dbab519511ae26aa Mon Sep 17 00:00:00 2001 From: arielbackenroth Date: Thu, 5 May 2011 10:56:11 -0700 Subject: [PATCH 20/26] more cleanup and refactoring, beginning to refactor to move to v2 apis --- catchapi/__init__.py | 151 +++++++++++++++++++------------------------ 1 file changed, 66 insertions(+), 85 deletions(-) diff --git a/catchapi/__init__.py b/catchapi/__init__.py index cf5af6e..20c5c31 100644 --- a/catchapi/__init__.py +++ b/catchapi/__init__.py @@ -26,34 +26,31 @@ from urllib import urlencode import urlparse -def Property(func): - return property(**func()) - class CatchError(Exception): - """ - Base class for Catch errors. + """ + Base class for Catch errors. - The SnaptiError class exposes the following properties:: + The SnaptiError class exposes the following properties:: - catch_error.message # read only - catch_error.status # read only - catch_error.response # read only - """ + catch_error.message # read only + catch_error.status # read only + catch_error.response # read only + """ - @property - def message(self): - """Returns the first argument used to construct this error.""" - return self.args[0] + @property + def message(self): + """Returns the first argument used to construct this error.""" + return self.args[0] - @property - def status(self): - """Returns the HTTP status code used to construct this error.""" - return self.args[1] + @property + def status(self): + """Returns the HTTP status code used to construct this error.""" + return self.args[1] - @property - def response(self): - """Returns HTTP response body used to construct this error.""" - return self.args[2] + @property + def response(self): + """Returns HTTP response body used to construct this error.""" + return self.args[2] class User(object): """ @@ -168,7 +165,7 @@ def dictionary(self): """ Returns text from the note packaged as a dictionary. - Returns: + Returns: A dictionary containing selected attributes from the note. """ #Working on adding dates/location/media and other fields to this dictionary. Right now you can just update text. -htormey @@ -186,22 +183,17 @@ class Api(object): To fetch all users notes and print an attribute: >>> [n.created_at for n in api.notes] - ['2010-03-08T17:49:08.850Z', '2010-03-06T20:02:32.501Z', '2010-03-06T01:35:14.851Z', '2010-03-05T04:13:00.616Z', '2010-03-01T00:09:38.566Z', '2010-02-18T04:09:55.471Z', '2010-02-18T02:26:35.990Z', - '2010-02-12T23:28:22.612Z', '2010-02-10T03:06:50.590Z', '2010-02-10T06:02:57.068Z', '2010-02-08T05:14:07.000Z', '2010-02-08T02:28:20.391Z', '2010-02-05T06:57:54.323Z', '2010-02-07T07:26:34.469Z', - '2010-01-25T02:11:24.075Z', '2010-01-24T23:37:07.411Z'] + ['2010-03-08T17:49:08.850Z', '2010-03-06T20:02:32.501Z', ...] To fetch a subset of a users notes use a cursor. To get the first 20 notes and print an attribute: >>> [n.text for n in api.get_notes_from_cursor(-1)] - ['Harry says catch is da bomb #food #ice', 'Harry says catch is da bomb #food #ice', 'Harry says catch is da bomb', 'Harry says catch is da bomb', 'post number 99', 'post number 98', - 'post number 97', 'post number 96', 'post number 95', 'post number 94', 'post number 93', 'post number 92', 'post number 91', 'post number 90', 'post number 89', 'post number 88', 'post number 87', - 'post number 86', 'post number 85', 'post number 84'] + ['Harry says catch is da bomb #food #ice', 'Harry says catch is da bomb #food #ice', ...] To get the next 20 notes use cursor 1 (cursor 0 returns all notes in a users account): >>> [n.text for n in api.get_notes_from_cursor(1)] - ['post number 83', 'post number 82', 'post number 81', 'post number 80', 'post number 79', 'post number 78', 'post number 77', 'post number 76', 'post number 75', 'post number 74', 'post number 73', - 'post number 72', 'post number 71', post number 70', 'post number 69', 'post number 68', 'post number 67', 'post number 66', 'post number 65', 'post number 64'] + ['post number 83', 'post number 82', 'post number 81', 'post number 80', ...] To post a note: @@ -210,7 +202,7 @@ class Api(object): "notes":[ { "summary":"Harry says catch is da bomb", - "user": { + "user": { "user_name":"harry12", "id":1813083}, "created_at":"2010-04-22T04:19:16.543Z", @@ -268,20 +260,7 @@ class Api(object): ]} """ - API_SERVER = "api.catch.com" - API_VERSION = "v1" - HTTP_GET = "GET" - HTTP_POST = "POST" - HTTP_DELETE = "DELETE" - API_ENDPOINT_NOTES_JSON = "/notes.json" - API_ENDPOINT_TAGS_JSON = "/tags/tags.json" - API_ENDPOINT_NOTES = "/notes/" - API_ENDPOINT_IMAGES = "/images/" - API_ENDPOINT_IMAGES_VIEW = "/viewImage.action?viewNodeId=" - API_ENDPOINT_USER_JSON = "/user.json" - API_ENDPOINT_CURSOR = "?cursor=" - - def __init__(self, username=None, password=None, url=API_SERVER, + def __init__(self, username=None, password=None, url="api.catch.com", use_ssl=True, port=443, timeout=10, cookie_epass=None): """ Args: @@ -309,9 +288,9 @@ def set_credentials(self, username=None, password=None, cookie_epass=None): Set username/password or cookie. Args: - username: + username: catch username. - password: + password: catch password. cookie_epass: catch authentication cookie @@ -334,7 +313,7 @@ def load_image_and_add_to_note_with_id(self, filename, id): filename: filename of image to load data from. id: id of note to which image will be appended. """ - try: + try: fin = open(filename, 'r') data = fin.read() self.add_image_to_note_with_id(filename, data, id) @@ -354,9 +333,14 @@ def add_image_to_note_with_id(self, filename, data, id): Returns: The server's response page. """ - page = "/" + self.API_VERSION + self.API_ENDPOINT_IMAGES + str(id) +".json" + page = "/v1/images/%s.json" % str(id) return self._post_multi_part(self._url, page, [("image", filename, data)]) + + @property + def _user_agent(self): + return ' '.join(("python", "catch.api-%s" % __version__)) + def _post_multi_part(self, host, selector, files): """ Post files to an http host as multipart/form-data. @@ -373,12 +357,9 @@ def _post_multi_part(self, host, selector, files): content_type, body = self._encode_multi_part_form_data(files) handler = httplib.HTTPConnection(host) headers = self._get_auth_headers() - h = { - 'User-Agent': 'INSERT USERAGENTNAME',#Change this to library version? -htormey - 'Content-Type': content_type - } + h = {'User-Agent': self._user_agent, 'Content-Type': content_type} headers.update(h) - handler.request(self.HTTP_POST, selector, body, headers) + handler.request("POST", selector, body, headers) response = handler.getresponse() data = response.read() handler.close() @@ -429,7 +410,7 @@ def delete_note(self, id):#Change this to just take a note Returns: The server's response page. """ - return self._request(self.HTTP_DELETE, id) + return self._request("DELETE", id) def edit_note(self, note): """ @@ -440,7 +421,7 @@ def edit_note(self, note): Returns: The server's response page. """ - return self._request(self.HTTP_POST, note) + return self._request("POST", note) def post_note(self, note): """ @@ -451,7 +432,7 @@ def post_note(self, note): Returns: The server's response page. """ - return self._request(self.HTTP_POST, note) #change this to note_text to be a little clearer -htormey + return self._request("POST", note) #change this to note_text to be a little clearer -htormey def _request(self, http_method, note): #Clean this up a little -htormey """ @@ -462,20 +443,20 @@ def _request(self, http_method, note): #Clean this up a little -htormey Returns: The server's response page. """ - if http_method == self.HTTP_POST: + if http_method == "POST": headers = { 'Content-type' : "application/x-www-form-urlencoded" } if isinstance(note, Note): #Edit an existing note params = urlencode(note.dictionary) - page = "/" + self.API_VERSION + self.API_ENDPOINT_NOTES + str(note.note_id) + '.json' + page = "/v1/notes/%s.json" % str(note.note_id) else: - params = urlencode(dict(text=note)) - page = "/" + self.API_VERSION + self.API_ENDPOINT_NOTES_JSON - handle = self._basic_auth_request(page, headers=headers, method=self.HTTP_POST, params=params) - elif http_method == self.HTTP_DELETE: - page = "/" + self.API_VERSION + self.API_ENDPOINT_NOTES + str(note) - handle = self._basic_auth_request(page, method=self.HTTP_DELETE) - + params = urlencode(dict(text=note)) + page = "/v1/notes.json" + handle = self._basic_auth_request(page, headers=headers, method="POST", params=params) + elif http_method == "DELETE": + page = "/v1/notes/%s.json" % str(note) + handle = self._basic_auth_request(page, method="DELETE") + response = handle.getresponse() data = response.read() handle.close() @@ -493,14 +474,14 @@ def get_image_with_id(self, id): Returns: Data associated with image id. """ - url = self.API_ENDPOINT_IMAGES_VIEW + str(id) + url = "/viewImage.action?viewNodeId=%s" % str(id) return self._fetch_url(url) def get_user_id(self): """ Get ID of API user. - Returns: + Returns: Id of catch user associated with API instance. """ if self._user: @@ -508,8 +489,8 @@ def get_user_id(self): else: raise CatchError("Error user id not set, try calling GetNotes.") - @Property - def notes(): + @property + def notes(self): doc = "A parsed list of note objects" def fget(self): if self._notes: @@ -525,7 +506,7 @@ def get_notes(self): Returns: A list of Note objects from the catch users account. """ - url = "/" + self.API_VERSION + self.API_ENDPOINT_NOTES_JSON + url = "/v1/notes.json" json_notes = self._fetch_url(url) self._notes = self._parse_notes(json_notes) return self._notes @@ -533,7 +514,7 @@ def get_notes(self): def get_notes_from_cursor(self, cursor_position): """ Get a batch of upto 20 notes from a given cursor position. See - description given for json_cursor for further details on how + description given for json_cursor for further details on how cursors work with catch. Args: @@ -547,7 +528,7 @@ def get_notes_from_cursor(self, cursor_position): def get_cursor_information(self, cursor_position): """ - Gets information about cursor at a given position. See json_cursor for further + Gets information about cursor at a given position. See json_cursor for further details on how cursors work with catch. Args: @@ -580,12 +561,12 @@ def get_user(self): Returns: A user object. """ - url = "/" + self.API_VERSION + self.API_ENDPOINT_USER_JSON - user_info = self._fetch_url(url) + url = "/v1/user.json" + user_info = self._fetch_url(url) self._parse_user_info(user_info) return self._user - @Property + @property def json(): doc = "Json object of notes stored in account." def fget(self): @@ -602,7 +583,7 @@ def get_json(self): Returns: A json object representing all notes in a users account. """ - url = "/" + self.API_VERSION + self.API_ENDPOINT_NOTES_JSON + url = "/v1/notes.json" self._json = self._fetch_url(url) return self._json @@ -613,8 +594,8 @@ def get_tags(self): Returns: A json object containing tags and related information (number of notes per tag, etc). """ - url = "/" + self.API_VERSION + self.API_ENDPOINT_TAGS_JSON - tags = self._fetch_url(url) + url = "/v2/tags.json" + tags = self._fetch_url(url) return tags def json_cursor(self, cursor_position): @@ -629,7 +610,7 @@ def json_cursor(self, cursor_position): Returns: A json object containing notes from cursor position requested. """ - url = "/" + self.API_VERSION + self.API_ENDPOINT_NOTES_JSON + self.API_ENDPOINT_CURSOR + str(cursor_position) + url = "/v1/notes.json?cursor=%s" % str(cursor_position) cursor = self._fetch_url(url) return cursor @@ -701,10 +682,10 @@ def _make_cookie_auth_headers(self, cookie_epass): raise CatchError("Error making cookie auth headers with\ cookie:{0}".format(cookie_epass)) - def _basic_auth_request(self, path, method=HTTP_GET, headers={}, params={}): + def _basic_auth_request(self, path, method="GET", headers={}, params={}): """ Make a HTTP request with basic auth header and supplied method. - Defaults to operating over SSL. + Defaults to operating over SSL. Args:: @@ -776,7 +757,7 @@ def _parse_notes( self, source, get_image_data=False): user = self._user.id user = self._user.id if 'location' in note: - pass + pass if 'tags' in note: for tag in note['tags']: tags.append(tag) @@ -788,6 +769,6 @@ def _parse_notes( self, source, get_image_data=False): image_data = self._fetch_url(item['src']) media.append(Image(item['type'], None, item['id'], item['revision_id'], item['width'], item['height'], item['src'], image_data)) - notes.append(Note(note['created_at'], note['modified_at'], note['reminder_at'], note['id'], note['text'], note['summary'], note['source'], + notes.append(Note(note['created_at'], note['modified_at'], note['reminder_at'], note['id'], note['text'], note['summary'], note['source'], note['source_url'], user, note['children'], media, tags, location)) return notes From 277941e2e0c427fd7f6d8d615c2332f879d98647 Mon Sep 17 00:00:00 2001 From: arielbackenroth Date: Thu, 5 May 2011 15:15:50 -0700 Subject: [PATCH 21/26] midway through rewrite - refactoring object model and moving to v2 apis --- catchapi/__init__.py | 672 ++++++++----------------------------------- 1 file changed, 121 insertions(+), 551 deletions(-) diff --git a/catchapi/__init__.py b/catchapi/__init__.py index 20c5c31..50b8706 100644 --- a/catchapi/__init__.py +++ b/catchapi/__init__.py @@ -17,14 +17,8 @@ __author__ = 'ariel@catch.com' __version__ = '0.5' -import mimetypes -import base64 -import httplib -import os +import mimetypes, base64, httplib, urllib, os, sys, urlparse, datetime import simplejson as json -import sys -from urllib import urlencode -import urlparse class CatchError(Exception): """ @@ -52,38 +46,72 @@ def response(self): """Returns HTTP response body used to construct this error.""" return self.args[2] -class User(object): +class User(dict): """ A class representing the User structure used by the Catch API. - - The User class exposes the following properties:: - user.id # read only - user.user_name # read only - user.created_at # read only - user.email # read only """ - def __init__(self, id=None, user_name=None, created_at=None, email=None): - self._id = id - self._user_name = user_name - self._created_at = created_at - self._email = email - - @property - def id(self): - return self._id + def __init__(self, session, *args, **kwds): + super(User, self).__init__(*args, **kwds) + self._session = session @property - def user_name(self): - return self._user_name + def access_token(self): + return self.get('access_token', None) @property - def created_at(self): - return self._created_at + def tags(self): + data = self._session._request("GET", '/v1/tags.json', body={'access_token': self.access_token}) + for tag in data['tags']: + tag['modified'] = datetime.datetime.strptime(tag['modified'], '%Y-%m-%dT%H:%M:%S.%fZ') + return tuple(data['tags']) + + def post_note(self, text, **kwds): + params = {"text": text} + params.update(kwds) + data = self._session._request("POST", + "/v2/notes.json?access_token=%s" % self.access_token, + body=params) + return Note(self, self._session, data['notes'][0]) + + def get_note(self, id): + data = self._session._request("GET", + "/v2/notes/%s.json" % id, + body={"access_token": self.access_token}) + return Note(self, self._session, data['notes'][0]) @property - def email(self): - return self._email + def notes(self): + class NoteIterator: + def __init__(self, user): + self._user = user + self._offset = 0 + self._limit = 100 + self._count = -1 + self._next_batch() + + def __len__(self): return self._count + def __iter__(self): return self + + def next(self): + if not self._data: + self._next_batch() + return Note(self, self._user._session, self._data.pop(0)) + + def _next_batch(self): + if self._count == 0 or (self._count >= 0 and self._offset > self._count): + raise StopIteration + limit = min(self._count - self._offset, self._limit) if self._count >= 0 else self._limit + self._data, self._count = self._user.get_notes(offset=self._offset, limit=limit) + self._offset += self._limit + + return NoteIterator(self) + + def get_notes(self, offset=0, limit=20): + data = self._session._request("GET", "/v2/notes.json", + body={"offset": offset, "limit": limit, 'full': 'true', + 'access_token': self.access_token}) + return [Note(self, self._session, n) for n in data['notes']], data['count'] #Perhaps I should refactor this into a class hierarchy and subclass for image/sound/etc? -htormey class Image(object): @@ -111,44 +139,34 @@ def __init__(self, type="image", md5=None, id=None, revision_id=None, width=0, h self.src = src self.data = data -class Note(object): +class Note(dict): """ - A class representing the Note structure used by the Catch API. - - The Note structure exposes the following properties:: - - note.created_at - note.modified_at - note.reminder_at - note.note_id - note.text - note.summary - note.source - note.source_url - note.user - note.children - note.media - note.tags - note.location - note.has_media # read only - note.dictionary # read only """ - def __init__(self, created_at, modified_at, reminder_at, note_id, text, - summary, source, source_url, user, children, media = [], tags = [], location = []): - self.created_at = created_at - self.modified_at = modified_at - self.reminder_at = reminder_at - self.note_id = note_id - self.text = text - self.summary = summary - self.source = source - self.source_url = source_url - self.user = user - self.children = children - self.media = media - self.tags = tags - self.location = location + def __init__(self, user, session, *args, **kwds): + self._user = user + self._session = session + self._dirty = False + super(Note, self).__init__(*args, **kwds) + + @property + def deleted(self): + return getattr(self, "_deleted", False) + + def delete(self): + self._session._request("DELETE", "/v2/notes/%s.json" % self['id'], + body={"access_token": self._user.access_token, + "server_modified_at": self['server_modified_at']}) + self._deleted = True + + def edit(self, **kwds): + kwds.setdefault('server_modified_at', self['server_modified_at']) + data = self._session._request( + "POST", + "/v2/notes/{id}.json?access_token={token}".format(id=self['id'], + token=self._user.access_token), + body=kwds) + self.update(data['notes'][0]) @property def has_media(self): @@ -160,149 +178,50 @@ def has_media(self): """ return len(self.media) > 0 - @property - def dictionary(self): - """ - Returns text from the note packaged as a dictionary. - - Returns: - A dictionary containing selected attributes from the note. - """ - #Working on adding dates/location/media and other fields to this dictionary. Right now you can just update text. -htormey - return dict(text=self.text) - -class Api(object): +class CatchSession(object): """ - Example usage: - - To create an instance of the catch.Api class with basic authentication: - - >>> import catchapi - >>> api = catch.Api("username", "password") - - To fetch all users notes and print an attribute: - - >>> [n.created_at for n in api.notes] - ['2010-03-08T17:49:08.850Z', '2010-03-06T20:02:32.501Z', ...] - - To fetch a subset of a users notes use a cursor. To get the first 20 notes and print an attribute: - - >>> [n.text for n in api.get_notes_from_cursor(-1)] - ['Harry says catch is da bomb #food #ice', 'Harry says catch is da bomb #food #ice', ...] - - To get the next 20 notes use cursor 1 (cursor 0 returns all notes in a users account): - - >>> [n.text for n in api.get_notes_from_cursor(1)] - ['post number 83', 'post number 82', 'post number 81', 'post number 80', ...] - - To post a note: - - >>> api.post_note("Harry says catch is da bomb") - { - "notes":[ - { - "summary":"Harry says catch is da bomb", - "user": { - "user_name":"harry12", - "id":1813083}, - "created_at":"2010-04-22T04:19:16.543Z", - "mode":"private", - "modified_at":"2010-04-22T04:19:16.543Z", - "reminder_at":null, - "id":2276722, - "text":"Harry says catch is da bomb", - "tags":[], - "source":"3banana", - "location":null, - "source_url":"https://catch.com/", - "children":0 - }]} - - To delete a note: - - >>> id = api.notes[1].note_id - >>> api.delete_note(id) - - To add an image to the above note - - >>> id = api.notes[1].note_id - >>> api.load_image_and_add_to_note_with_id("myimage.jpg", id) - - To edit a note: - - >>> n[0].text='Harry says coolio' - >>> api.edit_note(n[0]) - - To download image data from a note: - - >>> api.notes[1].has_media - True - >>> id = api.notes[1].note_id - >>> d = api.get_image_with_id(id) - >>> filename = "/Users/harrytormey/%s.jpg" % id - >>> fout = open(filename, "wb") - >>> fout.write(d) - >>> fout.close() - - To get a json object of a users tags - - >>> api.get_tags() - { - "tags":[ - { - "name":"food", - "count":"1", - }, - { - "name":"ice", - "count":"1", - }, - ]} """ - def __init__(self, username=None, password=None, url="api.catch.com", - use_ssl=True, port=443, timeout=10, cookie_epass=None): - """ - Args: - username: The username of the catch account. - password: The password of the catch account. - url: The url of the api server which will handle the http(s) API requests. - use_ssl: Use ssl for basic auth or not. - port: The port to make http(s) requests on. - timeout: number of seconds to wait before giving up on a request. - """ - self._url = url - self._use_ssl = use_ssl - self._port = port - self._timeout = timeout - self._user = None - self._notes = None - self._json = None - if cookie_epass: - self.set_credentials(cookie_epass=cookie_epass) - else: - self.set_credentials(username=username, password=password) + def __init__(self, host="https://api.catch.com", timeout=10): + self.host = host + self._timeout = timeout - def set_credentials(self, username=None, password=None, cookie_epass=None): - """ - Set username/password or cookie. + @property + def host(self): + return "%s://%s" % ({80: "http", 443: "https"}[self._api_port], self._api_host) + + @host.setter + def host(self, host): + host = urlparse.urlsplit(host) + self._api_host = host.netloc + self._api_port = {"http": 80, "https": 443}[host.scheme] + self._conn_class = {"http": httplib.HTTPConnection, + "https": httplib.HTTPSConnection}[host.scheme] + + def _request(self, method, url, body=None, headers=None): + headers = headers or {} + headers.setdefault('User-Agent', self._user_agent) + if isinstance(body, dict): + if method in ("GET", "DELETE"): + url = "%s%s%s" % (url, "&" if "?" in url else "?", urllib.urlencode(body, doseq=True)) + body = None + else: + headers.setdefault('Content-Type', 'application/x-www-form-urlencoded') + body = urllib.urlencode(body, doseq=True) + headers.setdefault("Content-Length", len(body or "")) + + conn = self._conn_class(self._api_host, self._api_port) + conn.request(method, url, body=body, headers=headers) + response = conn.getresponse() + data = json.loads(response.read()) + conn.close() + return data - Args: - username: - catch username. - password: - catch password. - cookie_epass: - catch authentication cookie - """ - if username and password: - self._username = username - self._password = password - elif cookie_epass: - self._cookie_epass = cookie_epass - else: - raise CatchError("No username/password combination\ - or cookie authentication provided") + def login(self, username, password): + data = self._request("POST", "/v2/user", headers={ + 'Authorization': "Basic %s" % base64.standard_b64encode(":".join((username, password))) + }) + return User(self, data['user']) def load_image_and_add_to_note_with_id(self, filename, id): """ @@ -336,7 +255,6 @@ def add_image_to_note_with_id(self, filename, data, id): page = "/v1/images/%s.json" % str(id) return self._post_multi_part(self._url, page, [("image", filename, data)]) - @property def _user_agent(self): return ' '.join(("python", "catch.api-%s" % __version__)) @@ -401,70 +319,6 @@ def _get_content_type(self, filename): """ return mimetypes.guess_type(filename)[0] or 'application/octet-stream' - def delete_note(self, id):#Change this to just take a note - """ - Delete a note. - - Args: - id: id of note to be deleted. - Returns: - The server's response page. - """ - return self._request("DELETE", id) - - def edit_note(self, note): - """ - Edit a note. - - Args: - note: note object to be edited - Returns: - The server's response page. - """ - return self._request("POST", note) - - def post_note(self, note): - """ - Post a note. - - Args: - note: text of note to be posted. - Returns: - The server's response page. - """ - return self._request("POST", note) #change this to note_text to be a little clearer -htormey - - def _request(self, http_method, note): #Clean this up a little -htormey - """ - Perform a http request on a note. - - Args: - http_metod: what kind of http request is being made (i.e POST/DELETE/GET) - Returns: - The server's response page. - """ - if http_method == "POST": - headers = { 'Content-type' : "application/x-www-form-urlencoded" } - if isinstance(note, Note): - #Edit an existing note - params = urlencode(note.dictionary) - page = "/v1/notes/%s.json" % str(note.note_id) - else: - params = urlencode(dict(text=note)) - page = "/v1/notes.json" - handle = self._basic_auth_request(page, headers=headers, method="POST", params=params) - elif http_method == "DELETE": - page = "/v1/notes/%s.json" % str(note) - handle = self._basic_auth_request(page, method="DELETE") - - response = handle.getresponse() - data = response.read() - handle.close() - - if response.status != 200: - raise CatchError("Http error posting/editing/deleting note ", response.status, data) - return data - def get_image_with_id(self, id): """ Get image data associated with a given id. @@ -488,287 +342,3 @@ def get_user_id(self): return self._user.id else: raise CatchError("Error user id not set, try calling GetNotes.") - - @property - def notes(self): - doc = "A parsed list of note objects" - def fget(self): - if self._notes: - return self._notes - else: - return self.get_notes() - return locals() - - def get_notes(self): - """ - Get notes and update the Api's internal cache. - - Returns: - A list of Note objects from the catch users account. - """ - url = "/v1/notes.json" - json_notes = self._fetch_url(url) - self._notes = self._parse_notes(json_notes) - return self._notes - - def get_notes_from_cursor(self, cursor_position): - """ - Get a batch of upto 20 notes from a given cursor position. See - description given for json_cursor for further details on how - cursors work with catch. - - Args: - cursor_position: cursor position to grab 20 notes from (i.e -1 is most recent 20) - Returns: - A list of note objects based on the contents of the users account. - """ - json_notes = self.json_cursor(cursor_position) - notes = self._parse_notes(json_notes) - return notes - - def get_cursor_information(self, cursor_position): - """ - Gets information about cursor at a given position. See json_cursor for further - details on how cursors work with catch. - - Args: - cursor_position: cursor position you want to find out about. - Returns: - A dictionary containing previous_cursor, next_cursor and note count. - """ - json_notes = self.json_cursor(cursor_position) - return self._parse_cursor_info(json_notes) - - def _parse_cursor_info(self, source): - """ - Parse cursor information with notes returned from catch. - - Args: - source: A json object consisting of notes and cursor information - Returns: - A dictionary containing previous_cursor, next_cursor and note count. - """ - cursor_info = json.loads(source) - if 'next_cursor' in cursor_info and 'previous_cursor' in cursor_info and 'count' in cursor_info: - return {"previous_cursor": cursor_info['previous_cursor'], "next_cursor": cursor_info['next_cursor'], "count": cursor_info['count'] } - else: - CatchError("Error keys missing from source JSON passed to _parse_cursor_info") - - def get_user(self): - """ - Get user info. - - Returns: - A user object. - """ - url = "/v1/user.json" - user_info = self._fetch_url(url) - self._parse_user_info(user_info) - return self._user - - @property - def json(): - doc = "Json object of notes stored in account." - def fget(self): - if self._json: - return self._json #should I return json.load(sef._json) ? -htormey - else: - return self.get_json() - return locals() - - def get_json(self): - """ - Get json object and update the cache. - - Returns: - A json object representing all notes in a users account. - """ - url = "/v1/notes.json" - self._json = self._fetch_url(url) - return self._json - - def get_tags(self): - """ - Fetch json object containing tags from users account. - - Returns: - A json object containing tags and related information (number of notes per tag, etc). - """ - url = "/v2/tags.json" - tags = self._fetch_url(url) - return tags - - def json_cursor(self, cursor_position): - """ - Get batches of 20 notes in JSON format from a given cursor position i.e -1, 1, - etc. For example: -1 returns the most recent 20 notes, 1 returns the previous 20 - before that, etc. One exeption to note is that 0 returns a JSON object for all - notes in a given account. - - Args: - cursor_position: cursor position to grab 20 notes from (i.e -1 is most recent 20). - Returns: - A json object containing notes from cursor position requested. - """ - url = "/v1/notes.json?cursor=%s" % str(cursor_position) - cursor = self._fetch_url(url) - return cursor - - def _fetch_url(self, url): - """ - Perform a basic auth request on a given catch API endpoint. - - Args: - url: Catch Api endpoint (i.e /v1/notes.json etc). - Returns: - The server's response page. - """ - handler = self._basic_auth_request(url) - response = handler.getresponse() - data = response.read() - handler.close() - if response.status != 200: - raise CatchError("Http error", response.status, data) - return data - - def _get_auth_headers(self): - """ - Switch between basic auth and cookie auth depending on which properties - self has. - """ - if hasattr(self, "_username") and hasattr(self, "_password"): - return self._make_basic_auth_headers(self._username, self._password) - elif hasattr(self, "_cookie_epass"): - return self._make_cookie_auth_headers(self._cookie_epass) - else: - raise CatchError("No username/password combination\ - or cookie authentication provided") - - def _make_basic_auth_headers(self, username, password): - """ - Encode headers for basic auth request. - - Args:: - - username: catch username to be used. - password: password to be used. - - Returns: - Dictionary with encoded basic auth values. - """ - if username and password: - headers = dict(Authorization="Basic %s" - %(base64.b64encode("%s:%s" %(username, password)))) - else: - raise CatchError("Error making basic auth headers with username: %s, password: %s" % (username, password)) - return headers - - def _make_cookie_auth_headers(self, cookie_epass): - """ - Encode headers for cookie auth request. - - Args:: - - cookie_epass: cookie auth token to be used. - - Returns: - Dictionary with encoded basic auth values. - """ - if cookie_epass: - return { - "Cookie": "cookie_epass={0}".format(cookie_epass) - } - else: - raise CatchError("Error making cookie auth headers with\ - cookie:{0}".format(cookie_epass)) - - def _basic_auth_request(self, path, method="GET", headers={}, params={}): - """ - Make a HTTP request with basic auth header and supplied method. - Defaults to operating over SSL. - - Args:: - - path: Catch API endpoint - metthod: which http method to use (PUT/DELETE/GET) - headers: Additional header to use with request. - params: Other parameters to use - - Returns: - The server's response page. - """ - h = self._get_auth_headers() - h.update(headers) - if self._use_ssl: - handler = httplib.HTTPSConnection - else: - handler = httplib.HTTPConnection - - # 'timeout' parameter is only available in Python 2.6+ - if sys.version_info[:2] < (2, 6): - conn = handler(self._url, self._port) - else: - conn = handler(self._url, self._port, timeout=self._timeout) - conn.request(method, path, params, headers=h) - return conn - - def _parse_user_info(self, source): - """ - Parse JSON user returned from catch, instantiate a User object from it. - - Args: - source: Json object representing a user - Returns: - A User object. - """ - user_info = json.loads(source) - - if 'user' in user_info: - self._user = User(user_info['user']['id'], user_info['user']['user_name'], user_info['user']['created_at'], user_info['user']['email']) - else: - raise CatchError("Error no user key found in source JSON passed to _parse_user_info") - - def _parse_notes( self, source, get_image_data=False): - """ - parse JSON notes returned from catch, instantiate a list of note objects from it. - - Args:: - - source: A json object representing a list of notes. - get_images: if images are associated with notes, download them now. - Returns: - A list of note objects. - """ - - notes = [] - json_notes = json.loads(source) - - for note in json_notes['notes']: - media = [] - location = [] - tags = [] - user = None - source = None - - if 'id' in note: - if 'user' in note: - if self._user == None: - self. get_user() - user = self._user.id - user = self._user.id - if 'location' in note: - pass - if 'tags' in note: - for tag in note['tags']: - tags.append(tag) - if 'media' in note: - for item in note['media']: - if item['type'] == 'image': - image_data = None - if get_image_data: - image_data = self._fetch_url(item['src']) - media.append(Image(item['type'], None, item['id'], item['revision_id'], item['width'], item['height'], item['src'], image_data)) - - notes.append(Note(note['created_at'], note['modified_at'], note['reminder_at'], note['id'], note['text'], note['summary'], note['source'], - note['source_url'], user, note['children'], media, tags, location)) - return notes From 3c992d376e9780e6f82509be470e88f771dd782d Mon Sep 17 00:00:00 2001 From: arielbackenroth Date: Thu, 5 May 2011 15:26:50 -0700 Subject: [PATCH 22/26] midway through rewrite - refactoring object model and moving to v2 apis From 3ba70831fec80030b76b37f02b15b3ccaed0dc92 Mon Sep 17 00:00:00 2001 From: arielbackenroth Date: Thu, 5 May 2011 15:42:19 -0700 Subject: [PATCH 23/26] adding unit tests --- test_catchapi.py | 87 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 test_catchapi.py diff --git a/test_catchapi.py b/test_catchapi.py new file mode 100644 index 0000000..54aa575 --- /dev/null +++ b/test_catchapi.py @@ -0,0 +1,87 @@ +# Copyright 2011 Catch.com, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import simplejson as json +import sys, unittest, catchapi +from getpass import getpass + +class TestCatchAPI(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls._api_host = raw_input("api host [https://api.catch-branch.com]: ") or "https://api.catch-branch.com" + cls._username = raw_input("username or email [apitest]: ") or "apitest" + cls._password = getpass("Password for %s: " % cls._username) + + def setUp(self): + self.api = catchapi.CatchSession(self.__class__._api_host) + + def login(self, username=None, password=None): + return self.api.login(username or self.__class__._username, + password or self.__class__._password) + + def test_tags(self): + tags = self.login().tags + self.failUnless(tags) + self.failUnless(isinstance(tags, tuple)) + + def test_notes_property(self): + # Verify that .notes returns a list of notes greater than 0 from test account with notes in it. + notes = self.login().notes + assert len(notes) > 0 + return notes + + def test_get_note(self): + u = self.login() + notes = u.notes + note = notes.next() + self.failUnless(note['text']) + self.assertEquals(note['text'], u.get_note(note['id'])['text']) + return note + + def test_get_notes(self): + # Verify that get_notes returns a list of notes greater than 0 from test account with notes in it. + u = self.login() + notes, count = u.get_notes() + self.failUnless(len(notes)) + return notes, count + + def test_post_note(self, user=None): + # Verify posting a note. + user = user or self.login() + data_before_post = user.notes + note = user.post_note("Testing 123") + self.assertEquals(note['text'], "Testing 123") + data_after_post = user.notes + self.assertEquals(len(data_before_post) + 1, len(data_after_post)) + return note + + def test_edit_note(self): + # Verify editing a note. + u = self.login() + note = u.post_note("test edit") + self.assertEquals(note['text'], 'test edit') + note.edit(text="edited text") + self.assertEquals(note['text'], 'edited text') + self.assertEquals(u.get_note(note['id'])['text'], note['text']) + note.delete() + + def test_delete_note(self): + # Verify deleting a note. + u = self.login() + n = self.test_post_note(user=u) + data_before_delete = u.notes + n.delete() + self.failUnless(n.deleted) + self.assertEquals(len(data_before_delete) -1, len(u.notes)) From 42694c69684bb348ce65d84920c3a2aa2a093670 Mon Sep 17 00:00:00 2001 From: arielbackenroth Date: Thu, 5 May 2011 17:11:59 -0700 Subject: [PATCH 24/26] adding some media support --- catch_logo.png | Bin 0 -> 6352 bytes catchapi/__init__.py | 226 ++++++++++--------------------------------- test_catchapi.py | 13 ++- 3 files changed, 62 insertions(+), 177 deletions(-) create mode 100644 catch_logo.png diff --git a/catch_logo.png b/catch_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..5a3b0fd9b1adc897109c602c901367639b48aabe GIT binary patch literal 6352 zcmV;>7%%6EP)CR0?4oo!vF&_%=X^9eLUTLw*K!-$_sDad-wkT-`%Ir`ObHiemM6Vhl~N><3R8s zOt6Fq%74H=mv8(+{*L^8!vFGjDqs9gd8=Mpe`r7Rn<$?X-=8wd`{jZ1zFdtw0Fm5H zb(r==?U(Wm>wi>-w9lwLt6Y_!eb@KOFJ&yWDIv%Vz}_SstbJ=&{dJ)F z;5JQ?b=KvSyR*s2f0n0IA&>}?V7OG5VEe7!-F}_r0dC=sKFFJc*YIV(R-RRb%l}jv zi*V{@$O7v-`ef)ARF_&8F9H%_)%Q`RhqB;$XVwv*?R$BzX+@cfh&2o*W7W-W0?T2u z!MEN7Tb^0xP#&21dqNkAY_f?AeN{kULM3eDwmrhOIVM;Yw`ceysCARe+I+Jch^OYMhLa3Hb+5k@w}4w@1) zm^xKzmO(yl(hutUK{I57V5qSsj3%r$E-M0WP*VW60JZ3DN~$~vW-;9c61as0spt&4 z$H=;opdWyh35kM&Lr^ZCmb~VR7?(5!keln$vMEvBU2Gdxw~k!k?O)YsLkgQ+LnYDZ z>@%-JQP5hVpa$p~r|MO-pC3>)l1NZWwL4s^%3$kpXsdHaa@|mCSK%o1iDhNS1rc-* zW}OQ-QdDGV8(Wu&^6a5;3d&Fic)m?VsY%^PgWoY*E@d}sxy@{@jaU|w#l(H6BwoN! z9uT7~H&gGShr}=FZptF+v)bfQH>qqu!4>|>sJ>4SLgtEL&Z?kEwKPRERKKPIkk8~j zoi`OxYC$GO@zks)83EA!#H=O> zrU^aF2M>|ap^n_NI}@(Lwym57JQR#Kjh1JLFc4MWsRL_w{%G2aVx<&_LWfXEEl@BZ zi{hXV0>Do281z7Qoy_-%yu`wEFOaaa9OxjpgE4m>0E6ptgL%?HG4K%#H;8j@^G$%! zi8>Hhi$N7o>V<;=_P~TKnugIagE6;g$wa=nZlrEKNCCd8^V5MHOzcLLjzO(jMRF!+ zSg8n%K1Wgyoz0h!fkbsY0fZ# zxEzBN_3J1xfjP{!vOvCr{Kb4MuBn^JV`MTxH>C)~Bg9cd?o$>#R1_bSnjfkbFVt29 zBunGZ9j`1Wb1ceMHRm;$rUdV(OG~d4KU8z_iYcwukZBFKNEs{o`eict{>sKGsg)X8QLJWo7Q7;D- zI6CoAlu{q50aq7i0S5Jy8_LG4JOpsb3eRS|_B`PB&HR{Ah>1|#L4Z!B+#y!=I4hU} zO$X&;k7eZX=v0{0B9&cbnOKUnykTuM+zJ0Xx2-*V`M!XA9SXR|{s1$}fF`_A+g<~B z{du5G&jak(47g~S#4ECeT`(FMdzNp)ox5~U3~&)5cQUu8j(6zt2EK6^h887K$fKRx zVRXhX0iO1Ld;H{v%OO8}GspeQ0x!itKHNenxHKD6U|bonf)pVcQjJM4uUSQ|8=}@g zmM;KH*?_nX!jQTtyn;7|(n3hbo(p)=1%QX046x!rn0n360=@FTKo8vl`CEU&&luZ+ z2+78dI8pV!E+(t@ZVX2*OXLeOtt#)OBhzlA5Z+M82P(4@-_YhnKwS!rubAHnRKCWGt98O;r_VVu zPY={WekGe`SMmUj@U(elO9Jdb6a(~0E;c7=acgw zv%zT&@FyCcqOYXtu<@XyAsull;9GtS;OYBe{%gMnxNuqTKM4ofMU_&5QSp7o8OS86 zSwHFlw8e{I<22sM9SL&XR=;U5iXj~afe@Jxn8js`X%7w3G|MWvqnbG;iwZP#0z2LS zJmzdjmwuKPGxnJma`50kL0RLHgDD<%XY;KgV{ck zFbA^qz6Y7k6=~--z~g@kMwflwIoO8B`JYLjRX2Yn7%hTOZ8a1rA0^LFSP27ZKupPk zFp98Nv){@%0}i;6cwB}a+*&>%%X(ShWB2#X2cgwroMjY~Z!REYmRbALii6m4KG|;$ z`~z0P|(ebFHeefXVS_QH4k}GN7~?17gSyrMUNG47phlXqhAj)sWrKDAZSZd@>!U zojV}C_ZI>7IKc1Y4Vb_F5`gFai;-*PR5HCFYT4|Ttq@X3EndN$Tlw+)`JM*>?0py$ zj01R~ivh~}o1O(&|0vKlhSQm)1|KCRp_vtJtR;K)!kbwRY46pn2us`L+cworXp7af z&0Mvwz{t1$py^7O2y%f;pDP?AEGiY4r(!`yJWJ94~zz z=(&ejNhVbhJ>K%7o@MsI_0BUVWPWyxA><`-TrDNugpjD|RI-FJf5Om#dmRjT@`c@W z^KG91TDO)Lxu?-d_g9|ptGk|qa~!40{L)Dm0j@p~V9ClUS4L%n?))>z_x~;1{Y(=Y zZ(cg;d|@{7khqD{8Y`ug(w?4 zXh)&S0AtV4I!~?DRZIR2i%3|RQabz;fTer;Y+qjw`GGGtKO=Biv><88B8A*rBF{_| zcv!Fu($9RNYVz)0c;Hch7k?UH<$*By#r>Q$h%d&c!QQyk;C2JCwnr&x8>ZBv`!1lx5+XABNF~{sG`q?*)2w9nWP^BQ@r) z(9=LU#4buCsKM~q93h}F9IKHH(lW}L3bepsC-&O()O`$bFSCX&w*aj@r&U)wxQi3v zI4E!4$Uf`e0Xc>$&!Ke>0&tXG7q#YHfJdF-Ro1gU3dEK`&pgyJ6AY{H+6xUzBFq(H zQUYU1MbPZ_CY1Rd3`J{T^b3DiVYGXd_>5t#3V5$o^>bSHP}}Iq>|hVDS#EjpI0c4r z<-x49d-*jC|LL5MLjK|v?2{J81ZMp?Ye^zl7RFYBMzg^iF9ID;A;+aGi^K<2I$Zoj z+@Qjm9MkN+Sm$>#sxe1ub~@%w`q$3@-2IolQTJ9MwyqWa^!96E^wWO;>5MCtvA~F- zaPs8+KueZ0{CpPhTh|HI|D@07KmQRXH(d=_w7ZNw;b#_~R;-^PsoR`4US>GD8uE8< zZ_Kp3H?yqzLVEorCK^mIIKbW&&gF0a70_*e477D)^@HiWYZ&@3@_l40gS}W*ufGU5 z8XMJykZiR!0`v{Un9cTX8wyPgUO}b64eLe~XFk)z3T=Ni6wavCXDDV{l7zlFq-2EI zuup-f{)0D%sW%Iw_!y=6MRW6A3`KL>eXv7MU|3t+#+cgy#4tqh^O~_>R<>h{d~I$H zo3F(+rvv_wTV#1}a>EB8-}g5)%TX+m%2!E@vj1Sy6Ow>a9DyFc6Y}5x3ec7fg}Sjd zZ-o4n-vZe5oL^MZKm|t2d}0KKNfbZ3Tv0>VU7;j^f1a+lm zFX^VnK&=So2qJp&g;2XROw8o_VmJXkm2+9908KTsUJbTej=n1ktm+JGR2; z?BDD+XA5Kbou6Thznca4V!d|!k*~9}T|EeVr93jok%gjapTe({cG6$BV5uk}_3|h0 z$Z7O=kenR|8$)Vm+SKe}-k&GR-{0a^s7;oMLM@_wM=4u-K-IOxmJ18kKJ^{e?pH&c zF$1F?|44JLnY^-D>+71;NI~oP|Khk;><{T(AMod}Q=eRa3DDO+&e8Zo<^X9;qtFX3iPWiTL+F=9FVoPjHnQtRN)_#6$|v@YTkCKoX3!N!9gcF} zi`~Q&6$_47JSatkbdrZj?mxSsO3(kxbj6ix3taKb5*ORGC9a;VZW9o zD_RMe1(r)rY}>0fV-CyrpVr9aMI_oWAT5WUShMA{ZBxqzl;(dX3AFZBUR&@H`y9#s z?@wDPd;6wJehM`&_mueu6PeD%$zg%!Myczg*v0-h^F`{ax){nk7*k|PlBM;6gbsn$ z&_LcWLD76ujq*$Rx@4|LSKQcwK+A(&0He2`S2F^%{*elWl?E^0x8}S`T##@4Fwn!d zw2VPvgp#hoLym({iN5dsYbJ8ckdC zk2gsRaH+&#>mLUwX_?iYf-06*t*Wjh1r`o(5A&-GbaKM0JGbF;52dnHH|jcr0z`rVS#6YzRf;M zyfrBEn-7UaM-*BfgL>;q-`&CMnsUyuplYrb@^_9r=da=oVqP7Scz)&wjo%MxI)u|f z$s$n}++-GzoqJ_#n)0uBJ84zNLaj?2RUj~pz00(=Y;A2DRy45uUN*d>Yi5?TV%zKv zKXNj&RHAsWG=7~|X%s1$XQuu2^E_8+bY+CvL6ky*xO`TGT9|2pN-9k(^KGt@?!i*g zEn6_Vm7g_dUAMq7R9PR2#4QI}GUlawNnx#|rD}yWcfQnnDgx7m;OF@UoV)6fNliSV zCuFpt+%S@c`L%H5L`_@MLYfTK^I%?nX`ao89G9e%qhHgs2sn&(i-P2@iovERl(Z?b0;H<|_NKc_@ z>k2yUrv9K4T3sj`ueZ`c#+q1kCx}vt!LC19$3pD8V-#a+keo0yQbHeyc4|g_)Ipj- zNG;ik-ecYsq~y93&-R$JMkh!lM^mPbi@4IEHBkl}>03razn=DphorJ5#%de0G{9rm z`zPA2>;TbzM$Jx3L65oxd9z^bgak|Yg;sYtKZHV62?sr#dg@FXDBE??G(s2}+9Bk< z+5%eYgiOs5thcc{jO;iaGY_rJ3jD~BH*uuUoNs!D!5_yi9aof+uSA4hq@K{yX5Lm9 z4=uMIi*`yei+EehfoqT*@J(Hxf=*6%h+<igH8LDxJeM01fDfN&DFgT_SyEs3K-W=bZqoUaK zhu!a%DA3IoWlFVcGFu5HfUTW=3B7O`f}w@B)zo)a67~&(6s^x8B1g2!dYwaTt3%~Z zut5v#q>R^9yq6sH&{@bdN0>82NC6e>SpqTH19c4~DWuV?Oaoqyp+OHX#uId;M_^*J zyi`u(K{IQ~Dn2r$E<0@bC&}QY5bdhw6L=!hk2R8;YB=A6p3dqI>yo<&g79|;z%ixw zOq#L){V8fip`|iX%T{-K;${&Bs0h z8dRwm~EENt!Q2Jwp_lXG(%E)%sKC_Ci!3?`lzB{e1J^SKx?z!?;27EAxo1ez5& ziwxxgg&o2`)|2~3qJGTe8F}2MN*xY`GqND@RR-#W9^uI>uUe8ET_o)2 zx>N0N=pYm}5&P1G|G9w1M{n>sx9Xv`v-SsX(O8unNV8+?lrbDsicV9eAZJOvc!uHV z{on+3IOd$eVnMWp{j3!QkAQrh9BhI7^w`Whgfu%O{-Z2#K|+A`gs zId=+S5DUqDB6>?OQP^}ljaJpVJ}>D>gsK*mm&r; zc7C+^n4I*4Hu|5HQ8o*{4hnr96#j0IT9W-4A1!2({h5qeCU1A}9UuHZ0R{jdE0xwi S>d1fq0000 0 + data = self._session._request( + "POST", + "/v2/media/{id}.json?access_token={token}".format(id=self['id'], + token=self._user.access_token), + body=body, + headers={'Content-Type': 'multipart/form-data; boundary=%s' % BOUNDARY}) + + m = Media(self._user, self._session, self, data) + self['media'] = tuple(list(self['media']) + [m]) + return m class CatchSession(object): """ @@ -223,122 +217,6 @@ def login(self, username, password): }) return User(self, data['user']) - def load_image_and_add_to_note_with_id(self, filename, id): - """ - Load image from filename and append to note. - - Args:: - - filename: filename of image to load data from. - id: id of note to which image will be appended. - """ - try: - fin = open(filename, 'r') - data = fin.read() - self.add_image_to_note_with_id(filename, data, id) - except IOError: - raise CatchError("Error reading filename") - - def add_image_to_note_with_id(self, filename, data, id): - """ - Add image data to note. - - Args:: - - filename: filename of image. - data: loaded image data to be appended to note. - id: id of note to which image data will be appended. - - Returns: - The server's response page. - """ - page = "/v1/images/%s.json" % str(id) - return self._post_multi_part(self._url, page, [("image", filename, data)]) - @property def _user_agent(self): return ' '.join(("python", "catch.api-%s" % __version__)) - - def _post_multi_part(self, host, selector, files): - """ - Post files to an http host as multipart/form-data. - - Args:: - - host: server to send request to - selector: API endpoint to send to the server - files: sequence of (name, filename, value) elements for data to be uploaded as files - - Returns: - Return the server's response page. - """ - content_type, body = self._encode_multi_part_form_data(files) - handler = httplib.HTTPConnection(host) - headers = self._get_auth_headers() - h = {'User-Agent': self._user_agent, 'Content-Type': content_type} - headers.update(h) - handler.request("POST", selector, body, headers) - response = handler.getresponse() - data = response.read() - handler.close() - if response.status != 200: - raise CatchError("Error posting files ", response.status, data) - - def _encode_multi_part_form_data(self, files): - """ - Encode multi part form data to be posted to server. - - Args: - Files is a sequence of (name, filename, value) elements for data to be uploaded as files - Return: - sequence of (content_type, body) ready for httplib.HTTPConnection instance - """ - BOUNDARY = '----------ThIs_Is_tHe_bouNdaRY_$' - CRLF = '\r\n' - L = [] - for (key, filename, value) in files: - L.append('--' + BOUNDARY) - L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) - L.append('Content-Type: %s' % self._get_content_type(filename)) - L.append('') - L.append(value) - L.append('--' + BOUNDARY + '--') - L.append('') - body = CRLF.join(L) - content_type = 'multipart/form-data; boundary=%s' % BOUNDARY - return content_type, body - - def _get_content_type(self, filename): - """ - Attempt to guess mimetype of file. - - Args: - filename: filename to be guessed. - Returns: - File type or default value. - """ - return mimetypes.guess_type(filename)[0] or 'application/octet-stream' - - def get_image_with_id(self, id): - """ - Get image data associated with a given id. - - Args: - id: id of image to be fetched. - Returns: - Data associated with image id. - """ - url = "/viewImage.action?viewNodeId=%s" % str(id) - return self._fetch_url(url) - - def get_user_id(self): - """ - Get ID of API user. - - Returns: - Id of catch user associated with API instance. - """ - if self._user: - return self._user.id - else: - raise CatchError("Error user id not set, try calling GetNotes.") diff --git a/test_catchapi.py b/test_catchapi.py index 54aa575..1e7da6f 100644 --- a/test_catchapi.py +++ b/test_catchapi.py @@ -13,7 +13,7 @@ # limitations under the License. import simplejson as json -import sys, unittest, catchapi +import sys, unittest, catchapi, os from getpass import getpass class TestCatchAPI(unittest.TestCase): @@ -65,7 +65,7 @@ def test_post_note(self, user=None): self.assertEquals(note['text'], "Testing 123") data_after_post = user.notes self.assertEquals(len(data_before_post) + 1, len(data_after_post)) - return note + note.delete() def test_edit_note(self): # Verify editing a note. @@ -80,8 +80,15 @@ def test_edit_note(self): def test_delete_note(self): # Verify deleting a note. u = self.login() - n = self.test_post_note(user=u) + n = u.post_note(text="test_delete_note") data_before_delete = u.notes n.delete() self.failUnless(n.deleted) self.assertEquals(len(data_before_delete) -1, len(u.notes)) + + def test_media(self): + u = self.login() + n = u.post_note(text="test_media") + m = n.add_media(os.path.join(os.path.dirname(__file__), 'catch_logo.png')) + m.delete() + self.failUnless(m.deleted) From 2d30a6c23943fdb49eafcff5f6a6871dc7870e82 Mon Sep 17 00:00:00 2001 From: arielbackenroth Date: Thu, 5 May 2011 17:54:05 -0700 Subject: [PATCH 25/26] some comment support --- catchapi/__init__.py | 39 +++++++++++++++++++++++++++++++++++++++ test_catchapi.py | 26 ++++++++++++++++++-------- 2 files changed, 57 insertions(+), 8 deletions(-) diff --git a/catchapi/__init__.py b/catchapi/__init__.py index 9e33a64..7c03cf1 100644 --- a/catchapi/__init__.py +++ b/catchapi/__init__.py @@ -109,6 +109,28 @@ def delete(self): self._deleted = True return True +class Comment(dict): + + def __init__(self, user, session, note, *args, **kwds): + self._user = user + self._session = session + self._note = note + super(Comment, self).__init__(*args, **kwds) + + @property + def deleted(self): + return self._deleted + + def delete(self): + data = self._session._request("DELETE", "/v2/comment/%s.json" % self['id'], + body={"access_token": self._user.access_token}) + # not quite ready for this... + # "server_modified_at": self['server_modified_at']}) + if data['status'] == 'ok': + self._note['comments'] = (c for c in self._note._comments if c is not self) + self._deleted = True + return True + class Note(dict): def __init__(self, user, session, *args, **kwds): @@ -128,6 +150,23 @@ def delete(self): "server_modified_at": self['server_modified_at']}) self._deleted = True + def add_comment(self, **opts): + data = self._session._request("POST", + "/v2/comments/{id}.json?access_token={token}".format( + id=self['id'], + token=self._user.access_token), + body=opts) + return Comment(self._user, self._session, self, data['notes'][0]) + + @property + def comments(self): + if not hasattr(self, "_comments"): + data = self._session._request("GET", + "/v2/comments/{id}.json".format(id=self['id']), + body={"access_token": self._user.access_token}) + self._comments = [Comment(self._user, self._session, self, c) for c in data['notes']] + return self._comments + def edit(self, **kwds): kwds.setdefault('server_modified_at', self['server_modified_at']) data = self._session._request( diff --git a/test_catchapi.py b/test_catchapi.py index 1e7da6f..ce17b9c 100644 --- a/test_catchapi.py +++ b/test_catchapi.py @@ -31,18 +31,18 @@ def login(self, username=None, password=None): return self.api.login(username or self.__class__._username, password or self.__class__._password) - def test_tags(self): + def zest_tags(self): tags = self.login().tags self.failUnless(tags) self.failUnless(isinstance(tags, tuple)) - def test_notes_property(self): + def zest_notes_property(self): # Verify that .notes returns a list of notes greater than 0 from test account with notes in it. notes = self.login().notes assert len(notes) > 0 return notes - def test_get_note(self): + def zest_get_note(self): u = self.login() notes = u.notes note = notes.next() @@ -50,14 +50,14 @@ def test_get_note(self): self.assertEquals(note['text'], u.get_note(note['id'])['text']) return note - def test_get_notes(self): + def zest_get_notes(self): # Verify that get_notes returns a list of notes greater than 0 from test account with notes in it. u = self.login() notes, count = u.get_notes() self.failUnless(len(notes)) return notes, count - def test_post_note(self, user=None): + def zest_post_note(self, user=None): # Verify posting a note. user = user or self.login() data_before_post = user.notes @@ -67,7 +67,7 @@ def test_post_note(self, user=None): self.assertEquals(len(data_before_post) + 1, len(data_after_post)) note.delete() - def test_edit_note(self): + def zest_edit_note(self): # Verify editing a note. u = self.login() note = u.post_note("test edit") @@ -77,7 +77,7 @@ def test_edit_note(self): self.assertEquals(u.get_note(note['id'])['text'], note['text']) note.delete() - def test_delete_note(self): + def zest_delete_note(self): # Verify deleting a note. u = self.login() n = u.post_note(text="test_delete_note") @@ -86,9 +86,19 @@ def test_delete_note(self): self.failUnless(n.deleted) self.assertEquals(len(data_before_delete) -1, len(u.notes)) - def test_media(self): + def zest_media(self): u = self.login() n = u.post_note(text="test_media") m = n.add_media(os.path.join(os.path.dirname(__file__), 'catch_logo.png')) m.delete() + n.delete() self.failUnless(m.deleted) + + def test_comments(self): + u = self.login() + n = u.post_note(text="test_comments") + c = n.add_comment(text="a comment") + self.assertEquals(c['text'], 'a comment') + self.assertEquals(len(n.comments), 1) + c.delete() + n.delete() From 56b1324bfa3ad8c3304d7d3f50f1eec5940cbc2e Mon Sep 17 00:00:00 2001 From: arielbackenroth Date: Thu, 5 May 2011 17:56:58 -0700 Subject: [PATCH 26/26] reenable tests --- test_catchapi.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test_catchapi.py b/test_catchapi.py index ce17b9c..88a6674 100644 --- a/test_catchapi.py +++ b/test_catchapi.py @@ -31,18 +31,18 @@ def login(self, username=None, password=None): return self.api.login(username or self.__class__._username, password or self.__class__._password) - def zest_tags(self): + def test_tags(self): tags = self.login().tags self.failUnless(tags) self.failUnless(isinstance(tags, tuple)) - def zest_notes_property(self): + def test_notes_property(self): # Verify that .notes returns a list of notes greater than 0 from test account with notes in it. notes = self.login().notes assert len(notes) > 0 return notes - def zest_get_note(self): + def test_get_note(self): u = self.login() notes = u.notes note = notes.next() @@ -50,14 +50,14 @@ def zest_get_note(self): self.assertEquals(note['text'], u.get_note(note['id'])['text']) return note - def zest_get_notes(self): + def test_get_notes(self): # Verify that get_notes returns a list of notes greater than 0 from test account with notes in it. u = self.login() notes, count = u.get_notes() self.failUnless(len(notes)) return notes, count - def zest_post_note(self, user=None): + def test_post_note(self, user=None): # Verify posting a note. user = user or self.login() data_before_post = user.notes @@ -67,7 +67,7 @@ def zest_post_note(self, user=None): self.assertEquals(len(data_before_post) + 1, len(data_after_post)) note.delete() - def zest_edit_note(self): + def test_edit_note(self): # Verify editing a note. u = self.login() note = u.post_note("test edit") @@ -77,7 +77,7 @@ def zest_edit_note(self): self.assertEquals(u.get_note(note['id'])['text'], note['text']) note.delete() - def zest_delete_note(self): + def test_delete_note(self): # Verify deleting a note. u = self.login() n = u.post_note(text="test_delete_note") @@ -86,7 +86,7 @@ def zest_delete_note(self): self.failUnless(n.deleted) self.assertEquals(len(data_before_delete) -1, len(u.notes)) - def zest_media(self): + def test_media(self): u = self.login() n = u.post_note(text="test_media") m = n.add_media(os.path.join(os.path.dirname(__file__), 'catch_logo.png'))