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/catch_logo.png b/catch_logo.png new file mode 100644 index 0000000..5a3b0fd Binary files /dev/null and b/catch_logo.png differ diff --git a/catchapi/__init__.py b/catchapi/__init__.py new file mode 100644 index 0000000..7c03cf1 --- /dev/null +++ b/catchapi/__init__.py @@ -0,0 +1,261 @@ +# 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. + +'''A python interface to the Catch API''' + +__author__ = 'ariel@catch.com' +__version__ = '0.5' + +import mimetypes, base64, httplib, urllib, os, sys, urlparse, datetime +import simplejson as json + +class User(dict): + """ + A class representing the User structure used by the Catch API. + """ + + def __init__(self, session, *args, **kwds): + super(User, self).__init__(*args, **kwds) + self._session = session + + @property + def access_token(self): + return self.get('access_token', None) + + @property + 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 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'] + +class Media(dict): + + def __init__(self, user, session, note, *args, **kwds): + self._user = user + self._session = session + self._note = note + super(Media, self).__init__(*args, **kwds) + + @property + def deleted(self): + return self._deleted + + def delete(self): + data = self._session._request("DELETE", "/v2/media/%s/%s.json" % (self._note['id'], 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['media'] = (m for m in self._note['media'] if m is not 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): + self._user = user + self._session = session + self._dirty = False + super(Note, self).__init__(*args, **kwds) + self['media'] = (Media(self._user, self._session, self) for m in self['media']) + + @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 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( + "POST", + "/v2/notes/{id}.json?access_token={token}".format(id=self['id'], + token=self._user.access_token), + body=kwds) + self.update(data['notes'][0]) + + def add_media(self, filename, **opts): + BOUNDARY = '----------ThIs_Is_tHe_bouNdaRY_$' + + def multipart(parts): + L = [] + for (key, fn, value) in parts: + L.append('--' + BOUNDARY) + if fn: + L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, fn)) + else: + L.append('Content-Disposition: form-data; name="%s"' % key) + content_type = mimetypes.guess_type(fn)[0] or 'application/octet-stream' + L.append('Content-Type: %s' % content_type) + L.append('') + L.append(value) + L.append('--' + BOUNDARY + '--') + L.append('') + return '\r\n'.join(L) + + with open(filename) as body: + parts = [('data', filename, body.read())] + parts.extend([(k, None, v) for k, v in opts.iteritems()]) + body = multipart(parts) + + 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): + """ + """ + + def __init__(self, host="https://api.catch.com", timeout=10): + self.host = host + self._timeout = timeout + + @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 + + 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']) + + @property + def _user_agent(self): + return ' '.join(("python", "catch.api-%s" % __version__)) 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/index.txt b/docs/index.txt new file mode 100644 index 0000000..9538bab --- /dev/null +++ b/docs/index.txt @@ -0,0 +1,137 @@ +.. 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. + +python-snaptic +=================================== + +*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-api-quickstart-guide) +which this library implements for python programmers. + +Build instructions +=================== + +**From source:** + +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:: + + easy_install simplejson + +To install, check out the latest version of the snaptic python API and +run ``setup.py`` :: + + git clone git://github.com/snaptic/python-api.git + cd python-api + python setup.py install + +Testing +========== + +**Requirements** + +- Python with JSON support. Python 2.6+ has it built in, Python 2.5 and under + need the ``simplejson`` module. You can install simplejson with the command: + ``easy_install simplejson``. + +- ``nose`` test runner. http://somethingaboutorange.com/mrl/projects/nose/. + +- ``nose-testconfig`` plugin. To install nose-testconfig, run the 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://api.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 + + +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-api + +Documentation +================== + +View the latest python-snaptic API documentation here: + +.. toctree:: + :maxdepth: 2 + + 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-api-quickstart-guide + +Contributors +================== + +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. + + 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 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` + + + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..500bc50 --- /dev/null +++ b/setup.py @@ -0,0 +1,37 @@ +# 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. + +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/snaptic.py b/snaptic.py deleted file mode 100644 index 079dc74..0000000 --- a/snaptic.py +++ /dev/null @@ -1,546 +0,0 @@ -# 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. - - -'''A library that provides a python interface to the Snaptic API''' - -__author__ = 'harry@snaptic.com' -__version__ = '0.4-devel' - -import mimetypes -import base64 -import httplib -import os -import simplejson as json -import sys -from urllib import urlencode -import urlparse - -def Property(func): - return property(**func()) - -class SnapticError(Exception): - '''Base class for Snaptic errors''' - - @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 response(self): - '''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. - - The User structure exposes the following properties: - user.id - user.user_name - ''' - - def __init__(self, id=None, user_name=None, created_at=None, auth_token=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 - def id(self): - return self._id - - @property - def user_name(self): - return self._user_name - - @property - def created_at(self): - return self._created_at - - @property - def auth_token(self): - return self._auth_token - - @property - def email(self): - return self._email - -#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. - - The Image structure exposes the following properties: - 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): - self.type = type - self.md5 = md5 - self.id = id - self.revision_id = revision_id - self.width = width - self.height = height - self.src = src - self.data = data - -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 - ''' - - def __init__(self, created_at, modified_at, reminder_at, note_id, text, - summary, source, source_url, user, children, media = [], labels = [], 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.labels = labels - self.location = location - - @property - def has_media(self): - return len(self.media) > 0 - - @property - def dictionary(self): - ''' - return a dictionary version of 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() - ''' - - API_SERVER = "api.snaptic.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, password=None, url=API_SERVER, use_ssl=True, port=443, timeout=10): - self._url = url - self._use_ssl = use_ssl - self._port = port - self._timeout = timeout - self._user = None - self._notes = None - self._json = None - self.set_credentials(username, password) - - def set_credentials(self, username, password): - - ''' - Set username/password - - Args: - username: snaptic username - 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. - ''' - try: - fin = open(filename, 'r') - data = fin.read() - self.add_image_to_note_with_id(filename, data, id) - except IOError: - raise SnapticError("Error reading filename") - - def add_image_to_note_with_id(self, filename, data, id): - ''' - Add image data to note - ''' - page = "/" + self.API_VERSION + self.API_ENDPOINT_IMAGES + id +".json" - return self._post_multi_part(self._url, page, [("image", filename, data)]) - - 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. - """ - content_type, body = self._encode_multi_part_form_data(files) - handler = httplib.HTTPConnection(host) - headers = self._make_basic_auth_headers(self._username, self._password) - h = { - 'User-Agent': 'INSERT USERAGENTNAME',#Change this to library version? -htormey - 'Content-Type': content_type - } - headers.update(h) - handler.request(self.HTTP_POST, selector, body, headers) - response = handler.getresponse() - data = response.read() - handler.close() - if response.status != 200: - raise SnapticError("Error posting files ", response.status, data) - - 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 - """ - 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): - return mimetypes.guess_type(filename)[0] or 'application/octet-stream' - - def delete_note(self, id):#Change this to just take a note - return self._request(self.HTTP_DELETE, id) - - def edit_note(self, note): - return self._request(self.HTTP_POST, note) - - def post_note(self, note): - 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 - if http_method == self.HTTP_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 + 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 - handle = self._basic_auth_request(page, method=self.HTTP_DELETE) - - response = handle.getresponse() - data = response.read() - handle.close() - - if response.status != 200: - raise SnapticError("Http error posting/editing/deleting note ", response.status, data) - return data - - def get_image_with_id(self, id): - ''' - Get image data using the following id - ''' - url = self.API_ENDPOINT_IMAGES_VIEW + id - return self._fetch_url(url) - - def get_user_id(self): - ''' - Get ID of API user. - ''' - if self._user: - return self._user.id - else: - raise SnapticError("Error user id not set, try calling GetNotes.") - - @Property - def notes(): - 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 cache - ''' - 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. - ''' - json_notes = self.json_cursor(cursor_position) - notes = self._parse_notes(json_notes) - return notes - - 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. - ''' - 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. - ''' - 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: - SnapticError("Error keys missing from source JSON passed to _parse_cursor_info") - - def get_user(self): - ''' - Get user info - ''' - url = "/" + self.API_VERSION + self.API_ENDPOINT_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 - ''' - 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 - ''' - url = "/" + self.API_VERSION + API_ENDPOINT_TAGS_JSON - tags = self._fetch_url(url) - return tags - - def json_cursor(self, cursor_position): - ''' - Return 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. - ''' - 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): - handler = self._basic_auth_request(url) - response = handler.getresponse() - data = response.read() - handler.close() - if response.status != 200: - raise SnapticError("Http error", response.status, data) - return data - - def _make_basic_auth_headers(self, username, password): - if username and 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)) - 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. ''' - h = self._make_basic_auth_headers(self._username, self._password) - 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 snaptic, instantiate a User object from it. - ''' - 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']) - else: - 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. - ''' - notes = [] - json_notes = json.loads(source) - - for note in json_notes['notes']: - media = [] - location = [] - labels = [] - 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 'labels' in note: - labels = [] - for label in note['labels']: - labels.append(label) - 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'], 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)) - return notes diff --git a/test_catchapi.py b/test_catchapi.py new file mode 100644 index 0000000..88a6674 --- /dev/null +++ b/test_catchapi.py @@ -0,0 +1,104 @@ +# 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, os +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)) + note.delete() + + 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 = 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() + 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()