GithubHelp home page GithubHelp logo

prkumar / uplink Goto Github PK

View Code? Open in Web Editor NEW
1.0K 17.0 63.0 2.03 MB

A Declarative HTTP Client for Python

Home Page: https://uplink.readthedocs.io/

License: MIT License

Python 100.00%
python http http-client rest-api retrofit requests api-client aiohttp

uplink's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

uplink's Issues

Support string formatting with `uplink.Header` annotation

Dynamic HTTP Header fields are declared using the uplink.Header annotation:

@get("/user")
def get_user(self, authorization: Header):
    """Get an authenticated user."""

Adding support for string formatting could simplify the method's call and further abstract the HTTP:

@get("/user")
def get_user(self, authorization: Header(pattern="Basic {}")): 
    """Get an authenticated user."""

Memory leak in consumer class

Describe the bug
Memory usage keeps increasing over time. Even after gc.collect()

To Reproduce
use example consumer GetHub:

from uplink import Consumer, get, headers, Path, Query
from pympler import muppy
from pympler import summary

class GitHub(Consumer):
   """A Python Client for the GitHub API."""

   @get("users/{user}/repos")
   def get_repos(self, user: Path, sort_by: Query("sort")):
      """Get user's public repositories."""

Along with pympler library to capture leak objects:

In [73]: import gc

In [74]: gc.collect()
Out[74]: 289

In [75]: sum1 = summary.summarize(muppy.get_objects())

In [76]: for i in range(10):
    ...:     github = GitHub(base_url="https://api.github.com/")
    ...:     github.get_repos(user="octocat", sort_by="created")
    ...:     

In [77]: gc.collect()
Out[77]: 83

In [78]: sum2 = summary.summarize(muppy.get_objects())

In [79]: diff = summary.get_diff(sum1, sum2)

In [80]: summary.print_(diff, limit=200)
                                                      types |   # objects |   total size
=========================================================== | =========== | ============
                                               <class 'list |       13887 |      1.27 MB
                                                <class 'str |       14004 |   1021.85 KB
                                                <class 'int |        3507 |     95.94 KB
                                               <class 'dict |         239 |     43.27 KB
                                  <class 'collections.deque |          31 |     19.13 KB
                            <class 'collections.OrderedDict |          41 |     17.33 KB
                                              <class 'tuple |         146 |      9.17 KB
                                                <class 'set |          20 |      4.38 KB
                         <class 'builtin_function_or_method |          62 |      4.36 KB
                                        function (<lambda>) |          20 |      2.66 KB
                        <class 'urllib3.poolmanager.PoolKey |          10 |      2.27 KB
                                <class 'threading.Condition |          31 |      1.70 KB
                                      <class '_thread.RLock |          30 |      1.41 KB
                                            <class 'weakref |          15 |      1.17 KB
                    <class 'urllib3.poolmanager.PoolManager |          20 |      1.09 KB
                           <class 'urllib3.util.retry.Retry |          20 |      1.09 KB
         <class 'urllib3._collections.RecentlyUsedContainer |          20 |      1.09 KB
                      <class 'requests.adapters.HTTPAdapter |          20 |      1.09 KB
                                      <class 'ssl.SSLSocket |          10 |      1.02 KB
                                     <class 'ssl.SSLContext |          10 |    960     B
                                             <class 'method |          12 |    768     B
         <class 'urllib3.connection.VerifiedHTTPSConnection |          10 |    560     B
                       <class 'urllib3.util.timeout.Timeout |          10 |    560     B
                                      <class 'ssl.SSLObject |          10 |    560     B
            <class 'requests.structures.CaseInsensitiveDict |          10 |    560     B
                          <class 'requests.sessions.Session |          10 |    560     B
                 <class 'http.cookiejar.DefaultCookiePolicy |          10 |    560     B
                            <class 'http.client.HTTPMessage |          10 |    560     B
                                    <class 'queue.LifoQueue |          10 |    560     B
                           <class 'http.client.HTTPResponse |          10 |    560     B
                 <class 'requests.cookies.RequestsCookieJar |          10 |    560     B
         <class 'urllib3.connectionpool.HTTPSConnectionPool |          10 |    560     B
                                       <class '_thread.lock |          12 |    480     B
                                               <class 'code |           2 |    288     B
                                               <class 'cell |           5 |    240     B
                                          function (thread) |           1 |    136     B
                                         function (wrapper) |           1 |    136     B
                                           function (ready) |           1 |    136     B
                                  <class 'functools.partial |           1 |     80     B
         <class 'prompt_toolkit.document._ImmutableLineList |           0 |     72     B
                                              <class 'float |           3 |     72     B
            <class 'uplink.clients.requests_.RequestsClient |           1 |     56     B
                        <class 'uplink.hooks.RequestAuditor |           1 |     56     B
                                   <class 'threading.Thread |           1 |     56     B
                                    <class 'threading.Event |           1 |     56     B
                             <class 'uplink.session.Session |           1 |     56     B
                             <class 'uplink.builder.Builder |           1 |     56     B
                                    <class '__main__.GitHub |           1 |     56     B
                                module(app.stocks.services) |           0 |      0     B
                               module(pygments.lexers.perl) |           0 |      0     B
                                   module(werkzeug.contrib) |           0 |      0     B
                                   function (socket_accept) |           0 |      0     B

