diff --git a/.travis.yml b/.travis.yml index 2b16f13a..4bcb0d07 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,4 +21,5 @@ deploy: secure: 2Gbn5BKdn4VovYg/iQTvVWndfzKm8941aF7mPcZ+Ped4Y1asDW8EEqhBP3Ocknive4HOwe22B4phIqnZ31/g2p/20lo5z/ywULwOCCuoRGTz5lMCFQt4MkJp3fvJwVUShOPJHPW4450UUOqmCoylaXFZsgq5+HLuplCMUgWro7ZiM8mfq6X45iCrHRGXSUh1SSmgSMLYZ7cM80bjvGjP0SlSsh+5ZUS6srDlUxFilH6Cc7+y0CjrnOxk1YIEhk+usLccaewpn0tpdhQf5gLQ6Q+3hj/o/ovnUiPyy4kYeCHjOgcv50JKPWNzM8Ie+9iWNZycs3tvwwZyWRueRMGAjnIO+AigQjuzIoaN8QEt3GyU0Rxxt1qU+KMgBvgSXV66l6w8Q5htpCD1fxIQxdElx+7gZQ1FUm9sHP7BVNVHgKPDP0etO+k5nx9d/1dXVw1CxSO/4zwfJ6VfxFIkIzOleoT7lydzdKsFY/5PlCD2WjErQfbdO8HIqinBwTFG+gfqZnblp64sRNMAcmzkqxN9GocltctRwmPHRbhUP960akeqbKlfQsAXrB2cxMIwYsX8AG21MQ+4hAOgQinAc5AiZ4Gmy7Nq0dxC126uAC9t8Y+4sq2Cwft7xzy/iSMM2hmPOlQx0kWbI7T1eI3FcXvz2aUdLhOz2o5L6VGfie7/RNo= on: tags: true + python: '3.6' distributions: "sdist bdist_wheel" diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100644 index 00000000..3836f9ae --- /dev/null +++ b/CHANGELOG.rst @@ -0,0 +1,27 @@ +Changelog +********* + +All notable changes to this project will be documented in this file. + +The format is based on `Keep a Changelog`_, and this project adheres to the +`Semantic Versioning`_ scheme. + +0.1.0 - 2017-10-19 +================== + +Added +----- +- Python ports for almost all method and argument annotations in Retrofit_. +- Adherence to the variation of the semantic versioning scheme outlined in + the official Python package distribution tutorial. +- MIT License +- Documentation with introduction, instructions for installing, and quick + getting started guide covering the builder and all method and argument + annotations. +- README that contains GitHub API v3 example, installation instructions with + ``pip``, and link to online documentation. + + +.. _Retrofit: http://square.github.io/retrofit/ +.. _`Keep a Changelog`: http://keepachangelog.com/en/1.0.0/ +.. _`Semantic Versioning`: https://packaging.python.org/tutorials/distributing-packages/#semantic-versioning-preferred \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in index d8afa927..e8176257 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,2 @@ -include README.rst LICENSE requirements.txt +include README.rst CHANGELOG.rst LICENSE requirements.txt recursive-include tests *.py diff --git a/README.rst b/README.rst index 42f929e4..d54e0bb3 100644 --- a/README.rst +++ b/README.rst @@ -5,10 +5,12 @@ Python HTTP Made Expressive. Inspired by `Retrofit `__. These +templates can contain parameters enclosed in braces (e.g., :code:`{name}`) +for method arguments to handle at runtime. + +To map a method argument to a declared URI path parameter for expansion, use +the :py:class:`uplink.Path` annotation. For instance, we can define a consumer +method to query any GitHub user's metadata by declaring the +`path segment parameter `__ +:code:`{/username}` in the method's URL. + +.. code-block:: python + + class GitHub(object): + @get("users{/username}") + def get_user(self, username: Path("username")): pass + +With an instance of this consumer, we can invoke the :code:`get_user` +method like so + +.. code-block:: python + + github.get_user("prkumar") + +to create an HTTP request with a URL ending in :code:`users/prkumar`. + +.. _implicit_path_annotations: + +Implicit :code:`Path` Annotations +---------------------------------- + +When building the consumer instance, :py:func:`uplink.build` will try to resolve +unannotated method arguments by matching their names with URI path parameters. + +For example, consider the consumer defined below, in which the method +:py:meth:`get_user` has an unannotated argument, :py:attr:`username`. +Since its name matches the URI path parameter ``{username}``, +:py:mod:`uplink` will auto-annotate the argument with :py:class:`Path` +for us: + +.. code-block:: python + + class GitHub(object): + @uplink.get("users{/username}") + def get_user(self, username): pass + +Important to note, failure to resolve all unannotated function arguments +raises an :py:class:`~uplink.InvalidRequestDefinitionError`. + +Query Parameters +================ + +To set unchanging query parameters, you can append a query string to the +static URL. For instance, GitHub offers the query parameter :code:`q` +for adding keywords to a search. With this, we can define a consumer +that queries all GitHub repositories written in Python: + +.. code-block:: python + :emphasize-lines: 2 + + class GitHub(object): + @uplink.get("/search/repositories?q=language:python") + def search_python_repos(self): pass + +Note that we have hard-coded the query parameter into the URL, so that all +requests that this method handles include that search term. + +Alternatively, we can set query parameters at runtime using method +arguments. To set dynamic query parameters, use the :py:class:`uplink.Query` and +:py:class:`uplink.QueryMap` argument annotations. + +For instance, to set the search term :code:`q` at runtime, we can +provide a method argument annotated with :py:class:`uplink.Query`: + +.. code-block:: python + :emphasize-lines: 3 + + class GitHub(object): + @uplink.get("/search/repositories") + def search_repos(self, q: uplink.Query) + +Further, the :py:class:`uplink.QueryMap` annotation indicates that an +argument handles a mapping of query parameters. For example, let's use this +annotation to transform keyword arguments into query parameters: + +.. code-block:: python + :emphasize-lines: 3 + + class GitHub(object): + @uplink.get("/search/repositories") + def search_repos(self, **params: uplink.QueryMap) + +This serves as a nice alternative to adding a :py:class:`uplink.Query` +annotated argument for each supported query parameter. For instance, +we can now optionally modify how the GitHub search results are sorted, +leveraging the :code:`sort` query parameter: + +.. code-block:: python + + # Search for Python repos and sort them by number of stars. + github.search_repos(q="language:python", sort="stars").execute() + +.. note:: + + Another approach for setting dynamic query parameters is to use `path + variables`_ in the static URL, with `"form-style query expansion" + `_. + +HTTP Headers +============ + +To add literal headers, use the :py:class:`uplink.headers` method annotation, +which has accepts the input parameters as :py:class:`dict`: + +.. code-block:: python + :emphasize-lines: 2,3 + + class GitHub(object): + # This header explicitly requests version v3 of the GitHub API. + @uplink.headers({"Accept": "application/vnd.github.v3.full+json"}) + @uplink.get("/repositories") + def get_repos(self): pass + +Alternatively, we can use the :py:class:`uplink.Header` argument annotation to +pass a header as a method argument at runtime: + +.. code-block:: python + :emphasize-lines: 6 + + class GitHub(object): + @uplink.get("/users{/username}") + def get_user( + self, + username, + last_modified: uplink.Header("If-Modified-Since") + ): + """Fetch a GitHub user if modified after given date.""" + +Further, you can annotate an argument with :py:class:`uplink.HeaderMap` to +accept a mapping of header fields. + +URL-Encoded Request Body +======================== + +For ``POST``/``PUT``/``PATCH`` requests, the format of the message body +is an important detail. A common approach is to url-encode the body and +set the header ``Content-Type: application/x-www-form-urlencoded`` +to notify the server. + +To submit a url-encoded form with Uplink, decorate the consumer method +with :py:class:`uplink.form_url_encoded` and annotate each argument +accepting a form field with :py:class:`uplink.Field`. For instance, +let's provide a method for reacting to a specific GitHub issue: + +.. code-block:: python + :emphasize-lines: 2,7 + + class GitHub(object): + @uplink.form_url_encoded + @uplink.patch("/user") + def update_blog_url( + self, + access_token: uplink.Query, + blog_url: uplink.Field + ): + """Update a user's blog URL.""" + +Further, you can annotate an argument with :py:class:`uplink.FieldMap` to +accept a mapping of form fields. + +Send Multipart Form Data +======================== + +`Multipart requests +`__ are commonly +used to upload files to a server. + +To send a multipart message, decorate a consumer method with +:py:class:`uplink.multipart`. Moreover, use the :py:class:`uplink.Part` argument +annotation to mark a method argument as a form part. + +.. todo:: + + Add a code block that illustrates an example of how to define a + consumer method that sends multipart requests. + +Further, you can annotate an argument with :py:class:`uplink.PartMap` to +accept a mapping of form fields to parts. + +JSON Requests, and Other Content Types +====================================== + +Nowadays, many HTTP webservices nowadays accept JSON requests. (GitHub's +API is an example of such a service.) Given the format's growing +popularity, Uplink provides the decorator :py:class:`uplink.json`. + +When using this decorator, you should annotate a method argument with +:py:class:`uplink.Body`, which indicates that the argument's value +should become the request's body. Moreover, this value is expected to be +an instance of :py:class:`dict` or a subclass of +:py:class:`uplink.Mapping`. + +Note that :py:class:`uplink.Body` can annotate the keyword argument, which +often enables concise method signatures: + +.. code-block:: python + :emphasize-lines: 2,7 + + class GitHub(object): + @uplink.json + @uplink.patch("/user") + def update_user( + self, + access_token: uplink.Query, + **info: uplink.Body + ): + """Update an authenticated user.""" + + +Further, you may be able to send other content types by using +:py:class:`uplink.Body` and setting the ``Content-Type`` header +appropriately with the decorator :py:class:`uplink.header`. \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index 1c5a8b20..e2f3c7f9 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -5,18 +5,37 @@ Uplink 📡 ========== -A Declarative HTTP Client for Python, inspired by `Retrofit +A Declarative HTTP Client for Python. Inspired by `Retrofit `__. |Release| |Python Version| |License| |Coverage Status| +.. note:: + + Uplink is currently in initial development and, therefore, not + production ready at the moment. Furthermore, as the package follows a + `semantic versioning `__ + scheme, the public API outlined in this documentation should be + considered tentative until the :code:`v1.0.0` release. + + However, while Uplink is under construction, we invite eager users + to install early and provide open feedback, which can be as simple as + opening a GitHub issue when you notice a missing feature, latent defect, + documentation oversight, etc. + + Moreover, for those interested in contributing, I plan to publish a + contribution guide soon, but in the meantime, please feel free to fork + the `repository on GitHub `__ and open + a pull request ('tis + `Hacktoberfest `__, after all)! + ---- A Quick Walkthrough, with GitHub API v3: ---------------------------------------- Using decorators and function annotations, you can turn any plain old Python -class into an HTTP API consumer: +class into a self-describing consumer of your favorite HTTP webservice: .. code-block:: python @@ -25,7 +44,7 @@ class into an HTTP API consumer: # To register entities that are common to all API requests, you can # decorate the enclosing class rather than each method separately: @headers({"Accept": "application/vnd.github.v3.full+json"}) - class GitHubService(object): + class GitHub(object): @get("/users/{username}") def get_user(self, username): @@ -40,20 +59,20 @@ To construct a consumer instance, use the helper function :py:func:`uplink.build .. code-block:: python - github = build(GitHubService, base_url="https://api.github.com/") + github = build(GitHub, base_url="https://api.github.com/") To access the GitHub API with this instance, we simply invoke any of the methods that we defined in the interface above. To illustrate, let's update my GitHub -user's bio: +profile bio: .. code-block:: python r = github.update_user(token, bio="Beam me up, Scotty!").execute() -*Voila*, :py:meth:`update_user` seamlessly builds the request (using the +*Voila*, :py:meth:`update_user` builds the request seamlessly (using the decorators and annotations from the method's definition), and :py:meth:`execute` sends that synchronously over the network. Furthermore, -the returned response :py:obj:`r` is a :py:class:`requests.Response` +the returned response :py:obj:`r` is simply a :py:class:`requests.Response` (`documentation `__): @@ -61,40 +80,37 @@ the returned response :py:obj:`r` is a :py:class:`requests.Response` print(r.json()) # {u'disk_usage': 216141, u'private_gists': 0, ... -In essence, **Uplink** delivers API consumers that are self-describing, -reusable, and fairly compact, with minimal user effort. - ----- - -.. note:: - - **Uplink** is currently in initial development and, therefore, not - production ready at the moment. Furthermore, as the package follows the - `Semantic Versioning Specification `__, the public - API outlined in this documentation should not be considered stable until the - release of :code:`v1.0.0`. - - However, while **Uplink** is under construction, we invite eager users - to install early and provide open feedback, which can be as simple as - opening a GitHub issue when you notice a missing feature, latent defect, - documentation oversight, etc. - - Moreover, for those interested in contributing, I plan to publish a - contribution guide soon, but in the meantime, please feel free to fork - the `repository on GitHub `__ and open - a pull request ('tis - `Hacktoberfest `__, after all)! +In essence, Uplink delivers reusable and self-sufficient objects for +accessing HTTP webservices, with minimal code and user pain ☺️. The User Manual --------------- -This guide describes the package's Public API. +Follow this guide to get up and running with Uplink. .. toctree:: :maxdepth: 2 install.rst - types.rst + introduction.rst + getting_started.rst + +.. + The Public API + -------------- + + .. todo:: + + Most of this guide is unfinished and completing it is a planned + deliverable for the ``v0.2.0`` release. At the least, this work will + necessitate adding docstrings to the classes enumerated below. + + .. toctree:: + :maxdepth: 3 + + decorators.rst + types.rst + .. |Coverage Status| image:: https://coveralls.io/repos/github/prkumar/uplink/badge.svg?branch=master :target: https://coveralls.io/github/prkumar/uplink?branch=master diff --git a/docs/source/install.rst b/docs/source/install.rst index 6d54cf4f..0a2f4277 100644 --- a/docs/source/install.rst +++ b/docs/source/install.rst @@ -1,12 +1,10 @@ Installation ============ -Getting started with Uplink begins with installation. +Using :program:`pip` +-------------------- -Using ``pip`` -------------- - -With ``pip`` or ``pipenv`` installed, you can install Uplink simply by +With :program:`pip` (or :program:`pipenv`), you can install Uplink simply by typing: :: @@ -20,13 +18,14 @@ Download the Source Code Uplink's source code is in a `public repository hosted on GitHub `__. -As an alternative to installing with ``pip``, you could clone the repository +As an alternative to installing with :program:`pip`, you could clone the +repository :: $ git clone https://github.com/prkumar/uplink.git -Then, install; e.g., with ``setup.py``: +Then, install; e.g., with :file:`setup.py`: :: diff --git a/docs/source/introduction.rst b/docs/source/introduction.rst new file mode 100644 index 00000000..7aeed7ac --- /dev/null +++ b/docs/source/introduction.rst @@ -0,0 +1,200 @@ +Introduction +************ + +Uplink delivers reusable and self-sufficient objects for accessing +HTTP webservices, with minimal code and user pain. + +Defining similar objects with other Python HTTP clients, such as +:code:`requests`, often requires writing boilerplate code and layers of +abstraction. With Uplink, simply define your consumers using +decorators and function annotations, and we'll handle the REST for you! (Pun +intended, obviously.) 😎 + +**Method Annotations**: Static Request Handling +=============================================== + +Essentially, method annotations describe request properties that are relevant +to all invocations of a consumer method. + +For instance, consider the following GitHub API consumer: + +.. code-block:: python + :emphasize-lines: 2 + + class GitHub(object): + @uplink.timeout(60) + @uplink.get("/repositories") + def get_repos(self): + """Dump every public repository.""" + +Annotated with :py:class:`timeout`, the method :py:meth:`get_repos` will build +HTTP requests that wait an allotted number of seconds -- 60, in this case -- +for the server to respond before giving up. + +Applying Multiple Method Annotations +------------------------------------ + +As method annotations are simply decorators, you can stack one on top of another +for chaining: + +.. code-block:: python + :emphasize-lines: 2,3 + + class GitHub(object): + @uplink.headers({"Accept": "application/vnd.github.v3.full+json"}) + @uplink.timeout(60) + @uplink.get("/repositories") + def get_repos(self): + """Dump every public repository.""" + +A Shortcut for Annotating All Methods in a Class +------------------------------------------------ + +To apply an annotation across all methods in a class, you can simply +annotate the class rather than each method individually: + +.. code-block:: python + :emphasize-lines: 1,2 + + @uplink.timeout(60) + class GitHub(object): + @uplink.get("/repositories") + def get_repos(self): + """Dump every public repository.""" + + @uplink.get("/organizations") + def get_organizations(self): + """List all organizations.""" + +Hence, the consumer defined above is equivalent to the following, +slightly more verbose one: + +.. code-block:: python + + class GitHub(object): + @uplink.timeout(60) + @uplink.get("/repositories") + def get_repos(self): + """Dump every public repository.""" + + @uplink.timeout(60) + @uplink.get("/organizations") + def get_organizations(self): + """List all organizations.""" + +**Arguments Annotations**: Dynamic Request Handling +=================================================== + +In programming, parametrization drives a function's dynamic behavior; a +function's output depends normally on its inputs. With +:py:mod:`uplink`, function arguments parametrize an HTTP request, and +you indicate the dynamic parts of the request by appropriately +annotating those arguments. + +To illustrate, for the method :py:meth:`get_user` in the following +snippet, we have flagged the argument :py:attr:`username` as a URI +placeholder replacement using the :py:class:`~uplink.Path` annotation: + +.. code-block:: python + + class GitHub(object): + @uplink.get("users/{username}") + def get_user(self, username: uplink.Path("username")): pass + +Invoking this method on a consumer instance, like so: + +.. code-block:: python + + github.get_user(username="prkumar") + +Builds an HTTP request that has a URL ending with ``users/prkumar``. + +.. note:: + + As you probably took away from the above example: when parsing the + method's signature for argument annotations, :py:mod:`uplink` skips + the instance reference argument, which is the leading method + parameter and usually named :py:attr:`self`. + +Adopting the Argument's Name +---------------------------- + +When you initialize a named annotation, such as a +:py:class:`~uplink.Path` or :py:class:`~Field`, without a name (by +omitting the :py:attr:`name` parameter), it adopts the name of its +corresponding method argument. + +For example, in the snippet below, we can omit naming the +:py:class:`~uplink.Path` annotation since the corresponding argument's +name, :py:attr:`username`, matches the intended URI path parameter: + +.. code-block:: python + + class GitHub(object): + @uplink.get("users/{username}") + def get_user(self, username: uplink.Path): pass + +Annotating Your Arguments +------------------------- + +There are several ways to annotate arguments. Most examples in this +documentation use function annotations, but this approach is unavailable +for Python 2.7 users. Instead, you can use argument annotations as decorators +or utilize the method annotation :py:class:`~uplink.args`. + +Argument Annotations as Decorators +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For one, annotations can work as function decorators. With this approach, +annotations are mapped to arguments from "bottom-up". + +For instance, in the below definition, the :py:class:`~uplink.Url` +annotation corresponds to :py:attr:`commits_url`, and +:py:class:`~uplink.Path` to :py:attr:`sha`. + +.. code-block:: python + :emphasize-lines: 2,3 + + class GitHub(object): + @uplink.Path + @uplink.Url + @uplink.get + def get_commit(self, commits_url, sha): pass + +Using :py:class:`uplink.args` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The second approach involves using the method annotation +:py:class:`~uplink.args`, arranging annotations in the same order as +their corresponding function arguments (again, ignore :py:attr:`self`): + +.. code-block:: python + :emphasize-lines: 2 + + class GitHub(object): + @uplink.args(uplink.Url, uplink.Path) + @uplink.get + def get_commit(self, commits_url, sha): pass + +Function Annotations (Python 3 only) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Finally, when using Python 3, you can use these classes as function +annotations (:pep:`3107`): + +.. code-block:: python + :emphasize-lines: 3 + + class GitHub(object): + @uplink.get + def get_commit(self, commit_url: uplink.Url, sha: uplink.Path): + pass + +Integration with :code:`python-requests` +======================================== + +Experienced users of `Kenneth Reitz's `__ +well-established `Requests library `__ might be happy to read that Uplink uses +:code:`requests` behind-the-scenes and bubbles :code:`requests.Response` +instances back up to the user. diff --git a/docs/source/types.rst b/docs/source/types.rst index 928430d1..ede98037 100644 --- a/docs/source/types.rst +++ b/docs/source/types.rst @@ -1,147 +1,55 @@ Argument Annotations ******************** -Overview -======== +Replace URI ``Path`` Variables +============================== -In programming, parametrization drives a function's dynamic behavior; a -function's output depends normally on its inputs. With -:py:mod:`uplink`, function arguments parametrize an HTTP request, and -you indicate the dynamic parts of the request by appropriately -annotating those arguments with the classes defined here. - -To illustrate, for the method :py:meth:`get_user` in the following -snippet, we have flagged the argument :py:attr:`username` as a URI path -parameter using the :py:class:`~uplink.Path` annotation: - -.. code-block:: python - - class GitHubService(object): - - @uplink.get("users/{username}") - def get_user(self, username: uplink.Path("username")): pass - -Invoking this method on a consumer instance, like so: - -.. code-block:: python - - github.get_user(username="prkumar") - -Builds an HTTP request that has a URL ending with ``users/prkumar``. - -.. note:: - As you probably took away from the above example, :py:mod:`uplink` - ignores the instance reference argument (e.g., :py:attr:`self`), with - respect to argument annotations. - -Annotating Your Arguments -------------------------- - -There are several ways to annotate arguments. Most examples in this -documentation use function annotations, but this approach is unavailable -for Python 2.7 users. Instead, you can use argument annotations as decorators -or utilize the method annotation :py:class:`~uplink.args`. - -Argument Annotations as Decorators -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -For one, annotations can work as function decorators. With this approach, -annotations are mapped to arguments from "bottom-up". - -For instance, in the below definition, the :py:class:`~uplink.Url` -annotation corresponds to :py:attr:`commits_url`, and -:py:class:`~uplink.Path` to :py:attr:`sha`. - -.. code-block:: python - - class GitHubService(object): - - @uplink.Path - @uplink.Url - @uplink.get - def get_commit(self, commits_url, sha): pass - -Using :py:class:`uplink.args` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The second approach involves using the method annotation -:py:class:`~uplink.args`, arranging annotations in the same order as -their corresponding function arguments (again, ignore :py:attr:`self`): - -.. code-block:: python - - class GitHubService(object): - - @uplink.args(uplink.Url, uplink.Path) - @uplink.get - def get_commit(self, commits_url, sha): pass - - -Function Annotations (Python 3 only) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Finally, when using Python 3, you can use these classes as function -annotations (:pep:`3107`): - -.. code-block:: python - - class GitHubService(object): - - @uplink.get - def get_commit(self, commit_url: uplink.Url, sha: uplink.Path): - pass - - -Function Argument Name Adoption -------------------------------- - -When you initialize a named annotation, such as a -:py:class:`~uplink.Path` or :py:class:`~Field`, without a name (by -omitting the :py:attr:`name` parameter), it adopts the name of its -corresponding method argument. +.. autoclass:: uplink.Path + :members: -For example, in the snippet below, we can omit naming the -:py:class:`~uplink.Path` annotation since the corresponding argument's -name, :py:attr:`username`, matches the intended URI path parameter: +Append a URL ``Query`` Parameter +================================ -.. code-block:: python +.. autoclass:: uplink.Query - class GitHubService(object): +.. autoclass:: uplink.QueryMap - @uplink.get("users/{username}") - def get_user(self, username: uplink.Path): pass +Set HTTP ``Header`` Field +========================= +.. autoclass:: uplink.Header + :members: -Implicit :code:`Path` Annotations ----------------------------------- +.. autoclass:: uplink.HeaderMap + :members: -When building the consumer instance, :py:mod:`uplink` will try to resolve -unannotated method arguments by matching their names with URI path parameters. -For example, consider the consumer defined below, in which the method -:py:meth:`get_user` has an unannotated argument, :py:attr:`username`. -Since its name matches the URI path parameter ``{username}``, -:py:mod:`uplink` will auto-annotate the argument with :py:class:`Path` -for us: +Add URL-Encoded Form ``Field`` +============================== -.. code-block:: python +.. autoclass:: uplink.Field + :members: - class GitHubService(object): +.. autoclass:: uplink.FieldMap + :members: - @uplink.get("user/{username}") - def get_user(self, username): pass +Submit Form Data ``Part`` +========================= -Important to note, failure to resolve all unannotated function arguments -raises an :py:class:`~uplink.InvalidRequestDefinitionError`. +.. autoclass:: uplink.Part + :members: +.. autoclass:: uplink.PartMap + :members: -:code:`Path`: URL Path Variables -================================ +Control the Request ``Body`` +============================ -.. autoclass:: uplink.Path +.. autoclass:: uplink.Body :members: -:code:`Query`: URL Query Parameters -=================================== +Assign a ``Url`` at Runtime +=========================== -.. autoclass:: uplink.Query +.. autoclass:: uplink.Url + :members: \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 46c68808..a030917f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,4 @@ tox==2.7.0 # Documentation Sphinx==1.6.3 +sphinx-autobuild==0.7.1 diff --git a/setup.py b/setup.py index 5b6296e2..bbe27af8 100644 --- a/setup.py +++ b/setup.py @@ -26,6 +26,7 @@ def read(filename): "classifiers": [ "Development Status :: 2 - Pre-Alpha", "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", diff --git a/tests/test_types.py b/tests/test_types.py index 57b044dd..1e112a2e 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -247,7 +247,7 @@ def test_modify_request(self, request_builder): class TestField(ArgumentTestCase): type_cls = types.Field - expected_converter_type = converter.CONVERT_TO_REQUEST_BODY + expected_converter_type = converter.CONVERT_TO_STRING def test_modify_request(self, request_builder): types.Field("hello").modify_request(request_builder, "world") @@ -261,7 +261,7 @@ def test_modify_request_failure(self, request_builder): class TestFieldMap(ArgumentTestCase): type_cls = types.FieldMap - expected_converter_type = converter.Map(converter.CONVERT_TO_REQUEST_BODY) + expected_converter_type = converter.Map(converter.CONVERT_TO_STRING) def test_modify_request(self, request_builder): types.FieldMap().modify_request(request_builder, {"hello": "world"}) diff --git a/uplink/__about__.py b/uplink/__about__.py index 134f79e5..430135bd 100644 --- a/uplink/__about__.py +++ b/uplink/__about__.py @@ -3,4 +3,4 @@ that is used both in distribution (i.e., setup.py) and within the codebase. """ -__version__ = "0.1.0-rc.2" +__version__ = "0.1.0rc3" diff --git a/uplink/types.py b/uplink/types.py index 7f0147a6..c6741541 100644 --- a/uplink/types.py +++ b/uplink/types.py @@ -19,6 +19,8 @@ "HeaderMap", "Field", "FieldMap", + "Part", + "PartMap", "Body", "Url" ] @@ -220,26 +222,39 @@ def modify_request(self, request_builder, value): class Path(NamedArgument): """ - Substitution of a URL path parameter. + Substitution of a path variable in a `URI template + `__. - Here's a simple example: + URI template parameters are enclosed in braces (e.g., + :code:`{name}`). To map an argument to a declared URI parameter, use + the :py:class:`Path` annotation: .. code-block:: python - @get("todos{/id}") - def get_todo(self, todo_id: Path("id")): pass + class TodoService(object): + @get("todos{/id}") + def get_todo(self, todo_id: Path("id")): pass - Then, calling :code:`todo_service.get_todo(100)` would produce the - path :code:`"todos/100"`. - - `uplink` will try to match unannotated function arguments with - URL path parameters. For example, we can rewrite the previous - example as: + Then, invoking :code:`get_todo` with a consumer instance: .. code-block:: python - @get("todos{/todo_id}") - def get_todo(self, todo_id): pass + todo_service.get_todo(100) + + creates an HTTP request with a URL ending in :code:`todos/100`. + + Note: + When building the consumer instance, :py:func:`uplink.build` will try + match unannotated function arguments with URL path parameters. See + :ref:`implicit_path_annotations` for details. + + For example, we could rewrite the method from the previous + example as: + + .. code-block:: python + + @get("todos{/id}") + def get_todo(self, id): pass """ @property @@ -254,11 +269,6 @@ def modify_request(self, request_builder, value): class Query(NamedArgument): - """ - A URL query parameter. - - - """ @staticmethod def convert_to_string(value): @@ -279,9 +289,6 @@ def modify_request(self, request_builder, value): class QueryMap(TypedArgument): - """ - Mapping of URL query parameters. - """ @property def converter_type(self): @@ -327,7 +334,7 @@ def __init__(self, field): @property def converter_type(self): - return converter.CONVERT_TO_REQUEST_BODY + return converter.CONVERT_TO_STRING def modify_request(self, request_builder, value): try: @@ -348,7 +355,7 @@ class FieldMapUpdateFailed(exceptions.AnnotationError): @property def converter_type(self): - return converter.Map(converter.CONVERT_TO_REQUEST_BODY) + return converter.Map(converter.CONVERT_TO_STRING) def modify_request(self, request_builder, value): try: