piston-mini-client-0.7.5/0000775000175000017500000000000012113217741016430 5ustar anthonyanthony00000000000000piston-mini-client-0.7.5/doc/0000775000175000017500000000000012113217741017175 5ustar anthonyanthony00000000000000piston-mini-client-0.7.5/doc/tuning.rst0000664000175000017500000001731212005525657021247 0ustar anthonyanthony00000000000000Tuning your API =============== Sometimes the defaults ``piston_mini_client`` provides won't be what you need, or you just need to customize the requests you send out for one part or another of your API. In those cases, ``piston_mini_client`` will strive to stay out of your way. ================ Handling Failure ================ A common issue is what to do if the webservice returns an error. One possible solution (and the default for ``piston-mini-client``) is to raise an exception. This might not be the best solution for everybody, so piston-mini-client allows you to customize the way in which such failures are handled. You can do this by defining a ``FailHandler`` class. This class needs to provide a single method (``handle``), that receives the response headers and body, and decides what to do with it all. It can raise an exception, or modify the body in any way. If it returns a string this will be assumed to be the (possibly fixed) body of the response and will be deserialized by any decorators the method has. To use a different fail handler in your ``PistonAPI`` set the ``fail_handler`` class attribute. For example, to use the ``NoneFailHandler`` instead of the default ``ExceptionFailHandler``, you can use:: class MyAPI(PistonAPI): fail_handler = NoneFailHandler # ... rest of the client definition... ``piston-mini-client`` provides four fail handlers out of the box: * ``ExceptionFailHandler``: The default fail handler, raises ``APIError`` if anything goes wrong * ``NoneFailHandler``: Returns None if anything goes wrong. This will provide no information about *what* went wrong, so only use it if you don't really care. * ``DictFailHandler``: If anything goes wrong it returns a dict with all the request and response headers and body, requested url and method, for you to debug. * ``MultiExceptionFailHandler``: Raises a different exception according to what went wrong. =============================== Talking to dual http/https apis =============================== Often your API provides a set of public calls, and some other calls that are authenticated. Public calls sometimes are heavily used, so we'd like to serve them over http. They're public anyway. Authenticated calls involve some sensitive information passing with the user's credentials, so we like serving those over https. Once you've got all this set up on the server, you can ask piston_mini_client to make each call using the appropriate scheme by using the ``scheme`` optional argument when you call ``_get``, ``_post``, ``_put`` or ``_delete``:: class DualAPI(PistonAPI): default_service_root = 'http://myhostname.com/api/1.0' def public_method(self): return self._get('public_method/', scheme='http') def private_method(self): return self._post('private_method/', scheme='https') def either(self): return self._get('either/') In this case, no matter what scheme the service root uses, calls to ``public_method()`` will result in an http request, and calls to ``private_method()`` will result in an https request. Calls to ``either()`` will leave the scheme alone, so it will follow the scheme used to instantiate the api, or fall back to ``default_service_root``'s scheme. =========================== Customizing request headers =========================== If you need to send custom headers to the server in your requests you can specify these both in the ``PistonAPI`` instance as an instance or class variable, or when you make calls to ``_get``, ``_post``, ``_put`` or ``_delete``. Specifying headers as a class variable will add the same custom headers to all requests made by all instances of the class:: class MyAPI(PistonAPI): extra_headers = {'X-Requested-With': 'XMLHttpRequest'} # ... etc Here these ``extra_headers`` will be added to any and all requests made by ``MyAPI`` instances. You could also specify an extra header for a single instance of ``MyAPI``:: api = MyAPI() api.extra_headers = {'X-Somethingelse': 'dont forget to buy milk'} In this case you'll get this extra header in all requests made by this instance of ``MyAPI``. Finally, you can also pass in an optional ``extra_headers`` argument into each call to ``_get``, ``_post``, ``_put`` or ``_delete``, if only specific api calls need to be provided additional headers:: class MyAPI(PistonAPI): def crumble(self): return self._get('crumble') def icecream(self): return self._get('icecream', extra_headers={'X-secret-sauce': 'chocolate'}) Here calls to ``icecream`` will use the extra special header, but other calls (like ``crumble``) won't. ================================================ Customizing the serializer for each content type ================================================ ``piston_mini_client`` provides a set of default serializers, but sometimes you have your own serialization convention, set by the server, and the client just needs to comply. In that case, you can implement your own serializer and add *an instance* of it to the ``serializers`` class attribute. To define a serializer all you need to provide is a ``serialize`` method, that should take a single ``obj`` argument and return it serialized into a string:: class ReprSerializer(object): def serialize(self, obj): return repr(obj) class MyAPI(PistonAPI): serializers = {'application/json': ReprSerializer()} In this case, any POST/PUT request that goes out with a content type of ``application/json`` will use your ``ReprSerializer`` for serializing its data into the request body. If you need to serialize only arguments of a certain specific API call with this special serializer, you can serialize data before calling ``_post``/``_put``:: class GroceryAPI(PistonAPI): def order(self, shopping_list): serializer = ReprSerializer() self._post('somecall', data=serializer.serialize(shopping_list)) Passing a string into the ``data`` argument skips serialization altogether, so you can apply whichever serialization you want before calling ``_post`` or ``_put``, and ``piston_mini_client`` will avoid double-serializing your request body. ================= Logging to a file ================= If you need to debug the actual requests and responses on the wire, you can initialize a ``PistonAPI`` passing in a ``log_filename`` argument. ``piston_mini_client`` will append all requests and responses, including headers, status code and all, to this file. Also, if you're debugging an application that uses ``piston_mini_client`` but don't want to (or can't) start hacking at the code, you can set ``PISTON_MINI_CLIENT_LOG_FILENAME`` in the environment to point a file, and all ``PistonAPI`` instances will use this location by default. That is, unless they're explicitly being instantiated to log elsewhere. ================= Handling timeouts ================= When you instantiate a ``PistonAPI`` you can provide an optional ``timeout`` argument that will be used as a socket timeout for the requests that instance makes. To explicitly set no timeout, pass in ``timeout=0``. If you leave the default ``timeout=None``, the instance will first check for an environment variable ``PISTON_MINI_CLIENT_DEFAULT_TIMEOUT``, and if that is undefined or invalid, then the class's default timeout will be used; this can be defined by setting a ``default_timeout`` class attribute when writing the API class. Finally, if the class's default timeout is also ``None``, Python's system-wide socket default timeout will be used. You can't currently define timeouts on a per-request basis. If you need to change the timeout used for certain requests, you'll need to use a new ``PistonAPI`` instance. piston-mini-client-0.7.5/doc/index.rst0000664000175000017500000000361412057365531021052 0ustar anthonyanthony00000000000000.. piston_mini_client documentation master file, created by sphinx-quickstart on Mon Nov 15 01:44:53 2010. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Welcome to piston_mini_client's documentation! ============================================== Contents: .. toctree:: :maxdepth: 2 quickstart tuning reference envvars Overview ======== Piston_mini_client is a package that allows you to easily describe an API provided by a Django server using django-piston that takes care of: * Serializing call arguments and deserializing responses from the api. It will deserialize json provided by Piston into light-weight objects. * Making the http calls for you. You should be able to call a method on an api object instead of having to fetch a particular URL. * Provide a in-code description of your API. Developers should be able to know your API by looking at the client code you provide. Piston_mini_client is written with the following principles in mind: * It should have a small set of dependencies. We depend on httplib2 mainly because it provides caching, but you should not need to install a pile of packages just to use a rest client. Other dependencies like ``oauthlib`` and ``socks`` are only imported if you need to use oauth authentication or support proxies, respectively. * Errors should be informative. Backtraces should point you in the right direction to solve your problem. * The client library shouldn't restrict the way you layout your API. Piston_mini_client should be able to work with just about any rest api your server implements. * There should be good documentation available. Clear code is great, and it should be really easy to get started with a couple of simple examples, but there should be documentation available explaining, amongst other things, these principles. :) piston-mini-client-0.7.5/doc/conf.py0000664000175000017500000001573012113211247020475 0ustar anthonyanthony00000000000000# -*- coding: utf-8 -*- # # piston_mini_client documentation build configuration file, created by # sphinx-quickstart on Mon Nov 15 01:44: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.insert(0, os.path.abspath('.')) sys.path.insert(0, os.path.abspath('..')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # 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 = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'piston_mini_client' copyright = u'2010-2011, Canonical Ltd.' # 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.7.5' # The full version, including alpha/beta/rc tags. release = '0.7.5' # 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 patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_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. See the documentation for # a list of builtin themes. 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_domain_indices = 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, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = 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 = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'piston_mini_clientdoc' # -- 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', 'piston_mini_client.tex', u'piston_mini_client Documentation', u'Anthony Lenton', '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 # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = 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_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'piston_mini_client', u'piston_mini_client Documentation', [u'Anthony Lenton'], 1) ] piston-mini-client-0.7.5/doc/reference.rst0000664000175000017500000000214612005525657021700 0ustar anthonyanthony00000000000000Reference ========= ========= Resources ========= .. autoclass:: piston_mini_client.PistonAPI :members: __init__, _get, _post, _put, _delete .. autoclass:: piston_mini_client.PistonResponseObject .. autoclass:: piston_mini_client.PistonSerializable :members: as_serializable =========================== Deserializing response data =========================== These decorators can be applied to your ``PistonAPI`` methods to control how the retrieved data is handled. .. autofunction:: piston_mini_client.returns_json .. autofunction:: piston_mini_client.returns .. autofunction:: piston_mini_client.returns_list_of ========== Validators ========== .. automodule:: piston_mini_client.validators :members: validate_pattern, validate, validate_integer, oauth_protected, basic_protected ============= Fail handlers ============= .. automodule:: piston_mini_client.failhandlers :members: =========== Serializers =========== .. automodule:: piston_mini_client.serializers :members: .. _authentication: ============== Authentication ============== .. automodule:: piston_mini_client.auth :members: piston-mini-client-0.7.5/doc/envvars.rst0000664000175000017500000000233712005525657021430 0ustar anthonyanthony00000000000000Environment variables ===================== These environment variables affect the behaviour of ``piston_mini_client``: * ``PISTON_MINI_CLIENT_DEBUG``: If set, all ``APIError`` exceptions will report the full request and response when printed, not only the headers. * ``PISTON_MINI_CLIENT_DISABLE_SSL_VALIDATION``: If set, ``piston_mini_client`` will ask ``httplib2`` to skip server SSL certificate validation. * ``PISTON_MINI_CLIENT_LOG_FILENAME``: If set, ``piston_mini_client`` will log all requests and responses, including headers, to this location. * ``PISTON_MINI_CLIENT_DEFAULT_TIMEOUT``: Is used as a socket timeout for instances that don't explicitly set a timeout. Should be in seconds. * ``http_proxy`` / ``https_proxy``: ``piston_mini_client`` will check these variables to determine if a proxy should be used for each scheme. The `SocksiPy `_ module is needed for proxy support. A copy is included with ``piston_mini_client``'s code. .. note:: Versions of ``httplib2`` before 0.7.0 don't support SSL certificate validation. If you're using an older version of ``httplib2`` setting ``PISTON_MINI_CLIENT_DISABLE_SSL_VALIDATION`` will have no effect. piston-mini-client-0.7.5/doc/Makefile0000664000175000017500000001103612005525657020646 0ustar anthonyanthony00000000000000# 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 singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man 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 " singlehtml to make a single large HTML file" @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 " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @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." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 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/piston_mini_client.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/piston_mini_client.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/piston_mini_client" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/piston_mini_client" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." make -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 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." piston-mini-client-0.7.5/doc/quickstart.rst0000664000175000017500000001573712005525657022146 0ustar anthonyanthony00000000000000Getting Started =============== All you really need to do is inherit from PistonAPI and define the methods your api provides. Each method can specify what arguments it takes, what authentication method should be used, and how to process the response. ================= One simple method ================= Say your server's urls.py defines a single pattern for the web API like:: urlpatterns = patterns('', url(r'^myapicall$', myapicall_resource), ) Then all you'll need to define your client code is something like:: from piston_mini_client import PistonAPI, returns_json class MyAPI(PistonAPI): default_service_root = 'http://myhostname.com/api/1.0' @returns_json def myapicall(self): return self._get('myapicall') And that's it. Some things worth noticing here: * Your api client class should provide a default service root, that will be used if instantiated with no service root. When instantiating ``MyAPI`` your users will be able to override this. * The default service root should usually include any path needed to reach your API urls.py. For example, in this case you can imagine the project's ``urls.py`` has a line like:: urlpatterns = patterns('', (r'^api/1.0/', include('myproject.api.urls')), ) * ``@returns_json`` tells ``piston_mini_client`` that the data returned by the server will be encoded with json, and so it will be decoded accordingly into Python data structures. * PistonAPI provides ``_get``, ``_post``, ``_put`` and ``_delete`` methods to take care of making the lower level http calls via ``httplib2``. ====================================== Validating arguments to your API calls ====================================== If your server's ``urls.py`` specifies placeholders for resource arguments, as in:: urlpatterns = patterns('', url(r'^foo/(?P[^/]+)/(?P\d+)/frobble$', myapicall_resource), ) You can validate arguments on the client side to avoid unnecessary server roundtrips with:: from piston_mini_client import PistonAPI from piston_mini_client.validators import validate_pattern class MyAPI(PistonAPI): default_service_root = 'http://myhostname.com/api/1.0' @validate_pattern('foobar_id', r'\d+') @validate_pattern('language', r'[^/]+', required=False) def myapicall(self, foobar_id, language=None): if language is None: language = 'en' return self._get('foo/%s/%s/frobble' % (language, foobar_id)) The ``validate_pattern`` decorator checks that the necessary keyword argument is provided and that it matches the pattern that the server is then going to match against. If you specify ``required=False`` the argument can be omitted, in which case the client code should take care of providing a sane default value. You could also have used the ``validate`` validator in this case that just checks that a keyword argument is of a certain type:: from piston_mini_client import PistonAPI from piston_mini_client.validators import validate, validate_pattern class MyAPI(PistonAPI): default_service_root = 'http://myhostname.com/api/1.0' @validate('foobar_id', int) @validate_pattern('language', r'[^/]+', required=False) def myapicall(self, foobar_id, language=None): if language is None: language = 'en' return self._get('foo/%s/%s/frobble' % (language, foobar_id)) Then again, if we use this we'd need to then ensure that ``foobar_id >= 0``. ============================================== Getting back light-weight objects from the API ============================================== If your api handlers return JSON, your api handlers can easily specify that the response should be parsed into small objects that resemble your server-side models. For example if your ``handlers.py`` on the server contains:: class FooBarHandler(BaseHandler): model = FooBar fields = ( 'name', 'length', ) allowed_methods = ('GET',) def read(self, request, foobar_id): try: return FooBar.objects.get(pk=foobar_id) except FooBar.DoesNotExist: return rc.NOT_FOUND Then, assuming the right url matches this handler in your ``urls.py``, your Piston client code could use something like:: from piston_mini_client import PistonResponse, returns class FooBarResponse(PistonResponse): def __str__(self): return '' % self.name class MyAPI(PistonAPI): default_service_root = 'http://myhostname.com/api/1.0' @validate('foobar_id', int) @returns(FooBarResponse) def get_foobar(self, foobar_id): return self._get('foobar/%s/' % foobar_id) ...and calls to ``api.get_foobar()`` will return a ``FooBarResponse``, that will have the right ``name`` and ``length`` attributes. Note that we could have just skipped the definition of ``FooBarResponse`` and specified ``@returns(PistonResponse)`` but it might be nice to be able to print one of these responses and get a meaningful output, or we might want to attach some other method to ``FooBarResponse``. =========================================== Passing light-weight objects into API calls =========================================== Same as receiving light-weight objects as responses, ``piston_mini_client`` defines a way to pass in light-weight objects to your API calls, and have them serialized accordingly. When calling ``_post`` or ``_put`` you need to pass in a ``data`` argument, that will be serialized into the body of the request. This can tipically be a string (in which case no serialization is performed) or a simple Python data structure (list, dict, tuple, etc...), but you can also pass in any object with a ``as_serializable`` method. ``PistonSerializable`` is one such class, that allows you to easily define a set of attributes that will be serialized into a request's body:: class AccountRequest(PistonSerializable): _atts = ('username', 'password', 'fullname') class RegistrationAPI(PistonAPI): @validate('account', AccountRequest) def register(self, account): self._post('register', data=account) After defining this light-weight ``AccountRequest`` class you can set it up when you instantiate it or by assigning attributes:: account = AccountRequest(username='joeb', password='easy123') account.fullname = 'Joe Blogs' api.register(account) ``PistonSerializable`` will take care of checking that all the needed attributes have been set when it's serialized, and complain otherwise. All attributes will be packed into a dictionary for serializing. If you have special serialization needs you can redefine the ``as_serializable`` method, or use your own light-weight object entirely. All you need to provide is an ``as_serializable`` method to ensure it works with ``piston_mini_client``'s serialization mechanism. piston-mini-client-0.7.5/setup.cfg0000664000175000017500000000021312113217741020245 0ustar anthonyanthony00000000000000[nosetests] verbosity = 2 with-coverage = 1 cover-package = piston_mini_client [egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 piston-mini-client-0.7.5/piston_mini_client/0000775000175000017500000000000012113217741022316 5ustar anthonyanthony00000000000000piston-mini-client-0.7.5/piston_mini_client/tests/0000775000175000017500000000000012113217741023460 5ustar anthonyanthony00000000000000piston-mini-client-0.7.5/piston_mini_client/tests/test_pep8.py0000664000175000017500000000113412057365531025754 0ustar anthonyanthony00000000000000import os import subprocess import unittest import piston_mini_client class PackagePep8TestCase(unittest.TestCase): packages = [piston_mini_client] exclude = ['socks.py'] # Leave 3rd party dep. alone. def test_all_code(self): res = 0 py_files = [] for package in self.packages: py_files.append(os.path.dirname(package.__file__)) res += subprocess.call( ["pep8", "--repeat", "--exclude", "".join(self.exclude)] + py_files) self.assertEqual(res, 0) if __name__ == "__main__": unittest.main() piston-mini-client-0.7.5/piston_mini_client/tests/test_resource.py0000664000175000017500000004730312111514240026717 0ustar anthonyanthony00000000000000# -*- coding: utf-8 -*- # Copyright 2010-2012 Canonical Ltd. This software is licensed under the # GNU Lesser General Public License version 3 (see the file LICENSE). import os import sys from mock import patch from unittest import TestCase import httplib2 import shutil import tempfile from piston_mini_client import ( APIError, PistonAPI, returns_json, returns, returns_list_of, PistonResponseObject, PistonSerializable, OfflineModeException, safename) from piston_mini_client.auth import BasicAuthorizer class PistonAPITestCase(TestCase): class CoffeeAPI(PistonAPI): default_service_root = 'http://localhost:12345' def brew(self): self._get('/brew') @patch('httplib2.Http.request') def test_request(self, mock_request): mock_request.return_value = ({'status': '200'}, 'hello world!') api = self.CoffeeAPI() api._request('/foo', 'POST', body='foo=bar') mock_request.assert_called_with( 'http://localhost:12345/foo', body='foo=bar', headers={}, method='POST') @patch('httplib2.Http.request') def test_request_cached(self, mock_request): path = "/foo" # setup mock cache tmpdir = tempfile.mkdtemp() http = httplib2.Http(cache=tmpdir) cachekey = self.CoffeeAPI.default_service_root + path http.cache.set( cachekey, "header\r\n\r\nmy_cached_body\n".encode("utf-8")) # ensure that we trigger a error like when offline (no dns) mock_request.side_effect = httplib2.ServerNotFoundError("") api = self.CoffeeAPI(cachedir=tmpdir, offline_mode=True) res = api._request(path, 'GET') # check that we get the data we expect self.assertEqual(res, "my_cached_body\n") # check for nonexisting url res = api._request('/bar', 'GET') self.assertEqual(res, None) # ensure errors on POST, PUT self.assertRaises(OfflineModeException, api._request, path, 'POST') self.assertRaises(OfflineModeException, api._request, path, 'PUT') # cleanup shutil.rmtree(tmpdir) @patch('httplib2.Http.request') def test_request_cached_long_names(self, mock_request): # construct a really long path that triggers our safename code path = "/foo_with_a_" + 30 * "long_name" self.assertTrue(len(path) > 143) # setup mock cache tmpdir = tempfile.mkdtemp() cache = httplib2.FileCache(tmpdir, safe=safename) http = httplib2.Http(cache=cache) cachekey = self.CoffeeAPI.default_service_root + path http.cache.set( cachekey.encode('utf-8'), "header\r\n\r\nmy_cached_body_from_long_path\n".encode('utf-8')) # ensure that we trigger a error like when offline (no dns) mock_request.side_effect = httplib2.ServerNotFoundError("") api = self.CoffeeAPI(cachedir=tmpdir, offline_mode=True) res = api._request(path, 'GET') # check that we get the data we expect self.assertEqual(res, "my_cached_body_from_long_path\n") # cleanup shutil.rmtree(tmpdir) @patch('httplib2.Http.request') def test_auth_request(self, mock_request): mock_request.return_value = ({'status': '200'}, '""') api = self.CoffeeAPI(auth=BasicAuthorizer(username='foo', password='bar')) api._request('/fee', 'GET') kwargs = mock_request.call_args[1] self.assertEqual(kwargs['headers']['Authorization'], 'Basic Zm9vOmJhcg==') self.assertEqual(kwargs['method'], 'GET') @patch('httplib2.Http.request') def test_post_no_content_type(self, mock_request): mock_request.return_value = ({'status': '200'}, '""') api = self.CoffeeAPI() api._post('/serve', data={'foo': 'bar'}) kwargs = mock_request.call_args[1] self.assertEqual(kwargs['headers']['Content-Type'], 'application/json') self.assertEqual(kwargs['method'], 'POST') @patch('httplib2.Http.request') def test_post_piston_serializable(self, mock_request): mock_request.return_value = ({'status': '200'}, '""') class MyCoffeeRequest(PistonSerializable): _atts = ('strength',) api = self.CoffeeAPI() api._post('/serve', data=MyCoffeeRequest(strength='mild')) kwargs = mock_request.call_args[1] self.assertEqual(kwargs['headers']['Content-Type'], 'application/json') self.assertEqual(kwargs['method'], 'POST') @patch('httplib2.Http.request') def test_post_explicit_content_type(self, mock_request): mock_request.return_value = ({'status': '200'}, '""') api = self.CoffeeAPI() api._post('/serve', data={'foo': 'bar'}, content_type='application/x-www-form-urlencoded') kwargs = mock_request.call_args[1] self.assertEqual(kwargs['headers']['Content-Type'], 'application/x-www-form-urlencoded') self.assertEqual(kwargs['method'], 'POST') @patch('httplib2.Http.request') def test_get_no_args(self, mock_request): mock_request.return_value = ({'status': '200'}, '""') api = self.CoffeeAPI() api._get('/stew') args, kwargs = mock_request.call_args self.assertTrue(args[0].endswith('/stew')) self.assertEqual(kwargs['method'], 'GET') @patch('httplib2.Http.request') def test_get_with_args(self, mock_request): mock_request.return_value = ({'status': '200'}, '""') api = self.CoffeeAPI() api._get('/stew', args={'foo': 'bar'}) args, kwargs = mock_request.call_args self.assertTrue(args[0].endswith('/stew?foo=bar')) self.assertEqual(kwargs['method'], 'GET') @patch('httplib2.Http.request') def test_valid_status_codes_dont_raise_exception(self, mock_request): for status in ['200', '201', '304']: response = {'status': status} expected_body = '"hello world!"' mock_request.return_value = (response, expected_body) api = self.CoffeeAPI() body = api._get('/simmer') self.assertEqual(expected_body, body) mock_request.assert_called_with('http://localhost:12345/simmer', body='', headers={}, method='GET') @patch('httplib2.Http.request') def test_get_with_extra_args(self, mock_request): mock_request.return_value = ({'status': '200'}, '""') api = self.CoffeeAPI() api._get('/stew?zot=ping', args={'foo': 'bar'}) args, kwargs = mock_request.call_args self.assertTrue(args[0].endswith('/stew?zot=ping&foo=bar')) self.assertEqual(kwargs['method'], 'GET') def test_path2url_with_no_ending_slash(self): resource = PistonAPI('http://example.com/api') expected = 'http://example.com/api/frobble' self.assertEqual(expected, resource._path2url('frobble')) def test_path2url_with_ending_slash(self): resource = PistonAPI('http://example.com/api/') expected = 'http://example.com/api/frobble' self.assertEqual(expected, resource._path2url('frobble')) def test_instantiation_fails_with_no_service_root(self): try: self.CoffeeAPI.default_service_root = None self.assertRaises(ValueError, self.CoffeeAPI) finally: self.CoffeeAPI.default_service_root = 'http://localhost:12345' def test_instantiation_fails_with_invalid_scheme(self): self.assertRaises(ValueError, self.CoffeeAPI, 'ftp://foobar.baz') @patch('httplib2.Http.request') def test_request_scheme_switch_to_https(self, mock_request): mock_request.return_value = ({'status': '200'}, '""') api = self.CoffeeAPI() api._request('/foo', 'GET', scheme='https') mock_request.assert_called_with('https://localhost:12345/foo', body='', headers={}, method='GET') @patch('httplib2.Http.request') def test_get_scheme_switch_to_https(self, mock_request): mock_request.return_value = ({'status': '200'}, '""') api = self.CoffeeAPI() api._get('/foo', scheme='https') mock_request.assert_called_with('https://localhost:12345/foo', body='', headers={}, method='GET') @patch('httplib2.Http.request') def test_post_scheme_switch_to_https(self, mock_request): mock_request.return_value = ({'status': '200'}, '""') api = self.CoffeeAPI() api._post('/foo', scheme='https') mock_request.assert_called_with( 'https://localhost:12345/foo', body='null', headers={'Content-Type': 'application/json'}, method='POST') @patch('httplib2.Http.request') def test_put_no_data(self, mock_request): mock_request.return_value = ({'status': '200'}, '""') api = self.CoffeeAPI() api._put('/serve') kwargs = mock_request.call_args[1] self.assertEqual(kwargs['body'], 'null') self.assertEqual(kwargs['method'], 'PUT') @patch('httplib2.Http.request') def test_put_no_content_type(self, mock_request): mock_request.return_value = ({'status': '200'}, '""') api = self.CoffeeAPI() api._put('/serve', data={'foo': 'bar'}) kwargs = mock_request.call_args[1] self.assertEqual(kwargs['headers']['Content-Type'], 'application/json') self.assertEqual(kwargs['method'], 'PUT') @patch('httplib2.Http.request') def test_put_piston_serializable(self, mock_request): mock_request.return_value = ({'status': '200'}, '""') class MyCoffeeRequest(PistonSerializable): _atts = ('strength',) api = self.CoffeeAPI() api._put('/serve', data=MyCoffeeRequest(strength='mild')) kwargs = mock_request.call_args[1] self.assertEqual(kwargs['headers']['Content-Type'], 'application/json') self.assertEqual(kwargs['method'], 'PUT') @patch('httplib2.Http.request') def test_put_no_scheme(self, mock_request): mock_request.return_value = ({'status': '200'}, '""') api = self.CoffeeAPI() api._put('/serve') args, kwargs = mock_request.call_args self.assertTrue(args[0].startswith('http://')) self.assertEqual(kwargs['method'], 'PUT') @patch('httplib2.Http.request') def test_put_scheme_switch_to_https(self, mock_request): mock_request.return_value = ({'status': '200'}, '""') api = self.CoffeeAPI() api._put('/foo', scheme='https') mock_request.assert_called_with( 'https://localhost:12345/foo', body='null', headers={'Content-Type': 'application/json'}, method='PUT') @patch('httplib2.Http.request') def test_put(self, mock_request): mock_request.return_value = ({'status': '200'}, '""') api = self.CoffeeAPI() api._put('/serve', data={'foo': 'bar'}, content_type='application/x-www-form-urlencoded') kwargs = mock_request.call_args[1] self.assertEqual(kwargs['body'], 'foo=bar') self.assertEqual(kwargs['headers']['Content-Type'], 'application/x-www-form-urlencoded') self.assertEqual(kwargs['method'], 'PUT') @patch('httplib2.Http.request') def test_customize_headers_on_instance(self, mock_request): mock_request.return_value = ({'status': '200'}, '""') api = self.CoffeeAPI() api.extra_headers = {'X-Foo': 'bar'} api._get('/foo') expected_headers = {'X-Foo': 'bar'} mock_request.assert_called_with( 'http://localhost:12345/foo', body='', headers=expected_headers, method='GET') api._delete('/foo') mock_request.assert_called_with( 'http://localhost:12345/foo', body='', headers=expected_headers, method='DELETE') expected_headers['Content-Type'] = 'application/json' api._post('/foo') mock_request.assert_called_with( 'http://localhost:12345/foo', body='null', headers=expected_headers, method='POST') api._put('/foo') mock_request.assert_called_with( 'http://localhost:12345/foo', body='null', headers=expected_headers, method='PUT') @patch('httplib2.Http.request') def test_customize_headers_on_method_call(self, mock_request): mock_request.return_value = ({'status': '200'}, '""') api = self.CoffeeAPI() api._get('/foo', extra_headers={'X-Foo': 'bar'}) expected_headers = {'X-Foo': 'bar'} mock_request.assert_called_with( 'http://localhost:12345/foo', body='', headers=expected_headers, method='GET') api._delete('/foo', extra_headers={'X-Foo': 'bar'}) mock_request.assert_called_with( 'http://localhost:12345/foo', body='', headers=expected_headers, method='DELETE') expected_headers['Content-Type'] = 'application/json' api._post('/foo', extra_headers={'X-Foo': 'bar'}) mock_request.assert_called_with( 'http://localhost:12345/foo', body='null', headers=expected_headers, method='POST') api._put('/foo', extra_headers={'X-Foo': 'bar'}) mock_request.assert_called_with( 'http://localhost:12345/foo', body='null', headers=expected_headers, method='PUT') @patch('httplib2.Http.request') def test_customize_serializer(self, mock_request): mock_request.return_value = ({'status': '200'}, '""') expected = "serialized!" class MySerializer(object): def serialize(self, obj): return expected api = self.CoffeeAPI() api.serializers = {'application/json': MySerializer()} api._post('/foo', data=[]) mock_request.assert_called_with( 'http://localhost:12345/foo', body=expected, headers={'Content-Type': 'application/json'}, method='POST') api._put('/foo', data=None) mock_request.assert_called_with( 'http://localhost:12345/foo', body=expected, headers={'Content-Type': 'application/json'}, method='PUT') @patch('httplib2.Http.request') def test_delete_no_scheme(self, mock_request): mock_request.return_value = ({'status': '200'}, '""') api = self.CoffeeAPI() api._delete('/roast') args, kwargs = mock_request.call_args self.assertTrue(args[0].startswith('http://')) self.assertEqual(kwargs['method'], 'DELETE') @patch('httplib2.Http.request') def test_delete_scheme_switch_to_https(self, mock_request): mock_request.return_value = ({'status': '200'}, '""') api = self.CoffeeAPI() api._delete('/sugar/12', scheme='https') mock_request.assert_called_with('https://localhost:12345/sugar/12', body='', headers={}, method='DELETE') def test_cachedir_crash_race_lp803280(self): def _simulate_race(path): """ this helper simulates the actual race when after os.path.exists() a different process creates the dir """ patcher.stop() os.makedirs(path) # this simulates a race when multiple piston-mini-client helpers # try to create the cachedir at the same time (LP: #803280) patcher = patch('os.path.exists') mock = patcher.start() mock.return_value = False mock.side_effect = _simulate_race tmpdir = os.path.join(tempfile.mkdtemp(), "foo") api = self.CoffeeAPI(cachedir=tmpdir) self.assertNotEqual(api, None) class PistonResponseObjectTestCase(TestCase): def test_from_response(self): obj = PistonResponseObject.from_response('{"foo": "bar"}') self.assertEqual('bar', obj.foo) def test_from_dict(self): obj = PistonResponseObject.from_dict({"foo": "bar"}) self.assertEqual('bar', obj.foo) class ReturnsJSONTestCase(TestCase): def test_returns_json(self): class MyAPI(PistonAPI): default_service_root = 'http://foo' @returns_json def func(self): return '{"foo": "bar", "baz": 42}' result = MyAPI().func() self.assertEqual({"foo": "bar", "baz": 42}, result) def test_returns_json_error(self): # If the method does not return JSON, we raise a ValueError saying # what it did return. not_json = 'This cannot be valid JSON' class MyAPI(PistonAPI): default_service_root = 'http://foo' @returns_json def its_a_lie(self): return not_json # If we used unittest2 or testtools, we wouldn't need this. try: result = MyAPI().its_a_lie() except APIError: # This is the Python 2 & 3 compatible way of getting an exception. exc_type, exc_value, tb = sys.exc_info() self.assertEqual(not_json, exc_value.body) else: self.fail( "MyAPI().its_a_lie did not raise. Returned %r instead" % (result,)) class ReturnsTestCase(TestCase): def test_returns(self): class MyAPI(PistonAPI): default_service_root = 'http://foo' @returns(PistonResponseObject) def func(self): return '{"foo": "bar", "baz": 42}' result = MyAPI().func() self.assertTrue(isinstance(result, PistonResponseObject)) def test_returns_none_allowed(self): class MyAPI(PistonAPI): default_service_root = 'http://foo' @returns(PistonResponseObject, none_allowed=True) def func(self): return 'null' result = MyAPI().func() self.assertEqual(result, None) def test_returns_none_allowed_normal_response(self): class MyAPI(PistonAPI): default_service_root = 'http://foo' @returns(PistonResponseObject, none_allowed=True) def func(self): return '{"foo": "bar", "baz": 42}' result = MyAPI().func() self.assertTrue(isinstance(result, PistonResponseObject)) class ReturnsListOfTestCase(TestCase): def test_returns(self): class MyAPI(PistonAPI): default_service_root = 'http://foo' @returns_list_of(PistonResponseObject) def func(self): return '[{"foo": "bar"}, {"baz": 42}]' result = MyAPI().func() self.assertEqual(2, len(result)) self.assertEqual('bar', result[0].foo) self.assertEqual(42, result[1].baz) class PistonSerializableTestCase(TestCase): class MySerializable(PistonSerializable): _atts = ('foo',) def test_init_with_extra_variables(self): obj = self.MySerializable(foo='bar', baz=42) self.assertEqual('bar', obj.foo) self.assertEqual(42, obj.baz) def test_init_with_missing_variables(self): obj = self.MySerializable() self.assertFalse(hasattr(obj, 'foo')) def test_missing_required_arguments(self): obj = self.MySerializable() self.assertRaises(ValueError, obj.as_serializable) def test_can_assign_required_arguments_after_init(self): obj = self.MySerializable() obj.foo = 'bar' self.assertEqual({'foo': 'bar'}, obj.as_serializable()) def test_extra_args_arent_serialized(self): obj = self.MySerializable(foo='bar', baz=42) self.assertEqual({'foo': 'bar'}, obj.as_serializable()) def test__as_serializable(self): """_as_serializable should still work, although it's deprecated.""" obj = self.MySerializable(foo='bar') self.assertEqual({'foo': 'bar'}, obj._as_serializable()) if __name__ == "__main__": import unittest unittest.main() piston-mini-client-0.7.5/piston_mini_client/tests/test_log_to_file.py0000664000175000017500000000534412111514240027351 0ustar anthonyanthony00000000000000# -*- coding: utf-8 -*- # Copyright 2010-2012 Canonical Ltd. This software is licensed under the # GNU Lesser General Public License version 3 (see the file LICENSE). import os from mock import patch from tempfile import NamedTemporaryFile from unittest import TestCase from piston_mini_client import ( PistonAPI, returns_json, ) from piston_mini_client.consts import LOG_FILENAME_ENVVAR class AnnoyAPI(PistonAPI): default_service_root = 'http://test.info/api/1.0/' @returns_json def poke(self, data=None): return self._post('/poke/', data=data, extra_headers={'location': 'ribs'}) class LogToFileTestCase(TestCase): @patch('httplib2.Http.request') def test_requests_are_dumped_to_file(self, mock_request): mock_request.return_value = ({'status': '201', 'x-foo': 'bar'}, '"Go away!"') with NamedTemporaryFile() as fp: api = AnnoyAPI(log_filename=fp.name) api.poke([1, 2, 3]) fp.seek(0) data = fp.read() lines = data.decode("utf-8").split('\n') self.assertEqual(10, len(lines)) self.assertTrue(lines[0].endswith( 'Request: POST http://test.info/api/1.0/poke/')) self.assertEqual( set(['Content-Type: application/json', 'location: ribs', '', '[1, 2, 3]']), set(lines[1:5])) self.assertTrue(lines[5].endswith('Response: 201')) self.assertEqual( set(['x-foo: bar', '', '"Go away!"', '']), set(lines[6:])) @patch('httplib2.Http.request') def test_invalid_logfile_location_doesnt_fail(self, mock_request): mock_request.return_value = ({'status': '201'}, '"Go away!"') unlikely_path = 'two/pangolins/walk/into/a/bar/....../so?' self.assertFalse(os.path.exists(unlikely_path)) api = AnnoyAPI(log_filename=unlikely_path) response = api.poke() self.assertEqual('Go away!', response) @patch('httplib2.Http.request') def test_perms_issue_doesnt_fail(self, mock_request): mock_request.return_value = ({'status': '201'}, '"Go away!"') forbidden_path = '/usr/bin/bash' self.assertRaises(IOError, open, forbidden_path, 'a') api = AnnoyAPI(log_filename=forbidden_path) response = api.poke() self.assertEqual('Go away!', response) @patch('os.environ.get') def test_log_filename_is_fetched_from_env(self, mock_get): """Check that log_filename is initialized from the environment""" sentinel = object() mock_get.return_value = sentinel api = AnnoyAPI() self.assertEqual(api.log_filename, sentinel) mock_get.assert_any_call(LOG_FILENAME_ENVVAR) piston-mini-client-0.7.5/piston_mini_client/tests/__init__.py0000664000175000017500000000000012005525657025567 0ustar anthonyanthony00000000000000piston-mini-client-0.7.5/piston_mini_client/tests/test_timeout.py0000664000175000017500000000535512006004046026560 0ustar anthonyanthony00000000000000# -*- coding: utf-8 -*- # Copyright 2010-2012 Canonical Ltd. This software is licensed under the # GNU Lesser General Public License version 3 (see the file LICENSE). import socket from unittest import TestCase from mock import patch from piston_mini_client import PistonAPI from piston_mini_client.failhandlers import APIError class LazyAPI(PistonAPI): default_service_root = 'http://test.info/api/1.0/' default_timeout = 42 def sleep(self, amount): return self._get('/snooze/%s/' % amount) class TimeoutTestCase(TestCase): @patch('os.environ.get') @patch('httplib2.Http') def test_timeout_in_constructor_wins(self, mock_http, mock_get): mock_get.return_value = '3.14' api = LazyAPI(timeout=1) self.assertEqual(1, api._timeout) mock_http.assert_called_with(cache=None, proxy_info=None, timeout=1, disable_ssl_certificate_validation=True) @patch('os.environ.get') @patch('httplib2.Http') def test_timeout_in_env_beats_class_default(self, mock_http, mock_get): mock_get.return_value = '3.14' api = LazyAPI() self.assertEqual(3.14, api._timeout) mock_http.assert_called_with(cache=None, proxy_info=None, timeout=3.14, disable_ssl_certificate_validation=True) @patch('os.environ.get') @patch('httplib2.Http') def test_no_envvar_falls_back_to_class_default(self, mock_http, mock_get): mock_get.return_value = None api = LazyAPI() self.assertEqual(42, api._timeout) mock_http.assert_called_with(cache=None, proxy_info=None, timeout=42) @patch('os.environ.get') @patch('httplib2.Http') def test_no_nothing_falls_back_to_system_default(self, mock_http, mock_get): class DefaultAPI(PistonAPI): default_service_root = 'http://test.info/api/1.0/' mock_get.return_value = None api = DefaultAPI() self.assertEqual(None, api._timeout) mock_http.assert_called_with(cache=None, proxy_info=None, timeout=None) @patch('os.environ.get') @patch('httplib2.Http') def test_invalid_envvar_uses_class_default(self, mock_http, mock_get): mock_get.return_value = 'invalid' api = LazyAPI() self.assertEqual(42, api._timeout) mock_http.assert_called_with(cache=None, proxy_info=None, timeout=42, disable_ssl_certificate_validation=True) @patch('httplib2.HTTPConnectionWithTimeout.connect') def test_timeout_is_handled_by_failhandler(self, mock_connect): mock_connect.side_effect = socket.timeout api = LazyAPI() self.assertRaises(APIError, api.sleep, 2) piston-mini-client-0.7.5/piston_mini_client/tests/test_failhandlers.py0000664000175000017500000003145212111514240027522 0ustar anthonyanthony00000000000000# -*- coding: utf-8 -*- # Copyright 2010-2012 Canonical Ltd. This software is licensed under the # GNU Lesser General Public License version 3 (see the file LICENSE). from mock import patch from unittest import TestCase from piston_mini_client import ( PistonAPI, PistonResponseObject, returns, returns_json, returns_list_of, ) from piston_mini_client.failhandlers import ( APIError, BadRequestError, DictFailHandler, ExceptionFailHandler, ForbiddenError, format_response, InternalServerErrorError, MultiExceptionFailHandler, NoneFailHandler, NotFoundError, ServiceUnavailableError, UnauthorizedError, ) from piston_mini_client.consts import DEBUG_ENVVAR class GardeningAPI(PistonAPI): """Just a dummy API so we can play around with""" fail_handler = NoneFailHandler default_service_root = 'http://localhost:12345' @returns_json def grow(self): return self._post('/grow', {'plants': 'all'}) @returns(PistonResponseObject) def get_plant(self): return self._get('/plant') @returns_list_of(PistonResponseObject) def get_plants(self): return self._get('/plant') class APIErrorTestCase(TestCase): def test_default_repr(self): """Check that usually only msg is printed out""" err = APIError(msg='foo', body='bar') err.debug = False self.assertEqual('foo', str(err)) def test_body_in_verbose_repr(self): """Check that body is also included if in verbose mode""" err = APIError(msg='foo', body='bar') err.debug = True self.assertTrue('bar' in str(err)) def test_data_in_verbose_repr(self): """Check that request data is also included if in verbose mode""" data = { 'url': 'foo', 'method': 'PIDGEON', 'request_body': 'bla', 'headers': {'X-SpamLevel': '100'}, } err = APIError(msg='foo', data=data) err.debug = True output = str(err) for key, value in data.items(): if key == 'headers': for k, v in value.items(): self.assertTrue("%s: %s" % (k, v) in output) else: self.assertTrue(value in output) def test_response_in_verbose_repr(self): data = { 'url': 'foo', 'method': 'PIDGEON', 'request_body': 'bla', 'headers': {'X-SpamLevel': '100'}, 'response': {'status': 200, 'Boo-Yah': 'Waka-waka'}, } body = 'This is my body' err = APIError(msg='foo', body=body, data=data) err.debug = True output = str(err) self.assertTrue( format_response(data['response'], body) in output) @patch('os.environ.get') def test_debug_gets_set_from_environment(self, mock_get): """Check that debug is initialized from the environment""" sentinel = object() mock_get.return_value = sentinel err = APIError('foo') self.assertEqual(err.debug, sentinel) mock_get.assert_called_with(DEBUG_ENVVAR, False) class ExceptionFailHandlerTestCase(TestCase): """As this is the default fail handler, we can skip most tests""" def test_no_status(self): """Check that an exception is raised if no status in response""" handler = ExceptionFailHandler('/foo', 'GET', '', {}) self.assertRaises(APIError, handler.handle, {}, '') def test_bad_status_codes(self): """Check that APIError is raised if bad status codes are returned""" bad_status = ['404', '500', '401'] handler = ExceptionFailHandler('/foo', 'GET', '', {}) for status in bad_status: self.assertRaises(APIError, handler.handle, {'status': status}, '') class NoneFailHandlerTestCase(TestCase): def test_no_status(self): handler = NoneFailHandler('/foo', 'GET', '', {}) self.assertEqual(None, handler.handle({}, 'not None')) def test_bad_status_codes(self): """Check that None is returned if bad status codes are returned""" bad_status = ['404', '500', '401'] handler = NoneFailHandler('/foo', 'GET', '', {}) for status in bad_status: self.assertEqual(None, handler.handle({'status': status}, '')) @patch('httplib2.Http.request') def test_interacts_well_with_returns_json_on_fail(self, mock_request): """Check that NoneFailHandler interacts well with returns_json""" mock_request.return_value = {'status': '500'}, 'invalid json' api = GardeningAPI() self.assertEqual(None, api.grow()) def test_set_via_class_variable(self): """fail_handler can be overridden by specifying it as a class variable. """ api = GardeningAPI() self.assertEqual(NoneFailHandler, api.fail_handler) def test_forwarding(self): """Setting fail_handler on a PistonAPI instance actually works.""" sentinel = object() api = GardeningAPI() api.fail_handler = sentinel self.assertEqual(sentinel, api.fail_handler) # Not a public API, so OK if future changes break this. self.assertEqual(sentinel, api._requester._fail_handler) @patch('httplib2.Http.request') def test_interacts_well_with_returns_on_fail(self, mock_request): """Check that NoneFailHandler interacts well with returns""" mock_request.return_value = {'status': '500'}, 'invalid json' api = GardeningAPI() self.assertEqual(None, api.get_plant()) @patch('httplib2.Http.request') def test_interacts_well_with_returns_list_of_on_fail(self, mock_request): """Check that NoneFailHandler interacts well with returns_list_of""" mock_request.return_value = {'status': '500'}, 'invalid json' api = GardeningAPI() self.assertEqual(None, api.get_plants()) @patch('httplib2.Http.request') def test_interacts_well_with_returns_json(self, mock_request): """Check that NoneFailHandler interacts well with returns_json""" mock_request.return_value = {'status': '200'}, '{"foo": "bar"}' api = GardeningAPI() self.assertEqual({'foo': 'bar'}, api.grow()) @patch('httplib2.Http.request') def test_interacts_well_with_returns(self, mock_request): """Check that NoneFailHandler interacts well with returns""" mock_request.return_value = {'status': '200'}, '{"foo": "bar"}' api = GardeningAPI() self.assertTrue(isinstance(api.get_plant(), PistonResponseObject)) @patch('httplib2.Http.request') def test_interacts_well_with_returns_list_of(self, mock_request): """Check that NoneFailHandler interacts well with returns_list_of""" mock_request.return_value = {'status': '200'}, '[]' api = GardeningAPI() self.assertEqual([], api.get_plants()) class DictFailHandlerTestCase(TestCase): def setUp(self): self.response = {'status': '500'} self.body = 'invalid json' self.expected = { 'url': '/foo', 'method': 'GET', 'request_headers': {}, 'request_body': '', 'response': self.response, 'response_body': self.body, } self.api = GardeningAPI() self.api.fail_handler = DictFailHandler def test_no_status(self): handler = DictFailHandler('/foo', 'GET', '', {}) del self.response['status'] self.assertEqual(self.expected, handler.handle({}, self.body)) def test_bad_status_codes(self): bad_status = ['404', '500', '401'] handler = DictFailHandler('/foo', 'GET', '', {}) for status in bad_status: self.response['status'] = status self.assertEqual(self.expected, handler.handle( self.expected['response'], self.expected['response_body'])) @patch('httplib2.Http.request') def test_interacts_well_with_returns_json_on_fail(self, mock_request): """Check that DictFailHandler interacts well with returns_json""" mock_request.return_value = self.response, self.body self.expected['method'] = 'POST' self.expected['request_body'] = '{"plants": "all"}' self.expected['request_headers'] = {'Content-Type': 'application/json'} self.expected['url'] = 'http://localhost:12345/grow' self.assertEqual(self.expected, self.api.grow()) @patch('httplib2.Http.request') def test_interacts_well_with_returns_on_fail(self, mock_request): """Check that NoneFailHandler interacts well with returns""" mock_request.return_value = self.response, self.body self.expected['url'] = 'http://localhost:12345/plant' self.assertEqual(self.expected, self.api.get_plant()) @patch('httplib2.Http.request') def test_interacts_well_with_returns_list_of_on_fail(self, mock_request): """Check that NoneFailHandler interacts well with returns_list_of""" mock_request.return_value = self.response, self.body self.expected['url'] = 'http://localhost:12345/plant' self.assertEqual(self.expected, self.api.get_plants()) @patch('httplib2.Http.request') def test_interacts_well_with_returns_json(self, mock_request): """Check that NoneFailHandler interacts well with returns_json""" mock_request.return_value = {'status': '200'}, '{"foo": "bar"}' self.assertEqual({'foo': 'bar'}, self.api.grow()) @patch('httplib2.Http.request') def test_interacts_well_with_returns(self, mock_request): """Check that NoneFailHandler interacts well with returns""" mock_request.return_value = {'status': '200'}, '{"foo": "bar"}' self.assertTrue(isinstance(self.api.get_plant(), PistonResponseObject)) @patch('httplib2.Http.request') def test_interacts_well_with_returns_list_of(self, mock_request): """Check that NoneFailHandler interacts well with returns_list_of""" mock_request.return_value = {'status': '200'}, '[]' self.assertEqual([], self.api.get_plants()) class MultiExceptionFailHandlerTestCase(TestCase): def setUp(self): self.api = GardeningAPI() self.api.fail_handler = MultiExceptionFailHandler def test_no_status(self): handler = MultiExceptionFailHandler('/foo', 'GET', '', {}) self.assertRaises(APIError, handler.handle, {}, '') def test_bad_status_codes(self): bad_status = { '400': BadRequestError, '401': UnauthorizedError, '403': ForbiddenError, '404': NotFoundError, '500': InternalServerErrorError, '503': ServiceUnavailableError, } handler = MultiExceptionFailHandler('/foo', 'GET', '', {}) for status, exception in bad_status.items(): self.assertRaises(exception, handler.handle, {'status': status}, '') @patch('httplib2.Http.request') def test_interacts_well_with_returns_json_on_fail(self, mock_request): """ Check that MultiExceptionFailHandler interacts well with returns_json""" mock_request.return_value = {'status': '401'}, '' self.assertRaises(UnauthorizedError, self.api.grow) @patch('httplib2.Http.request') def test_interacts_well_with_returns_on_fail(self, mock_request): """Check that MultiExceptionFailHandler interacts well with returns""" mock_request.return_value = {'status': '404'}, '' self.assertRaises(NotFoundError, self.api.get_plant) @patch('httplib2.Http.request') def test_interacts_well_with_returns_list_of_on_fail(self, mock_request): """ Check that MultiExceptionFailHandler interacts well with returns_list_of""" mock_request.return_value = {'status': '500'}, '' self.assertRaises(InternalServerErrorError, self.api.get_plants) @patch('httplib2.Http.request') def test_interacts_well_with_returns_json(self, mock_request): """ Check that MultiExceptionFailHandler interacts well with returns_json""" mock_request.return_value = {'status': '200'}, '{"foo": "bar"}' self.assertEqual({'foo': 'bar'}, self.api.grow()) @patch('httplib2.Http.request') def test_interacts_well_with_returns(self, mock_request): """Check that MultiExceptionFailHandler interacts well with returns""" mock_request.return_value = {'status': '200'}, '{"foo": "bar"}' self.assertTrue(isinstance(self.api.get_plant(), PistonResponseObject)) @patch('httplib2.Http.request') def test_interacts_well_with_returns_list_of(self, mock_request): """ Check that MultiExceptionFailHandler interacts well with returns_list_of""" mock_request.return_value = {'status': '200'}, '[]' self.assertEqual([], self.api.get_plants()) piston-mini-client-0.7.5/piston_mini_client/tests/test_proxy.py0000664000175000017500000000442412005525657026266 0ustar anthonyanthony00000000000000# -*- coding: utf-8 -*- # Copyright 2010-2012 Canonical Ltd. This software is licensed under the # GNU Lesser General Public License version 3 (see the file LICENSE). import os from unittest import TestCase from piston_mini_client import PistonAPI class DentistAPI(PistonAPI): default_service_root = 'http://localhost:12345' def appointments(self): self._get('/appointments') class ProxySupportTestCase(TestCase): def setUp(self): for envvar in ("http_proxy", "https_proxy"): if envvar in os.environ: del os.environ[envvar] def test_basic_proxy(self): os.environ["http_proxy"] = "http://localhost:3128" api = DentistAPI() proxy_info = api._http["http"].proxy_info self.assertNotEqual(proxy_info, None) self.assertEqual(proxy_info.proxy_host, "localhost") self.assertEqual(proxy_info.proxy_port, 3128) def test_basic_https_proxy(self): # not the https this time os.environ["https_proxy"] = "https://localhost:3128" api = DentistAPI(service_root="https://localhost:12345") proxy_info = api._http["https"].proxy_info self.assertNotEqual(proxy_info, None) self.assertEqual(proxy_info.proxy_host, "localhost") self.assertEqual(proxy_info.proxy_port, 3128) def test_auth_proxy(self): os.environ["http_proxy"] = "http://user:pass@localhost:3128" api = DentistAPI() proxy_info = api._http["http"].proxy_info self.assertNotEqual(proxy_info, None) self.assertEqual(proxy_info.proxy_host, "localhost") self.assertEqual(proxy_info.proxy_port, 3128) self.assertEqual(proxy_info.proxy_user, "user") self.assertEqual(proxy_info.proxy_pass, "pass") def test_no_proxy(self): api = DentistAPI() proxy_info = api._http["http"].proxy_info self.assertEqual(proxy_info, None) def test_httplib2_proxy_type_http_no_tunnel(self): # test that patching httplib2 with our own socks.py works import httplib2 import piston_mini_client self.assertEqual(piston_mini_client.socks.PROXY_TYPE_HTTP_NO_TUNNEL, httplib2.socks.PROXY_TYPE_HTTP_NO_TUNNEL) if __name__ == "__main__": import unittest unittest.main() piston-mini-client-0.7.5/piston_mini_client/tests/test_disable_ssl_check.py0000664000175000017500000000333712006004046030511 0ustar anthonyanthony00000000000000# -*- coding: utf-8 -*- # Copyright 2010-2012 Canonical Ltd. This software is licensed under the # GNU Lesser General Public License version 3 (see the file LICENSE). import os from mock import patch from unittest import TestCase from piston_mini_client import PistonAPI from piston_mini_client.consts import DISABLE_SSL_VALIDATION_ENVVAR class DentistAPI(PistonAPI): default_service_root = 'http://localhost:12345' def appointments(self): self._get('/appointments') class DisableSSLVerificationTestCase(TestCase): def setUp(self): if DISABLE_SSL_VALIDATION_ENVVAR in os.environ: self.orig_disable_ssl = os.environ[DISABLE_SSL_VALIDATION_ENVVAR] del os.environ[DISABLE_SSL_VALIDATION_ENVVAR] def tearDown(self): if DISABLE_SSL_VALIDATION_ENVVAR in os.environ: del os.environ[DISABLE_SSL_VALIDATION_ENVVAR] if hasattr(self, 'orig_disable_ssl'): os.environ[DISABLE_SSL_VALIDATION_ENVVAR] = self.orig_disable_ssl @patch('httplib2.Http') def test_dont_disable(self, mock_http): api = DentistAPI() self.assertTrue('disable_ssl_certificate_validation' not in mock_http.call_args[1]) @patch('httplib2.Http') def test_disable_via_constructor(self, mock_http): api = DentistAPI(disable_ssl_validation=True) self.assertTrue('disable_ssl_certificate_validation' in mock_http.call_args[1]) @patch('httplib2.Http') def test_disable_via_envvar(self, mock_http): os.environ[DISABLE_SSL_VALIDATION_ENVVAR] = '1' api = DentistAPI() self.assertTrue('disable_ssl_certificate_validation' in mock_http.call_args[1]) piston-mini-client-0.7.5/piston_mini_client/tests/test_serializers.py0000664000175000017500000000374612057365531027447 0ustar anthonyanthony00000000000000# -*- coding: utf-8 -*- # Copyright 2010-2012 Canonical Ltd. This software is licensed under the # GNU Lesser General Public License version 3 (see the file LICENSE). from unittest import TestCase from piston_mini_client.serializers import JSONSerializer, FormSerializer from piston_mini_client import PistonSerializable try: # Python 3. from urllib.parse import parse_qs except ImportError: # Python 2. from urlparse import parse_qs class JSONSerializerTestCase(TestCase): def test_simple_serialize(self): serializer = JSONSerializer() self.assertEqual('{"foo": "bar"}', serializer.serialize( {'foo': 'bar'})) def test_piston_serializable_serialize(self): class MySerializable(PistonSerializable): _atts = ('foo',) myarg = MySerializable(foo='bar') serializer = JSONSerializer() self.assertEqual('{"foo": "bar"}', serializer.serialize(myarg)) def test_serialize_fail(self): serializer = JSONSerializer() self.assertRaises(TypeError, serializer.serialize, {'foo': 3 + 1j}) class FormSerializerTestCase(TestCase): def test_simple_serialize(self): serializer = FormSerializer() self.assertEqual('foo=bar', serializer.serialize({'foo': 'bar'})) def test_piston_serializable_serialize(self): class MySerializable(PistonSerializable): _atts = ('foo', 'bar') myarg = MySerializable(foo='baz', bar=42) serializer = FormSerializer() # Argument order is undefined, so parse these into dictionaries which # can be compared. want = parse_qs('foo=baz&bar=42') got = parse_qs(serializer.serialize(myarg)) self.assertEqual(want, got) def test_serialize_nested(self): # Maybe we should flatly refuse to serialize nested structures? serializer = FormSerializer() self.assertEqual('foo=%7B%27a%27%3A+%27b%27%7D', serializer.serialize({'foo': {'a': 'b'}})) piston-mini-client-0.7.5/piston_mini_client/tests/test_auth.py0000664000175000017500000000706512111514240026032 0ustar anthonyanthony00000000000000# -*- coding: utf-8 -*- # Copyright 2010-2012 Canonical Ltd. This software is licensed under the # GNU Lesser General Public License version 3 (see the file LICENSE). from mock import patch from unittest import TestCase from piston_mini_client import PistonAPI, returns_json from piston_mini_client.validators import oauth_protected from piston_mini_client.auth import OAuthAuthorizer, BasicAuthorizer class BasicAuthorizerTestCase(TestCase): def test_sign_request(self): auth = BasicAuthorizer(username='foo', password='bar') url = 'http://example.com/api' headers = {} auth.sign_request(url=url, method='GET', body='', headers=headers) self.assertTrue('Authorization' in headers) self.assertTrue(headers['Authorization'].startswith('Basic ')) self.assertEqual(headers['Authorization'], 'Basic Zm9vOmJhcg==') def test_long_creds_dont_wrap(self): auth = BasicAuthorizer(username='foo', password='a' * 500) url = 'http://example.com/api' headers = {} auth.sign_request(url=url, method='GET', body='', headers=headers) self.assertTrue('Authorization' in headers) self.assertFalse('\n' in headers['Authorization']) class OAuthAuthorizerTestCase(TestCase): def test_sign_request(self): auth = OAuthAuthorizer('tkey', 'tsecret', 'ckey', 'csecret') url = 'http://example.com/api' headers = {} auth.sign_request(url=url, method='GET', body='', headers=headers) self.assertTrue('Authorization' in headers) self.assertTrue(headers['Authorization'].startswith('OAuth ')) @patch('httplib2.Http.request') def test_body_is_signed_if_urlencoded(self, mock_request): formencoded = 'application/x-www-form-urlencoded' class BathAPI(PistonAPI): default_content_type = formencoded default_service_root = 'http://example.com' @returns_json @oauth_protected def soak(self): return self._post('/soak/', data={'time': 900}) mock_request.return_value = {'status': '200'}, '"done"' auth = OAuthAuthorizer('tkey', 'tsecret', 'ckey', 'csecret') api = BathAPI(auth=auth) response = api.soak() self.assertEqual('done', response) self.assertEqual(1, mock_request.call_count) args, kwargs = mock_request.call_args self.assertEqual(formencoded, kwargs['headers']['Content-Type']) auth_header = kwargs['headers']['Authorization'] self.assertTrue(auth_header.startswith('OAuth ')) @patch('httplib2.Http.request') def test_post_works_with_no_body(self, mock_request): cases = [ 'application/x-www-form-urlencoded', 'application/json', ] class ShowerAPI(PistonAPI): default_service_root = 'http://example.com' @returns_json @oauth_protected def soak(self): return self._post('/noop/', data='') auth = OAuthAuthorizer('tkey', 'tsecret', 'ckey', 'csecret') for content_type in cases: mock_request.return_value = {'status': '200'}, '"done"' api = ShowerAPI(auth=auth) api.default_content_type = content_type response = api.soak() self.assertEqual('done', response) args, kwargs = mock_request.call_args self.assertEqual(content_type, kwargs['headers']['Content-Type']) auth_header = kwargs['headers']['Authorization'] self.assertTrue(auth_header.startswith('OAuth ')) piston-mini-client-0.7.5/piston_mini_client/tests/test_validators.py0000664000175000017500000002143012006004046027232 0ustar anthonyanthony00000000000000# -*- coding: utf-8 -*- # Copyright 2010-2012 Canonical Ltd. This software is licensed under the # GNU Lesser General Public License version 3 (see the file LICENSE). from piston_mini_client.validators import ( basic_protected, oauth_protected, validate, validate_integer, validate_pattern, ValidationException, ) from piston_mini_client.auth import OAuthAuthorizer, BasicAuthorizer from unittest import TestCase class ValidatePatternTestCase(TestCase): def setUp(self): self.called = False def test_raises_if_arg_omitted(self): @validate_pattern('arg', 'foo') def func(arg): self.called = True self.assertRaises(ValidationException, func) self.assertFalse(self.called) def test_raises_if_arg_not_named(self): @validate_pattern('arg', 'foo') def func(arg): self.called = True self.assertRaises(ValidationException, func, 'foo') self.assertFalse(self.called) def test_raises_if_arg_not_a_string(self): @validate_pattern('arg', r'\d+') def func(arg): self.called = True self.assertRaises(ValidationException, func, arg=42) self.assertFalse(self.called) def test_raises_if_arg_doesnt_match(self): @validate_pattern('arg', 'foo') def func(arg): self.called = True self.assertRaises(ValidationException, func, 'bar') self.assertFalse(self.called) def test_match_must_be_complete(self): @validate_pattern('arg', 'foo') def func(arg): self.called = True self.assertRaises(ValidationException, func, arg='foobar') self.assertFalse(self.called) def test_match_success(self): @validate_pattern('arg', '\w{4,7}') def func(arg): self.called = True func(arg='foobar') self.assertTrue(self.called) def test_optional_can_be_omitted(self): @validate_pattern('arg', 'foo', required=False) def func(arg=None): self.called = True func() self.assertTrue(self.called) def test_optional_must_match_if_provided(self): @validate_pattern('arg', 'foo', required=False) def func(arg=None): self.called = True self.assertRaises(ValidationException, func, arg='bar') self.assertFalse(self.called) class ValidateIntegerTestCase(TestCase): def setUp(self): self.called = False def test_raises_if_arg_omitted(self): @validate_integer('arg') def func(arg): self.called = True self.assertRaises(ValidationException, func) self.assertFalse(self.called) def test_raises_if_arg_not_named(self): @validate_integer('arg') def func(arg): self.called = True self.assertRaises(ValidationException, func, 42) self.assertFalse(self.called) def test_raises_if_arg_not_an_int(self): @validate_integer('arg') def func(arg): self.called = True self.assertRaises(ValidationException, func, arg='foo') self.assertRaises(ValidationException, func, arg=7.5) self.assertFalse(self.called) def test_raises_if_arg_below_min(self): @validate_integer('arg', min=4) def func(arg): self.called = True self.assertRaises(ValidationException, func, arg=2) self.assertFalse(self.called) def test_raises_if_arg_above_max(self): @validate_integer('arg', max=4) def func(arg): self.called = True self.assertRaises(ValidationException, func, arg=6) self.assertFalse(self.called) def test_optional_can_be_omitted(self): @validate_integer('arg', required=False) def func(arg=None): self.called = True func() self.assertTrue(self.called) def test_optional_must_validate_if_provided(self): @validate_integer('arg', required=False) def func(arg=None): self.called = True self.assertRaises(ValidationException, func, arg='bar') self.assertFalse(self.called) def test_validate_success(self): @validate_integer('arg', min=4, max=10) def func(arg): self.called = True func(arg=7) self.assertTrue(self.called) class ValidateTestCase(TestCase): class SomeClass(object): pass def setUp(self): self.called = False def test_raises_if_arg_omitted(self): @validate('arg', self.SomeClass) def func(arg): self.called = True self.assertRaises(ValidationException, func) self.assertFalse(self.called) def test_raises_if_arg_not_named(self): @validate('arg', self.SomeClass) def func(arg): self.called = True self.assertRaises(ValidationException, func, True) self.assertFalse(self.called) def test_raises_if_arg_not_isinstance(self): @validate('arg', self.SomeClass) def func(arg): self.called = True self.assertRaises(ValidationException, func, arg='foo') self.assertRaises(ValidationException, func, arg=7.5) self.assertRaises(ValidationException, func, arg=1) self.assertFalse(self.called) def test_optional_can_be_omitted(self): @validate('arg', self.SomeClass, required=False) def func(arg=None): self.called = True func() self.assertTrue(self.called) def test_optional_must_validate_if_provided(self): @validate('arg', self.SomeClass, required=False) def func(arg=None): self.called = True self.assertRaises(ValidationException, func, arg='bar') self.assertFalse(self.called) def test_validate_success(self): @validate('arg', self.SomeClass) def func(arg): self.called = True func(arg=self.SomeClass()) self.assertTrue(self.called) class OAuthProtectedTestCase(TestCase): """ The oauth_protected decorator can only be applied to methods (or functions that receive a first 'self' arg), as it needs to check for the self._auth attribute. So we define a small class for each test to use here. """ class MyAPI(object): called = False @oauth_protected def method(self): self.called = True def test_fail_if_no_auth(self): api = self.MyAPI() self.assertRaises(ValidationException, api.method) self.assertFalse(api.called) def test_fail_if_auth_is_none(self): api = self.MyAPI() api._auth = None self.assertRaises(ValidationException, api.method) self.assertFalse(api.called) def test_fail_if_auth_isnt_oauth(self): api = self.MyAPI() api._auth = BasicAuthorizer('username', 'password') self.assertRaises(ValidationException, api.method) self.assertFalse(api.called) def test_success(self): auth = OAuthAuthorizer('tkey', 'tsecret', 'ckey', 'csecret') api = self.MyAPI() api._auth = auth api.method() self.assertTrue(api.called) def test_success_with_subclass(self): class MyOAuth(OAuthAuthorizer): pass auth = MyOAuth('tkey', 'tsecret', 'ckey', 'csecret') api = self.MyAPI() api._auth = auth api.method() self.assertTrue(api.called) class BasicProtectedTestCase(TestCase): """ The basic_protected decorator can only be applied to methods (or functions that receive a first 'self' arg), as it needs to check for the self._auth attribute. So we define a small class for each test to use here. """ class MyAPI(object): called = False @basic_protected def method(self): self.called = True def test_fail_if_no_auth(self): api = self.MyAPI() self.assertRaises(ValidationException, api.method) self.assertFalse(api.called) def test_fail_if_auth_is_none(self): api = self.MyAPI() api._auth = None self.assertRaises(ValidationException, api.method) self.assertFalse(api.called) def test_fail_if_auth_isnt_basic(self): api = self.MyAPI() api._auth = OAuthAuthorizer('tkey', 'tsecret', 'ckey', 'csecret') self.assertRaises(ValidationException, api.method) self.assertFalse(api.called) def test_success(self): auth = BasicAuthorizer('username', 'password') api = self.MyAPI() api._auth = auth api.method() self.assertTrue(api.called) def test_success_with_subclass(self): class MyBasic(BasicAuthorizer): pass auth = MyBasic('username', 'password') api = self.MyAPI() api._auth = auth api.method() self.assertTrue(api.called) piston-mini-client-0.7.5/piston_mini_client/serializers.py0000664000175000017500000000370512005525657025241 0ustar anthonyanthony00000000000000# -*- coding: utf-8 -*- # Copyright 2010-2012 Canonical Ltd. This software is licensed under the # GNU Lesser General Public License version 3 (see the file LICENSE). """Classes that define ways for your API methods to serialize arguments into a request.""" __all__ = [ 'JSONSerializer', 'FormSerializer', ] import json try: from urllib import urlencode except ImportError: # Python 3 from urllib.parse import urlencode from piston_mini_client import PistonSerializable class JSONSerializer(object): """A serializer that renders JSON. This is the default serializer for content type *application/json*. """ class PistonSerializableEncoder(json.JSONEncoder): def default(self, o): if isinstance(o, PistonSerializable): return o.as_serializable() return json.JSONEncoder.default(self, o) def serialize(self, obj): """Serialize ``obj`` into JSON. As well as the usual basic JSON-encodable types, this serializer knows how to serialize ``PistonSerializable`` objects. """ return json.dumps(obj, cls=self.PistonSerializableEncoder) class FormSerializer(object): """A serializer that renders form-urlencoded content. This is the default serializer for content type *application/x-www-form-urlencoded*. .. note:: this serializer doesn't support nested structures. It should be initialized with a dictionary, sequence of pairs, or ``PistonSerializable``. """ def serialize(self, obj): if isinstance(obj, PistonSerializable): obj = obj.as_serializable() try: return urlencode(obj) except TypeError: raise TypeError("Attempted to serialize invalid object %s" % obj) serializers = { 'application/json': JSONSerializer(), 'application/x-www-form-urlencoded': FormSerializer(), } def get_serializer(content_type): return serializers.get(content_type) piston-mini-client-0.7.5/piston_mini_client/failhandlers.py0000664000175000017500000001355212006004046025323 0ustar anthonyanthony00000000000000# -*- coding: utf-8 -*- # Copyright 2010-2012 Canonical Ltd. This software is licensed under the # GNU Lesser General Public License version 3 (see the file LICENSE). """A fail handler is passed the raw httplib2 response and body, and has a chance to raise an exception, modify the body or return it unaltered, or even return a completely different object. It's up to the client (and possibly decorators) to know what to do with these returned objects. """ __all__ = [ 'APIError', 'BaseFailHandler', 'ExceptionFailHandler', 'DictFailHandler', 'NoneFailHandler', 'MultiExceptionFailHandler', ] from piston_mini_client.consts import DEBUG_ENVVAR import os def format_request(url, method, body, headers): """Return a representation of a request""" if hasattr(headers, 'items'): headers = '\n'.join("%s: %s" % pair for pair in headers.items()) return """Request: {method} {url} {headers} {body} """.format(method=method, url=url, headers=headers, body=body) def format_response(response, body): """Return a representation of a response""" headers = response.copy() status = headers.pop('status', 'Unknown status code') headers = '\n'.join("%s: %s" % pair for pair in headers.items()) return """Response: {status} {headers} {body} """.format(status=status, headers=headers, body=body) class APIError(Exception): def __init__(self, msg, body=None, data=None): self.msg = msg self.body = body if data is None: data = {} self.data = data self.debug = os.environ.get(DEBUG_ENVVAR, False) def __str__(self): if self.debug: msg = self.msg + '\n' + format_request( url=self.data.get('url', ''), method=self.data.get('method', ''), body=self.data.get('request_body', ''), headers=self.data.get('headers', {})) msg += format_response( self.data.get('response', {}), self.body) return msg return self.msg class SocketError(APIError): def __init__(self, msg): self.msg = msg def __str__(self): return self.msg class TimeoutError(SocketError): pass class BaseFailHandler(object): """A base class for fail handlers. Child classes should at least define handle() """ def __init__(self, url, method, body, headers): """Store provided information for if somebody needs it""" self.data = { 'url': url, 'method': method, 'request_body': body, 'request_headers': headers, } def handle(self, response, body): raise NotImplementedError() def was_error(self, response): """Returns True if 'response' is a failure""" try: status = int(response.get('status')) except (ValueError, TypeError): return True return status >= 400 class ExceptionFailHandler(BaseFailHandler): """A fail handler that will raise APIErrors if anything goes wrong""" def handle(self, response, body): """Raise APIError if a strange status code is found""" if 'status' not in response: raise APIError('No status code in response') if self.was_error(response): raise APIError('%s: %s' % (response['status'], response), body, data=self.data) return body class NoneFailHandler(BaseFailHandler): """A fail handler that returns None if anything goes wrong. You probably only want to use this if you really don't care about what went wrong. """ def handle(self, response, body): """Return None if a strange status code is found""" if self.was_error(response): return None return body class DictFailHandler(BaseFailHandler): """A fail handler that returns error information in a dict""" def handle(self, response, body): """Return a dict if a strange status code is found. The returned dict will have two keys: * 'response': the httplib2 response header dict * 'body': the response body, as a string """ if self.was_error(response): self.data['response'] = response self.data['response_body'] = body return self.data return body class BadRequestError(APIError): """A 400 Bad Request response was received""" class UnauthorizedError(APIError): """A 401 Bad Request response was received""" class ForbiddenError(APIError): """A 403 Forbidden response was received""" class NotFoundError(APIError): """A 404 Not Found response was received""" class InternalServerErrorError(APIError): """A 500 Internal Server Error response was received""" class ServiceUnavailableError(APIError): """A 503 Service Unavailable Error response was received""" class MultiExceptionFailHandler(BaseFailHandler): """A fail handler that raises an exception according to what goes wrong""" exceptions = { '400': BadRequestError, '401': UnauthorizedError, '403': ForbiddenError, '404': NotFoundError, '500': InternalServerErrorError, '503': ServiceUnavailableError, } def handle(self, response, body): """Return an exception according to what went wrong. Status codes currently returning their own exception class are: * 400: BadRequestError, * 401: UnauthorizedError, * 403: ForbiddenError, * 404: NotFoundError, * 500: InternalServerErrorError, and * 503: ServiceUnavailableError """ if self.was_error(response): status = response.get('status') exception_class = self.exceptions.get(status, APIError) raise exception_class('%s: %s' % (status, response), body, data=self.data) return body piston-mini-client-0.7.5/piston_mini_client/auth.py0000664000175000017500000000631112111514240023622 0ustar anthonyanthony00000000000000# -*- coding: utf-8 -*- # Copyright 2010-2012 Canonical Ltd. This software is licensed under the # GNU Lesser General Public License version 3 (see the file LICENSE). """Classes for adding authentication headers to your API requests. You usually want to pass in an instance of one of these classes when you instantiate a ``PistonAPI`` object. """ import base64 def _unicodeify(s): if isinstance(s, bytes): return s.decode('utf-8') return s class OAuthAuthorizer(object): """Authenticate to OAuth protected APIs.""" def __init__(self, token_key, token_secret, consumer_key, consumer_secret, oauth_realm="OAuth"): """Initialize a ``OAuthAuthorizer``. ``token_key``, ``token_secret``, ``consumer_key`` and ``consumer_secret`` are required for signing OAuth requests. The ``oauth_realm`` to use is optional. """ # 2012-11-19 BAW: python-oauthlib requires unicodes for its tokens and # secrets. Assume utf-8 values. # https://github.com/idan/oauthlib/issues/68 self.token_key = _unicodeify(token_key) self.token_secret = _unicodeify(token_secret) self.consumer_key = _unicodeify(consumer_key) self.consumer_secret = _unicodeify(consumer_secret) self.oauth_realm = oauth_realm def sign_request(self, url, method, body, headers): """Sign a request with OAuth credentials.""" # 2012-11-19 BAW: In order to preserve API backward compatibility, # convert empty string body to None. The old python-oauth library # would treat the empty string as "no body", but python-oauthlib # requires None. if not body: content_type = headers.get('Content-Type') if content_type == 'application/x-www-form-urlencoded': body = '' else: body = None # Import oauthlib here so that you don't need it if you're not going # to use it. Plan B: move this out into a separate oauth module. from oauthlib.oauth1 import Client from oauthlib.oauth1.rfc5849 import SIGNATURE_PLAINTEXT oauth_client = Client(self.consumer_key, self.consumer_secret, self.token_key, self.token_secret, signature_method=SIGNATURE_PLAINTEXT, realm=self.oauth_realm) uri, signed_headers, body = oauth_client.sign( url, method, body, headers) headers.update(signed_headers) class BasicAuthorizer(object): """Authenticate to Basic protected APIs.""" def __init__(self, username, password): """Initialize a ``BasicAuthorizer``. You'll need to provide the ``username`` and ``password`` that will be used to authenticate with the server. """ self.username = username self.password = password def sign_request(self, url, method, body, headers): """Sign a request with Basic credentials.""" headers['Authorization'] = self.auth_header() def auth_header(self): s = '%s:%s' % (self.username, self.password) encoded = base64.b64encode(s.encode('utf-8')).decode('utf-8') return 'Basic ' + encoded piston-mini-client-0.7.5/piston_mini_client/__init__.py0000664000175000017500000007771412113206154024443 0ustar anthonyanthony00000000000000# -*- coding: utf-8 -*- # Copyright 2010-2012 Canonical Ltd. This software is licensed under the # GNU Lesser General Public License version 3 (see the file LICENSE). import httplib2 try: # ensure we have a version with a fix for # http://code.google.com/p/httplib2/issues/detail?id=38 # and if not, patch in our own socks with the fix from httplib2.socks import PROXY_TYPE_HTTP_NO_TUNNEL from httplib2 import socks except ImportError: from piston_mini_client import socks httplib2.socks = socks import errno import json import os import socket from datetime import datetime from functools import wraps try: from urlparse import urlparse, urlunparse except ImportError: # Python 3 from urllib.parse import urlparse, urlunparse try: from urllib import urlencode except ImportError: # Python 3 from urllib.parse import urlencode from piston_mini_client.failhandlers import ( APIError, ExceptionFailHandler, format_request, format_response, SocketError, TimeoutError, ) from piston_mini_client.consts import ( DISABLE_SSL_VALIDATION_ENVVAR, LOG_FILENAME_ENVVAR, TIMEOUT_ENVVAR, ) try: unicode bytes = str except NameError: # Python 3 basestring = unicode = str class OfflineModeException(Exception): pass # taken from lazr.restfulclients _browser.py file to work around # the problem that ecryptfs is very unhappy about long filenames # upstream commented here: # http://code.google.com/p/httplib2/issues/detail?id=92 MAXIMUM_CACHE_FILENAME_LENGTH = 143 from httplib2 import _md5, re_url_scheme, re_slash def safename(filename): """Return a filename suitable for the cache. Strips dangerous and common characters to create a filename we can use to store the cache in. """ # this is a stock httplib2 copy try: ufilename = filename if isinstance(filename, bytes): ufilename = filename.decode('utf-8') if re_url_scheme.match(ufilename.encode('utf-8')): filename = ufilename.encode('idna') except UnicodeError: pass if isinstance(filename, unicode): filename = filename.encode('utf-8') filemd5 = _md5(filename).hexdigest() filename = re_url_scheme.sub("".encode('utf-8'), filename) filename = re_slash.sub(",".encode('utf-8'), filename) # This is the part that we changed. In stock httplib2, the # filename is trimmed if it's longer than 200 characters, and then # a comma and a 32-character md5 sum are appended. This causes # problems on eCryptfs filesystems, where the maximum safe # filename length is closer to 143 characters. # # We take a (user-hackable) maximum filename length from # RestfulHttp and subtract 33 characters to make room for the comma # and the md5 sum. # # See: # http://code.google.com/p/httplib2/issues/detail?id=92 # https://bugs.launchpad.net/bugs/344878 # https://bugs.launchpad.net/bugs/545197 maximum_filename_length = MAXIMUM_CACHE_FILENAME_LENGTH maximum_length_before_md5_sum = maximum_filename_length - 32 - 1 if len(filename) > maximum_length_before_md5_sum: filename = filename[:maximum_length_before_md5_sum] r = ",".encode('utf-8').join((filename, filemd5.encode('utf-8'))) return r.decode("utf-8") def _parse_json(json_data): """Return a Python data structure corresponding to ``json_data``. Use this rather than ``json.loads`` directly to get a richer error message when JSON data cannot be decoded. :param json_data: A string containing JSON data. :raises ValueError: If the JSON data could not be parsed. :return: A Python data structure. """ try: return json.loads(json_data) except ValueError: raise APIError('No JSON object could be decoded', body=json_data) def returns_json(func): """The response data will be deserialized using a simple JSON decoder""" @wraps(func) def wrapper(*args, **kwargs): body = func(*args, **kwargs) if not isinstance(body, basestring): return body return _parse_json(body) return wrapper def returns(cls, none_allowed=False): """The response data will be deserialized into an instance of ``cls``. The provided class should be a descendant of ``PistonResponseObject``, or some other class that provides a ``from_response`` method. ``none_allowed``, defaulting to ``False``, specifies whether or not ``None`` is a valid response. If ``True`` then the api can return ``None`` instead of a ``PistonResponseObject``. """ def decorator(func): @wraps(func) def wrapper(self, *args, **kwargs): body = func(self, *args, **kwargs) if not isinstance(body, basestring): return body return cls.from_response(body, none_allowed) return wrapper return decorator def returns_list_of(cls): """The response data will be deserialized into a list of ``cls``. The provided class should be a descendant of ``PistonResponseObject``, or some other class that provides a ``from_response`` method. """ def decorator(func): @wraps(func) def wrapper(self, *args, **kwargs): body = func(self, *args, **kwargs) if not isinstance(body, basestring): return body data = _parse_json(body) items = [] for datum in data: items.append(cls.from_dict(datum)) return items return wrapper return decorator class PistonResponseObject(object): """Base class for objects that are returned from api calls.""" @classmethod def from_response(cls, body, none_allowed=False): data = _parse_json(body) if none_allowed and data is None: return data obj = cls.from_dict(data) return obj @classmethod def from_dict(cls, data): obj = cls() for key, value in data.items(): setattr(obj, key, value) return obj class PistonSerializable(object): """Base class for objects that want to be used as api call arguments. Children classes should at least redefine ``_atts`` to state the list of attributes that will be serialized into each request. """ _atts = () def __init__(self, **kwargs): for (key, value) in kwargs.items(): setattr(self, key, value) def as_serializable(self): """Return a serializable representation of this object.""" data = {} for att in self._atts: if not hasattr(self, att): raise ValueError( "Attempted to serialize attribute '%s'" % att) data[att] = getattr(self, att) return data def _as_serializable(self): """_as_serializable is deprecated; use as_serializable() instead.""" import warnings warnings.warn("_as_serializable is deprecated; " "use as_serializable instead", DeprecationWarning) return self.as_serializable() class PistonRequester(object): """This class provides methods to make http requests slightly easier. It's a wrapper around ``httplib2`` that takes care of a few common tasks around making HTTP requests: authentication, proxies, serialization, timeouts, etc. To use it, just instantiate it and call ``get``, ``post``, ``put`` or ``delete`` to do various requests. If you want to wrap an HTTP API, you probably want to make a subclass of ``PistonAPI``, which uses this class. """ SUPPORTED_SCHEMAS = ("http", "https") def __init__(self, cachedir=None, auth=None, offline_mode=False, disable_ssl_validation=False, log_filename=None, timeout=None, fail_handler=None, extra_headers=None, serializers=None, default_content_type='application/json'): """Initialize a ``PistonRequester``. ``cachedir`` will be used as ``httplib2``'s cache directory if provided. ``auth`` can be an instance of ``BasicAuthorizer`` or ``OAuthAuthorizer`` or any object that provides a ``sign_request`` method. If ``auth`` is ``None`` you'll only be able to make public API calls. See :ref:`authentication` for details. ``disable_ssl_validation`` will skip server SSL certificate validation when using secure connections. ``httplib2`` < 0.7.0 doesn't support certificate validation anyway, so if you're using an older ``httplib2`` this will have no effect. ``offline_mode`` will not touch the network. In this case only cached results will be available. If you pass in a ``log_filename``, all requests and responses including headers will be logged to this file. """ if cachedir: self._create_dir_if_needed(cachedir) self._httplib2_cache = httplib2.FileCache(cachedir, safe=safename) else: self._httplib2_cache = None self._auth = auth self._offline_mode = offline_mode self._disable_ssl_validation = disable_ssl_validation self._timeout = timeout # create one httplib2.Http object per scheme so that we can # have per-scheme proxy settings (see also Issue 26 # http://code.google.com/p/httplib2/issues/detail?id=26) self._http = {} for scheme in self.SUPPORTED_SCHEMAS: self._http[scheme] = self._get_http_obj_for_scheme(scheme) if serializers is None: serializers = {} self._serializers = {} self._log_filename = log_filename self._default_content_type = default_content_type if fail_handler is None: fail_handler = ExceptionFailHandler self._fail_handler = fail_handler self._extra_headers = extra_headers def _create_dir_if_needed(self, path): """ helper that checks/creates path if it does not exists """ try: os.makedirs(path) except OSError as e: # fixes race LP: #803280 if e.errno != errno.EEXIST: raise def _get_http_obj_for_scheme(self, scheme): proxy_info = self._get_proxy_info(scheme) http = None check_disabled_in_env = os.environ.get(DISABLE_SSL_VALIDATION_ENVVAR) if self._disable_ssl_validation or check_disabled_in_env: try: http = httplib2.Http( cache=self._httplib2_cache, timeout=self._timeout, disable_ssl_certificate_validation=True, proxy_info=proxy_info) except TypeError: # httplib2 < 0.7.0 doesn't support cert validation anyway pass if http is None: http = httplib2.Http(cache=self._httplib2_cache, timeout=self._timeout, proxy_info=proxy_info) return http def _get_proxy_info(self, scheme): envvar = "%s_proxy" % scheme if envvar in os.environ: url = urlparse(os.environ[envvar]) user_pass, sep, host_and_port = url.netloc.rpartition("@") user, sep, passw = user_pass.partition(":") host, sep, port = host_and_port.partition(":") if port: port = int(port) proxy_type = socks.PROXY_TYPE_HTTP if scheme == "http": # this will not require the CONNECT acl from squid and # is good enough for http connections proxy_type = socks.PROXY_TYPE_HTTP_NO_TUNNEL proxy_info = httplib2.ProxyInfo( proxy_type=proxy_type, proxy_host=host, proxy_port=port or 8080, proxy_user=user or None, proxy_pass=passw or None) return proxy_info return None def _prepare_request(self, data=None, content_type=None, extra_headers=None): """Put together a set of headers and a body for a request. If ``content_type`` is not provided, ``self._default_content_type`` will be assumed. You probably never need to call this method directly. """ if content_type is None: content_type = self._default_content_type body = self._prepare_body(data, content_type) headers = self._prepare_headers(content_type, extra_headers) return body, headers def _prepare_headers(self, content_type=None, extra_headers=None): """Put together and return a complete set of headers. If ``content_type`` is provided, it will be added as the Content-type header. Any provided ``extra_headers`` will be added last. You probably never need to call this method directly. """ headers = {} if content_type: headers['Content-Type'] = content_type if self._extra_headers is not None: headers.update(self._extra_headers) if extra_headers is not None: headers.update(extra_headers) return headers def _prepare_body(self, data=None, content_type=None): """Serialize data into a request body. ``data`` will be serialized into a string, according to ``content_type``. You probably never need to call this method directly. """ body = data if not isinstance(data, basestring): serializer = self._get_serializer(content_type) body = serializer.serialize(data) return body def request_url(self, url, method, body='', headers=None): """Perform an HTTP request. You probably want to call one of the ``get``, ``post``, ``put`` methods instead. """ if headers is None: headers = {} # in offline mode either get it from the cache or return None if self._offline_mode: if method in ('POST', 'PUT'): err = "method '%s' not allowed in offline-mode" % method raise OfflineModeException(err) return self._get_from_cache(url) scheme = urlparse(url).scheme if self._auth: self._auth.sign_request(url, method, body, headers) if self._log_filename: self._dump_request(url, method, body, headers) try: response, response_body = self._http[scheme].request( url, method=method, body=body, headers=headers) except AttributeError as e: # Special case out httplib2's way of telling us unable to connect if e.args[0] == "'NoneType' object has no attribute 'makefile'": raise APIError('Unable to connect to %s' % (url,)) else: raise except socket.timeout as e: raise TimeoutError('Timed out attempting to connect to %s' % (url,)) except socket.error as e: msg = 'connecting to %s: %s' % (url, e.strerror) raise SocketError(msg) if self._log_filename: self._dump_response(response, response_body) handler = self._fail_handler(url, method, body, headers) body = handler.handle(response, response_body) return body def _dump_request(self, url, method, body, headers): try: with open(self._log_filename, 'a') as f: formatted = format_request(url, method, body, headers) f.write("{0}: {1}".format(datetime.now(), formatted)) except IOError: pass def _dump_response(self, response, body): try: with open(self._log_filename, 'a') as f: formatted = format_response(response, body) f.write("{0}: {1}".format(datetime.now(), formatted)) except IOError: pass def _get_from_cache(self, url): """ get a given url from the cachedir even if its expired or return None if no data is available """ scheme = urlparse(url).scheme if self._http[scheme].cache: cached_value = self._http[scheme].cache.get( httplib2.urlnorm(url)[-1]) if cached_value: info, content = cached_value.decode("utf-8").split( '\r\n\r\n', 1) return content def _get_serializer(self, content_type=None): # Import here to avoid a circular import from piston_mini_client.serializers import get_serializer if content_type is None: content_type = self._default_content_type default_serializer = get_serializer(content_type) return self._serializers.get(content_type, default_serializer) def get(self, url, args=None, extra_headers=None): """Perform an HTTP GET request from ``url``. If provided, ``args`` should be a dict specifying additional GET arguments that will be encoded on to the end of the url. ``extra_headers`` is an optional dictionary of header key/values that will be added to the http request. """ if args is not None: if '?' in url: url += '&' else: url += '?' url += urlencode(args) headers = self._prepare_headers(extra_headers=extra_headers) return self.request_url(url, method='GET', headers=headers) def post(self, url, data=None, content_type=None, extra_headers=None): """Perform an HTTP POST request to ``url``. ``data`` should be: - A string, in which case it will be used directly as the request's body, or - A ``list``, ``dict``, ``int``, ``bool`` or ``PistonSerializable`` (something with an ``as_serializable`` method) or even ``None``, in which case it will be serialized into a string according to ``content_type``. If ``content_type`` is ``None``, ``self._default_content_type`` will be used. ``extra_headers`` is an optional dictionary of header key/values that will be added to the http request. """ body, headers = self._prepare_request( data, content_type, extra_headers=extra_headers) return self.request_url( url, method='POST', body=body, headers=headers) def put(self, url, data=None, content_type=None, extra_headers=None): """Perform an HTTP PUT request to ``url``. ``data`` should be: - A string, in which case it will be used directly as the request's body, or - A ``list``, ``dict``, ``int``, ``bool`` or ``PistonSerializable`` (something with an ``as_serializable`` method) or even ``None``, in which case it will be serialized into a string according to ``content_type``. If ``content_type`` is ``None``, ``self._default_content_type`` will be used. ``extra_headers`` is an optional dictionary of header key/values that will be added to the http request. """ body, headers = self._prepare_request( data, content_type, extra_headers=extra_headers) return self.request_url( url, method='PUT', body=body, headers=headers) def delete(self, url, extra_headers=None): """Perform an HTTP DELETE request on ``url``. ``extra_headers`` is an optional dictionary of header key/values that will be added to the http request. """ headers = self._prepare_headers(extra_headers=extra_headers) return self.request_url(url, method='DELETE', headers=headers) class _DeprecatedRequesterDecorator(object): __DEPRECATED_ATTRIBUTES = { '_httplib2_cache': '_httplib2_cache', '_auth': '_auth', '_offline_mode': '_offline_mode', '_disable_ssl_validation': '_disable_ssl_validation', '_timeout': '_timeout', '_http': '_http', } def __warn(self, name): import warnings warnings.warn( "PistonAPI.%s is deprecated; Use PistonAPI._requester.%s " "instead. Both are likely to break in the future. Please " "file a bug if you'd like them not to" % (name, name), DeprecationWarning, stacklevel=4) def __getattr__(self, name): if name in self.__DEPRECATED_ATTRIBUTES: self.__warn(name) return getattr(self._requester, self.__DEPRECATED_ATTRIBUTES[name]) raise AttributeError( '%r object has no attribute %r' % (self.__class__.__name__, name)) def __setattr__(self, name, value): if name in self.__DEPRECATED_ATTRIBUTES: self.__warn(name) setattr(self._requester, self.__DEPRECATED_ATTRIBUTES[name], value) else: super(_DeprecatedRequesterDecorator, self).__setattr__(name, value) def __deprecated_call(self, method_name, *args, **kwargs): self.__warn(method_name) method = getattr(self._requester, method_name) return method(*args, **kwargs) def _create_dir_if_needed(self, path): """ helper that checks/creates path if it does not exists """ return self.__deprecated_call('_create_dir_if_needed', path) def _get_http_obj_for_scheme(self, scheme): return self.__deprecated_call('_get_http_obj_for_scheme', scheme) def _get_proxy_info(self, scheme): return self.__deprecated_call('_get_proxy_info', scheme) def _prepare_request(self, data=None, content_type=None, extra_headers=None): """Put together a set of headers and a body for a request. If ``content_type`` is not provided, ``self.default_content_type`` will be assumed. You probably never need to call this method directly. """ return self.__deprecated_call( '_prepare_request', data=data, content_type=content_type, extra_headers=extra_headers) def _prepare_headers(self, content_type=None, extra_headers=None): """Put together and return a complete set of headers. If ``content_type`` is provided, it will be added as the Content-type header. Any provided ``extra_headers`` will be added last. You probably never need to call this method directly. """ return self.__deprecated_call( '_prepare_headers', content_type=content_type, extra_headers=extra_headers) def _prepare_body(self, data=None, content_type=None): """Serialize data into a request body. ``data`` will be serialized into a string, according to ``content_type``. You probably never need to call this method directly. """ return self.__deprecated_call( '_prepare_body', data=data, content_type=content_type) def _dump_request(self, url, method, body, headers): return self.__deprecated_call( '_dump_request', url, method, body, headers) def _dump_response(self, response, body): return self.__deprecated_call('_dump_response', response, body) def _get_from_cache(self, url): """ get a given url from the cachedir even if its expired or return None if no data is available """ return self.__deprecated_call('_get_from_cache', url) def _get_serializer(self, content_type=None): return self.__deprecated_call( '_get_serializer', content_type=content_type) class PistonAPI(_DeprecatedRequesterDecorator): """This class provides methods to make http requests slightly easier. It's a wrapper around ``httplib2`` to allow for a bit of state to be stored (like the service root) so that you don't need to repeat yourself as much. It's not intended to be used directly. Children classes should implement methods that actually call out to the api methods. When you define your API's methods you'll want to just call out to the ``_get``, ``_post``, ``_put`` or ``_delete`` methods provided by this class. """ SUPPORTED_SCHEMAS = PistonRequester.SUPPORTED_SCHEMAS default_service_root = '' default_timeout = None fail_handler = ExceptionFailHandler extra_headers = None serializers = None default_content_type = 'application/json' # Attributes that are forwarded to PistonRequester for backwards # compatibility reasons, but are not deprecated. # # Should only have attributes that users are expected to set on # constructed PistonAPI instances. __FORWARDED_ATTRIBUTES = { 'serializers': '_serializers', 'log_filename': '_log_filename', 'default_content_type': '_default_content_type', 'fail_handler': '_fail_handler', 'extra_headers': '_extra_headers', } def __init__(self, service_root=None, cachedir=None, auth=None, offline_mode=False, disable_ssl_validation=False, log_filename=None, timeout=None): """Initialize a ``PistonAPI``. ``service_root`` is the url to the server's service root. Children classes can provide a ``default_service_root`` class attribute that will be used if ``service_root`` is ``None``. ``timeout`` will be used as a socket timeout for all calls this instance makes. To explicitly set no timeout, set timeout=0. The default timeout=None will first check for an environment variable ``PISTON_MINI_CLIENT_DEFAULT_TIMEOUT`` and try to use that. If this environment variable is not found or it is an invalid float, the class's ``default_timeout`` will be used. Finally, if the class's default is also None, Python's default timeout for sockets will be used. All these should be in seconds. For all other arguments, see ``PistonRequester``. """ if timeout is None: try: timeout = float(os.environ.get(TIMEOUT_ENVVAR)) except (TypeError, ValueError): timeout = self.default_timeout if log_filename is None: log_filename = os.environ.get(LOG_FILENAME_ENVVAR) self._requester = PistonRequester( cachedir=cachedir, auth=auth, offline_mode=offline_mode, disable_ssl_validation=disable_ssl_validation, log_filename=log_filename, timeout=timeout, fail_handler=self.fail_handler, extra_headers=self.extra_headers, default_content_type=self.default_content_type, serializers=self.serializers) if service_root is None: service_root = self.default_service_root if not service_root: raise ValueError("No service_root provided, and no default found") parsed_service_root = urlparse(service_root) scheme = parsed_service_root.scheme if scheme not in self.SUPPORTED_SCHEMAS: raise ValueError("service_root's scheme must be http or https") self._service_root = service_root self._parsed_service_root = list(parsed_service_root) def __getattr__(self, name): if name in self.__FORWARDED_ATTRIBUTES: return getattr(self._requester, self.__FORWARDED_ATTRIBUTES[name]) return super(PistonAPI, self).__getattr__(name) def __setattr__(self, name, value): if name in self.__FORWARDED_ATTRIBUTES: setattr(self._requester, self.__FORWARDED_ATTRIBUTES[name], value) super(PistonAPI, self).__setattr__(name, value) def _request(self, path, method, body='', headers=None, scheme=None): """Perform an HTTP request. You probably want to call one of the ``_get``, ``_post``, ``_put`` methods instead. """ url = self._path2url(path, scheme) return self._requester.request_url( url, method, body=body, headers=headers) def _path2url(self, path, scheme=None): if scheme is None: service_root = self._service_root else: parts = [scheme] + self._parsed_service_root[1:] service_root = urlunparse(parts) return (service_root.strip('/') + '/' + path.lstrip('/')) def _get(self, path, args=None, scheme=None, extra_headers=None): """Perform an HTTP GET request. The provided ``path`` is appended to this resource's ``_service_root`` attribute to obtain the absolute URL that will be requested. If provided, ``args`` should be a dict specifying additional GET arguments that will be encoded on to the end of the url. ``scheme`` must be one of *http* or *https*, and will determine the scheme used for this particular request. If not provided the service_root's scheme will be used. ``extra_headers`` is an optional dictionary of header key/values that will be added to the http request. """ return self._requester.get( self._path2url(path, scheme), args=args, extra_headers=extra_headers) def _post(self, path, data=None, content_type=None, scheme=None, extra_headers=None): """Perform an HTTP POST request. The provided ``path`` is appended to this api's ``_service_root`` attribute to obtain the absolute URL that will be requested. ``data`` should be: - A string, in which case it will be used directly as the request's body, or - A ``list``, ``dict``, ``int``, ``bool`` or ``PistonSerializable`` (something with an ``as_serializable`` method) or even ``None``, in which case it will be serialized into a string according to ``content_type``. If ``content_type`` is ``None``, ``self.default_content_type`` will be used. ``scheme`` must be one of *http* or *https*, and will determine the scheme used for this particular request. If not provided the service_root's scheme will be used. ``extra_headers`` is an optional dictionary of header key/values that will be added to the http request. """ return self._requester.post( self._path2url(path, scheme), data=data, content_type=content_type, extra_headers=extra_headers) def _put(self, path, data=None, content_type=None, scheme=None, extra_headers=None): """Perform an HTTP PUT request. The provided ``path`` is appended to this api's ``_service_root`` attribute to obtain the absolute URL that will be requested. ``data`` should be: - A string, in which case it will be used directly as the request's body, or - A ``list``, ``dict``, ``int``, ``bool`` or ``PistonSerializable`` (something with an ``as_serializable`` method) or even ``None``, in which case it will be serialized into a string according to ``content_type``. If ``content_type`` is ``None``, ``self.default_content_type`` will be used. ``scheme`` must be one of *http* or *https*, and will determine the scheme used for this particular request. If not provided the service_root's scheme will be used. ``extra_headers`` is an optional dictionary of header key/values that will be added to the http request. """ return self._requester.put( self._path2url(path, scheme), data=data, content_type=content_type, extra_headers=extra_headers) def _delete(self, path, scheme=None, extra_headers=None): """Perform an HTTP DELETE request. The provided ``path`` is appended to this resource's ``_service_root`` attribute to obtain the absolute URL that will be requested. ``scheme`` must be one of *http* or *https*, and will determine the scheme used for this particular request. If not provided the service_root's scheme will be used. ``extra_headers`` is an optional dictionary of header key/values that will be added to the http request. """ return self._requester.delete( self._path2url(path, scheme), extra_headers=extra_headers) __version__ = '0.7.4' piston-mini-client-0.7.5/piston_mini_client/validators.py0000664000175000017500000001222312057365531025050 0ustar anthonyanthony00000000000000# -*- coding: utf-8 -*- # Copyright 2010-2012 Canonical Ltd. This software is licensed under the # GNU Lesser General Public License version 3 (see the file LICENSE). """These decorators can be applied to your ``PistonAPI`` methods to control how your method arguments are validated.""" import re from functools import wraps from .auth import BasicAuthorizer, OAuthAuthorizer try: unicode except NameError: # Python 3 basestring = unicode = str class ValidationException(Exception): pass def validate_pattern(varname, pattern, required=True): """Validate argument ``varname`` against regex pattern ``pattern``. The provided argument for ``varname`` will need to inherit from ``basestring``. If ``required`` is ``False`` then the argument can be omitted entirely. Your method signature will need to provide a default value in this case. """ if not pattern.endswith('$'): pattern = pattern + '$' def decorator(func): @wraps(func) def wrapper(*args, **kwargs): if varname in kwargs: if not isinstance(kwargs[varname], basestring): raise ValidationException( "Argument '%s' must be a string" % varname) if not re.match(pattern, kwargs[varname]): raise ValidationException( "Argument '%s' must match pattern '%s'" % (varname, pattern)) elif required: raise ValidationException( "Required named argument '%s' missing" % varname) return func(*args, **kwargs) return wrapper return decorator def validate(varname, cls, required=True): """Check that argument ``varname`` is of class ``cls``. If ``required`` is ``False`` then the argument can be omitted entirely. Your method signature will need to provide a default value in this case. """ def decorator(func): @wraps(func) def wrapper(*args, **kwargs): if varname in kwargs: if not isinstance(kwargs[varname], cls): raise ValidationException( "Argument '%s' must be a %s instead of %s" % ( varname, cls, type(kwargs[varname]))) elif required: raise ValidationException( "Required named argument '%s' missing" % varname) return func(*args, **kwargs) return wrapper return decorator def validate_integer(varname, min=None, max=None, required=True): """Check that argument ``varname`` is between ``min`` and ``max``. The provided argument for ``varname`` will need to be of type ``int``. If ``required`` is ``False`` then the argument can be omitted entirely. Your method signature will need to provide a default value in this case. """ def decorator(func): @wraps(func) def wrapper(*args, **kwargs): if varname in kwargs: var = kwargs[varname] if not isinstance(var, int): raise ValidationException( "Argument '%s' must be an int" % varname) elif min is not None and var < min: raise ValidationException( "Argument '%s' must be at least %s" % (varname, min)) elif max is not None and var > max: raise ValidationException( "Argument '%s' must be at most %s" % (varname, max)) elif required: raise ValidationException( "Required named argument '%s' missing" % varname) return func(*args, **kwargs) return wrapper return decorator def oauth_protected(func): """Only allow a method to be called with an ``OAuthAuthorizer`` available. To be able to call the method you've decorated you'll need to instantiate the ``PistonAPI`` providing a valid ``OAuthAuthorizer``. """ @wraps(func) def wrapper(self, *args, **kwargs): if not hasattr(self, '_auth') or self._auth is None: raise ValidationException( "This method is OAuth protected. " "Pass in an 'auth' argument to the constructor.") if not isinstance(self._auth, OAuthAuthorizer): raise ValidationException("self.auth must be an OAuthAuthorizer.") return func(self, *args, **kwargs) return wrapper def basic_protected(func): """Only allow a method to be called with an ``BasicAuthorizer`` available. To be able to call the method you've decorated you'll need to instantiate the ``PistonAPI`` providing a valid ``BasicAuthorizer``. """ @wraps(func) def wrapper(self, *args, **kwargs): if not hasattr(self, '_auth') or self._auth is None: raise ValidationException( "This method uses Basic auth. " "Pass in an 'auth' argument to the constructor.") if not isinstance(self._auth, BasicAuthorizer): raise ValidationException("self.auth must be a BasicAuthorizer.") return func(self, *args, **kwargs) return wrapper piston-mini-client-0.7.5/piston_mini_client/socks.py0000664000175000017500000004405712005525657024034 0ustar anthonyanthony00000000000000"""SocksiPy - Python SOCKS module. Version 1.00 Copyright 2006 Dan-Haim. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of Dan Haim nor the names of his contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY DAN HAIM "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL DAN HAIM OR HIS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMANGE. This module provides a standard socket-like interface for Python for tunneling connections through SOCKS proxies. """ """ Minor modifications made by Christopher Gilbert (http://motomastyle.com/) for use in PyLoris (http://pyloris.sourceforge.net/) Minor modifications made by Mario Vilas (http://breakingcode.wordpress.com/) mainly to merge bug fixes found in Sourceforge """ import socket import struct import sys import base64 import logging PROXY_TYPE_SOCKS4 = 1 PROXY_TYPE_SOCKS5 = 2 PROXY_TYPE_HTTP = 3 PROXY_TYPE_HTTP_NO_TUNNEL = 4 _defaultproxy = None _orgsocket = socket.socket class ProxyError(Exception): pass class GeneralProxyError(ProxyError): pass class Socks5AuthError(ProxyError): pass class Socks5Error(ProxyError): pass class Socks4Error(ProxyError): pass class HTTPError(ProxyError): pass _generalerrors = ("success", "invalid data", "not connected", "not available", "bad proxy type", "bad input") _socks5errors = ("succeeded", "general SOCKS server failure", "connection not allowed by ruleset", "Network unreachable", "Host unreachable", "Connection refused", "TTL expired", "Command not supported", "Address type not supported", "Unknown error") _socks5autherrors = ("succeeded", "authentication is required", "all offered authentication methods were rejected", "unknown username or invalid password", "unknown error") _socks4errors = ("request granted", "request rejected or failed", "request rejected because SOCKS server cannot connect to identd on the client", "request rejected because the client program and identd report different user-ids", "unknown error") def setdefaultproxy(proxytype=None, addr=None, port=None, rdns=True, username=None, password=None): """setdefaultproxy(proxytype, addr[, port[, rdns[, username[, password]]]]) Sets a default proxy which all further socksocket objects will use, unless explicitly changed. """ global _defaultproxy _defaultproxy = (proxytype, addr, port, rdns, username, password) def wrapmodule(module): """wrapmodule(module) Attempts to replace a module's socket library with a SOCKS socket. Must set a default proxy using setdefaultproxy(...) first. This will only work on modules that import socket directly into the namespace; most of the Python Standard Library falls into this category. """ if _defaultproxy != None: module.socket.socket = socksocket else: raise GeneralProxyError((4, "no proxy specified")) class socksocket(socket.socket): """socksocket([family[, type[, proto]]]) -> socket object Open a SOCKS enabled socket. The parameters are the same as those of the standard socket init. In order for SOCKS to work, you must specify family=AF_INET, type=SOCK_STREAM and proto=0. """ def __init__(self, family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0, _sock=None): _orgsocket.__init__(self, family, type, proto, _sock) if _defaultproxy != None: self.__proxy = _defaultproxy else: self.__proxy = (None, None, None, None, None, None) self.__proxysockname = None self.__proxypeername = None self.__httptunnel = True def __recvall(self, count): """__recvall(count) -> data Receive EXACTLY the number of bytes requested from the socket. Blocks until the required number of bytes have been received. """ data = self.recv(count) while len(data) < count: d = self.recv(count-len(data)) if not d: raise GeneralProxyError((0, "connection closed unexpectedly")) data = data + d return data def sendall(self, content, *args): """ override socket.socket.sendall method to rewrite the header for non-tunneling proxies if needed """ if not self.__httptunnel: content = self.__rewriteproxy(content) return super(socksocket, self).sendall(content, *args) def __rewriteproxy(self, header): """ rewrite HTTP request headers to support non-tunneling proxies (i.e. thos which do not support the CONNECT method). This only works for HTTP (not HTTPS) since HTTPS requires tunneling. """ host, endpt = None, None hdrs = header.split("\r\n") for hdr in hdrs: if hdr.lower().startswith("host:"): host = hdr elif hdr.lower().startswith("get") or hdr.lower().startswith("post"): endpt = hdr if host and endpt: hdrs.remove(host) hdrs.remove(endpt) host = host.split(" ")[1] endpt = endpt.split(" ") if (self.__proxy[4] != None and self.__proxy[5] != None): hdrs.insert(0, self.__getauthheader()) hdrs.insert(0, "Host: %s" % host) hdrs.insert(0, "%s http://%s%s %s" % (endpt[0], host, endpt[1], endpt[2])) return "\r\n".join(hdrs) def __getauthheader(self): auth = self.__proxy[4] + ":" + self.__proxy[5] return "Proxy-Authorization: Basic " + base64.b64encode(auth) def setproxy(self, proxytype=None, addr=None, port=None, rdns=True, username=None, password=None): """setproxy(proxytype, addr[, port[, rdns[, username[, password]]]]) Sets the proxy to be used. proxytype - The type of the proxy to be used. Three types are supported: PROXY_TYPE_SOCKS4 (including socks4a), PROXY_TYPE_SOCKS5 and PROXY_TYPE_HTTP addr - The address of the server (IP or DNS). port - The port of the server. Defaults to 1080 for SOCKS servers and 8080 for HTTP proxy servers. rdns - Should DNS queries be preformed on the remote side (rather than the local side). The default is True. Note: This has no effect with SOCKS4 servers. username - Username to authenticate with to the server. The default is no authentication. password - Password to authenticate with to the server. Only relevant when username is also provided. """ self.__proxy = (proxytype, addr, port, rdns, username, password) def __negotiatesocks5(self, destaddr, destport): """__negotiatesocks5(self,destaddr,destport) Negotiates a connection through a SOCKS5 server. """ # First we'll send the authentication packages we support. if (self.__proxy[4]!=None) and (self.__proxy[5]!=None): # The username/password details were supplied to the # setproxy method so we support the USERNAME/PASSWORD # authentication (in addition to the standard none). self.sendall(struct.pack('BBBB', 0x05, 0x02, 0x00, 0x02)) else: # No username/password were entered, therefore we # only support connections with no authentication. self.sendall(struct.pack('BBB', 0x05, 0x01, 0x00)) # We'll receive the server's response to determine which # method was selected chosenauth = self.__recvall(2) if chosenauth[0:1] != chr(0x05).encode(): self.close() raise GeneralProxyError((1, _generalerrors[1])) # Check the chosen authentication method if chosenauth[1:2] == chr(0x00).encode(): # No authentication is required pass elif chosenauth[1:2] == chr(0x02).encode(): # Okay, we need to perform a basic username/password # authentication. self.sendall(chr(0x01).encode() + chr(len(self.__proxy[4])) + self.__proxy[4] + chr(len(self.__proxy[5])) + self.__proxy[5]) authstat = self.__recvall(2) if authstat[0:1] != chr(0x01).encode(): # Bad response self.close() raise GeneralProxyError((1, _generalerrors[1])) if authstat[1:2] != chr(0x00).encode(): # Authentication failed self.close() raise Socks5AuthError((3, _socks5autherrors[3])) # Authentication succeeded else: # Reaching here is always bad self.close() if chosenauth[1] == chr(0xFF).encode(): raise Socks5AuthError((2, _socks5autherrors[2])) else: raise GeneralProxyError((1, _generalerrors[1])) # Now we can request the actual connection req = struct.pack('BBB', 0x05, 0x01, 0x00) # If the given destination address is an IP address, we'll # use the IPv4 address request even if remote resolving was specified. try: ipaddr = socket.inet_aton(destaddr) req = req + chr(0x01).encode() + ipaddr except socket.error: # Well it's not an IP number, so it's probably a DNS name. if self.__proxy[3]: # Resolve remotely ipaddr = None req = req + chr(0x03).encode() + chr(len(destaddr)).encode() + destaddr else: # Resolve locally ipaddr = socket.inet_aton(socket.gethostbyname(destaddr)) req = req + chr(0x01).encode() + ipaddr req = req + struct.pack(">H", destport) self.sendall(req) # Get the response resp = self.__recvall(4) if resp[0:1] != chr(0x05).encode(): self.close() raise GeneralProxyError((1, _generalerrors[1])) elif resp[1:2] != chr(0x00).encode(): # Connection failed self.close() if ord(resp[1:2])<=8: raise Socks5Error((ord(resp[1:2]), _socks5errors[ord(resp[1:2])])) else: raise Socks5Error((9, _socks5errors[9])) # Get the bound address/port elif resp[3:4] == chr(0x01).encode(): boundaddr = self.__recvall(4) elif resp[3:4] == chr(0x03).encode(): resp = resp + self.recv(1) boundaddr = self.__recvall(ord(resp[4:5])) else: self.close() raise GeneralProxyError((1,_generalerrors[1])) boundport = struct.unpack(">H", self.__recvall(2))[0] self.__proxysockname = (boundaddr, boundport) if ipaddr != None: self.__proxypeername = (socket.inet_ntoa(ipaddr), destport) else: self.__proxypeername = (destaddr, destport) def getproxysockname(self): """getsockname() -> address info Returns the bound IP address and port number at the proxy. """ return self.__proxysockname def getproxypeername(self): """getproxypeername() -> address info Returns the IP and port number of the proxy. """ return _orgsocket.getpeername(self) def getpeername(self): """getpeername() -> address info Returns the IP address and port number of the destination machine (note: getproxypeername returns the proxy) """ return self.__proxypeername def __negotiatesocks4(self,destaddr,destport): """__negotiatesocks4(self,destaddr,destport) Negotiates a connection through a SOCKS4 server. """ # Check if the destination address provided is an IP address rmtrslv = False try: ipaddr = socket.inet_aton(destaddr) except socket.error: # It's a DNS name. Check where it should be resolved. if self.__proxy[3]: ipaddr = struct.pack("BBBB", 0x00, 0x00, 0x00, 0x01) rmtrslv = True else: ipaddr = socket.inet_aton(socket.gethostbyname(destaddr)) # Construct the request packet req = struct.pack(">BBH", 0x04, 0x01, destport) + ipaddr # The username parameter is considered userid for SOCKS4 if self.__proxy[4] != None: req = req + self.__proxy[4] req = req + chr(0x00).encode() # DNS name if remote resolving is required # NOTE: This is actually an extension to the SOCKS4 protocol # called SOCKS4A and may not be supported in all cases. if rmtrslv: req = req + destaddr + chr(0x00).encode() self.sendall(req) # Get the response from the server resp = self.__recvall(8) if resp[0:1] != chr(0x00).encode(): # Bad data self.close() raise GeneralProxyError((1,_generalerrors[1])) if resp[1:2] != chr(0x5A).encode(): # Server returned an error self.close() if ord(resp[1:2]) in (91, 92, 93): self.close() raise Socks4Error((ord(resp[1:2]), _socks4errors[ord(resp[1:2]) - 90])) else: raise Socks4Error((94, _socks4errors[4])) # Get the bound address/port self.__proxysockname = (socket.inet_ntoa(resp[4:]), struct.unpack(">H", resp[2:4])[0]) if rmtrslv != None: self.__proxypeername = (socket.inet_ntoa(ipaddr), destport) else: self.__proxypeername = (destaddr, destport) def __negotiatehttp(self, destaddr, destport): """__negotiatehttp(self,destaddr,destport) Negotiates a connection through an HTTP server. """ # If we need to resolve locally, we do this now if not self.__proxy[3]: addr = socket.gethostbyname(destaddr) else: addr = destaddr headers = "CONNECT " + addr + ":" + str(destport) + " HTTP/1.1\r\n" headers += "Host: " + destaddr + "\r\n" if (self.__proxy[4] != None and self.__proxy[5] != None): headers += self.__getauthheader() + "\r\n" headers += "\r\n" self.sendall(headers.encode()) # We read the response until we get the string "\r\n\r\n" resp = self.recv(1) while resp.find("\r\n\r\n".encode()) == -1: resp = resp + self.recv(1) # We just need the first line to check if the connection # was successful statusline = resp.splitlines()[0].split(" ".encode(), 2) if statusline[0] not in ("HTTP/1.0".encode(), "HTTP/1.1".encode()): self.close() raise GeneralProxyError((1, _generalerrors[1])) try: statuscode = int(statusline[1]) except ValueError: self.close() raise GeneralProxyError((1, _generalerrors[1])) if statuscode != 200: self.close() raise HTTPError((statuscode, statusline[2])) self.__proxysockname = ("0.0.0.0", 0) self.__proxypeername = (addr, destport) def connect(self, destpair): """connect(self, despair) Connects to the specified destination through a proxy. destpar - A tuple of the IP/DNS address and the port number. (identical to socket's connect). To select the proxy server use setproxy(). """ # Do a minimal input check first if (not type(destpair) in (list,tuple)) or (len(destpair) < 2) or (type(destpair[0]) != type('')) or (type(destpair[1]) != int): raise GeneralProxyError((5, _generalerrors[5])) if self.__proxy[0] == PROXY_TYPE_SOCKS5: if self.__proxy[2] != None: portnum = self.__proxy[2] else: portnum = 1080 _orgsocket.connect(self, (self.__proxy[1], portnum)) self.__negotiatesocks5(destpair[0], destpair[1]) elif self.__proxy[0] == PROXY_TYPE_SOCKS4: if self.__proxy[2] != None: portnum = self.__proxy[2] else: portnum = 1080 _orgsocket.connect(self,(self.__proxy[1], portnum)) self.__negotiatesocks4(destpair[0], destpair[1]) elif self.__proxy[0] == PROXY_TYPE_HTTP: if self.__proxy[2] != None: portnum = self.__proxy[2] else: portnum = 8080 _orgsocket.connect(self,(self.__proxy[1], portnum)) self.__negotiatehttp(destpair[0], destpair[1]) elif self.__proxy[0] == PROXY_TYPE_HTTP_NO_TUNNEL: if self.__proxy[2] != None: portnum = self.__proxy[2] else: portnum = 8080 _orgsocket.connect(self,(self.__proxy[1],portnum)) if destpair[1] == 443: logging.warn("SSL connections (generally on port 443) require the use of tunneling - failing back to PROXY_TYPE_HTTP") self.__negotiatehttp(destpair[0],destpair[1]) else: self.__httptunnel = False elif self.__proxy[0] == None: _orgsocket.connect(self, (destpair[0], destpair[1])) else: raise GeneralProxyError((4, _generalerrors[4])) piston-mini-client-0.7.5/piston_mini_client/consts.py0000664000175000017500000000061512005525657024213 0ustar anthonyanthony00000000000000# -*- coding: utf-8 -*- # Copyright 2010-2012 Canonical Ltd. This software is licensed under the # GNU Lesser General Public License version 3 (see the file LICENSE). DISABLE_SSL_VALIDATION_ENVVAR = 'PISTON_MINI_CLIENT_DISABLE_SSL_VALIDATION' DEBUG_ENVVAR = 'PISTON_MINI_CLIENT_DEBUG' LOG_FILENAME_ENVVAR = 'PISTON_MINI_CLIENT_LOG_FILENAME' TIMEOUT_ENVVAR = 'PISTON_MINI_CLIENT_DEFAULT_TIMEOUT' piston-mini-client-0.7.5/README0000664000175000017500000000240512057365531017321 0ustar anthonyanthony00000000000000Overview ======== Piston mini client is a package that allows you to easily describe an API provided by a Django server using django-piston that takes care of: * Serializing call arguments and deserializing responses from the api. It can deserialize json provided by Piston into light-weight objects. * Making the http calls for you. You should be able to call a method on an api object instead of having to fetch a particular URL. * Provide a in-code description of your API. Developers should be able to know your API by looking at the client code you provide. You can read the `latest docs online`_. .. _latest docs online: http://packages.python.org/piston-mini-client/ Installation ============ piston-mini-client uses a standard distutils setup.py script, so you can use:: python setup.py install Running the tests ================= To run the tests, just install tox_ (can be in a virtualenv) and then run:: tox This will ensure the tests run on Python 2.6, 2.7 and 3.2, and the docs build correctly. Building the docs locally ========================= With sphinx_ installed, you should be able to build the documentation for this code with:: cd doc make html Enjoy! .. _tox: http://pypi.python.org/pypi/tox .. _sphinx: http://sphinx.pocoo.org/ piston-mini-client-0.7.5/setup.py0000664000175000017500000000073212113211214020131 0ustar anthonyanthony00000000000000#!/usr/bin/env python from setuptools import setup setup(name='piston-mini-client', version='0.7.5', description='A package to consume Django-Piston web services', url='https://launchpad.net/piston-mini-client', author='Anthony Lenton', author_email='anthony.lenton@canonical.com', packages=['piston_mini_client'], license='LGPLv3', install_requires=[ 'oauthlib', 'httplib2', ], test_suite = 'nose.collector', ) piston-mini-client-0.7.5/PKG-INFO0000664000175000017500000000044412113217741017527 0ustar anthonyanthony00000000000000Metadata-Version: 1.0 Name: piston-mini-client Version: 0.7.5 Summary: A package to consume Django-Piston web services Home-page: https://launchpad.net/piston-mini-client Author: Anthony Lenton Author-email: anthony.lenton@canonical.com License: LGPLv3 Description: UNKNOWN Platform: UNKNOWN piston-mini-client-0.7.5/MANIFEST.in0000664000175000017500000000020112005525657020167 0ustar anthonyanthony00000000000000include doc/*.rst include doc/Makefile include doc/conf.py include LICENSE include piston_mini_client/tests/*.py include tox.ini piston-mini-client-0.7.5/piston_mini_client.egg-info/0000775000175000017500000000000012113217741024010 5ustar anthonyanthony00000000000000piston-mini-client-0.7.5/piston_mini_client.egg-info/PKG-INFO0000664000175000017500000000044412113217741025107 0ustar anthonyanthony00000000000000Metadata-Version: 1.0 Name: piston-mini-client Version: 0.7.5 Summary: A package to consume Django-Piston web services Home-page: https://launchpad.net/piston-mini-client Author: Anthony Lenton Author-email: anthony.lenton@canonical.com License: LGPLv3 Description: UNKNOWN Platform: UNKNOWN piston-mini-client-0.7.5/piston_mini_client.egg-info/SOURCES.txt0000664000175000017500000000203412113217741025673 0ustar anthonyanthony00000000000000LICENSE MANIFEST.in README setup.cfg setup.py tox.ini doc/Makefile doc/conf.py doc/envvars.rst doc/index.rst doc/quickstart.rst doc/reference.rst doc/tuning.rst piston_mini_client/__init__.py piston_mini_client/auth.py piston_mini_client/consts.py piston_mini_client/failhandlers.py piston_mini_client/serializers.py piston_mini_client/socks.py piston_mini_client/validators.py piston_mini_client.egg-info/PKG-INFO piston_mini_client.egg-info/SOURCES.txt piston_mini_client.egg-info/dependency_links.txt piston_mini_client.egg-info/requires.txt piston_mini_client.egg-info/top_level.txt piston_mini_client/tests/__init__.py piston_mini_client/tests/test_auth.py piston_mini_client/tests/test_disable_ssl_check.py piston_mini_client/tests/test_failhandlers.py piston_mini_client/tests/test_log_to_file.py piston_mini_client/tests/test_pep8.py piston_mini_client/tests/test_proxy.py piston_mini_client/tests/test_resource.py piston_mini_client/tests/test_serializers.py piston_mini_client/tests/test_timeout.py piston_mini_client/tests/test_validators.pypiston-mini-client-0.7.5/piston_mini_client.egg-info/requires.txt0000664000175000017500000000002112113217741026401 0ustar anthonyanthony00000000000000oauthlib httplib2piston-mini-client-0.7.5/piston_mini_client.egg-info/dependency_links.txt0000664000175000017500000000000112113217741030056 0ustar anthonyanthony00000000000000 piston-mini-client-0.7.5/piston_mini_client.egg-info/top_level.txt0000664000175000017500000000002312113217741026535 0ustar anthonyanthony00000000000000piston_mini_client piston-mini-client-0.7.5/LICENSE0000664000175000017500000001674512005525657017462 0ustar anthonyanthony00000000000000 GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. piston-mini-client-0.7.5/tox.ini0000664000175000017500000000037512102751003017740 0ustar anthonyanthony00000000000000[tox] envlist = py26, py27, py32, docs [testenv] commands = nosetests deps=nose httplib2 mock pep8 [testenv:docs] basepython=python changedir=doc deps=sphinx commands= sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html