Expected behavior
Memory should be released along with consumer object

Additional context
Really appreciate your work
Anyway, issue should come this code where the it add reference to session obj while RequestClient already been delete

atexit.register(session.close)

Timeout as injectable hook

Is your feature request related to a problem? Please describe.
I have a need to set timeout on Consumer object initialization, but as I understand uplink.timeout decorator allows me to set timeout only on Consumer subclass definition. To solve this problem I wrapped uplink.decorators.timeout.modify_request into uplink.hooks.RequestAuditor and injected that hook in my Consumer.__init__ method as self._inject(timeout(t)). It would be very nice to have an ability to inject timeout like it is possible to inject headers or query parameters.

Describe the solution you'd like
I can think of 2 approaches:

  • First is to make timeout to be also a hook and have an audit_request() method.
  • Second is to make an argument Timeout subclass of NamedArgument and FuncDecoratorMixin which will behave similarly to Header or Query arguments. Also this will allow to have timeout argument in consumer methods.

Small issue in docs

I think there's a minor issue with this snippet in the docs at https://uplink.readthedocs.io/en/stable/user/quickstart.html#response-and-error-handling

def raise_for_status(response):
    """Checks whether or not the response was successful."""
    if 200 <= response.status <= 299:
        raise UnsuccessfulRequest(response.url)

    # Pass through the response.
    return response

I think condition of if describes a success case rather than a failure. Also if response is response of requests library it should probably be response.status_code (Not sure whether response.status also works or not. I don't think it does.)

Simple user defined response postprocessing

Converters handle all responses from all consumer methods for a particular consumer instance. Further, the converter layer is mainly an abstraction for (de)serialization frameworks.

For more general response handling (without the overhead of writing a converter), users could define response handling methods. Here's a rough draft of how we might introduce this:

class GitHub(uplink.Consumer):

    # A typical consumer method
    @uplink.get("/repositories")
    def get_repos(self):
        """Gets all public repositories."""
  
    # A method that handles all responses for the above method.
    @get_repos.postprocess
    def get_repos(self, response):
        return response.json()

Further, the postprocessing function would most likely run after the converter.

This feature is in draft phase, so any input is more than welcome!

Allow complete override of a Session's Headers

Is your feature request related to a problem? Please describe.
It would be nice if I could set the entire session.headers and not just update individual keys.

Describe the solution you'd like
I want to be able to override the session.headers property entirely:

self.session.headers = other_consumer_class.headers.copy()

This would require adding a setter for the headers property on the Session class:

class Session(object):
    ...
    @headers.setter
    def headers(self, headers):
        ...

More detailed documentation / tutorial

Hello, thank you for such a great library.

Can you write documentation or tutorials in retrofit-like way?

http://square.github.io/retrofit/

TODO (added by @prkumar):

  • Add scripts from gist to examples/ (see #35)
  • Add example with marshmallow
  • Add README under examples/, including relevant info, such as a brief summary for each example.
  • Consider rewriting root README.rst and/or documentation home page. An options is to adopt the tutorial structure used in Retrofit's landing page.

Add a way to override the execution order of certain class-level and method-level response handlers

Is your feature request related to a problem? Please describe.
Here's the original question raised by @liiight on gitter:

so i have a class response handler to handle request errors, which basically just does raise_for_status()
I have another response handler that I want to use in order to retry 404 status code via a retry lib I use
I set the 2nd response handler directly on the relevant method but it seems that the 1st one is the one that actually catches the exception
is there a way to decide on the order of those?

Describe the solution you'd like
There should be a way to specify that a particular method-level response handler should run before any or certain class-level handlers.

Additional context
Here's my response to the original question:

currently, class-level decorators run before method-level decorators, as you noticed in your usage. #72 (v0.4.1) details some of the rationale for this. Currently, Uplink doesn't give you a way to decide on the order between class-level and method-level decorators. From what I can tell, there are two existing workarounds, but both have drawbacks. First, you could make the retry response handler a class-level decorator. If you don't want all methods to be retried, the other workaround is to apply the raise_for_status decorator on each method, but this makes things more verbose.

urlparse.urljoin restricts URL patterns

The documentation for urljoin reveals that it has some very strange behavior. Calling build(... , base_url="https://example.com") and also having a method @get("//unrelated.com/users") means that the method would execute on "https://unrelated.com/users".

Is this the intended behavior? I personally think it is very confusing. The following code snippet does not work as one might expect:

request = utils.Request("METHOD", "/api.php?id={id}", {}, None)
uplink_builder.client = http_client_mock
uplink_builder.base_url = "https://example.com/feature"
request_preparer = builder.RequestPreparer(
    uplink_builder, request_definition
)
return_value = request_preparer.prepare_request(request)
assert return_value[0] == "METHOD"
assert return_value[1] == "https://example.com/feature/api.php?id={id}"
assert return_value[2] == {}

The above code snippet fails, as return_value[1]=https://example.com/api.php?id={id}.

Should we consider using a simple path.join() on URLs? Or should we allow for the complex, yet fairly convoluted behavior? If we want to allow for convoluted behavior (and continue using urljoin), I think it's worth raising an exception if base_url is not a true "base" (since its child paths will be trimmed anyway)

Support for Django Rest Framework serializers

I am using uplink in a project in order to communicate to an external API. In the project we are using django rest framework. DRF's serializers are much similar to Schemas of marshmallow. For that despite we are using DRF's serializers in other parts of code, for those parts pertinent to making requests and using uplink I had to introduced a new dependency (marshmellow) to the project.

I was wondering if support for serializers of django rest framework could be added along with marshmallow.

Thanks for the perfect library BTW.

Drop nomenclature of using decorators for argument annotation

As taken from https://uplink.readthedocs.io/en/latest/tips.html#annotating-your-arguments-for-python-2-7

For one, annotations can work as function decorators. With this approach, annotations are mapped to arguments from “bottom-up”.

The nomenclature used here is an extremely frustrating way of working with argument annotations outside of Python 3.

For myself, I am in the world of still trying to wrap my head around making my code portable, so for the time being, the obvious approach to using uplink is to make use of decorators in place of annotations.

However, on viewing the document around this, by far and a mile, the use of args as a substitute to an annotation, should be the primary explanation.

The current primary explanation in short, almost made my eyes bleed, whilst trying to wrap my head around it. To somebody that reads right to left and (or only) left to right, I could see this being a somewhat simpler concept, but I know for absolute certain I wouldn't want to encounter this approach again.

Technically, I understand why the "Bottom up" rule applied. With each executed decorator which processes top down, it is essentially creating a new call to be executed in the stack, whilst appending the existing and ordered stack to the end of itself.

With the arguments itself, they are read left to right as expected, with the left most (excluding self) popping the oldest (bottom most) item from the args stack, and continuing to pop the oldest for each arg.

But something is my mind at least, doesn't want to compute this (least of all 8am on a Monday as it did yesterday).

The @uplink.args methodology is the better description and much simpler to handle. Left to right, matching the actual arguments excluding self.

In all seriousness, the fact that they are implemented such that you could annotate each with a its own decorator is pretty novel, but the complicated comprehension when used practically, I would seriously suggest is removed from the docs, if not at minimum re-ordered.

From a coding standards perspective and even just basic ability to read code, the separate decorators and thus ordering, would not pass for personally. Though I could not answer this for others.

How should exceptions from underlying client libraries be handled?

Suppose I have a consumer for some API and suppose there is some network or server problems resulting in some sort of connection error. Currently ConnectionError from requests or ClientConnectionError from aiohttp are raised and it results in that calling code must know which library is used and it must handle different exceptions in synchronous and asynchronous case, this is rather inconvenient. Situation becomes worse if I make some another adapter, for example with pycurl: interface and calling convention remain the same but very different exceptions are raised.

I can see 2 ways to deal with it:

  • define such behaviour to be by design and probably reexport exception classes as attributes of consumer instance or do nothing for simplicity and put the burden on user's code
  • define some uplink's exceptions and wrap libraries' errors in them
    However both of these ways may lead to information loss which may be not desirable or may be not.

What are your thoughts about it?

And one more question: suppose I want to handle some exceptions or errors in Consumer to do something with them, for example to wrap them in my custom exception, how can I do it?

Support Basic Auth

In v0.4.0, we'll introduce an auth parameter to the Consumer constructor and include support for Basic Auth. Following Requests approach, users can pass their credentials using a 2-element tuple:

github = GitHub(BASE_URL, auth=("username", "password"))

Unable to send JSON list using @json annotation

I needed to post a JSON list and ran into difficulty using @JSON and Body. The code seems to assume that a json body will always be a dictionary and tries to loop over the items. Specifically, in decorators.py the old_body will be a list instead of the expected dictionary.

    @classmethod
    def set_json_body(cls, request_builder):
        body = request_builder.info.setdefault("json", {})
        old_body = request_builder.info.pop("data", {})
        for path in old_body:
            if isinstance(path, tuple):
                cls._sequence_path_resolver(path, old_body[path], body)
            else:
                body[path] = old_body[path]

As a workaround I found that I could just pass json.dumps as a converter-

    @headers({"Content-Type": "application/json"})
    @post("/collections/{name}/documents", args={"name": Path, "documents": Body(json.dumps)})
    def send_documents(self, name, documents):
        '''Send some documents'''

and then things will work

data = ["one", "two", "three"]
service.send_documents("testing", data)

AttributeError when following doc example

When creating the code for the Pull Request #50 I've also noticed that when I try to add .execute() to the .get_repos() like such:

response = github.get_repos().execute()

The code fails with:

AttributeError: 'Response' object has no attribute 'execute'

Does the documentation need updating or do we need to implement the execute() function?

Add contributors guide

Remaining work includes:

  • Write contributors guide (CONTRIBUTING.rst)
  • Update README.rst with Contributing section that links to CONTRIBUTION.rst.
  • Update opening note on documentation homepage (docs/source/index.rst) with link to CONTRIBUTION.rst.
  • Add AUTHORS.rst file (include @itstehkman's contribution)

Generic response object?

Considering possible different client adapters implementation, for example for curio or trio, and their different API regarding response, I thought that it will be useful to pass to callback not raw response from underlying library but some generic adapted version representing common interface. Not so long ago I've read an article where similar ideas are investigated and different http libraries were considered - https://snarky.ca/looking-for-commonality-among-http-request-apis/ . It shows that in simple cases where response body is fully consumed before processing it is possible to define the same interface for all libraries, however when response body is streamed requests, asyncio and twisted use different approaches which can't be reduced to one, at least without introducing much inconvenience for usual library users. What do you think of it?

Auto-generete Consumer classes from an OpenAPI/Swagger Specification

I've read a few posts of feedback (such as this Reddit comment) mentioning or asking for this support, so I've decided to open an issue to track the request.

Ideally, we would avoid adding this responsibility to this repository, to prevent "bloat". Rather, this should be the job of another library, which would (at least) depend on uplink and some OpenAPI Specification "parser".

Optional twisted/aiohttp dependencies

Hi @prkumar,

after seeing this comment, just wanted to check if you're open to a PR for this.

How about optional [twisted] and [aiohttp] extras with requests always being installed?

install_requires = [
    "requests>=2.18.0",
    "uritemplate>=3.0.0"
]
extras_require = {
    "twisted": ["twisted>=17.1.0"],
    "aiohttp:python_version>='3.4.2'": ["aiohttp>=2.3.0"]
}

Ideally, Python would give us the option of "install at least one of these 3", but I couldn't figure out if setuptools supports a "default" extra.

Alternatively see approach below, the serious downside being that uplink does not "work out of the box" if one were to pip install uplink.

extras_require = {
    "requests": ["requests>=2.18.0"],
    "twisted": ["twisted>=17.1.0"],
    "aiohttp:python_version>='3.4.2'": ["aiohttp>=2.3.0"]
} 

[Feature Request] Support for Deserializing Responses

Proposal

A response often details an entity (e.g., a GitHub user or repo) or a collection of entities, rendered in a format such as JSON or XML. Moreover, packages such as marshmallow can convert these formats into Python objects, and back. This issue proposes adding a layer to Uplink that handles deserialization simply and declaratively.

The purpose of this ticket is to gauge interest in this feature and track discussion on the possible impact to the public API. Implementation and design ideas are welcome too.

@itstehkman: Tagging you since you brought this up!

Best practice about pagination / Multiple Members

I am using uplink to query an API which enforces pagination on large datasets.

Their responses look like this:

{
  "data": [
    {
    },
    ...
  ],
  "meta": {
    "next_id":"175",
    "next_page":"/api/v4/logins?from_id=175&per_page=150"
  }
}

Whith an uplink request like

    @response_handler(tx_intercept)
    @returns.json(member="data")
    @get("transactions?account_id={account_id}&per_page=100")
    def get_transactions(self, account_id) -> TransactionSchema(many=True):
        """ """

I apparently miss out the meta field.

What would be the best practice here, without losing too much comfort, but still accessing the meta-field?
Furthermore, how/where would you implement the pagination? Can I somehow tell uplink to re-request until all pages are fetched?

Develop is behind master

The develop branch is behind the master branch. I'm assuming that "master" should be more stable than develop. Should develop be updated and all PRs be merged into develop rather than master?

Need a way to set request properties from constructor arguments

Here's @browniebroke initial question from Gitter:

Hi there :) I'm trying out Uplink to create a library providing a Consumer for a REST API (https://www.duedil.com/)
However, I want to create a installable libray, without my API key in there, and set the API key when I use that library in another project

cli = DuedilClient(key='my-very-secret-key')

Right now, I have decorated my whole class with @uplink.headers({...}) but I have to set the key at class definition
Any way I could provide that in the __init__ method without decorating all my endpoints?

Method annotations incorrectly decorate inherited consumer methods

Precondition

Consider the following consumer class:

class GitHub(uplink.Consumer):

    @uplink.get("/users/{username}")
    def get_user(self, username):
        """Get a single user."""

Use any method annotation. For this example, I'll create a custom method annotation, PrintMethodName, that simply prints the name of the consumer method that it wraps:

from uplink import decorators

class PrintMethodName(decorators.MethodAnnotation):

    def modify_request_definition(self, method):
        print(method.__name__)

Steps to recreate

Create a subclass of GitHub and decorate it with the method annotation:

@PrintMethodName()
class GitHubSubclass(GitHub):
    pass

Expected

No output to stdout, since GitHubSubclass doesn't define any consumer methods.

Actual

The method annotation decorates get_user from the parent class, GitHub.

Output:

get_user

Method headers cannot override previously defined Class headers

In an attempt to write a custom module to provide specific external system interaction, I observed that I cannot override my header at method level. I will move to a new decorator eventually, but custom headers are a good start, as I will use them extensively anyway and header behaviour is tested, so I thought it can prove code outside of my struggle to write a new decorator,

The example below uses a class level and method level header. Same key, new value, with what I thought would trigger an override, but doesn't.

The response is always uplink.abc.ExecClassOp, unless I remove the class level one. Commenting out the class level header (for the same header key) correctly returns uplink.abc.ExecMethodOp.

uri = 'https://jsonplaceholder.typicode.com/'

@timeout(10)
@headers({'X-UplinkAuthIntercept': 'uplink.abc.ExecClassOp'})
class JSONp(Consumer):
  @get('users')
  def get_users(self): pass
  
  @timeout(15)
  @headers({'X-UplinkAuthIntercept': 'uplink.abc.ExecMethodOp'})
  @get('users/{id}')
  def get_user(self, user_id: Path('id')): pass

class SessionWrapper(object):
  def __init__(self):
    self._session = requests.Session()
    
  def request(self, *args, **kwargs):
    print('Session.request: args={0}, kwargs: {1}'.format(args, kwargs))
    return self._session.request(*args, **kwargs)

api = JSONp(base_url=uri, client=RequestsClient(SessionWrapper()))
user = api.get_user(2)
print('Header: {0}'.format(user.request.headers))

Session wrapper here is to provide working insight only.

Set constructor defaults and extend behavior for `Consumer` sublcasses using `class Meta`

marshmallow does this interestingly with a class Meta inside a marshmallow.Schema definition. We could do something similar. For instance, users could set constructor defaults like so:

class GitHub(uplink.Consumer):
    class Meta(object):
        base_url = "https://api.github.com/"
        converter = uplink.MarshmallowConverter()

    @uplink.get("/repositories")
    def get_repos(self) -> RepoSchema(many=True) :
        """Gets all public repositories."""

Using a class Meta has the added bonus of not cluttering the namespace of the consumer subclass itself.

Of course, for setting default constructor values, users could always just provide these in an overriden __init__. However, introducing a class Meta still seems like an interesting idea and something to think about when considering consumer extendability.

How to use non-json request and response bodies with uplink, for example protobuf?

I could annotate request parameter with uplink.Body(RqModel), response with uplink.returns(RsModel) where RqModel and RsModel are corresponding request and response protobuf classes and make my own converter to and from them at 0.4.1 but in version 0.5.0 Body requires dict to be returned from converter and returns disappeared. I solved the issue by restoring old behaviour with my own decorators but I have a question: how one is supposed to use models and their custom serialization and deserialization scheme with uplink since 0.5.0?

Add doc strings to method annotation classes

Method annotation classes in uplink/decorators.py are missing class doc strings. To improve code documentation, we need to add doc strings to the following classes, adhering the Google Style Guide for consistency with the rest of the codebase:

  • uplink.decorators.headers
  • uplink.decorators.form_url_encode
  • uplink.decorators.multipart
  • uplink.decorators.json
  • uplink.decorators.timeout
  • uplink.decorators.args

How can pass parameters to request method?

I want to call HTTPS Rest API using uplink.Consumer. API provides authorization by specifying Authorization token in the headers as shown below:

BASE_URL = "https://localhost:3780/"

@uplink.headers({
    "Content-Type": "application/json",
    "Authorization":"Basic YWRtaW46Z3VydWppMTI="
})

So when I call any REST API it gives me SSL certifaction error as follows:

requests.exceptions.SSLError: HTTPSConnectionPool(host='localhost', port=3780): Max retries exceeded with url: /api/3/assets/1 (Caused by SSLError(SSLError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:645)'),))

Uplinks internally calls request method and request method has SSL to true by default ( i.e verify=True )
I want set verify property to false. How can I do that?

JSON Converter not converting

The following example gives this output:

<class 'requests.models.Response'>
Ran 2 tests in 0.743s
OK
<class 'requests.models.Response'>

Expected output:

<class '....JokeSchema'>
Ran 2 tests in 0.743s
OK
<class '....JokeSchema'>

Am I missing something here? Or is this a bug?

from marshmallow import Schema, fields

class Joke(object):
    def __init__(self, id, url, icon_url, value):
        self.id = id
        self.url = url
        self.icon_url = icon_url
        self.value = value

class JokeSchema(Schema):
    id = fields.Str()
    url = fields.Str()
    icon_url = fields.Str()
    value = fields.Float()
import typing
from uplink import Consumer, \
    get, \
    returns

from rest_client.lib.model.chuck.Joke import JokeSchema

BASE_URL = "https://api.chucknorris.io/jokes/"

class Chuck(Consumer):
    def __init__(self):
        super().__init__(base_url=BASE_URL)

    @get("random")
    @returns.json
    def get_random_joke(self) -> JokeSchema:
        """Get a random Joke."""

    @get("categories")
    def list_categories(self) -> typing.List[str]:
        """Fetch all categories"""
import unittest
from rest_client.lib.Chuck import Chuck

class ChuckTest(unittest.TestCase):

    chuck = Chuck()

    def test_random_joke(self):
        joke = self.chuck.get_random_joke()
        print(type(joke))

    def test_categories(self):
        categories = self.chuck.list_categories()
        print(type(categories))

if __name__ == '__main__':
    unittest.main()

Add doc strings to argument annotation classes

Argument Annotation classes in uplink/types.py are missing class doc strings. To improve code documentation, we need to add doc strings to the following classes, adhering the Google Style Guide for consistency with the rest of the codebase:

  • uplink.types.Query
  • uplink.types.QueryMap
  • uplink.types.Header
  • uplink.types.HeaderMap
  • uplink.types.Field
  • uplink.types.FieldMap
  • uplink.types.Part
  • uplink.types.PartMap
  • uplink.types.Body
  • uplink.types.Url

Add documentation for v0.4 features

Here are a some v0.4 features that may be missing documentation:

  • Basic Auth support (#58)
  • Registering response handlers (#62)
  • Registering error handlers (#63)
  • Set request properties from constructor arguments (#65)
  • Added Consumer._inject method and inject decorator (#67)

Add support for parsing JSON objects using `glom`

Is your feature request related to a problem? Please describe.
glom is a library that provides a lot of neat functionality in terms of parsing nested structures, like JSON objects.

Describe the solution you'd like
We can introduce a parser argument for @uplink.returns.json that supports custom parsing strategies for JSON responses:

@uplink.returns.json(key="a.b.c", parser="glom")

Further, glom provides support for converting dictionary-like objects into objects, which seems like a great fit for adding a custom ConverterFactory for glom, too.

Additional context
This issue is related to feedback provided by @liiight through the Uplink Gitter lobby: https://gitter.im/python-uplink/Lobby?at=5c1a04f7b4ef82024857910d. @liiight also proposed exposing a specific decorator for glom:

@uplink.returns.glom('a.b.c')

`client` parameter in `Consumer` constructor doesn't work as documented

Precondition

Consider the following consumer:

class GitHub(uplink.Consumer):

    @uplink.get("/users/{username}")
    def get_user(self, username):
        """Get a single user."""

Steps to recreate

Instantiate this consumer with a specific client instance:

GitHub(base_url="https://api.github.com/", client=uplink.RequestsClient())

Expected

Consumer instance builds properly and uses the given client instance.

Note: when the client parameter is given a uplink.clients.interfaces.HttpClientAdapter subclass, it should instantiate a client instance; otherwise the provided value should be used as given.

Actual

Exception raised on instantiation:

Traceback (most recent call last):
  File "/Users/prkumar/Library/Preferences/PyCharm2017.2/scratches/scratch_1.py", line 11, in <module>
    GitHub(base_url="https://api.github.com/", client=uplink.RequestsClient())
  File "/Users/prkumar/Developer/uplink/uplink/builder.py", line 170, in __init__
    self._build(builder)
  File "/Users/prkumar/Developer/uplink/uplink/builder.py", line 175, in _build
    caller = call_builder.build(self, definition_builder)
  File "/Users/prkumar/Developer/uplink/uplink/builder.py", line 149, in build
    RequestPreparer(self, definition),
  File "/Users/prkumar/Developer/uplink/uplink/builder.py", line 42, in __init__
    if issubclass(self._client, clients.interfaces.HttpClientAdapter):
TypeError: issubclass() arg 1 must be a class

Class-level decorators on Consumer classes do not apply to inherited methods

Describe the bug
For consumer classes that inherit consumer methods (i.e., methods decorated with @uplink.get, @uplink.post, etc.) from one or more parent classes, uplink decorators such as@response_handler or @timeout are not applied to those inherited methods when these decorators are used as class-level decorators. In other words, these decorators are strictly applied to consumer methods that are directly defined on the decorated consumer class.

To Reproduce
Consider the following consumer class:

class GitHub(uplink.Consumer):
    @uplink.get("/users/{username}")
    def get_user(self, username):
        """Get a single user."""

Create a subclass of GitHub and decorate it with any uplink decorator that should propagate to consumer methods when used as a class decorator. For this example, I apply a @response_handler that should make any consumer method return the integer 1, regardless of the actual response returned by the server:

@response_handler(lambda resp: 1)
class GitHubSubclass(GitHub):
    pass

Here’s a quick test that shows that the response handler is not applied to the inherited method (i.e., the assertion fails):

client = GitHubSubclass(...)
assert github.get_user(“prkumar”) == 1

Expected behavior
Applying a decorator to a Consumer class should propagate to ALL consumer methods available to that class, including inherited consumer methods.

Additional context
Prior to v0.3.0, the actual behavior reflected the expected behavior detailed above. However, as part of #27, we unnecessarily began restricting the application of class-level decorators to only those consumer methods defined directly on the decorated consumer class. Hence, a fix for this bug should effectively revert the changes made in #27. Notably, this means that the fix should make changes to the function uplink.helpers.get_api_definitions.

Consider generating consumer instance using instantiation instead of builder

For now, you must use uplink.build (or uplink.Builder) to construct a consumer instance. This behavior corresponds with Retrofit.Builder.

It seems that Retrofit needs to decouple the consumer's definition and its instantiation simply because the definition is an interface, so the builder is used to generate an implementation of the that interface. However, since our consumer definition is a class, we could rely on instantiation (instead of the uplink.build) and provide a base class (e.g., uplink.Consumer) for consumer definitions to inherit. As a side effect, this would couple the creation of a consumer with it's definition, but I can't think of a reason why this would be a problem for us.

For example, with this change, we could define an API consumer like so:

import uplink

class GitHub(uplink.Consumer):
    @get("/users/{username}")
    def get_user(self, username):
        """Get a single user."""

Then, constructing a consumer would simply involve instantiation, rather than delegating to uplink.build:

github = GitHub(base_url="http://api.github.com/")

Opening this issue to start the discussion.

Need a way to avoid sending optional query parameters.

Is your feature request related to a problem? Please describe.
I've just tried out uplink with an API I want to use (e.g. https://pagure.io/api/0/) which knows no special value for "unset" or "use the default". Currently, uplink always sends all query parameters and…

Describe the solution you'd like
I need a way to not send any parameters that aren't set by the user.

Add a `retry` decorator

Here's the original use case from @liiight on Gitter:

I'm implementing retries in my code, which is done using a package called retry. basically its a super simple decorator, just catch exception, and call the decoratored func until conditions apply, nothing fancy
when using with uplink, I'm implementing it in the layer above uplink, i.e, the usage of it. i was wondering if it's possible to do it via uplink itself
i.e, catch an error and retry the called method

Subclasses of Consumers Not Behaving as Expected

Consider the following scenario:

class StoreAPI(Consumer):
    @get("/api/item?id={id}")
    def get_item(self, id):
         pass

class ProtectedStoreAPI(StoreAPI):
    def __init__(self, user_id):
        super().__init__()
        self.user_id = user_id

    def get_item(self, id):
         if allowed(self.user_id):
             return super().get_item(id)

In the above scenario, I attempt to implement a "wrapper" around a consumer. The wrapper, being a subclass, can still be passed around just as the consumer was, but the wrapper has some extra security features.

The problem arises, however, when calling super().get_item() in the wrapper. Since everything is built dynamically, the method "ProtectedStoreAPI::get_movie" is not detected as a RequestDefinitionBuilder and is not turned into a callable. Neither is the StoreAPI::get_movie since its overriden. Thus, nothing is ever built into a CallBuilder and the StoreAPI cannot be called.

Note, however, that everything is fine when the protected API calls the method something other than get_item, since then StoreAPI::get_movie is exposed and can be built.

I've been working with this for the better part of a few hours and can't come up with a good solution. I've tried adding a new @overrides decorator and a new Mixin class.. both seemed promising, but no dice.

I think this is a pretty valid usecase and it's a shame it doesn't work. I'll do investigations in my free time and keep this issue updated. Please chime in if you know anything!

Include Rate Limiting Support

I wanted to check in and see if the maintainers thought it would be a good feature to add to uplink. The idea is that you can specify a requests/time period and the consumer would never make request faster than that. It would be up to the user to specify a rate somewhat lower than the actual limit for the api they're wrapping. I could see an issue that would make it unnecessary to add this feature, being that you can only limit the requests a for a single instance. Just wanted to put it up for consideration.

Passing list of values as query parameter is broken in uplink 0.6.0

Consider we have some consumer with defined query parameter:

class C(uplink.Consumer):
    @uplink.args(uplink.Query())
    @uplink.get('/')
    def root(self, p):
        pass

And if we call c.root(p=['p1', 'p2']) uplink transforms string parameters to list, then that list to string . Thus uplink makes request like following:

GET /?p=%5B%27p%27%2C+%271%27%5D&p=%5B%27p%27%2C+%272%27%5D HTTP/1.1

and after decoding query parameters we'll see "['p', '1']" as the first parameter and "['p', '2']" as the second but expected values are "p1" and "p2".

However if we specify query type to str: uplink.Query(type=str) such conversion does not happen and uplink makes correct request. Also specifying type=lambda x: x helps too. This is workaround I'll use for a while.

I found that triggering change was try to use value type to get converter:

-        argument_type, converter_key = self.type, self.converter_key
+        argument_type = self.type or type(value)

From investigating the code I found that the type of argument is used to find converter for all argument's parts if it is composite. In our case argument is list of strings ['p1', 'p2'], uplink uses key Sequence(CONVERT_TO_STRING) to find converter for query parameters, and then type list is used to find converter for 'p1' and 'p2' and corresponding Cast(list, StringConverter(...)) is found which is incorrect and mangles parameters.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.